一個 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 );
不過如果想要硬派的自己來也是可以的,可以試試看這篇文章的作法 : )
Summary