一個 C/C++ 程式可以透過 main() 的 argument list 取得 client 端輸入的 command line arguments:
int main( int argc, char* argv[] ) { ... }
如果好奇這是如何地從無到有,可以寫一段程式碼來 trace 。
先準備好 sample code:
#include <iostream> using namespace std; int main( int argc, char* argv[] ) { // 程式的內容不是重點 cout << "hello world"; cin.get(); return 0; }
透過 VC++ 或是 WinDbg 在 main() 設定 breakpoint 來追蹤:
- Compiler: VC++ 2005
- OS: Windows XP
- 在 cmd 中執行 CrtDemo05 test
0:000> bp CrtDemo05!main *** WARNING: Unable to verify checksum for CrtDemo05.exe 0:000> bl 0 e 004377d0 0001 (0001) 0:**** CrtDemo05!main 0:000> g Breakpoint 0 hit eax=00383088 ebx=7ffda000 ecx=00383028 edx=00000001 esi=010df742 edi=010df6f2 eip=004377d0 esp=0012ff58 ebp=0012ffb8 iopl=0 nv up ei pl zr na pe nc cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246 CrtDemo05!main: 004377d0 55 push ebp 0:000> k ChildEBP RetAddr 0012ff54 0044a203 CrtDemo05!main [f:\src\_experiment\crtdemo05\crtdemo05\main.cpp @ 6] 0012ffb8 00449fbd CrtDemo05!__tmainCRTStartup+0x233 [f:\dd\vctools\crt_bld\self_x86\crt\src\crt0.c @ 327] 0012ffc0 7c816d4f CrtDemo05!mainCRTStartup+0xd [f:\dd\vctools\crt_bld\self_x86\crt\src\crt0.c @ 196] 0012fff0 00000000 kernel32!BaseProcessStart+0x23
進入 main() 之前的兩個函式都是 CRT 的一部分,負責完成基本但必要的初始化,舉凡 Global variables、Heap、I/O 等等都屬於這個範疇。這樣一來,身處 main() 後頭的我們才能順利工作。所以照這情勢看來,想知道 argument list 怎麼來,就得去 trace 這兩個 functions ,幸運的, VC++ CRT 的 source code 是隨著安裝程式散發的,通常就在:
C:\Program Files\Microsoft Visual Studio 8\VC\crt
視安裝路徑而定。也可以 double click VC++ 的 Call Stack 進入程式碼裡頭。
如果想利用 debugger 追蹤或試驗 CRT ,不妨把 CRT link 改成 debug mode 並且是 static link 。這可以省下一些時間、增加 tracing 時的可讀性和便利性,因為像是 debug 下的 dynamic link 會使用 ILT ,它所引入的間接性,會讓定位 functions 或 symbols 徒增額外的時間,造成實驗的不便。
VC++ 有所謂的 Microsoft C++ ,其程式進入點有自己的一套方式來定義,至少在名稱上,就可以找到四種:
- main
- wmain
- WinMain
- wWinMain
不過它們使用的 CRT 是同一份程式碼,並且使用了跟 TCHAR.h 相同的手法來區別,但確實由 WPRFLAG 這個 macro 所控制凡;凡是見到 t、_t、__t 開頭的名稱,都有機會透過它來替換掉,不過 __tmainCRTStartup() 可說是例外。下表節錄與 command line 相關的 t、_t、_t 開頭 symbols:
ansi/console |
ansi/GUI |
wide/console |
wide/GUI |
|
_tmainCRTStartup |
mainCRTStartup |
WinMainCRTStartup |
wmainCRTStartup |
wWinMainCRTStartup |
_tcmdln |
_acmdln |
_acmdln |
_wcmdln |
_wcmdln |
_targv |
__argv |
__argv |
__wargv |
__wargv |
GetCommandLineT() |
GetCommandLineA |
GetCommandLineA |
__crtGetCommandLineW |
__crtGetCommandLineW |
_tsetargv() |
_setargv |
_setargv |
_wsetargv |
_wsetargv |
我們先從 ansi 版的 main() ,也是 C/C++ Standard 所描述的 main() 開始:
mainCRTStartup()
mainCRTStartup() 基本上是一個 forward function :
int _tmainCRTStartup( void ) { __security_init_cookie(); return __tmainCRTStartup(); }
__tmainCRTStartup()
就像前面提到的:__tmainCRTStartup() 就是正港的 __tmainCRTStartup() 沒有 ansi, wide 的替換,很快地就可以梳理出跟 command line 相關的程式:
int __tmainCRTStartup( void ) { // ... __try { // ... /* get wide cmd line info */ _tcmdln = (_TSCHAR *)GetCommandLineT(); /* get wide environ info */ _tenvptr = (_TSCHAR *)GetEnvironmentStringsT(); if ( _tsetargv() < 0 ) _amsg_exit(_RT_SPACEARG); if ( _tsetenvp() < 0 ) _amsg_exit(_RT_SPACEENV); // ... #ifdef _WINMAIN_ lpszCommandLine = _twincmdln(); mainret = _tWinMain( (HINSTANCE)&__ImageBase, NULL, lpszCommandLine, StartupInfo.dwFlags & STARTF_USESHOWWINDOW ? StartupInfo.wShowWindow : SW_SHOWDEFAULT ); #else /* _WINMAIN_ */ _tinitenv = _tenviron; mainret = _tmain(__argc, _targv, _tenviron); #endif /* _WINMAIN_ */ // ... } __except ( _XcptFilter(GetExceptionCode(), GetExceptionInformation()) ) { // ... } return mainret; }
從上面的程式碼可以發現,重點就在於兩個 functions 上:
- GetCommandLineT()
- _tsetargv()
但是當我們將 breakpoint 設在 GetCommandLineT() 時,卻無法 step in 。這時可以切換到 assembly 模式,透過 WinDbg diassembly window 或是 WinDbg 的 uf 幫助來持續追蹤:
0:000> uf __tmainCRTStartup ... CrtDemo05!__tmainCRTStartup+0x1b4 [f:\dd\vctools\crt_bld\self_x86\crt\src\crt0.c @ 300]: 300 0044a184 ff1564924b00 call dword ptr [CrtDemo05!_imp__GetCommandLineA (004b9264)] 300 0044a18a a3a4814b00 mov dword ptr [CrtDemo05!_acmdln (004b81a4)],eax 303 0044a18f e8a89efeff call CrtDemo05!ILT+55(___crtGetEnvironmentStringsA) (0043403c) 303 0044a194 a3d45e4b00 mov dword ptr [CrtDemo05!_aenvptr (004b5ed4)],eax 305 0044a199 e87a9ffeff call CrtDemo05!ILT+275(__setargv) (00434118) 305 0044a19e 85c0 test eax,eax 305 0044a1a0 7d0a jge CrtDemo05!__tmainCRTStartup+0x1dc (0044a1ac) ...
在 Windows 裡頭, _imp、__imp 開頭的 name mangling 代表: symbol 代表是從 DLL export 出的 ,可見 CrtDemo05!_imp__GetCommandLineA 存在與另外一個 DLL 之中。 dereference 該位址便可以找到真正的 symbol 。
0:000> dd 004b9264 004b9264 7c812c8d 7c93043d 7c812851 7c9305d4 004b9274 7c80aa49 7c80a0c7 7c809cad 7c832e2b
由於 CrtDemo05 裡頭使用 call 來呼叫這個位址,想必這個位址會是一段可以執行的指令,使用 uf 來查找:
0:000> uf 7c812c8d kernel32!GetCommandLineA: 7c812c8d a1f435887c mov eax,dword ptr [kernel32!BaseAnsiCommandLine+0x4 (7c8835f4)] 7c812c92 c3 ret
原來是落在 kernel32.dll 這個基礎函式庫裡頭,而且發現它的實做相當簡單:僅僅是從 kernel32!BaseAnsiCommandLine+0x4 位址讀取而已。為了驗證,我們以 dereference 該位址看是否真的儲存了要傳遞給 CrtDemo05 的 arguments 。
0:000> dd 7c8835f4 7c8835f4 00151ee0 000a0008 7c80e300 ffff02ff 7c883604 00000001 000a0008 7c831874 000a0008
接著
0:000> da 00151ee0 00151ee0 "C:\test\CrtDemo05.exe test"
若是覺得 dd 兩層的 dereference 有點麻煩,針對字串可以使用 dp* ,
kd> dpa 7c8835f4 L1 7c8835f4 001522f8 "CrtDemo05.exe"
無論是哪種方式,都驗證了我們的想法── kernel32!GetCommandLineA() 是取得 command line 的函式,並且有著極簡單的實做──讀取一個預先填好值的 variable。
wmain() 版本也是相同的:
0:000> uf CrtDemo05!__tmainCRTStartup ... CrtDemo05!__tmainCRTStartup+0x1b4 [f:\dd\vctools\crt_bld\self_x86\crt\src\crt0.c @ 300]: 300 0044a194 e8ad9efeff call CrtDemo05!ILT+65(___crtGetCommandLineW) (00434046) 300 0044a199 a3e4824b00 mov dword ptr [CrtDemo05!_wcmdln (004b82e4)],eax 303 0044a19e e8439ffeff call CrtDemo05!ILT+225(___crtGetEnvironmentStringsW) (004340e6) 303 0044a1a3 a3d85e4b00 mov dword ptr [CrtDemo05!_wenvptr (004b5ed8)],eax 305 0044a1a8 e845b1feff call CrtDemo05!ILT+4845(__wsetargv) (004352f2) 305 0044a1ad 85c0 test eax,eax 305 0044a1af 7d0a jge CrtDemo05!__tmainCRTStartup+0x1db (0044a1bb) ...
函式,你可能會好奇為什麼 wide 版本並不是呼叫 _imp__GetCommandLineW() ,其實它被包裝在 ___crtGetCommandLineW() 裡頭。
0:000> uf 00434046 ... CrtDemo05!__crtGetCommandLineW+0xf [f:\dd\vctools\crt_bld\self_x86\crt\src\aw_com.c @ 52]: 52 0046336f ff1530934b00 call dword ptr [CrtDemo05!_imp__GetCommandLineW (004b9330)] 52 00463375 85c0 test eax,eax 52 00463377 740c je CrtDemo05!__crtGetCommandLineW+0x25 (00463385) ...
並且同樣地,可以使用如同 ansi 版本的方式,去 trace :
0:000> dd 004b9330 004b9330 7c816cfb 7c80c6cf 7c801eee 7c873d83 004b9340 7c81abe4 7c80180e 7c810da6 7c80cd58 0:000> uf 7c816cfb kernel32!GetCommandLineW: 7c816cfb a10430887c mov eax,dword ptr [kernel32!BaseUnicodeCommandLine+0x4 (7c883004)] 7c816d00 c3 ret 0:000> dd 7c883004 7c883004 0002062c 00000000 7c809784 7c80979d 7c883014 7c8097d2 7c8097e7 7c8097b7 00000000 0:000> du 0002062c 0002062c "C:\test\CrtDemo05.exe test"
使用 Kernel Debugging
或許你會好奇為什麼 trace command 如何填入一個 process 需要動要到 kernel debug ?其實原因很簡單,因為當 user-mode debug 無法滿足時,以這次的參數傳遞來看,當我們利用 windbg 載入一個 program 時,program 需要執行到某個狀態後, user-mode debugger 才能介入,此時 arguments 已經設置完成。
Microsoft (R) Windows Debugger Version 6.11.0001.404 X86 Copyright (c) Microsoft Corporation. All rights reserved. CommandLine: C:\test\CrtDemo05.exe Symbol search path is: srv*C:\sym_cache*http://msdl.microsoft.com/download/symbols;C:\test Executable search path is: ModLoad: 00400000 004bb000 CrtDemo05.exe ModLoad: 7c920000 7c9b5000 ntdll.dll ModLoad: 7c800000 7c91d000 C:\WINDOWS\system32\kernel32.dll (7d8.568): Break instruction exception - code 80000003 (first chance) eax=00251eb4 ebx=7ffd8000 ecx=00000001 edx=00000002 esi=00251f48 edi=00251eb4 eip=7c921230 esp=0012fb20 ebp=0012fc94 iopl=0 nv up ei pl nz na po nc cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000202 ntdll!DbgBreakPoint: 7c921230 cc int 3 0:000> kvn # ChildEBP RetAddr Args to Child 00 0012fb1c 7c95edc0 7ffdf000 7ffd8000 00000000 ntdll!DbgBreakPoint (FPO: [0,0,0]) 01 0012fc94 7c941639 0012fd30 7c920000 0012fce0 ntdll!LdrpInitializeProcess+0xffa (FPO: [5,89,4]) 02 0012fd1c 7c92eac7 0012fd30 7c920000 00000000 ntdll!_LdrpInitialize+0x183 (FPO: [Non-Fpo]) 03 00000000 00000000 00000000 00000000 00000000 ntdll!KiUserApcDispatcher+0x7 0:000> da 00151ee0 00151ee0 "C:\test\CrtDemo05.exe test"
注意到沒?當 CrtDemo05 整支程式還在被 OS 的 loader 所初始化時,在 __tmainCRTStartup 還未執行到之前,arguments 已經填入了。因此這個例子需要透過 kernel debugging 來進行更早期的追蹤。 kernel debugging 的方式有許多種,使用 VMWare 算是便利的方法之一,可以參考 Windows Debugging – Kernel Debugging with WinDbg and VMWare 設定環境。
Catch Me If You Can
為了捕捉 kernel32!BaseUnicodeCommanLine 以及 kernel32!BaseAnsiCommandLine 是如何被填寫的?我們可以透過 WinDbg 的 break on access ,不過很快就會遇到第一個問題:
kd> x kernel32!BaseUnicodeCommandLine 7c885730 kernel32!BaseUnicodeCommandLine = <no type information> 7c883000 kernel32!BaseUnicodeCommandLine = <no type information>
遇到兩個同名的 symbols ,理論上這是件會造成 symbol resolve 上 ambiguous 的事。雖然暫時無解,但幸運的是,根據之前追蹤結果:
7c816cfb a10430887c mov eax,dword ptr [kernel32!BaseUnicodeCommandLine+0x4 (7c883004)]
我們可以直接選擇 7c885730 :
kd> ba r4 7c883004 kd> g Breakpoint 0 hit nt!MiCopyOnWrite+0x148: 0008:805222bc f3a5 rep movs dword ptr es:[edi],dword ptr [esi] kd> k ChildEBP RetAddr b22bad00 8051d019 nt!MiCopyOnWrite+0x148 b22bad4c 805406ec nt!MmAccessFault+0x9f9 b22bad4c 7c93a100 nt!KiTrap0E+0xcc 0012f938 7c93d8a0 ntdll!__security_init_cookie_ex+0x31 0012f944 7c93d83a ntdll!LdrpInitSecurityCookie+0x2f 0012fa40 7c939b78 ntdll!LdrpRunInitializeRoutines+0x124 0012faf0 7c939ba0 ntdll!LdrpGetProcedureAddress+0x1c6 0012fb0c 7c942334 ntdll!LdrGetProcedureAddress+0x18 0012fc94 7c941639 ntdll!LdrpInitializeProcess+0xd7a 0012fd1c 7c92eac7 ntdll!_LdrpInitialize+0x183 00000000 00000000 ntdll!KiUserApcDispatcher+0x7
這一次還不是我們想要的 stack:
kd> g Breakpoint 0 hit kernel32!_BaseDllInitialize+0x20f: 7c817ea8 ff157c10807c call dword ptr [kernel32!_imp__RtlUnicodeStringToAnsiString (7c80107c)] kd> bl 0 e 7c883004 r 4 0001 (0001) kernel32!BaseUnicodeCommandLine+0x4 kd> k ChildEBP RetAddr 0012f918 7c9211a7 kernel32!_BaseDllInitialize+0x20f 0012f938 7c93cbab ntdll!LdrpCallInitRoutine+0x14 0012fa40 7c939b78 ntdll!LdrpRunInitializeRoutines+0x344 0012faf0 7c939ba0 ntdll!LdrpGetProcedureAddress+0x1c6 0012fb0c 7c942334 ntdll!LdrGetProcedureAddress+0x18 0012fc94 7c941639 ntdll!LdrpInitializeProcess+0xd7a 0012fd1c 7c92eac7 ntdll!_LdrpInitialize+0x183 00000000 00000000 ntdll!KiUserApcDispatcher+0x7
這次 stack 看起來成功率比較高。 disassembly 一段 breakpoint 前的程式碼,使用 WinDbg 的 disassembly 視窗或是指令都可以做到:
kd> ub 7c817ea8 L10 kernel32!_BaseDllInitialize+0x1cc: ... 7c817e7f 64a118000000 mov eax,dword ptr fs:[00000018h] 7c817e85 8b4030 mov eax,dword ptr [eax+30h] 7c817e88 8b4010 mov eax,dword ptr [eax+10h] 7c817e8b 8b4840 mov ecx,dword ptr [eax+40h] 7c817e8e 6a01 push 1 7c817e90 890d0030887c mov dword ptr [kernel32!BaseUnicodeCommandLine (7c883000)],ecx 7c817e96 8b4044 mov eax,dword ptr [eax+44h] 7c817e99 680030887c push offset kernel32!BaseUnicodeCommandLine (7c883000) 7c817e9e 68f035887c push offset kernel32!BaseAnsiCommandLine (7c8835f0) 7c817ea3 a30430887c mov dword ptr [kernel32!BaseUnicodeCommandLine+0x4 (7c883004)],eax
解讀 fs:[00000018h]
可以發現,kernel32!BaseUnicodeCommandLine 是由 ecx 決定,而 ecx 則是由 fs:[00000018h] 所決定,這部份的解讀,需要一些的隱藏知識:在 x86 上Windows user mode 將每個 thread 的 TEB (Thread Environment Block),存放在 fs:[0];而在 kernel mode 中, fs:[0] 則是存放 KPCR (Process Control Region) 。
kd> dt _TEB ntdll!_TEB +0x000 NtTib : _NT_TIB +0x01c EnvironmentPointer : Ptr32 Void +0x020 ClientId : _CLIENT_ID +0x028 ActiveRpcHandle : Ptr32 Void +0x02c ThreadLocalStoragePointer : Ptr32 Void +0x030 ProcessEnvironmentBlock : Ptr32 _PEB ...
整個 ntdll!_TEB 有點龐大,不過根據 ub 的結果,fs:[00000018h] 落在第一個欄位 ── _NT_TIB (Thread Information Block)裡頭,再進行一次 dt :
kd> dt _NT_TIB ntdll!_NT_TIB +0x000 ExceptionList : Ptr32 _EXCEPTION_REGISTRATION_RECORD +0x004 StackBase : Ptr32 Void +0x008 StackLimit : Ptr32 Void +0x00c SubSystemTib : Ptr32 Void +0x010 FiberData : Ptr32 Void +0x010 Version : Uint4B +0x014 ArbitraryUserPointer : Ptr32 Void +0x018 Self : Ptr32 _NT_TIB
那為何不直接使用 fs:[0] 而要選擇 fs:[00000018h] ?因為 fs:[0] 並非 process 的 virtual address :
kd> dd fs:0 003b:00000000 0012fa30 00130000 0012f000 00000000 003b:00000010 00001e00 00000000 7ffdd000 00000000 003b:00000020 00000778 00000128 00000000 00000000
因此使用上,往往透過 fs:[00000018h] 來指向正確 TEB 或說 TIB 的位址。
重新解讀 ub
7c817e7f 64a118000000 mov eax,dword ptr fs:[00000018h] 7c817e85 8b4030 mov eax,dword ptr [eax+30h] 7c817e88 8b4010 mov eax,dword ptr [eax+10h] 7c817e8b 8b4840 mov ecx,dword ptr [eax+40h]
上述組語在知道 fs:[0] 的意義後,便可尋著指令進行 dt 的解碼,並寫出對應的虛擬碼:
_TEB teb = fs:[00000018h]; UNICODE_STRING kernel32!BaseUnicodeCommandLine = teb.ProcessEnvironmentBlock->ProcessParameters.CommandLine;
到了這個步驟,我們已經知道即使是 kernel32!_BaseDllInitialize() 這般低階的初始化,也僅僅只是將 PEB 中的某個變數值讀出,那麼 PEB 中的值又是給負責填寫呢?還記得 Win32 API 中的 CreateProcess() 嗎?它或許是有嫌疑的一份子。
CreateProcess()
kd> dt _RTL_USER_PROCESS_PARAMETERS ntdll!_RTL_USER_PROCESS_PARAMETERS +0x000 MaximumLength : Uint4B +0x004 Length : Uint4B +0x008 Flags : Uint4B +0x00c DebugFlags : Uint4B +0x010 ConsoleHandle : Ptr32 Void +0x014 ConsoleFlags : Uint4B +0x018 StandardInput : Ptr32 Void +0x01c StandardOutput : Ptr32 Void +0x020 StandardError : Ptr32 Void +0x024 CurrentDirectory : _CURDIR +0x030 DllPath : _UNICODE_STRING +0x038 ImagePathName : _UNICODE_STRING +0x040 CommandLine : _UNICODE_STRING +0x048 Environment : Ptr32 Void +0x04c StartingX : Uint4B +0x050 StartingY : Uint4B +0x054 CountX : Uint4B +0x058 CountY : Uint4B +0x05c CountCharsX : Uint4B +0x060 CountCharsY : Uint4B +0x064 FillAttribute : Uint4B +0x068 WindowFlags : Uint4B +0x06c ShowWindowFlags : Uint4B +0x070 WindowTitle : _UNICODE_STRING +0x078 DesktopInfo : _UNICODE_STRING +0x080 ShellInfo : _UNICODE_STRING +0x088 RuntimeData : _UNICODE_STRING +0x090 CurrentDirectores : [32] _RTL_DRIVE_LETTER_CURDIR
目前我們已經知道,一個程式的 command line 儲存在 _RTL_USER_PROCESS_PARAMETERS 這個 strcut 裡頭,所以要做的是,找到何時會去填寫,方法有兩種:
- Trace CreateProcess
- 尋找有關 ProcessParameter 的 function 。
我們使用方法 2. 來加速:
可以使用 WinDbg 的 x 指令:
kd> x *!*processparameter* 7c819f9e kernel32!BasePushProcessParameters = <no type information> 7c801488 kernel32!_imp__RtlCreateProcessParameters = <no type information> 7c801484 kernel32!_imp__RtlDestroyProcessParameters = <no type information> 7c9432ec ntdll!RtlDestroyProcessParameters = <no type information> 7c950ea3 ntdll!RtlCheckProcessParameters = <no type information> 7c9433c1 ntdll!RtlCreateProcessParameters = <no type information>
這幾個 functions 看起來都有機會,其中以 BasePushProcessParameters() 的名稱看來最有可能是 creater 建立 createe process parameter 的函式,為了驗證這想法,將 WinDbg 目前的 context 切換到 cmd.exe 下,並設定 breakpoint 來驗證:
kd> !process 0 0 **** NT ACTIVE PROCESS DUMP **** ... PROCESS 8200d7e8 SessionId: 0 Cid: 0194 Peb: 7ffdf000 ParentCid: 0608 DirBase: 08940240 ObjectTable: e1533828 HandleCount: 33. Image: cmd.exe kd> .process /r /P /p 8200d7e8 kd> bp 7c819f9e kd> g Breakpoint 1 hit kernel32!BasePushProcessParameters: 7c819f9e 68c0020000 push 2C0h kd> k ChildEBP RetAddr 0013f028 7c8199bc kernel32!BasePushProcessParameters 0013fa88 7c80235e kernel32!CreateProcessInternalW+0x184e 0013fac0 4ad031dd kernel32!CreateProcessW+0x2c 0013fc20 4ad02db0 cmd!ExecPgm+0x22b 0013fc54 4ad02e0e cmd!ECWork+0x84 0013fc6c 4ad05f9f cmd!ExtCom+0x40 0013fe9c 4ad013eb cmd!FindFixAndRun+0xcf 0013fee0 4ad0bbba cmd!Dispatch+0x137 0013ff44 4ad05164 cmd!main+0x216 0013ffc0 7c816d4f cmd!mainCRTStartup+0x125 0013fff0 00000000 kernel32!BaseProcessStart+0x23
stack 目前看來是支持我們的猜測, cmd.exe 透過 CreateProcess() 來建立 CrtDemo05.exe ,並且呼叫 BasePushProcessParameters() 來初始化 process parameter。在這邊,我們也可以切換到 CrtDemo05.exe 下,確定一下它目前的建立狀態,以瞭解它仍是否仍在建立中而非建立完成:
kd> !process 0 0 **** NT ACTIVE PROCESS DUMP **** ... PROCESS 8200d7e8 SessionId: 0 Cid: 0194 Peb: 7ffdf000 ParentCid: 0608 DirBase: 08940240 ObjectTable: e1533828 HandleCount: 35. Image: cmd.exe PROCESS 82476d00 SessionId: 0 Cid: 00c4 Peb: 7ffd4000 ParentCid: 0194 DirBase: 089402c0 ObjectTable: e1d3bf68 HandleCount: 36. Image: conime.exe PROCESS 8244e980 SessionId: 0 Cid: 05b4 Peb: 7ffdc000 ParentCid: 0400 DirBase: 08940300 ObjectTable: e16e0700 HandleCount: 123. Image: wuauclt.exe PROCESS 8244a460 SessionId: 0 Cid: 0224 Peb: 7ffd5000 ParentCid: 0194 DirBase: 08940380 ObjectTable: e17871a0 HandleCount: 1. Image: CrtDemo0
看到 Image name 被截斷,有點訝異,不過可能是因為 kernel 在填寫時被我們中斷了。現在來看看 PEB 裡頭的 command line 是否已經準備好了:
kd> !peb 7ffd5000 PEB at 7ffd5000 error 1 InitTypeRead( nt!_PEB at 7ffd5000)... kd> .process /r /P /p 8244a460 Implicit process is now 8244a460 .cache forcedecodeuser done Loading User Symbols PEB is paged out (Peb.Ldr = 7ffd500c). Type ".hh dbgerr001" for details kd> dt _PEB 7ffd5000 nt!_PEB +0x000 InheritedAddressSpace : 0 '' +0x001 ReadImageFileExecOptions : 0 '' +0x002 BeingDebugged : 0 '' +0x003 SpareBool : 0 '' +0x004 Mutant : 0xffffffff Void +0x008 ImageBaseAddress : 0x00400000 Void +0x00c Ldr : (null) +0x010 ProcessParameters : (null) +0x014 SubSystemData : (null) +0x018 ProcessHeap : (null) +0x01c FastPebLock : (null) ...
試了兩種方法,都無法正確讀取 PEB ,這正和我們的預期:表示 CrtDemo05.exe 正在被建立中,其 PEB 似乎也在混沌之中。緊接著回到 cmd.exe 的 stack 上,接下來的事就需要點耐心,由於目前只知道 BasePushProcessParameters() 跟 _RTL_USER_PROCESS_PARAMETERS 有關,但是找不到可以設立 breakpoint 的地方,根據 assembly 一步步追蹤是個方法,不過直接閱讀 assembly 來找尋跟 ProcessParameters 相關的指令或許會更快,一個很快速的想法是使用關鍵字來縮小範圍,這裡若是使用 uf 來 diassembly ,可能會遇上一些麻煩,因為 CreateProcessInternalW() 被 OMAP 最佳化過,無法依照 mapping 到記憶體上的 layout 顯示:
kd> uf CreateProcessInternalW Flow analysis was incomplete, some code may be missing kernel32!CreateProcessInternalW+0x308: ... ...
當遇到這種狀況,我們得放棄 diassembly 整個 function ,而是直接指明記憶體位址:
kd> x kernel32!CreateProcessInternalW 7c8191eb kernel32!CreateProcessInternalW =kd> u 7c8191eb L600 kernel32!CreateProcessInternalW: ... ; ProcessParameter ; ebp-234h: _RTL_USER_PROCESS_PARAMETERS* 7c81a11d 8d85ccfdffff lea eax,[ebp-234h] 7c81a123 50 push eax ; pProcessParameters 7c81a124 ff158814807c call dword ptr [kernel32!_imp__RtlCreateProcessParameters (7c801488)] 7c81a12a 33d2 xor edx,edx ; edx = 0 7c81a12c 3bc2 cmp eax,edx ... ... 7c81a538 ff158414807c call dword ptr [kernel32!_imp__RtlDestroyProcessParameters (7c801484)]
幸運的找到:
7c9432ec ntdll!RtlDestroyProcessParameters
7c9433c1 ntdll!RtlCreateProcessParameters
利用兩個 functions ,我們可以縮小可疑範圍,7c81a11d ~ 7c81a538 給了我們一個暗示:ebp-234h 是個 local variable ,並且傳遞至 RtlCreateProcessParameters() ,當結束後進行是否為 NULL 的比較,那麼 ebp-234h 基本上就很有機會就是一個 pointer point to _RTL_USER_PROCESS_PARAMETERS。另外在向上回溯,可以找找看是否還有其他參數被傳遞至 RtlCreateProcessParameters() :
7c81a0d7 ffd6 call esi 7c81a0d9 8d85b4fdffff lea eax,[ebp-24Ch] 7c81a0df 50 push eax ; parameter10 7c81a0e0 8d859cfdffff lea eax,[ebp-264h] 7c81a0e6 50 push eax ; parameter9 7c81a0e7 8d85a4fdffff lea eax,[ebp-25Ch] 7c81a0ed 50 push eax ; parameter8 7c81a0ee 8d8594fdffff lea eax,[ebp-26Ch] 7c81a0f4 50 push eaxe ; parameter7 7c81a0f5 ffb570fdffff push dword ptr [ebp-290h] ; parameter6 7c81a0fb 8d8584fdffff lea eax,[ebp-27Ch] 7c81a101 50 push eax ; parameter5 7c81a102 f7db neg ebx 7c81a104 1bdb sbb ebx,ebx 7c81a106 8d8578fdffff lea eax,[ebp-288h] 7c81a10c 23d8 and ebx,eax 7c81a10e 53 push ebx ; parameter4 7c81a10f 8d85acfdffff lea eax,[ebp-254h] 7c81a115 50 push eax ; parameter3 7c81a116 8d858cfdffff lea eax,[ebp-274h] 7c81a11c 50 push eax ; parameter2 7c81a11d 8d85ccfdffff lea eax,[ebp-234h] 7c81a123 50 push eax ; parameter1 7c81a124 ff158814807c call dword ptr [kernel32!_imp__RtlCreateProcessParameters (7c801488)]
看到了 10 個參數被傳遞至 RtlCreateProcessParameters() ,我們可以選擇 diassembly 它看是否有與 command line 相關的操作,或是繼續搜尋其他 7c81a11d ~ 7c81a538 內 ebp-234h 的操作,這裡有一個或許可行的快速篩選法,因為 CommandLine 位於 _RTL_USER_PROCESS_PARAMETERS offset 40 bytes 的地方:
kd> dt _RTL_USER_PROCESS_PARAMETERS ntdll!_RTL_USER_PROCESS_PARAMETERS ... +0x040 CommandLine : _UNICODE_STRING ...
我們便找尋 40h 的關鍵字,而搜尋結果的確有幾筆與 40h 有關的操作,但幸運地,都不是和 CommandLine 有關的。於是乎,把焦點放回 RtlCreateProcessParameters():
kd> x ntdll!RtlCreateProcessParameters 7c9433c1 ntdll!RtlCreateProcessParameters kd> u 7c9433c1 L200 ntdll!RtlCreateProcessParameters: ... 7c9435e1 6a04 push 4 ; Protect 7c9435e3 6800100000 push 1000h ; AllocationType 7c9435e8 8d45cc lea eax,[ebp-34h] 7c9435eb 50 push eax ; RegionSize 7c9435ec 53 push ebx ; ZeroBits 7c9435ed 8d45e4 lea eax,[ebp-1Ch] 7c9435f0 50 push eax ; BaseAddress 7c9435f1 6aff push 0FFFFFFFFh ; ProcessHandle 7c9435f3 e8e69efeff call ntdll!ZwAllocateVirtualMemory (7c92d4de) ...
由於 ZwAllocateVirtualMemory() 是公開的 API ,所以很快地可以對參數進行 mapping ,馬上就發現 local vaiable ebp-1ch 存放的就是 allocate 的結果,並且在稍後就有一個 +40h 的操作:
kd> u 7c9433c1 L200 ntdll!RtlCreateProcessParameters: ... 7c94369a 40 inc eax 7c94369b 40 inc eax 7c94369c 50 push eax 7c94369d 8b45e4 mov eax,dword ptr [ebp-1Ch] ; pBase 7c9436a0 83c040 add eax,40h ; pBase + 40 = CommandLine 7c9436a3 57 push edi 7c9436a4 50 push eax 7c9436a5 8d45d8 lea eax,[ebp-28h] 7c9436a8 50 push eax 7c9436a9 e8ab000000 call ntdll!RtlpCopyProcString (7c943759) ...
這和 CommandLine 的操作看似吻合,但我們仍需要找到更直接的證據,首先可以使用動態的證據,也就是設定 breakpoint 去證明,我們將 breakpoint 先設定在 add eax,40h:
kd> bp 7c9436a0 kd> g Breakpoint 2 hit ntdll!RtlCreateProcessParameters+0x2f5: 7c9436a0 83c040 add eax,40h kd> dd ebp-1c 0013ed00 00380000 0013ecd4 0013edf8 0013f018 0013ed10 7c92ee18 7c943748 00000000 0013f028 kd> dd 00380040 00380040 00000000 00000000 00010000 00000000 00380050 00000000 00000000 00000000 00000000
接著,執行數個指令後,等到 RtlpCopyProcString() 執行完成後:
kd> dd 00380040 00380040 00260024 003805c0 00010000 00000000 00380050 00000000 00000000 00000000 00000000 kd> du 003805c0 003805c0 "CrtDemo05.exe test"
再者,我們可以來個靜態分析:
kd> u 7c9433c1 L200 ntdll!RtlCreateProcessParameters: ... 7c94370b 8b45e4 mov eax,dword ptr [ebp-1Ch] 7c94370e 6689988a000000 mov word ptr [eax+8Ah],bx 7c943715 ff75e4 push dword ptr [ebp-1Ch] 7c943718 e8f6fbffff call ntdll!RtlDeNormalizeProcessParams (7c943313) 7c94371d 8b4d08 mov ecx,dword ptr [ebp+8] 7c943720 8901 mov dword ptr [ecx],eax ...
很快地就有一個可疑的證物: ebp+8 也就是 RtlCreateProcessParameters() 的第一個參數,它被 eax 給賦值。雖然 eax 在第一行被指定為 ebp-1Ch ,看似符合我們的假設,但因為中間有個 RtlDeNormalizeProcessParams() 的呼叫,不能掉以輕心,必須進去瞧瞧:
kd> uf RtlDeNormalizeProcessParams Flow analysis was incomplete, some code may be missing ntdll!RtlDeNormalizeProcessParams: 7c943313 8bff mov edi,edi 7c943315 55 push ebp 7c943316 8bec mov ebp,esp 7c943318 8b4508 mov eax,dword ptr [ebp+8] 7c94331b 85c0 test eax,eax 7c94331d 7478 je ntdll!RtlDeNormalizeProcessParams+0x84 (7c943397) ...
幸運的, RtlDeNormalizeProcessParams() 沒有可觀的實作,也僅在 function 開頭將 eax 給予 ebp+8 所代表的位址;而 ebp+8 也正就是 ebp-1Ch,其餘的對應操作都是 read 。確定了 RtlDeNormalizeProcessParams() 不會更改 ebp-1Ch 後,就可以放心回到 RtlCreateProcessParameters():
kd> u 7c9433c1 L200 ntdll!RtlCreateProcessParameters: ... 7c94345b 8b7d18 mov edi,dword ptr [ebp+18h] ... 7c943685 e8cf000000 call ntdll!RtlpCopyProcString (7c943759) 7c94368a 668b07 mov ax,word ptr [edi] 7c94368d 663b4702 cmp ax,word ptr [edi+2] 7c943691 0f84f79a0200 je ntdll!RtlCreateProcessParameters+0x2e9 (7c96d18e) 7c943697 0fb7c0 movzx eax,ax 7c94369a 40 inc eax 7c94369b 40 inc eax 7c94369c 50 push eax 7c94369d 8b45e4 mov eax,dword ptr [ebp-1Ch] ; pBase 7c9436a0 83c040 add eax,40h ; pBase + 40 = CommandLine 7c9436a3 57 push edi 7c9436a4 50 push eax 7c9436a5 8d45d8 lea eax,[ebp-28h] 7c9436a8 50 push eax 7c9436a9 e8ab000000 call ntdll!RtlpCopyProcString (7c943759)
可以發現,可疑的參數有:
- 7c94369a inc eax
7c94369b inc eax
7c94369c push eax - 7c94345b mov edi,dword ptr [ebp+18h]
7c9436a3 push edi - 7c9436a5 lea eax,[ebp-28h]
7c9436a8 push eax
為了找出實際的賦值給 ebp-1Ch+40h 的對象,這邊可以使用 breakpoint 去檢驗,分別是:
kd> bp 7c94369c kd> g Breakpoint 5 hit ntdll!RtlCreateProcessParameters+0x2f1: 7c94369c 50 push eax kd> r eax=00000026 ebx=00000000 ecx=00380034 edx=00000000 esi=00000208 edi=0013edac eip=7c94369c esp=0013ecd4 ebp=0013ed1c iopl=0 nv up ei pl nz na po cy cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000203 kd> dpu 0013edac+4 L1 0013edb0 0015adc8 "CrtDemo05.exe test" kd> bp 7c9436a8 kd> g Breakpoint 6 hit kd> dd ebp-28h 0013ecf4 003805c0 00000000 00000634 00380000 0013ed04 0013ecd4 0013edf8 0013f018 7c92ee18 kd> dpu 003805c0 003805c0 00000000
透過 breakpoint 的實驗,就可以知道 edi 才是賦值給 ebp-1Ch+40h 的來源,而且還是 RtlCreateProcessParameters() 的第五個參數。所以可以反推 edi 是怎麼來的,並一路反推,便可得到 command line 最初的來源。
kd> uf kernel32!CreateProcessW kernel32!CreateProcessW: 7c802332 8bff mov edi,edi 7c802334 55 push ebp 7c802335 8bec mov ebp,esp 7c802337 6a00 push 0 ; parameter12 7c802339 ff752c push dword ptr [ebp+2Ch] ; lpProcessInformation 7c80233c ff7528 push dword ptr [ebp+28h] ; lpStartupInfo 7c80233f ff7524 push dword ptr [ebp+24h] ; lpCurrentDirectory 7c802342 ff7520 push dword ptr [ebp+20h] ; lpEnvironment 7c802345 ff751c push dword ptr [ebp+1Ch] ; dwCreationFlags 7c802348 ff7518 push dword ptr [ebp+18h] ; bInheritHandles 7c80234b ff7514 push dword ptr [ebp+14h] ; lpThreadAttributes 7c80234e ff7510 push dword ptr [ebp+10h] ; lpProcessAttributes 7c802351 ff750c push dword ptr [ebp+0Ch] ; lpCommandLine 7c802354 ff7508 push dword ptr [ebp+8] ; lpApplicationName 7c802357 6a00 push 0 ; parameter1 7c802359 e88d6e0100 call kernel32!CreateProcessInternalW (7c8191eb) 7c80235e 5d pop ebp 7c80235f c22800 ret 28h kd> x kernel32!CreateProcessInternalW 7c8191eb kernel32!CreateProcessInternalW = <no type information> kd> u 7c8191eb L600 kernel32!CreateProcessInternalW: ... 7c819214 8b4510 mov eax,dword ptr [ebp+10h] ; lpCommandLine 7c819217 8985e0f8ffff mov dword ptr [ebp-720h],eax ... 7c819958 8b8de0f8ffff mov ecx,dword ptr [ebp-720h] ; lpCommandLine ... 7c819964 ffb56cf9ffff push dword ptr [ebp-694h] ; parameter13 7c81996a ffb500f7ffff push dword ptr [ebp-900h] ; parameter12 7c819970 8a85b7f8ffff mov al,byte ptr [ebp-749h] 7c819976 f6d8 neg al 7c819978 1bc0 sbb eax,eax 7c81997a 83e002 and eax,2 7c81997d 50 push eax ; parameter11 7c81997e ff751c push dword ptr [ebp+1Ch] ; parameter10 7c819981 8b85f4f6ffff mov eax,dword ptr [ebp-90Ch] 7c819987 0b4520 or eax,dword ptr [ebp+20h] 7c81998a 50 push eax ; parameter9 7c81998b 8d8560f7ffff lea eax,[ebp-8A0h] 7c819991 50 push eax ; parameter8 7c819992 ffb5b0f8ffff push dword ptr [ebp-750h] ; parameter7 7c819998 51 push ecx ; parameter6 7c819999 ffb550f7ffff push dword ptr [ebp-8B0h] ; parameter5 7c81999f ffb5e4f8ffff push dword ptr [ebp-71Ch] ; parameter4 7c8199a5 ffb5b8f7ffff push dword ptr [ebp-848h] ; parameter3 7c8199ab ffb594f9ffff push dword ptr [ebp-66Ch] ; parameter2 7c8199b1 ffb534f7ffff push dword ptr [ebp-8CCh] ; parameter1 7c8199b7 e8e2050000 call kernel32!BasePushProcessParameters (7c819f9e) kernel32!BasePushProcessParameters: ... 7c819fd3 8b4d1c mov ecx,dword ptr [ebp+1Ch] ; 7c819fd6 898d5cfdffff mov dword ptr [ebp-2A4h],ecx ; srcCmdLine ... 7c81a045 8d85d0fdffff lea eax,[ebp-230h] 7c81a04b 50 push eax 7c81a04c 8d858cfdffff lea eax,[ebp-274h] 7c81a052 50 push eax 7c81a053 ffd6 call esi 7c81a055 ffb55cfdffff push dword ptr [ebp-2A4h] ; srcCmdLine 7c81a05b 8d8584fdffff lea eax,[ebp-27Ch] 7c81a061 50 push eax ; commandLine 7c81a062 ffd6 call esi ; RtlInitUnicodeString ... 7c81a0cf 50 push eax 7c81a0d0 8d8594fdffff lea eax,[ebp-26Ch] 7c81a0d6 50 push eax 7c81a0d7 ffd6 call esi 7c81a0d9 8d85b4fdffff lea eax,[ebp-24Ch] 7c81a0df 50 push eax ; runtimeData 7c81a0e0 8d859cfdffff lea eax,[ebp-264h] 7c81a0e6 50 push eax ; shellInfo 7c81a0e7 8d85a4fdffff lea eax,[ebp-25Ch] 7c81a0ed 50 push eax ; desktopInfo 7c81a0ee 8d8594fdffff lea eax,[ebp-26Ch] 7c81a0f4 50 push eax ; windowTitle 7c81a0f5 ffb570fdffff push dword ptr [ebp-290h] ; environment 7c81a0fb 8d8584fdffff lea eax,[ebp-27Ch] 7c81a101 50 push eax ; commandLine, parameter5 7c81a102 f7db neg ebx 7c81a104 1bdb sbb ebx,ebx 7c81a106 8d8578fdffff lea eax,[ebp-288h] 7c81a10c 23d8 and ebx,eax 7c81a10e 53 push ebx ; currentDir 7c81a10f 8d85acfdffff lea eax,[ebp-254h] 7c81a115 50 push eax ; dllPath 7c81a116 8d858cfdffff lea eax,[ebp-274h] 7c81a11c 50 push eax ; imagePath ; ProcessParameter ; ebp-234h: _RTL_USER_PROCESS_PARAMETERS* 7c81a11d 8d85ccfdffff lea eax,[ebp-234h] 7c81a123 50 push eax ; pProcessParameters 7c81a124 ff158814807c call dword ptr [kernel32!_imp__RtlCreateProcessParameters (7c801488)]
最後,我們還沒來得及關心 creater 是如何的把 createe 的 PEB 建立完成,其實就在 RtlCreateProcessParameters() 稍後,透過 NtWriteVirtualMemory() 完成。
7c81a1eb 8b35f413807c mov esi,dword ptr [kernel32!_imp__NtWriteVirtualMemory (7c8013f4)] ... 7c81a38b 6a04 push 4 ; Protect 7c81a38d bb00100000 mov ebx,1000h 7c81a392 53 push ebx ; AllocationtType 7c81a393 8d85c8fdffff lea eax,[ebp-238h] 7c81a399 50 push eax ; RegionSize 7c81a39a 52 push edx ; ZeroBits 7c81a39b 8d85c4fdffff lea eax,[ebp-23Ch] 7c81a3a1 50 push eax ; BaseAddress 7c81a3a2 ffb580fdffff push dword ptr [ebp-280h] ; hNewProcessHandle 7c81a3a8 8b3d9011807c mov edi,dword ptr [kernel32!_imp__NtAllocateVirtualMemory (7c801190)] 7c81a3ae ffd7 call edi 7c81a3b0 898574fdffff mov dword ptr [ebp-28Ch],eax 7c81a3b6 8b85c8fdffff mov eax,dword ptr [ebp-238h] 7c81a3bc 898558fdffff mov dword ptr [ebp-2A8h],eax 7c81a3c2 83bd74fdffff00 cmp dword ptr [ebp-28Ch],0 7c81a3c9 0f8c04950200 jl kernel32!BasePushProcessParameters+0x504 (7c8438d3) 7c81a3cf 8b8dccfdffff mov ecx,dword ptr [ebp-234h] 7c81a3d5 8901 mov dword ptr [ecx],eax 7c81a3d7 f6452b10 test byte ptr [ebp+2Bh],10h 7c81a3db 0f85fa940200 jne kernel32!BasePushProcessParameters+0x51d (7c8438db) 7c81a3e1 f6452b20 test byte ptr [ebp+2Bh],20h 7c81a3e5 0f85ff940200 jne kernel32!BasePushProcessParameters+0x52d (7c8438ea) 7c81a3eb f6452b40 test byte ptr [ebp+2Bh],40h 7c81a3ef 0f8504950200 jne kernel32!BasePushProcessParameters+0x53d (7c8438f9) 7c81a3f5 6a00 push 0 ; nBytesWritten 7c81a3f7 8b85ccfdffff mov eax,dword ptr [ebp-234h] 7c81a3fd ff7004 push dword ptr [eax+4] ; nBytesToWrite : pProcParameters->Length 7c81a400 50 push eax ; Buffer : pProcParameters 7c81a401 ffb5c4fdffff push dword ptr [ebp-23Ch] ; BaseAddress : 7c81a407 ffb580fdffff push dword ptr [ebp-280h] ; hNewProcessHandle: ; NtWriteVirtualMemory( *(ebp-280h), *(ebp-23Ch), eax, ???, 0 ) 7c81a40d ffd6 call esi ; _imp__NtWriteVirtualMemory
網路上可以找到人家 reverse 過的 RtlCreateProcessParameters() prototype 可以加速 trace 的速度:
NTSTATUS RtlCreateProcessParameters( PRTL_USER_PROCESS_PARAMETERS *ProcessParameters, PUNICODE_STRING ImagePathName, PUNICODE_STRING DllPath, PUNICODE_STRING CurrentDirectory, PUNICODE_STRING CommandLine, PWSTR Environment, // Not sured PUNICODE_STRING WindowTitle, PUNICODE_STRING DesktopInfo, PUNICODE_STRING ShellInfo, PUNICODE_STRING RuntimeData );
不過如果想要硬派的自己來也是可以的,可以試試看這篇文章的作法 : )
4 則留言:
好文一篇,推~~
呵,算是給自己做個筆記,怕以後忘了,感謝一推~
完整而且精彩,感謝分享. =)
哈,又回頭看了一遍,發現很多小地方不甚流暢,描述細節的地方還帶加強 XD
張貼留言