WOW64からのシステムコール概要
もうちょっと実装よりに、Win7(RC)x64 で以下のコードを用いて違いを確認する。
int _tmain() { __debugbreak(); FlushProcessWriteBuffers(); return 0; }
FlushProcessWriteBuffersは、引数なし戻り値なしのとても単純なAPIなので、処理の概要を理解するのに適している。
Visual StudioではWOW64を追いかけることができないため、デバッガにはWinDbgを使う。前にも書いたとおり、WOW64の仕組みは完全にユーザーモードで完結しているので、カーネルデバッグなどは特に必要ない。
x64(ネイティブ)
x64としてビルドすると、FlushProcessWriteBuffersはntdll!NtFlushProcessWriteBuffersを指しており、直ちにsyscallを発行する。
ntdll!NtFlushProcessWriteBuffers: 00000000`778df140 4c8bd1 mov r10,rcx 00000000`778df143 b8c4000000 mov eax,0C4h ; eax = システムコール番号 00000000`778df148 0f05 syscall 00000000`778df14a c3 ret ;0:000> r ;rax=00000000cccccccc rbx=0000000000000000 rcx=0000000000000001 ;rdx=0000000000462580 rsi=0000000000000000 rdi=000000000014fb70 ;rip=000000007778f140 rsp=000000000014fb48 rbp=0000000000000000 ; r8=00000000004626a0 r9=00000000000000fe r10=00000000000000e0 ;r11=000000000014f4a8 r12=0000000000000000 r13=0000000000000000 ;r14=0000000000000000 r15=0000000000000000 ;iopl=0 nv up ei pl nz na po nc ;cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000206
コメント部分は、WinDbgのrコマンドでレジスタの状態をダンプしたもの。どこでダンプを出したものかはeip/ripの値をみればおk。
このときロードされるモジュールは以下のとおりで、これについて特筆すべきことはない。
Base Address Size Image Path ModLoad: 00000001`3fac0000 00000001`3facd000 pj00.exe ModLoad: 00000000`77740000 00000000`778e8000 ntdll.dll ModLoad: 00000000`77520000 00000000`7763e000 C:\Windows\system32\kernel32.dll ModLoad: 000007fe`fd7e0000 000007fe`fd849000 C:\Windows\system32\KERNELBASE.dll ModLoad: 00000000`74730000 00000000`748e0000 C:\Windows\system32\MSVCR100D.dll
x86(WOW64)
次にx86としてビルドして実行する。ロードされるモジュールは以下のとおりで、wow64が構築されていることがわかる。
Base Address Size Image Path ModLoad: 00000000`00f70000 00000000`00f8b000 pj00.exe ModLoad: 00000000`77740000 00000000`778e8000 ntdll.dll ModLoad: 00000000`77920000 00000000`77aa0000 ntdll32.dll ModLoad: 00000000`75430000 00000000`7546f000 C:\Windows\SYSTEM32\wow64.dll ModLoad: 00000000`753d0000 00000000`7542c000 C:\Windows\SYSTEM32\wow64win.dll ModLoad: 00000000`753c0000 00000000`753c8000 C:\Windows\SYSTEM32\wow64cpu.dll ModLoad: 00000000`763a0000 00000000`764a0000 C:\Windows\syswow64\kernel32.dll ModLoad: 00000000`76270000 00000000`762b4000 C:\Windows\syswow64\KERNELBASE.dll ModLoad: 00000000`70010000 00000000`70171000 C:\Windows\SysWOW64\MSVCR100D.dll
さて、システムコールのシーケンスを見ていく。
WOW64におけるFlushProcessWriteBuffersは32ビットntdll.dllのNtFlushProcessWriteBuffersを指している。
ntdll32!NtFlushProcessWriteBuffers: 77940b2c b8c4000000 mov eax,0C4h ; eax = システムコール番号 77940b31 33c9 xor ecx,ecx ; ecx = 0 77940b33 8d542404 lea edx,[esp+4] ; edx = スタックポインタの場所 77940b37 64ff15c0000000 call dword ptr fs:[0C0h] ; fs:0053:000000c0=00000000 77940b3e 83c404 add esp,4 77940b41 c3 ret ;0:000:x86> r ;eax=000000c4 ebx=7efde000 ecx=00000000 edx=002cf724 esi=002cf724 edi=002cf7f0 ;eip=77940b37 esp=002cf720 ebp=002cf7f0 iopl=0 nv up ei pl zr na pe nc ;cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246
eaxにシステムコール番号を入れて、fs:[0C0h]へ飛ぶ。これは次に示すwow64cpu!X86SwitchTo64BitModeを指している。
wow64cpu!X86SwitchTo64BitMode: 753c2320 ea1e273c753300 jmp 0033:753C271E ;0:000:x86> r ;eax=000000c4 ebx=7efde000 ecx=00000000 edx=002cf724 esi=002cf724 edi=002cf7f0 ;eip=753c2320 esp=002cf71c ebp=002cf7f0 iopl=0 nv up ei pl zr na pe nc ;cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246
ここでもさらに0033:753C271Eというセグメント間ジャンプを行う。これは以下のwow64cpu!CpupReturnFromSimulatedCodeを指している。
64ビットモードへの切り替え
; アセンブリにはコメントで(このときの)実効アドレスをつけている wow64cpu!CpupReturnFromSimulatedCode: 00000000`753c271e 67448b0424 mov r8d,dword ptr [esp] ; ds:00000000`002cf71c=77940b3e 00000000`753c2723 458985bc000000 mov dword ptr [r13+0BCh],r8d ; ds:00000000`0009fddc=7793fa62 00000000`753c272a 4189a5c8000000 mov dword ptr [r13+0C8h],esp ; ds:00000000`0009fde8=002cf354 00000000`753c2731 498ba42480140000 mov rsp,qword ptr [r12+1480h] ; ds:00000000`7efdc480=000000000009e580 00000000`753c2739 4983a4248014000000 and qword ptr [r12+1480h],0 ; ds:00000000`7efdc480=000000000009e580 00000000`753c2742 448bda mov r11d,edx wow64cpu!TurboDispatchJumpAddressStart: 00000000`753c2745 41ff24cf jmp qword ptr [r15+rcx*8] ; ds:00000000`753c2450={wow64cpu!TurboDispatchJumpAddressEnd (00000000`753c2749)} ;0:000> r ;rax=00000000000000c4 rbx=000000007efde000 rcx=0000000000000000 ;rdx=00000000002cf724 rsi=00000000002cf724 rdi=00000000002cf7f0 ;rip=00000000753c271e rsp=00000000002cf71c rbp=00000000002cf7f0 ; r8=000000000000002b r9=000000007793fa62 r10=0000000000000000 ;r11=000000000009dc90 r12=000000007efdb000 r13=000000000009fd20 ;r14=000000000009e5f0 r15=00000000753c2450 ;iopl=0 nv up ei pl zr na po nc ;cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246
先のジャンプによってCSレジスタの値が0x23から0x33へ変わり、プロセッサが64ビットモードになっている。この関数の仕事は、
- [r13+0BCh]にリターンアドレスを保存
- [r13+0C8h]にespを保存
- [r12+1480h]からx64スタックポインタを復元
- [r12+1480h]を0クリア
- r11にedxの値を保存
- ジャンプ
ジャンプアドレスはr15を基点とし、rcxを添え字とした関数配列へのジャンプであることがわかるので、ためしにr15の値でシンボルの調査してみる(dpsコマンドは dump pointer_size symbol って感じの意味で、ddsにするとDWORDサイズで解釈したりする。この辺はWinDbgのリファレンスをどうぞー)。
0:000> dps 753c2450 753c2450+100 00000000`753c2450 00000000`753c2749 wow64cpu!TurboDispatchJumpAddressEnd 00000000`753c2458 00000000`753c2d8d wow64cpu!Thunk0Arg 00000000`753c2460 00000000`753c2bb3 wow64cpu!Thunk0ArgReloadState 00000000`753c2468 00000000`753c2d3d wow64cpu!Thunk1ArgSp 00000000`753c2470 00000000`753c2d4e wow64cpu!Thunk1ArgNSp 00000000`753c2478 00000000`753c2d4a wow64cpu!Thunk2ArgNSpNSp 00000000`753c2480 00000000`753c2c66 wow64cpu!Thunk2ArgNSpNSpReloadState 00000000`753c2488 00000000`753c2d57 wow64cpu!Thunk2ArgSpNSp 00000000`753c2490 00000000`753c2d39 wow64cpu!Thunk2ArgSpSp 00000000`753c2498 00000000`753c2d28 wow64cpu!Thunk2ArgNSpSp 00000000`753c24a0 00000000`753c2d46 wow64cpu!Thunk3ArgNSpNSpNSp 00000000`753c24a8 00000000`753c2d35 wow64cpu!Thunk3ArgSpSpSp 00000000`753c24b0 00000000`753c2d60 wow64cpu!Thunk3ArgSpNSpNSp 00000000`753c24b8 00000000`753c2ba8 wow64cpu!Thunk3ArgSpNSpNSpReloadState 00000000`753c24c0 00000000`753c2d71 wow64cpu!Thunk3ArgSpSpNSp 00000000`753c24c8 00000000`753c2d24 wow64cpu!Thunk3ArgNSpSpNSp 00000000`753c24d0 00000000`753c2d53 wow64cpu!Thunk3ArgSpNSpSp 00000000`753c24d8 00000000`753c2d42 wow64cpu!Thunk4ArgNSpNSpNSpNSp 00000000`753c24e0 00000000`753c2d6d wow64cpu!Thunk4ArgSpSpNSpNSp 00000000`753c24e8 00000000`753c2ae2 wow64cpu!Thunk4ArgSpSpNSpNSpReloadState 00000000`753c24f0 00000000`753c2d7e wow64cpu!Thunk4ArgSpNSpNSpNSp 00000000`753c24f8 00000000`753c2ba4 wow64cpu!Thunk4ArgSpNSpNSpNSpReloadState 00000000`753c2500 00000000`753c2d20 wow64cpu!Thunk4ArgNSpSpNSpNSp 00000000`753c2508 00000000`753c2d31 wow64cpu!Thunk4ArgSpSpSpNSp 00000000`753c2510 00000000`753c27b5 wow64cpu!QuerySystemTime 00000000`753c2518 00000000`753c277a wow64cpu!GetCurrentProcessorNumber 00000000`753c2520 00000000`753c297f wow64cpu!ReadWriteFile 00000000`753c2528 00000000`753c28c5 wow64cpu!DeviceIoctlFile 00000000`753c2530 00000000`753c2a30 wow64cpu!RemoveIoCompletion 00000000`753c2538 00000000`753c27f3 wow64cpu!WaitForMultipleObjects 00000000`753c2540 00000000`753c27fa wow64cpu!WaitForMultipleObjects32 00000000`753c2548 00000000`753c2779 wow64cpu!ThunkNone 00000000`753c2550 6666cccc`cccccccc ; ここでおわりみたい
引数の数に応じた関数へのジャンプになるようだ。たとえばNtFlushKeyではecxに3を設定しており、wow64cpu!Thunk1ArgSpへジャンプすることがわかる。
; NtFlushKey(HANDLE KeyHandle) ntdll32!NtFlushKey: 77940b10 b8c3000000 mov eax,0C3h 77940b15 b903000000 mov ecx,3 ; wow64cpu!Thunk1ArgSp 77940b1a 8d542404 lea edx,[esp+4] 77940b1e 64ff15c0000000 call dword ptr fs:[0C0h] 77940b25 83c404 add esp,4 77940b28 c20400 ret 4
シンボル名のSpは非ポインタ型、NSpはポインタ形を意味している。
; NtAlertResumeThread(HANDLE ThreadHandle, PULONG SuspendCount); ntdll32!NtAlertResumeThread: 77940278 b869000000 mov eax,69h 7794027d b907000000 mov ecx,7 ; wow64cpu!Thunk2ArgSpNSp 77940282 8d542404 lea edx,[esp+4] 77940286 64ff15c0000000 call dword ptr fs:[0C0h] 7794028d 83c404 add esp,4 77940290 c20800 ret 8
専用の関数が用意されているものもある。
; NtGetCurrentProcessorNumber(void); ntdll32!NtGetCurrentProcessorNumber: 77940bd8 b8cb000000 mov eax,0CBh 77940bdd b919000000 mov ecx,19h ; wow64cpu!GetCurrentProcessorNumber 77940be2 8d542404 lea edx,[esp+4] 77940be6 64ff15c0000000 call dword ptr fs:[0C0h] 77940bed 83c404 add esp,4 77940bf0 c3 ret
また、ReloadStateは待機系の関数(ZwDelayExecutionやZwWaitForSingleObjectなど)につくようだ。それと引数の数と無関係な関数を使うものもある。詳細は調べてない。
とりあえずNtFlushProcessWriteBuffersではwow64cpu!TurboDispatchJumpAddressEndへジャンプするということ。
ネイティブシステムコールの呼び出し
wow64cpu!TurboDispatchJumpAddressStart: 00000000`753c2745 41ff24cf jmp qword ptr [r15+rcx*8] ; ds:00000000`753c2450={wow64cpu!TurboDispatchJumpAddressEnd (00000000`753c2749)} wow64cpu!TurboDispatchJumpAddressEnd: 00000000`753c2749 4189b5a4000000 mov dword ptr [r13+0A4h],esi ; ds:00000000`0009fdc4=007e0000 00000000`753c2750 4189bda0000000 mov dword ptr [r13+0A0h],edi ; ds:00000000`0009fdc0=007e4fe8 00000000`753c2757 41899da8000000 mov dword ptr [r13+0A8h],ebx ; ds:00000000`0009fdc8=007e4fe0 00000000`753c275e 4189adb8000000 mov dword ptr [r13+0B8h],ebp ; ds:00000000`0009fdd8=002cf388 00000000`753c2765 8bc8 mov ecx,eax ; ecx = システムコール番号 00000000`753c2767 ff1513e9ffff call qword ptr [wow64cpu!_imp_Wow64SystemServiceEx (00000000`753c1080)] ; ds:00000000`753c1080={wow64!Wow64SystemServiceEx (00000000`7543ceb0)} 00000000`753c276d 418985b4000000 mov dword ptr [r13+0B4h],eax ; ds:00000000`0009fdd4=00000000 00000000`753c2774 e998feffff jmp wow64cpu!CpuSimulate+0x61 (00000000`753c2611)
ここでの仕事はWOW64用のレジスタを保存し、wow64!Wow64SystemServiceExを呼び出すこと。
wow64!Wow64SystemServiceExは少し大きめなので要点のみを。全体はこちら。
- ecxに含まれるシステムコール番号が wow64!ServiceTables[index].Fields3 の値より大きければエラーSTATUS_INVALID_SYSTEM_SERVICEを返す。
- indexは32ビットシステムコールの呼び出し元のスタック位置に応じて 0-3 になる。今回は0。
- wow64!ServiceTables[index].Fields1[system_call_number] の関数を呼び出す。今回はwow64!whNtFlushProcessWriteBuffers。
- wow64!pfnWow64LogSystemServiceが非NULLなら、上記呼び出しの前後でこの関数も呼び出す。今回はNULLのため呼び出さない。
- wow64!ServiceTables[index].Fields1[system_call_number] の関数内では rcx レジスタを起点としてスタックから引数を回収する。
- wow64!ServiceTables[index].Fields1[system_call_number] の戻り値を eax に入れて戻る。
結果、
[r13+0A0h]にediを保存
[r13+0A4h]にesiを保存
[r13+0A8h]にebxを保存
[r13+0ACh]-----------*1
[r13+0B0h]-----------*2
[r13+0B4h]にeaxを保存(システムコールの戻り値)
[r13+0B8h]にebpを保存
[r13+0BCh]にリターンアドレスを保存(64ビット)
[r13+0C0h]にリターンアドレスを保存(64ビット)
[r13+0C4h]-----------
[r13+0C8h]にespを保存
このようになり、wow64cpu!CpuSimulate+0x61へと移動する。
;0:000> r ;rax=0000000000000000 rbx=000000007efde000 rcx=00007549153a0000 ;rdx=000000000009dd38 rsi=00000000002cf724 rdi=00000000002cf7f0 ;rip=00000000753c2611 rsp=000000000009e580 rbp=00000000002cf7f0 ; r8=000000000009dc88 r9=00000000002cf7f0 r10=0000000000000000 ;r11=0000000000000306 r12=000000007efdb000 r13=000000000009fd20 ;r14=000000000009e5f0 r15=00000000753c2450 ;iopl=0 nv up ei pl nz na po nc ;cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000206 00000000`753c2611 4183a5d002000001 and dword ptr [r13+2D0h],1 00000000`753c2619 0f84af000000 je wow64cpu!CpuSimulate+0x11e (00000000`753c26ce) ; ... 00000000`753c26ce 418bbda0000000 mov edi,dword ptr [r13+0A0h] ; ds:00000000`0009fdc0=002cf7f0 00000000`753c26d5 418bb5a4000000 mov esi,dword ptr [r13+0A4h] ; ds:00000000`0009fdc4=002cf724 00000000`753c26dc 418b9da8000000 mov ebx,dword ptr [r13+0A8h] ; ds:00000000`0009fdc8=7efde000 00000000`753c26e3 418badb8000000 mov ebp,dword ptr [r13+0B8h] ; ds:00000000`0009fdd8=002cf7f0 00000000`753c26ea 418b85b4000000 mov eax,dword ptr [r13+0B4h] ; ds:00000000`0009fdd4=00000000 00000000`753c26f1 4989a42480140000 mov qword ptr [r12+1480h],rsp; ds:00000000`7efdc480=0000000000000000 00000000`753c26f9 41c7460423000000 mov dword ptr [r14+4],23h ; ds:00000000`0009e5f4=00000023 00000000`753c2701 41b82b000000 mov r8d,2Bh 00000000`753c2707 418ed0 mov ss,r8w 00000000`753c270a 418ba5c8000000 mov esp,dword ptr [r13+0C8h] ; 00000000`753c2711 458b8dbc000000 mov r9d,dword ptr [r13+0BCh] ; ds:00000000`0009fddc=77940b3e 00000000`753c2718 45890e mov dword ptr [r14],r9d ; ds:00000000`0009e5f0=7793fa62 00000000`753c271b 41ff2e jmp fword ptr [r14] ; ds:00000000`0009e5f0=0023 77940b3e
wow64cpu!CpuSimulate+0x61では、冒頭で[r13+2D0h]のフラグを確認しているが、これは例外関連の処理らしい(分岐のフローを見ると iretq 命令で終わっている)。今回の流れでは関係ないので省略した。
WOW64用のレジスタを復元させ、[r12+1480h]にrspを保存する。最後に 0023h:[r13+0BCh] なジャンプを行い、CSを23hにして32ビット版ntdll.dllへと戻っていく。
ntdll32!NtFlushProcessWriteBuffers: 77940b2c b8c4000000 mov eax,0C4h 77940b31 33c9 xor ecx,ecx 77940b33 8d542404 lea edx,[esp+4] 77940b37 64ff15c0000000 call dword ptr fs:[0C0h] ; callで行って ; ↓ここに戻ってくる 77940b3e 83c404 add esp,4 ; jmpで帰ってきたのでスタックの調整を行う 77940b41 c3 ret
以上、お疲れ様でした。
わかんない、見てない、そのうち調べる(?)ところ
- 引数ありの場合の処理
- wow64!ServiceTables[index] の index が0以外になるタイミング
- NtGetCurrentProcessorが専用関数になっている理由
- WOW64の初期化処理。より具体的には
- qword ptr gs:[30h](00000000'7efdb000)は何者?
今回ではいきなり[r13+*]や[r12+1480h]にコンテキストを保存しているけど、当然、事前にr13やr12には値が入ってる。この初期化は、ざっと見た感じだと前述のwow64cpu!CpuSimulate関数で行われているらしくて、
0:000> k Child-SP RetAddr Call Site 00000000`0013e6a0 00000000`7543d07e wow64cpu!CpuSimulate <- 初期化処理中のここ! 00000000`0013e760 00000000`7543c549 wow64!RunCpuSimulation+0xa 00000000`0013e7b0 00000000`77783d59 wow64!Wow64LdrpInitialize+0x429 00000000`0013ed00 00000000`77775c83 ntdll!LdrpInitializeProcess+0x17e2 00000000`0013f200 00000000`7776403e ntdll! ?? ::FNODOBFM::`string'+0x28c40 00000000`0013f270 00000000`00000000 ntdll!LdrInitializeThunk+0xe
そのうえ中では結局はどっちも gs:[30h] を基にしてるみたい。
00000000`753c25f4 4c8d742470 lea r14,[rsp+70h] ; r14の初期化 00000000`753c25f9 654c8b242530000000 mov r12,qword ptr gs:[30h] ; r12の初期化 ; r15の初期化 00000000`753c2602 4c8d3d47feffff lea r15,[wow64cpu!CpupSaveLegacyFloatingPointState+0x60 (00000000`753c2450)] 00000000`753c2609 4d8bac2488140000 mov r13,qword ptr [r12+1488h] ; r13の初期化 ; ; この後はすぐにx86モードに遷移してしまうので、以後64ビットレジスタは変更されないと思われる ;
r13やr12っていたけど、結局はどっちも gs:[30h] を基にしてるってことだとすると
「じゃあ、gs:[30h] ってナニよ?」
って事になるし、それは誰もわからない。ヤバイ。誰にも分からないなんて凄すぎる。
ついき。WOW64は32bitと64bit両方のPEB,TEBをもってます。それぞれ、PEB64,TEB64という構造体で、gs:30hはTEB64構造体を採ってきます。