隨着64位操作系統的普及,都開始大力進軍x64,X64下的調試機制也發生了改變,與x86相比,添加了許多自己的新特性,之前學習了Windows x64的調試機制,這里本着“拿來主義”的原則與大家分享。
本文屬於譯文,英文原文鏈接:http://www.codemachine.com/article_x64deepdive.html
翻譯原文地址:深入Windows X64 調試
在正式開始這篇譯文之前,譯者先定義下面兩個關於棧幀的翻譯:
- frame pointer:棧幀寄存器、棧幀指針,在X86平台上,是EBP所指的位置
- stack pointer:棧頂寄存器、棧頂指針,在X86平台上,是ESP所指的位置
這個教程討論一些在 X64 CPU 上代碼執行的要點,如:編譯器優化、異常處理、參數傳遞和參數恢復,並且介紹這幾個topic之間的關聯。我們會涉及與上述topic相關的一些重要的調試命令,並且提供必要的背景知識去理解這些命令的輸出。同時,也會重點介紹X64平台的調試與X86平台的不同,以及這些不同對調試的影響。最后,我們會活學活用,利用上面介紹的知識來展示如何將這些知識應用於X64平台的基於寄存器存儲的參數恢復上,當然,這也是X64平台上調試的難點。
0x00 編譯器優化
這一節主要討論影響X64 code生成的編譯器優化,首先從X64寄存器開始,然后,介紹優化細節,如:函數內聯處理(function in-lining),消除尾部調用(tail call elimination), 棧幀指針優化(frame pointer optimization)和基於棧頂指針的局部變量訪問(stack pointer based local variable access)。
- 寄存器的變化
X64平台上的所有寄存器,除了段寄存器和EFlags寄存器,都是64位的,這就意味着在x64平台上所有內存的操作都是按64位寬度進行的。同樣,X64指令有能力一次性處理64位的地址和數據。增加了8個新的寄存器,如: r8~r15,與其他的使用字母命名的寄存器不同,這些寄存器都是使用數字命名。下面的調試命令輸出了 X64 平台上寄存器的信息:
1: kd> r rax=fffffa60005f1b70 rbx=fffffa60017161b0 rcx=000000000000007f rdx=0000000000000008 rsi=fffffa60017161d0 rdi=0000000000000000 rip=fffff80001ab7350 rsp=fffffa60005f1a68 rbp=fffffa60005f1c30 r8=0000000080050033 r9=00000000000006f8 r10=fffff80001b1876c r11=0000000000000000 r12=000000000000007b r13=0000000000000002 r14=0000000000000006 r15=0000000000000004 iopl=0 nv up ei ng nz na pe nc cs=0010 ss=0018 ds=002b es=002b fs=0053 gs=002b efl=00000282 nt!KeBugCheckEx: fffff800`01ab7350 48894c2408 mov qword ptr [rsp+8],rcx ss:0018:fffffa60`005f1a70=000000000000007f
相比較X86平台,一些寄存器的用法已經發生變化,這些變化可以按如下分組:
- 不可變寄存器是那些在函數調用過程中,值被保存起來的寄存器。X64平台擁有一個擴展的不可變寄存器集合,在這個集合中,以前x86平台下原有的不可變寄存器也包含在內,新增的寄存器是從R12到R15,這些寄存器對於函數參數的恢復很重要。
- Fastcall寄存器用於傳遞函數參數。Fastcall是X64平台上默認的調用約定,前4個參數通過RCX, RDX, R8, R9傳遞。
- RBP不再用作棧幀寄存器。現在RBP和RBX,RCX一樣都是通用寄存器,調試前不再使用RBP來回溯調用棧。
- 在X86 CPU中,FS段寄存器用於指向線程環境塊(TEB)和處理器控制區(Processor Control Region, KPCR),但是,在X64上,GS段寄存器在用戶態是指向TEB,在內核態是指向KPCR。然而,當運行WOW64程序中,FS 寄存器仍然指向32位的TEB。
在X64平台上,trap frame的數據結構(nt!_KTRAP_FRAME)中不包含不可變寄存器的合法內容。如果X64函數會使用到這些不可變寄存器,那么,指令的序言部分會保存不可變寄存器的值。這樣,調試器能夠一直從棧中取到這些不可變寄存器原先的值,而不是從trap frame中去取。在X64內核模式調試狀態下,`.trap`命令的輸出會打印一個NOTE,用於告訴用戶所有從trap frame中取出的寄存器信息可能不准確,如下所示:
1: kd> kv Child-SP RetAddr : Args to Child . . . nt!KiDoubleFaultAbort+0xb8 (TrapFrame @ fffffa60`005f1bb0) . . . 1: kd> .trap fffffa60`005f1bb0 NOTE: The trap frame does not contain all registers. Some register values may be zeroed or incorrect
- 函數內聯處理(Function in-lining)
如果滿足一定的規則以后,X64編譯器會執行內聯函數的擴展,這樣會將所有內聯函數的調用部分用函數體來替換。內聯函數的優點是避免函數調用過程中的棧幀創建以及函數退出時的棧平衡,缺點是由於指令的重復對導致可執行程序的大小增大不少,同時,也會導致cache未命中和page fault的增加。內聯函數同樣也會影響調試,因為當用戶嘗試在內聯函數上設置斷點時,調試器是不能找到對應的符號的。源碼級別的內聯可以通過編譯器的/Ob flag 進行控制,並且可以通過__declspec(noinline)禁止一個函數的內聯過程。圖1顯示函數2和函數3被內聯到函數1的過程。

Figure 1 : Function In-lining
- 消除尾部調用(Tail Call Elimination)
X64編譯器可以使用jump指令替換函數體內最后的call指令,通過這種方式來優化函數的調用過程。這種方法可以避免被調函數的棧幀創建,調用函數與被調函數共享相同的棧幀,並且,被調函數可以直接返回到自己爺爺級別的調用函數,這種優化在調用函數與被調函數擁有相同參數的情況下格外有用,因為如果相應的參數已經被放在指定的寄存器中,並且沒有改變,那么,它們就不用被重新加載。圖2顯示了TCE,我們在函數1的最后調用函數4:

Figure 2 : Tail Call Elimination
- 棧幀指針省略(Frame Pointer Omission, FPO)
在X86平台下,EBP寄存器用於訪問棧上的參數與局部變量,而在X64平台下,RBP寄存器不再使用充當同樣的作用。取而代之的是,在X64環境下,使用RSP作為棧幀寄存器和棧頂寄存器,具體是如何使用的,我們會在后續的章節中做詳細的敘述。(譯者注:請區分X86中的FPO與X64中的FPO,有很多相似的地方,也有不同之處。關於 X86上的FPO,請參考《軟件調試》中關於棧的描述)所以,在X64環境下,RBP寄存器已經不再擔當棧幀寄存器,而是作為一般的通用寄存器使用。但是,有一個例外情況,當使用alloca()動態地在棧上分配空間的時候,這時,會和X86環境一樣,使用RBP作為棧幀寄存器。 下面的匯編代碼片段展示了X86環境下的KERNELBASE!Sleep函數,可以看到EBP寄存器被用作棧幀寄存器。當調用SleepEx()函數的時候,參數被壓到棧上,然后,使用call指令調用SleepEx()。
0:009> uf KERNELBASE!Sleep KERNELBASE!Sleep: 75ed3511 8bff mov edi,edi 75ed3513 55 push ebp 75ed3514 8bec mov ebp,esp 75ed3516 6a00 push 0 75ed3518 ff7508 push dword ptr [ebp+8] 75ed351b e8cbf6ffff call KERNELBASE!SleepEx (75ed2beb) 75ed3520 5d pop ebp 75ed3521 c20400 ret 4.
下面的代碼片段展示的是X64環境下相同的函數,與X86的code比起來有明顯的不同。X64版本的看起來非常緊湊,主要是由於不需要保存、恢復RBP寄存器。
0:000> uf KERNELBASE!Sleep KERNELBASE!Sleep: 000007fe`fdd21140 xor edx,edx 000007fe`fdd21142 jmp KERNELBASE!SleepEx (000007fe`fdd21150)
- 基於棧頂指針的局部變量訪問(Stack Pointer based local variable access)
在X86平台上,EBP的最重要作用就是可以通過EBP訪問實參和局部變量,而在X64平台上,如我們前面所述, RBP寄存器不再充當棧幀寄存器的作用,所以,在X64平台上,RSP即充當棧幀寄存器(frame pointer),又充當棧頂寄存器(stack pointer)。所以,X64上所有的引用都是基於RSP的。由於這個原因,依賴於RSP的函數,其棧幀在函數體執行過程中是固定不變的,從而可以方便訪問局部變量和參數。因為PUSH和POP指令會改變棧頂指針,所以,X64函數會限制這些指令只能在函數的首尾使用。如圖3所示,X64函數的結構:

Figure 3 : Static Stack Pointer
下面的代碼片段展示了函數 user32!DrawTestExW 的完整信息,這個函數的首部以指令“sub rsp,48h”結束,尾部以“add rsp,48h”開始。因為首尾之間的指令通過RSP訪問棧上的內容,所以,沒有PUSH或者POP之類的指令在函數體內。
0:000> uf user32!DrawTextExW user32!DrawTextExW: 00000000`779c9c64 sub rsp,48h 00000000`779c9c68 mov rax,qword ptr [rsp+78h] 00000000`779c9c6d or dword ptr [rsp+30h],0FFFFFFFFh 00000000`779c9c72 mov qword ptr [rsp+28h],rax 00000000`779c9c77 mov eax,dword ptr [rsp+70h] 00000000`779c9c7b mov dword ptr [rsp+20h],eax 00000000`779c9c7f call user32!DrawTextExWorker (00000000`779ca944) 00000000`779c9c84 add rsp,48h 00000000`779c9c88 ret
0x01 異常處理(Exception Handling)
這一節討論X64函數用於異常處理的底層機制和數據結構,以及調試器如何使用這些數據結構回溯調用棧的,同時,也介紹一些X64調用棧上特有的內容。
- RUNTIME_FUNCTION
X64 可執行文件使用了一種 PE 文件格式的變種,叫做 PE32+,這種文件有一個額外的段,叫做“.pdata”或者Exception Directory,用於存放處理異常的信息。這個“Exception Directory”包含一系列RUNTIME_FUNCTION 結構,每一個non-leaf函數都會有一個RUNTIME_FUNCTION,這里所謂的non-leaf函數是指那些不再調用其他函數的函數。每一個RUNTIME_FUNCTION結構包含函數第一條指令和最后一條指令的偏移,以及一個指向unwind information結構的指針。Unwind information結構用於描述在異常發生的時候,函數調用棧該如何展開。 圖4展示了一個模塊的RUNTIME_FUNCTION結構。

Figure 4 : RUNTIME_FUNCTION
下面的匯編代碼片段展示了X86平台與X64平台上異常處理的不同。在X86平台上,當高級語言使用了結構化異常處理,編譯器會在函數的首尾生成特定的代碼片段,用於在運行時構建異常棧幀。這些可以在下面的代碼片段中看到,如:調用了ntdll!_SEH_prolog4和 ntdll!_SEH_epilog4.
0:009> uf ntdll!__RtlUserThreadStart ntdll!__RtlUserThreadStart: 77009d4b push 14h 77009d4d push offset ntdll! ?? ::FNODOBFM::`string'+0xb5e (76ffc3d0) 77009d52 call ntdll!_SEH_prolog4 (76ffdd64) 77009d57 and dword ptr [ebp-4],0 77009d5b mov eax,dword ptr [ntdll!Kernel32ThreadInitThunkFunction (770d4224)] 77009d60 push dword ptr [ebp+0Ch] 77009d63 test eax,eax 77009d65 je ntdll!__RtlUserThreadStart+0x25 (77057075) ntdll!__RtlUserThreadStart+0x1c: 77009d6b mov edx,dword ptr [ebp+8] 77009d6e xor ecx,ecx 77009d70 call eax 77009d72 mov dword ptr [ebp-4],0FFFFFFFEh 77009d79 call ntdll!_SEH_epilog4 (76ffdda9) 77009d7e ret 8
然而,在X64環境上的相同函數中,沒有任何跡象表明當前函數使用了結構化異常處理,因為沒有運行時的異常棧幀。通過從可執行文件中提取相應的信息,可以使用RUNTIME_FUNCTION結構和RIP一起確定相應的異常處理信息。
0:000> uf ntdll!RtlUserThreadStart Flow analysis was incomplete, some code may be missing ntdll!RtlUserThreadStart: 00000000`77c03260 sub rsp,48h 00000000`77c03264 mov r9,rcx 00000000`77c03267 mov rax,qword ptr [ntdll!Kernel32ThreadInitThunkFunction (00000000`77d08e20)] 00000000`77c0326e test rax,rax 00000000`77c03271 je ntdll!RtlUserThreadStart+0x1f (00000000`77c339c5) ntdll!RtlUserThreadStart+0x13: 00000000`77c03277 mov r8,rdx 00000000`77c0327a mov rdx,rcx 00000000`77c0327d xor ecx,ecx 00000000`77c0327f call rax 00000000`77c03281 jmp ntdll!RtlUserThreadStart+0x39 (00000000`77c03283) ntdll!RtlUserThreadStart+0x39: 00000000`77c03283 add rsp,48h 00000000`77c03287 ret ntdll!RtlUserThreadStart+0x1f: 00000000`77c339c5 mov rcx,rdx 00000000`77c339c8 call r9 00000000`77c339cb mov ecx,eax 00000000`77c339cd call ntdll!RtlExitUserThread (00000000`77bf7130) 00000000`77c339d2 nop 00000000`77c339d3 jmp ntdll!RtlUserThreadStart+0x2c (00000000`77c53923)
- UNWIND_INFO和UNWIND_CODE
RUNTIME_FUNCTION結構的BeginAddress和EndAddress存放着虛擬地址空間上的函數首地址和尾地址所對應的偏移,這些偏移是相對於模塊基址的。當函數產生異常時,OS 會掃描內存中 PE,尋找當前指令地址所在的RUNTIME_FUNCTION結構。UnwindData域指向另外一個結構,用於告訴OS如何去展開棧。這個UNWIND_INFO結構包含各種UNWIND_CODE結構,每一個UNWIND_CODE都代表函數首部對應的操作。對 於 動 態 生 成 的 代 碼 , OS 支 持 下 面 兩 個 函 數 RtlAddFunctionTable() andRtlInstallFunctionTableCallback(),可以用於在運行過程中創建RUNTIME_FUNCTION 。
圖5展示RUNTIME_FUNCTION和UNWIND_INFO的關系

Figure 5 : Unwind Information
調試器命令“.fnent”可以顯示指定函數的 RUNTIME_FUNCTIOIN 結構,下面的例子,使用”.fnent”顯示 ntdll!RtlUserThreadStart
0:000> .fnent ntdll!RtlUserThreadStart Debugger function entry 00000000`03be6580 for: (00000000`77c03260) ntdll!RtlUserThreadStart | (00000000`77c03290) ntdll!RtlRunOnceExecuteOnce Exact matches: ntdll!RtlUserThreadStart = BeginAddress = 00000000`00033260 EndAddress = 00000000`00033290 UnwindInfoAddress = 00000000`00128654 Unwind info at 00000000`77cf8654, 10 bytes version 1, flags 1, prolog 4, codes 1 frame reg 0, frame offs 0 handler routine: ntdll!_C_specific_handler (00000000`77be50ac), data 3 00: offs 4, unwind op 2, op info 8 UWOP_ALLOC_SMALL
如果上面的 BeginAddress 加上 NTDLL 的基址,結果是 0x0000000077c03260,也就是函數RtlUserThreadStart 的首地址,如下面所示:
0:000> ?ntdll+00000000`00033260 Evaluate expression: 2009084512 = 00000000`77c03260 0:000> u ntdll+00000000`00033260 ntdll!RtlUserThreadStart: 00000000`77c03260 sub rsp,48h 00000000`77c03264 mov r9,rcx 00000000`77c03267 mov rax,qword ptr [ntdll!Kernel32ThreadInitThunkFunction (00000000`77d08e20)] 00000000`77c0326e test rax,rax 00000000`77c03271 je ntdll!RtlUserThreadStart+0x1f (00000000`77c339c5) 00000000`77c03277 mov r8,rdx 00000000`77c0327a mov rdx,rcx 00000000`77c0327d xor ecx,ecx
如果EndAddress也用同樣的方法計算,其結果指向上面函數的末尾
0:000> ?ntdll+00000000`00033290 Evaluate expression: 2009084560 = 00000000`77c03290 0:000> ub 00000000`77c03290 L10 ntdll!RtlUserThreadStart+0x11: 00000000`77c03271 je ntdll!RtlUserThreadStart+0x1f (00000000`77c339c5) 00000000`77c03277 mov r8,rdx 00000000`77c0327a mov rdx,rcx 00000000`77c0327d xor ecx,ecx 00000000`77c0327f call rax 00000000`77c03281 jmp ntdll!RtlUserThreadStart+0x39 (00000000`77c03283) 00000000`77c03283 add rsp,48h 00000000`77c03287 ret 00000000`77c03288 nop 00000000`77c03289 nop 00000000`77c0328a nop 00000000`77c0328b nop 00000000`77c0328c nop 00000000`77c0328d nop 00000000`77c0328e nop 00000000`77c0328f nop
所以,RUNTIME_FUNCTION結構中的BeginAddress和EndAddress描述了相應的函數在memory中的位置。然而,在鏈接過程中的優化可能會改變上述的內容,我們會在后續的章節中介紹。
雖然UNWIND_INFO和UNWIND_CODE的主要目的是用於描述異常發生時,如何展開棧的。但是,調試器也可以利用這些信息,在沒有symbol的時候,回溯函數調用棧。每一個UNWIND_CODE結構可以描述下面的一種操作,這些操作都會在函數首部中執行。
- SAVE_NONVOL 將不可變寄存器的值保存在棧上
- PUSH_NONVOL 將不可變寄存器的值壓入棧
- ALLOC_SMALL 在棧上分配空間,最多128 bytes Ø ALLOC_LARGE – 在棧上分配空間,最多4GB
所以,本質上,UNWIND_CODE是函數首部指令所對應的元指令,或者說是偽代碼。 圖6展示了函數首部操作棧的指令與UNWIND_CODE之間的關系。UNWIND_CODE結構與它們所對應的指令呈相反的順序,這樣,在異常發生的時候,棧可以按照創建時相反的方向進行展開。

Figure 6 : Unwind Code
下面的例子展示了X64下的notepad.exe的`.pdata`段的HEADER信息,`virtual address`域指示了.pdata 段的位置是在可執行文件的0x13000的偏移處。
T:\link -dump -headers c:\windows\system32\notepad.exe . . . SECTION HEADER #4 .pdata name 6B4 virtual size 13000 virtual address (0000000100013000 to 00000001000136B3) 800 size of raw data F800 file pointer to raw data (0000F800 to 0000FFFF) 0 file pointer to relocation table 0 file pointer to line numbers 0 number of relocations 0 number of line numbers 40000040 flags Initialized Data Read Only .
下面一個例子是顯示相同可執行文件的UNWIND_INFO和UNWIND_CODE,每一個UNWIND_CODE描述了一個操作,像PUSH_NONVOL或ALLOC_SMALL,這些指令是在函數首部執行的,並在棧展開時撤銷的。”.fnent”命令可以顯示這兩個結構的內容,但是,不夠詳細,而"link -dump -unwindinfo"命令可以顯示完整的內容。
T:\link -dump -unwindinfo c:\windows\system32\notepad.exe . . . 00000018 00001234 0000129F 0000EF68 Unwind version: 1 Unwind flags: None Size of prologue: 0x12 Count of codes: 5 Unwind codes: 12: ALLOC_SMALL, size=0x28 0E: PUSH_NONVOL, register=rdi 0D: PUSH_NONVOL, register=rsi 0C: PUSH_NONVOL, register=rbp 0B: PUSH_NONVOL, register=rbx. . . .
上述的ALLOC_SMALL代表函數首部的sub指令,這會在棧空間上分配0x28字節的空間,每一個PUSH_NONVOL對應一個push指令,用於將不可變寄存器壓入棧,並使用pop指令進行還原。這些指令可以在函數的匯編代碼中看到
0:000> ln notepad+1234 (00000000`ff971234) notepad!StringCchPrintfW | (00000000`ff971364) notepad!CheckSave Exact matches: notepad!StringCchPrintfW = notepad!StringCchPrintfW = 0:000> uf notepad!StringCchPrintfW notepad!StringCchPrintfW: 00000001`00001234 mov qword ptr [rsp+18h],r8 00000001`00001239 mov qword ptr [rsp+20h],r9 00000001`0000123e push rbx 00000001`0000123f push rbp 00000001`00001240 push rsi 00000001`00001241 push rdi 00000001`00001242 sub rsp,28h 00000001`00001246 xor ebp,ebp 00000001`00001248 mov rsi,rcx 00000001`0000124b mov ebx,ebp 00000001`0000124d cmp rdx,rbp 00000001`00001250 je notepad!StringCchPrintfW+0x27 (00000001`000077b5) ... notepad!StringCchPrintfW+0x5c: 00000001`00001294 mov eax,ebx 00000001`00001296 add rsp,28h 00000001`0000129a pop rdi 00000001`0000129b pop rsi 00000001`0000129c pop rbp 00000001`0000129d pop rbx
- 性能優化(Performance Optimization)
Windows 操作系統中的可執行文件采用了一種叫做 Basic Block Tools(BBT)的優化,這種優化會提升代碼的局部性。頻繁執行的函數塊被放在一起,這樣會更可能放在相同的頁上,而對於那些不頻繁使用的部分被移到其他位置。這種方法減少了需要同時保留在內存中的頁數,從而導致整個working set的減少。為了使用這種優化方案,可執行文件會被鏈接、執行、評測,最后,使用評測結果重新組合那些頻繁執行的函數部分。 在重組過的函數中,一些函數塊被移出函數主體,這些原本是定義在RUNTIME_FUNCTION結構中的。由於函數塊的移動,導致函數體被分割成多個不同的部分。因此,鏈接過程中生成的UNTIME_FUNCTION結構已經不能再准確地描述這個函數。為了解決這個問題,BBT過程新增了多個 RUNTIME_FUNCTION 結構,每一個 RUNTIME_FUNCTION 對應一個優化過的函數塊。這些RUNTIME_FUNCTION被鏈在一起,以最初的RUNTIME_FUNTION結尾,這樣,最后的這個RUNTIME_FUNTION的BeginAddress會一直指向函數的首地址。 圖7展示了由3個基礎塊組成的函數。在BBT優化以后,#2塊被移除函數體,從而導致原先的RUNTIME_FUNCTION 的信息失效。所以,BBT優化過程創建了第二個RUNTIME_FUNCTION結構,並將它串聯到第一個,下圖描述了整個過程。

Figure 7 : Performance Optimization : Basic Block Tools
當前公開版本的調試器不能回溯RUNTIME_FUNCTION的完整鏈,所以,調試器不能正確地顯示優化過的函數名,相應的返回地址映射到那些被移出函數體的函數塊。
下面的例子展示了函數的調用棧,其中,函數名不能正常顯示,取而代之的是ntdll! ?? ::FNODOBFM::`string'。調 試 器 錯 誤 地 將 返 回 地 址 0x00000000`77c17623 轉 成 #0x0c 號 棧 幀 的 函 數 名 ntdll! ?? ::FNODOBFM::`string'+0x2bea0
0:000> kn # Child-SP RetAddr Call Site 00 00000000`0029e4b8 000007fe`fdd21726 ntdll! ?? ::FNODOBFM::`string'+0x6474 01 00000000`0029e4c0 000007fe`fdd2dab6 KERNELBASE!BaseSetLastNTError+0x16 02 00000000`0029e4f0 00000000`77ad108f KERNELBASE!AccessCheck+0x64 03 00000000`0029e550 00000000`77ad0d46 kernel32!BasepIsServiceSidBlocked+0x24f 04 00000000`0029e670 00000000`779cd161 kernel32!LoadAppInitDlls+0x36 05 00000000`0029e6e0 00000000`779cd42d user32!ClientThreadSetup+0x22e 06 00000000`0029e950 00000000`77c1fdf5 user32!_ClientThreadSetup+0x9 07 00000000`0029e980 000007fe`ffe7527a ntdll!KiUserCallbackDispatcherContinue 08 00000000`0029e9d8 000007fe`ffe75139 gdi32!ZwGdiInit+0xa 09 00000000`0029e9e0 00000000`779ccd1f gdi32!GdiDllInitialize+0x11b 0a 00000000`0029eb40 00000000`77c0c3b8 user32!UserClientDllInitialize+0x465 0b 00000000`0029f270 00000000`77c18368 ntdll!LdrpRunInitializeRoutines+0x1fe 0c 00000000`0029f440 00000000`77c17623 ntdll!LdrpInitializeProcess+0x1c9b 0d 00000000`0029f940 00000000`77c0308e ntdll! ?? ::FNODOBFM::`string'+0x2bea0 0e 00000000`0029f9b0 00000000`00000000 ntdll!LdrInitializeThunk+0xe
下面的例子將使用上面用到的返回地址 0x00000000`77c17623 來顯示錯誤函數名的 RUNTIME_FUNCTION, UNWIND_INFO和UNWIND_CODEs。顯示的信息包含一個名為”Chained Info”的段,用於指示函數代碼塊被移出函數體。
0:000> .fnent 00000000`77c17623 Debugger function entry 00000000`03b35da0 for: (00000000`77c55420) ntdll! ?? ::FNODOBFM::`string'+0x2bea0 | (00000000`77c55440) ntdll! ?? ::FNODOBFM::`string' BeginAddress = 00000000`000475d3 EndAddress = 00000000`00047650 UnwindInfoAddress = 00000000`0012eac0 Unwind info at 00000000`77cfeac0, 10 bytes version 1, flags 4, prolog 0, codes 0 frame reg 0, frame offs 0 Chained info: BeginAddress = 00000000`000330f0 EndAddress = 00000000`000331c0 UnwindInfoAddress = 00000000`0011d08c Unwind info at 00000000`77ced08c, 20 bytes version 1, flags 1, prolog 17, codes a frame reg 0, frame offs 0 handler routine: 00000000`79a2e560, data 0 00: offs f0, unwind op 0, op info 3 UWOP_PUSH_NONVOL 01: offs 3, unwind op 0, op info 0 UWOP_PUSH_NONVOL 02: offs c0, unwind op 1, op info 3 UWOP_ALLOC_LARGE FrameOffset: d08c0003 04: offs 8c, unwind op 0, op info d UWOP_PUSH_NONVOL 05: offs 11, unwind op 0, op info 0 UWOP_PUSH_NONVOL 06: offs 28, unwind op 0, op info 0 UWOP_PUSH_NONVOL 07: offs 0, unwind op 0, op info 0 UWOP_PUSH_NONVOL 08: offs 0, unwind op 0, op info 0 UWOP_PUSH_NONVOL 09: offs 0, unwind op 0, op info 0 UWOP_PUSH_NONVOL
上面說到的Chained Info中BeginAddress指向原先函數的首地址,可以使用`ln`命令看看這個函數的實際函數名。
0:000> ln ntdll+000330f0 (00000000`77c030f0) ntdll!LdrpInitialize | (00000000`77c031c0) ntdll!LdrpAllocateTls
Exact matches:
ntdll!LdrpInitialize =
調試器的`uf`命令可以顯示完整的函數匯編代碼,這個命令之所以可以做到這點,是通過每個代碼塊最后的 jmp/jCC指令來訪問所有的代碼塊。下面的輸出展示了函數ntdll!LdrpInitialize的匯編代碼,函數主體是從00000000`77c030f0到00000000`77c031b3,然而,有一個代碼塊是在00000000`77bfd1a4。這樣的代碼移動是由於 BBT 優化的結果,調試器嘗試將這個地址與最近的符號對應起來,也就是上面說到
的 "ntdll! ?? ::FNODOBFM::`string'+0x2c01c"
0:000> uf 00000000`77c030f0 ntdll! ?? ::FNODOBFM::`string'+0x2c01c: 00000000`77bfd1a4 48c7842488000000206cfbff mov qword ptr [rsp+88h],0FFFFFFFFFFFB6C20h 00000000`77bfd1b0 443935655e1000 cmp dword ptr [ntdll!LdrpProcessInitialized (00000000`77d0301c)],r14d 00000000`77bfd1b7 0f856c5f0000 jne ntdll!LdrpInitialize+0x39 (00000000`77c03129) . . . ntdll!LdrpInitialize: 00000000`77c030f0 48895c2408 mov qword ptr [rsp+8],rbx 00000000`77c030f5 4889742410 mov qword ptr [rsp+10h],rsi 00000000`77c030fa 57 push rdi 00000000`77c030fb 4154 push r12 00000000`77c030fd 4155 push r13 00000000`77c030ff 4156 push r14 00000000`77c03101 4157 push r15 00000000`77c03103 4883ec40 sub rsp,40h 00000000`77c03107 4c8bea mov r13,rdx 00000000`77c0310a 4c8be1 mov r12,rcx . . . ntdll!LdrpInitialize+0xac: 00000000`77c0319c 488b5c2470 mov rbx,qword ptr [rsp+70h] 00000000`77c031a1 488b742478 mov rsi,qword ptr [rsp+78h] 00000000`77c031a6 4883c440 add rsp,40h 00000000`77c031aa 415f pop r15 00000000`77c031ac 415e pop r14 00000000`77c031ae 415d pop r13 00000000`77c031b0 415c pop r12 00000000`77c031b2 5f pop rdi 00000000`77c031b3 c3 ret
經過BBT優化過的模塊可以被`!lmi`命令識別出來,在命令的輸出中,”Characteristics”域會標示為”perf”。
0:000> !lmi notepad Loaded Module Info: [notepad] Module: notepad Base Address: 00000000ff4f0000 Image Name: notepad.exe Machine Type: 34404 (X64) Time Stamp: 4a5bc9b3 Mon Jul 13 16:56:35 2009 Size: 35000 CheckSum: 3e749 Characteristics: 22 perf Debug Data Dirs: Type Size VA Pointer CODEVIEW 24, b74c, ad4c RSDS - GUID: {36CFD5F9-888C-4483-B522-B9DB242D8478} Age: 2, Pdb: notepad.pdb CLSID 4, b748, ad48 [Data not mapped] Image Type: MEMORY - Image read successfully from loaded memory. Symbol Type: PDB - Symbols loaded successfully from symbol server. c:\symsrv\notepad.pdb\36CFD5F9888C4483B522B9DB242D84782\notepad.pdb Load Report: public symbols , not source indexed c:\symsrv\notepad.pdb\36CFD5F9888C4483B522B9DB242D84782\notepad.pdb
0x02 參數傳遞(Parameter Passing)
本節討論X64平台上參數是如何傳遞的,函數棧幀是如何構建的,以及調試器如何使用這些信息回溯調用棧。
-
基於寄存器的參數傳遞(Register based parameter passing)
在X64平台上,函數的前4個參數是通過寄存器傳遞,剩余的參數是通過棧傳遞。這是調試過程中最主要的痛苦之一,因為寄存器的值在函數執行過程中會被修改,從而導致很難確定傳入函數的參數值是什么。另外一個問題是參數恢復問題,X64平台上的調試與X86平台上的調試有很大的差異。 圖8展示了X64匯編代碼如何在調用函數與被調函數之間傳遞參數的:

Figure 8 : Parameter Passing on X64
下面的調用棧展示函數kernel32!CreateFileWImplementation調用 KERNELBASE!CreateFileW。
0:000> kn # Child-SP RetAddr Call Site 00 00000000`0029bbf8 000007fe`fdd24d76 ntdll!NtCreateFile 01 00000000`0029bc00 00000000`77ac2aad KERNELBASE!CreateFileW+0x2cd 02 00000000`0029bd60 000007fe`fe5b9ebd kernel32!CreateFileWImplementation+0x7d . . .
從MSDN的文檔上來看,函數CreateFileW()有7個參數,函數原型如下:
HANDLE WINAPI
CreateFile(
__in LPCTSTR lpFileName,
__in DWORD dwDesiredAccess,
__in DWORD dwShareMode,
__in_opt LPSECURITY_ATTRIBUTES lpSecurityAttributes,
__in DWORD dwCreationDisposition,
__in DWORD dwFlagsAndAttributes,
__in_opt HANDLE hTemplateFile );
從上面的調用棧可以看出,函數KERNELBASE!CreateFileW的返回地址是00000000`77ac2aad。可以反向顯示這個地址的匯編代碼,那樣,就可以看到調用 KERNELBASE!CreateFileW 之前的代碼。下面這 4 條指令:"mov rcx,rdi", "mov edx,ebx", "mov r8d,ebp", "mov r9,rsi" 是在做調用kernel32!CreateFileW函數的准備工作,將前4個參數放在寄存器上。同樣,下面這幾條指令:"mov dword ptr [rsp+20h],eax", "mov dword ptr [rsp+28h],eax" and "mov qword ptr [rsp+30h],rax" 是將參數放在棧幀上。
0:000> ub 00000000`77ac2aad L10 kernel32!CreateFileWImplementation+0x35: 00000000`77ac2a65 lea rcx,[rsp+40h] 00000000`77ac2a6a mov edx,ebx 00000000`77ac2a6c call kernel32!BaseIsThisAConsoleName (00000000`77ad2ca0) 00000000`77ac2a71 test rax,rax 00000000`77ac2a74 jne kernel32!zzz_AsmCodeRange_End+0x54fc (00000000`77ae7bd0) 00000000`77ac2a7a mov rax,qword ptr [rsp+90h] 00000000`77ac2a82 mov r9,rsi 00000000`77ac2a85 mov r8d,ebp 00000000`77ac2a88 mov qword ptr [rsp+30h],rax 00000000`77ac2a8d mov eax,dword ptr [rsp+88h] 00000000`77ac2a94 mov edx,ebx 00000000`77ac2a96 mov dword ptr [rsp+28h],eax 00000000`77ac2a9a mov eax,dword ptr [rsp+80h] 00000000`77ac2aa1 mov rcx,rdi 00000000`77ac2aa4 mov dword ptr [rsp+20h],eax 00000000`77ac2aa8 call kernel32!CreateFileW (00000000`77ad2c88)
-
Homing Space
雖然前4個參數被放在寄存器上,但是,在棧幀空間上依然會分配相應的空間。這個叫做參數的Homing Space, 用於存放參數的值,如果參數是傳址而不是傳值,或者函數編譯過程中打開/homeparams標志。這個Homing Space 的最小空間尺寸是0x20個字節,即便函數的參數小於4個。如果Homing Space沒有用於存放參數的值,編譯器會用它們存放不可變寄存器的值。 圖9展示了棧空間上的Homing Space,以及在函數初始階段是如何將不可變寄存器的值存放在Homing Space中。

Figure 9 : Parameter Homing Space
在下面的例子中,指令"sub rsp, 20h"表明函數初始階段在棧空間上分配了0x20個字節的空間,這已足以存放4 個64位的值。下面一部分顯示msvcrt!malloc()是一個no-leaf函數,它會調用其他的函數。
0:000> uf msvcrt!malloc msvcrt!malloc: 000007fe`fe6612dc mov qword ptr [rsp+8],rbx 000007fe`fe6612e1 mov qword ptr [rsp+10h],rsi 000007fe`fe6612e6 push rdi 000007fe`fe6612e7 sub rsp,20h 000007fe`fe6612eb cmp qword ptr [msvcrt!crtheap (000007fe`fe6f1100)],0 000007fe`fe6612f3 mov rbx,rcx 000007fe`fe6612f6 je msvcrt!malloc+0x1c (000007fe`fe677f74) . . . 0:000> uf /c msvcrt!malloc msvcrt!malloc (000007fe`fe6612dc) msvcrt!malloc+0x6a (000007fe`fe66132c): call to ntdll!RtlAllocateHeap (00000000`77c21b70) msvcrt!malloc+0x1c (000007fe`fe677f74): call to msvcrt!core_crt_dll_init (000007fe`fe66a0ec) msvcrt!malloc+0x45 (000007fe`fe677f83): call to msvcrt!FF_MSGBANNER (000007fe`fe6ace0c) msvcrt!malloc+0x4f (000007fe`fe677f8d): call to msvcrt!NMSG_WRITE (000007fe`fe6acc10) msvcrt!malloc+0x59 (000007fe`fe677f97): call to msvcrt!_crtExitProcess (000007fe`fe6ac030) msvcrt!malloc+0x83 (000007fe`fe677fad): call to msvcrt!callnewh (000007fe`fe696ad0) msvcrt!malloc+0x8e (000007fe`fe677fbb): call to msvcrt!errno (000007fe`fe661918) . . .
下面的匯編代碼片段是WinMain函數的初始階段,4個不可變寄存器將被保存在棧空間上的Homing Space。
0:000> u notepad!WinMain notepad!WinMain: 00000000`ff4f34b8 mov rax,rsp 00000000`ff4f34bb mov qword ptr [rax+8],rbx 00000000`ff4f34bf mov qword ptr [rax+10h],rbp 00000000`ff4f34c3 mov qword ptr [rax+18h],rsi 00000000`ff4f34c7 mov qword ptr [rax+20h],rdi 00000000`ff4f34cb push r12 00000000`ff4f34cd sub rsp,70h 00000000`ff4f34d1 xor r12d,r12d
-
Parameter Homing
如上一節所描述,所有的X64 non-leaf函數都會在他們的棧空間中分配相應的Homing Space。如X64的調用約定,調用函數使用 4 個寄存器傳遞參數給被調函數。當使用/homeparams 標志開啟參數空間時,只有被調函數的代碼會受到影響。使用Windows Driver Kit(WDK)編譯環境,在checked/debug build中,這個標志一直是打開的。被調函數的初始化階段從寄存器中讀取參數的值,並將這些值存放在參數的homing space中。 圖10展示了調用函數的匯編代碼,它將參數傳到相應的寄存器中。同時,也展示了被調函數的初始化階段,這個函數使用了/homeparams 標志,從而,會將參數放在 homing space 上。被調函數的初始化階段從寄存器中讀取參數,並將這些值存放在棧上的參數homing space中。

Figure 10 : Parameter Homing
下面的代碼片段展示了寄存器的值被存放在homing area上
0:000> uf msvcrt!printf msvcrt!printf: 000007fe`fe667e28 mov rax,rsp 000007fe`fe667e2b mov qword ptr [rax+8],rcx 000007fe`fe667e2f mov qword ptr [rax+10h],rdx 000007fe`fe667e33 mov qword ptr [rax+18h],r8 000007fe`fe667e37 mov qword ptr [rax+20h],r9 000007fe`fe667e3b push rbx 000007fe`fe667e3c push rsi 000007fe`fe667e3d sub rsp,38h 000007fe`fe667e41 xor eax,eax 000007fe`fe667e43 test rcx,rcx 000007fe`fe667e46 setne al 000007fe`fe667e49 test eax,eax 000007fe`fe667e4b je msvcrt!printf+0x25 (000007fe`fe67d74b) . . .
0x03 堆棧使用(Stack Usage)
X64函數的棧幀包括下面內容:
- 返回地址
- 不可變寄存器的值
- 局部變量
- 基於棧的參數 Ø 基於寄存器的參數 除了返回地址之前,其他都是在函數初始階段存放的。棧空間由局部變量、基於棧的參數和參數Homing Space組成,並且都是由這樣的一條指令完成空間分配的:"sub rsp, xxx"。為基於棧的參數所預留的空間可以為調用者提供空間存放絕大多數的參數,基於寄存器的參數homing space只在non-leaf函數中保留。 圖11展示X64 CPU上函數棧幀的布局。

Figure 11 : Stack Usage
調試器的”knf”命令可以顯示調用棧上每一個棧幀所需的空間,這個值被放在”Memory”一欄。
0:000> knf # Memory Child-SP RetAddr Call Site 00 00000000`0029bbf8 000007fe`fdd24d76 ntdll!NtCreateFile 01 8 00000000`0029bc00 00000000`77ac2aad KERNELBASE!CreateFileW+0x2cd 02 160 00000000`0029bd60 000007fe`fe5b9ebd kernel32!CreateFileWImplementation+0x7d 03 60 00000000`0029bdc0 000007fe`fe55dc08 usp10!UniStorInit+0xdd 04 a0 00000000`0029be60 000007fe`fe5534af usp10!InitUnistor+0x1d8
下面的匯編代碼片段展示CreateFileW函數的初始階段,將不可變寄存器R8D和EDX的值保存在參數空間中,將RBX,RBP,RSI,RDI壓入棧上,然后,分配0x138字節的空間,用於存放局部變量和將要傳給被調函數的參數。
0:000> uf KERNELBASE!CreateFileW KERNELBASE!CreateFileW: 000007fe`fdd24ac0 mov dword ptr [rsp+18h],r8d 000007fe`fdd24ac5 mov dword ptr [rsp+10h],edx 000007fe`fdd24ac9 push rbx 000007fe`fdd24aca push rbp 000007fe`fdd24acb push rsi 000007fe`fdd24acc push rdi 000007fe`fdd24acd sub rsp,138h 000007fe`fdd24ad4 mov edi,dword ptr [rsp+180h] 000007fe`fdd24adb mov rsi,r9 000007fe`fdd24ade mov rbx,rcx 000007fe`fdd24ae1 mov ebp,2 000007fe`fdd24ae6 cmp edi,3 000007fe`fdd24ae9 jne KERNELBASE!CreateFileW+0x449 (000007fe`fdd255ff)
- Child-SP
調試命令`k`顯示的Child-SP寄存器的值代表着RSP寄存器所指向的地址,也就是所顯示的函數在完成函數初始階段之后,棧頂指針的位置。隨后被壓入棧的是函數的返回地址,由於X64函數在函數初始化以后不會修改RSP,任何涉及棧訪問的操作都是通過這個棧指針(RSP)完成的,包括訪問參數和局部變量。圖12展示函數f2的棧幀以及它與命令`k`所顯示的調用棧之間的關系。返回地址RA1指向函數f2在調用`call f1` 這條指令之后的位置,這個地址出現在調用棧上緊鄰RSP2所指向的位置。

Figure 12 : Relationship between Child-SP and function frames
在下面的調用棧中,棧幀#1的Child-SP是00000000`0029bc00,這是函數CreateFileW()的初始化階段結束以后,RSP的值。
0:000> knf # Memory Child-SP RetAddr Call Site 00 00000000`0029bbf8 000007fe`fdd24d76 ntdll!NtCreateFile 01 8 00000000`0029bc00 00000000`77ac2aad KERNELBASE!CreateFileW+0x2cd 02 160 00000000`0029bd60 000007fe`fe5b9ebd kernel32!CreateFileWImplementation+0x7d 03 60 00000000`0029bdc0 000007fe`fe55dc08 usp10!UniStorInit+0xdd 04 a0 00000000`0029be60 000007fe`fe5534af usp10!InitUnistor+0x1d8 . . .
如上所述,函數#01的RSP(value is 00000000`0029bc00)所指位置之前的8個字節應該是函數#00的返回地址。
0:000> dps 00000000`0029bc00-8 L1 00000000`0029bbf8 000007fe`fdd24d76 KERNELBASE!CreateFileW+0x2cd
-
回溯調用棧(Walking the call stack)
在X86 CPU上,調試器使用EBP chain來回溯調用棧,從最近的函數棧幀到最遠的函數棧幀。通常情況下,調試器可以回溯棧幀,而不依賴於調試符號。然而,EBP chain 可能會在某些情況下被破壞,如 frame pointer omitted(FPO)。這種情況下,調試器需要使用相應的調試符號才能正確地回溯棧幀。在X64函數中,並沒有使用RBP作為棧幀指針,從而,調試器沒有EBP chain來做棧回溯。在這種情況下,調試器通過定位RUNTIME_FUNCTION, UNWIND_INFO和UNWIND_CODE這些結構,去計算每一個函數所需的棧幀空間,然后,加上相應的RSP,便可以計算出下面Child-SP的值。圖13展示函數棧幀的布局,棧幀的大小=返回地址(8個字節)+不可變寄存器+局部變量+基於棧的參數+基於寄存器的參數(0x20個字節)。UNWIND_CODE中的信息包含了不可變寄存器的數量,以及棧上的局部變量和參數信息。

Figure 13 : Walking the x64 call stack
下面的調用棧中,棧幀#1(CreateFileW)所對應的棧幀空間時 0x160 個字節,下一節會告訴你,這個數值是如何計算出來的,以及調試器是如何計算棧幀#2的Child-SP的。注意:函數#1棧幀空間的值是在函數#2的Memory 欄。
0:000> knf # Memory Child-SP RetAddr Call Site 00 00000000`0029bbf8 000007fe`fdd24d76 ntdll!NtCreateFile 01 8 00000000`0029bc00 00000000`77ac2aad KERNELBASE!CreateFileW+0x2cd 02 160 00000000`0029bd60 000007fe`fe5b9ebd kernel32!CreateFileWImplementation+0x7d 03 60 00000000`0029bdc0 000007fe`fe55dc08 usp10!UniStorInit+0xdd 04 a0 00000000`0029be60 000007fe`fe5534af usp10!InitUnistor+0x1d8 . . .
下面是UNWIND_CODE的輸出信息。共有4個不可變寄存器被壓入棧中,分配了0x138字節的空間給局部變量和參數使用。
0:000> .fnent kernelbase!CreateFileW Debugger function entry 00000000`03be6580 for: (000007fe`fdd24ac0) KERNELBASE!CreateFileW | (000007fe`fdd24e2c) KERNELBASE!SbSelectProcedure Exact matches: KERNELBASE!CreateFileW = BeginAddress = 00000000`00004ac0 EndAddress = 00000000`00004b18 UnwindInfoAddress = 00000000`00059a48 Unwind info at 000007fe`fdd79a48, 10 bytes version 1, flags 0, prolog 14, codes 6 frame reg 0, frame offs 0 00: offs 14, unwind op 1, op info 0 UWOP_ALLOC_LARGE FrameOffset: 138 02: offs d, unwind op 0, op info 7 UWOP_PUSH_NONVOL 03: offs c, unwind op 0, op info 6 UWOP_PUSH_NONVOL 04: offs b, unwind op 0, op info 5 UWOP_PUSH_NONVOL 05: offs a, unwind op 0, op info 3 UWOP_PUSH_NONVOL
根據上面的分析,棧幀空間應該是0x138+(8*4)=0x158字節
0:000> ?138+(8*4) Evaluate expression: 344 = 00000000`00000158
再加上8個字節的返回地址,正好是0x160字節。這與調試命令`knf`所顯示的一致。
0:000> ?158+8 Evaluate expression: 352 = 00000000`00000160
根據`knf`命令的輸出,調試器在棧幀#01的RSP(00000000`0029bc00)基礎上加上0x160,正好可以得到棧幀#02的RSP,即:00000000`0029bd60
0:000> ?00000000`0029bc00+160 Evaluate expression: 2735456 = 00000000`0029bd60
所以,每一個棧幀所需的空間可以通過PE文件中的RUNTIME_FUNCTION,UNWIND_INFO以及UNWIND_CODE計算出。由於這個原因,調試器可以無需調試符號的情況下回溯棧幀。下面的調用棧是vmswitch模塊的狀態,雖然沒有調試符號,但是,這並不影響調試器正常地顯示和回溯棧幀。這里告訴了一個事實:X64調用棧可以在沒有調試符號的情況下回溯。
1: kd> kn # Child-SP RetAddr Call Site 00 fffffa60`005f1a68 fffff800`01ab70ee nt!KeBugCheckEx 01 fffffa60`005f1a70 fffff800`01ab5938 nt!KiBugCheckDispatch+0x6e . . . 21 fffffa60`01718840 fffffa60`0340b69e vmswitch+0x5fba 22 fffffa60`017188f0 fffffa60`0340d5cc vmswitch+0x769e 23 fffffa60`01718ae0 fffffa60`0340e615 vmswitch+0x95cc 24 fffffa60`01718d10 fffffa60`009ae31a vmswitch+0xa615 . . . 44 fffffa60`0171aed0 fffffa60`0340b69e vmswitch+0x1d286 45 fffffa60`0171af60 fffffa60`0340d4af vmswitch+0x769e 46 fffffa60`0171b150 fffffa60`034255a0 vmswitch+0x94af 47 fffffa60`0171b380 fffffa60`009ac33c vmswitch+0x215a0 . . .
0x04 參數找回(Parameter Retrieval)
在之前的章節中,我們通過調試器輸出的調用棧的信息剖析了X64的內部工作機理。在本節中,這些理論知識將被用於找回基於寄存器的參數。很不幸,並沒有什么特別有效的方法去找回這些參數,這里所介紹的技巧依賴於X64 匯編指令。如果參數不能在memory中找到,那么,並沒有什么簡單的方法去獲取這種參數。即便有調試符號,也沒有什么幫助,因為,調試符號會告訴相應函數的參數類型以及數量,但是,並不會告訴我們這些參數是什么。
0x05 技術總結(Summary of Techniques)
本節討論是假設X64函數並沒有使用/homeparams編譯,當使用了/homeparams,找回基於寄存器的參數並沒有意義,因為它們已經被放在棧上的homing parameters區域。同樣,無論是否使用/homeparams,第五個以及更高的參數也被放在棧上,所以,找回這些參數也不是什么問題。 在live debugging中,在函數上設置斷點是最簡單的方法去獲取傳入的參數,因為在函數的初始化階段,前四個參數肯定是放在RCX,RDX,R8和R9上的。 然而,在函數體內,參數寄存器的內容可能已經改變了,所以,在函數執行的任何時刻,確定寄存器參數的值,我們需要知道,這些參數是從哪里讀取的,以及將被寫入到什么地方?可以按照下面這些過程來回答這些問題:
- 參數是否是從內存中加載到寄存器中的,如果是的話,相應的內存位置存放參數值
- 參數是否是從不可變寄存器中加載的,並且,這些不可變寄存器被被調函數保存,如果是的話,不可變寄存器存放參數
- 參數是否是從寄存器中保存到內存中,如果是的話,相應的內存位置存放參數值
- 參數是否是保存到不可變寄存器中,並且,這些不可變寄存器被被調函數保存,如果是的話,不可變寄存器存放參數
在下面章節中,會用例子詳細描述上面介紹的技巧,每一個技巧都需要反匯編相應調用函數與被調函數。在圖 14 中,為了找出函數f2的參數,frame 02用於從源頭找出參數,frame 00用於從目標找出參數。

Figure 14 : Finding Register Based Parameters
- 識別參數的讀取目標(Identifying Parameter Sources)
這個技巧是用於識別被加載到參數寄存器的值所對應的源是什么,對常量、全局數據、棧地址和存放在棧上的數據有效。如圖15所示,反匯編X64Caller可以看到加載到RCX,RDX,R8和R9的值,被作為參數傳入X64Callee。

Figure 15 : Identifying parameter sources
下面的例子用這個技巧來找出函數NtCreateFile()的第三個參數的值
0:000> kn # Child-SP RetAddr Call Site 00 00000000`0029bbf8 000007fe`fdd24d76 ntdll!NtCreateFile 01 00000000`0029bc00 00000000`77ac2aad KERNELBASE!CreateFileW+0x2cd 02 00000000`0029bd60 000007fe`fe5b9ebd kernel32!CreateFileWImplementation+0x7d . . .
從函數NtCreateFile()的原型可以知道,第三個參數的類型是POBJECT_ATTRIBUTES
NTSTATUS NtCreateFile(
__out PHANDLE FileHandle,
__in ACCESS_MASK DesiredAccess,
__in POBJECT_ATTRIBUTES ObjectAttributes,
__out PIO_STATUS_BLOCK IoStatusBlock, .
.
. );
用返回地址反匯編調用者,顯示下面的指令。加載到R8寄存器的值是RSP+0xC8。根據上面`kn`命令的輸出,此時的RSP是函數KERNELBASE!CreateFileW的RSP,即:00000000`0029bc00
0:000> ub 000007fe`fdd24d76 KERNELBASE!CreateFileW+0x29d: 000007fe`fdd24d46 and ebx,7FA7h 000007fe`fdd24d4c lea r9,[rsp+88h] 000007fe`fdd24d54 lea r8,[rsp+0C8h] 000007fe`fdd24d5c lea rcx,[rsp+78h] 000007fe`fdd24d61 mov edx,ebp 000007fe`fdd24d63 mov dword ptr [rsp+28h],ebx 000007fe`fdd24d67 mov qword ptr [rsp+20h],0 000007fe`fdd24d70 call qword ptr [KERNELBASE!_imp_NtCreateFile]
手工重構被加載到R8的值
0:000> dt ntdll!_OBJECT_ATTRIBUTES 00000000`0029bc00+c8 +0x000 Length : 0x30 +0x008 RootDirectory : (null) +0x010 ObjectName : 0x00000000`0029bcb0 _UNICODE_STRING "\??\C:\Windows\Fonts\staticcache.dat" +0x018 Attributes : 0x40 +0x020 SecurityDescriptor : (null) +0x028 SecurityQualityOfService : 0x00000000`0029bc68
- 不可變寄存器做參數讀取目標(Non-Volatile Registers as parameter sources)
圖 16 顯示調用函數(X64Caller)和被調函數(X64Callee)的匯編代碼。從下面的匯編代碼可以看出,被加載到參數寄存器中的值是從不可變寄存器中讀取的,並且,這些不可變寄存器又被保存在被調函數的棧上。這些保存的值可以被找回,也就間接地說明之前傳入的參數也可以被找回。

Figure 16 : Non-Volatile Registers as parameter sources
下面的例子使用這個技巧,用於找回函數CreateFileW()的第一個參數
0:000> kn # Child-SP RetAddr Call Site 00 00000000`0029bbf8 000007fe`fdd24d76 ntdll!NtCreateFile 01 00000000`0029bc00 00000000`77ac2aad KERNELBASE!CreateFileW+0x2cd 02 00000000`0029bd60 000007fe`fe5b9ebd kernel32!CreateFileWImplementation+0x7d . . .
函數CreateFile()的原型如下,第一個參數的類型是LPCTSTR
HANDLE WINAPI
CreateFile(
__in LPCTSTR lpFileName,
__in DWORD dwDesiredAccess,
__in DWORD dwShareMode,
__in_opt LPSECURITY_ATTRIBUTES lpSecurityAttributes, .
.
. );
使用frame 1的返回地址,反匯編調用函數。加載到RCX的值是RDI,一個不可變寄存器。下一步是看看被調函數如何保存RDI
0:000> ub 00000000`77ac2aad L B kernel32!CreateFileWImplementation+0x4a: 00000000`77ac2a7a mov rax,qword ptr [rsp+90h] 00000000`77ac2a82 mov r9,rsi 00000000`77ac2a85 mov r8d,ebp 00000000`77ac2a88 mov qword ptr [rsp+30h],rax 00000000`77ac2a8d mov eax,dword ptr [rsp+88h] 00000000`77ac2a94 mov edx,ebx 00000000`77ac2a96 mov dword ptr [rsp+28h],eax 00000000`77ac2a9a mov eax,dword ptr [rsp+80h] 00000000`77ac2aa1 mov rcx,rdi 00000000`77ac2aa4 mov dword ptr [rsp+20h],eax 00000000`77ac2aa8 call kernel32!CreateFileW (00000000`77ad2c88)
反匯編被調函數,看看函數的初始階段指令。RDI是被指令`push rdi`壓入棧中,這個值與RCX的值一致。下一步是找回RDI的值
0:000> u KERNELBASE!CreateFileW KERNELBASE!CreateFileW: 000007fe`fdd24ac0 mov dword ptr [rsp+18h],r8d 000007fe`fdd24ac5 mov dword ptr [rsp+10h],edx 000007fe`fdd24ac9 push rbx 000007fe`fdd24aca push rbp 000007fe`fdd24acb push rsi 000007fe`fdd24acc push rdi 000007fe`fdd24acd sub rsp,138h 000007fe`fdd24ad4 mov edi,dword ptr [rsp+180h]
調試器的`.frame /r`命令顯示不可變寄存器的值,所以,可以用於找回上述的不可變寄存器RDI。下面的命令顯示RDI為000000000029beb0,這個值可以用於顯示CreateFile()函數的第一個參數file name.
0:000> .frame /r 2 02 00000000`0029bd60 000007fe`fe5b9ebd kernel32!CreateFileWImplementation+0x7d rax=0000000000000005 rbx=0000000080000000 rcx=000000000029bc78 rdx=0000000080100080 rsi=0000000000000000 rdi=000000000029beb0 rip=0000000077ac2aad rsp=000000000029bd60 rbp=0000000000000005 r8=000000000029bcc8 r9=000000000029bc88 r10=0057005c003a0043 r11=00000000003ab0d8 r12=0000000000000000 r13=ffffffffb6011c12 r14=0000000000000000 r15=0000000000000000 0:000> du /c 100 000000000029beb0 00000000`0029beb0 "C:\Windows\Fonts\staticcache.dat"
- 識別參數存儲目標(Identifying parameter destinations)
這個技巧是找出參數寄存器中的值是否被寫入內存。當函數使用/homeparams編譯時,函數的初始階段將保存寄存器參數到棧上的參數homing區域。然而,對於那些沒有使用/homeparams編譯的函數,參數寄存器的內容可能被寫入到任意的內存區域。圖17展示函數的匯編代碼,這里寄存器RCX,RDX,R8和R9的值被寫入棧上。所以,可以使用當前棧幀的RSP 來確定相應參數的內容。

Figure 17 : Identifying parameter destinations
下面的例子使用這個技巧找出函數DispatchClientMessage()的第三個和第四個參數的值。
0:000> kn # Child-SP RetAddr Call Site . . . 26 00000000`0029dc70 00000000`779ca01b user32!UserCallWinProcCheckWow+0x1ad 27 00000000`0029dd30 00000000`779c2b0c user32!DispatchClientMessage+0xc3 28 00000000`0029dd90 00000000`77c1fdf5 user32!_fnINOUTNCCALCSIZE+0x3c 29 00000000`0029ddf0 00000000`779c255a ntdll!KiUserCallbackDispatcherContinue . . .
函數的第三個和第四個參數分別被放置在R8和R9寄存器上。反匯編函數DispatchClientMessage(),查看R8 和R9被寫入到什么位置。可以看到這兩個寄存器分別被這兩條指令寫入棧上,’mov qword ptr [rsp+20h],r8’ and ’mov qword ptr [rsp+28h],r9’。由於這兩條指令並非在函數的初始階段,而只是函數體首部的一部分。值得注意的是,在保存r8,r9之前,很有可能這兩個寄存器的值已經被修改,所以,我們在使用這個技巧的時候,需要注意這個細節。當然,我們可以看到,這個例子中並沒有這樣的問題。
0:000> uf user32!DispatchClientMessage user32!DispatchClientMessage: 00000000`779c9fbc sub rsp,58h 00000000`779c9fc0 mov rax,qword ptr gs:[30h] 00000000`779c9fc9 mov r10,qword ptr [rax+840h] 00000000`779c9fd0 mov r11,qword ptr [rax+850h] 00000000`779c9fd7 xor eax,eax 00000000`779c9fd9 mov qword ptr [rsp+40h],rax 00000000`779c9fde cmp edx,113h 00000000`779c9fe4 je user32!DispatchClientMessage+0x2a (00000000`779d7fe3) user32!DispatchClientMessage+0x92: 00000000`779c9fea lea rax,[rcx+28h] 00000000`779c9fee mov dword ptr [rsp+38h],1 00000000`779c9ff6 mov qword ptr [rsp+30h],rax 00000000`779c9ffb mov qword ptr [rsp+28h],r9 00000000`779ca000 mov qword ptr [rsp+20h],r8 00000000`779ca005 mov r9d,edx 00000000`779ca008 mov r8,r10 00000000`779ca00b mov rdx,qword ptr [rsp+80h] 00000000`779ca013 mov rcx,r11 00000000`779ca016 call user32!UserCallWinProcCheckWow (00000000`779cc2a4) . . .
使用函數#27的RSP,可以分別找出r8和r9中的值
0:000> dp 00000000`0029dd30+20 L1 00000000`0029dd50 00000000`00000000 0:000> dp 00000000`0029dd30+28 L1 00000000`0029dd58 00000000`0029de70
-
參數的存儲目標是不可變寄存器(Non-Volatile Registers as Parameter Destinations)
圖18展示x64caller與x64callee的匯編代碼。左邊的代碼說明寄存器參數被存放在不可變寄存器(RDI,RSI, RBX,RBP)上,右邊的代碼說明這些不可變寄存器的值被保存在棧上,所以,我們可以間接地找出傳入的參數。

Figure 18 : Non-Volatile Registers as Parameter Destinations
下面的例子將找出函數CreateFileW ()的前4個參數(譯者注:原文是找出函數CreateFileWImplementation() 的參數,可能是作者的筆誤)
0:000> kn # Child-SP RetAddr Call Site 00 00000000`0029bbf8 000007fe`fdd24d76 ntdll!NtCreateFile 01 00000000`0029bc00 00000000`77ac2aad KERNELBASE!CreateFileW+0x2cd 02 00000000`0029bd60 000007fe`fe5b9ebd kernel32!CreateFileWImplementation+0x7d 03 00000000`0029bdc0 000007fe`fe55dc08 usp10!UniStorInit+0xdd
函數CreateFileWImplementation()完整的匯編代碼如下,從函數初始階段的指令來看,參數寄存器被保存在不可變寄存器中。注意:檢查在調用 CreateFileW之前,這些不可變寄存器沒有被修改過,這很重要!下一步是反匯編CreateFileW函數,找出這些保存參數的不可變寄存器是否被保存在棧上。
0:000> uf kernel32!CreateFileWImplementation kernel32!CreateFileWImplementation: 00000000`77ac2a30 mov qword ptr [rsp+8],rbx 00000000`77ac2a35 mov qword ptr [rsp+10h],rbp 00000000`77ac2a3a mov qword ptr [rsp+18h],rsi 00000000`77ac2a3f push rdi 00000000`77ac2a40 sub rsp,50h 00000000`77ac2a44 mov ebx,edx 00000000`77ac2a46 mov rdi,rcx 00000000`77ac2a49 mov rdx,rcx 00000000`77ac2a4c lea rcx,[rsp+40h] 00000000`77ac2a51 mov rsi,r9 00000000`77ac2a54 mov ebp,r8d 00000000`77ac2a57 call qword ptr [kernel32!_imp_RtlInitUnicodeStringEx (00000000`77b4cb90)] 00000000`77ac2a5d test eax,eax 00000000`77ac2a5f js kernel32!zzz_AsmCodeRange_End+0x54ec (00000000`77ae7bc0) . .
下面函數 CreateFileW()的匯編代碼可以看出,這些不可變寄存器都被保存在棧上,從而使用命令’.frame /r’ 來顯示這些值。
0:000> u KERNELBASE!CreateFileW KERNELBASE!CreateFileW: 000007fe`fdd24ac0 mov dword ptr [rsp+18h],r8d 000007fe`fdd24ac5 mov dword ptr [rsp+10h],edx 000007fe`fdd24ac9 push rbx 000007fe`fdd24aca push rbp 000007fe`fdd24acb push rsi 000007fe`fdd24acc push rdi 000007fe`fdd24acd sub rsp,138h 000007fe`fdd24ad4 mov edi,dword ptr [rsp+180h]
在棧幀#2上運行命令’.frame /r’,可以發現函數CreateFileWImplementation()棧幀上的不可變寄存器的值。
0:000> .frame /r 02 02 00000000`0029bd60 000007fe`fe5b9ebd kernel32!CreateFileWImplementation+0x7d rax=0000000000000005 rbx=0000000080000000 rcx=000000000029bc78 rdx=0000000080100080 rsi=0000000000000000 rdi=000000000029beb0 rip=0000000077ac2aad rsp=000000000029bd60 rbp=0000000000000005 r8=000000000029bcc8 r9=000000000029bc88 r10=0057005c003a0043 r11=00000000003ab0d8 r12=0000000000000000 r13=ffffffffb6011c12 r14=0000000000000000 r15=0000000000000000 iopl=0 nv up ei pl zr na po nc cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000244 kernel32!CreateFileWImplementation+0x7d: 00000000`77ac2aad mov rbx,qword ptr [rsp+60h] ss:00000000`0029bdc0={usp10!UspFreeForUniStore (000007fe`fe55d8a0)}
觀察相應的mov指令可以看到不可變寄存器與參數之間的關系,如下:
- P1 = RCX = RDI = 000000000029beb0
- P2 = EDX = EBX = 0000000080000000
- P3 = R8D = EBP = 0000000000000005
- P4 = R9 = RSI = 0000000000000000
使用上面的技能找回 x64 調用棧上的參數的時候,可能會比較耗時和麻煩。CodeMachine 提供了一個 windbg extension,可以自動完成上述的過程,找回參數,有興趣可以繼續閱讀相關的文章。
http://www.codemachine.com/tool_cmkd.html#stack
