隨着64位操做系統的普及,都開始大力進軍x64,X64下的調試機制也發生了改變,與x86相比,添加了許多本身的新特性,以前學習了Windows x64的調試機制,這裏本着「拿來主義」的原則與你們分享。php
本文屬於譯文,英文原文連接:http://www.codemachine.com/article_x64deepdive.htmlhtml
翻譯原文地址:深刻Windows X64 調試express
在正式開始這篇譯文以前,譯者先定義下面兩個關於棧幀的翻譯:windows
這個教程討論一些在 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)。app
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平臺上,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
若是知足必定的規則之後,X64編譯器會執行內聯函數的擴展,這樣會將全部內聯函數的調用部分用函數體來替換。內聯函數的優勢是避免函數調用過程當中的棧幀建立以及函數退出時的棧平衡,缺點是因爲指令的重複對致使可執行程序的大小增大很多,同時,也會致使cache未命中和page fault的增長。內聯函數一樣也會影響調試,由於當用戶嘗試在內聯函數上設置斷點時,調試器是不能找到對應的符號的。源碼級別的內聯能夠經過編譯器的/Ob flag 進行控制,而且能夠經過__declspec(noinline)禁止一個函數的內聯過程。圖1顯示函數2和函數3被內聯到函數1的過程。
Figure 1 : Function In-lining
X64編譯器可使用jump指令替換函數體內最後的call指令,經過這種方式來優化函數的調用過程。這種方法能夠避免被調函數的棧幀建立,調用函數與被調函數共享相同的棧幀,而且,被調函數能夠直接返回到本身爺爺級別的調用函數,這種優化在調用函數與被調函數擁有相同參數的狀況下格外有用,由於若是相應的參數已經被放在指定的寄存器中,而且沒有改變,那麼,它們就不用被從新加載。圖2顯示了TCE,咱們在函數1的最後調用函數4:
Figure 2 : Tail Call Elimination
在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)
在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調用棧上特有的內容。
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)
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結構能夠描述下面的一種操做,這些操做都會在函數首部中執行。
因此,本質上,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
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)
雖然前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
如上一節所描述,全部的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函數的棧幀包括下面內容:
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)
調試命令`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
在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 . . .
在以前的章節中,咱們經過調試器輸出的調用棧的信息剖析了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
這個技巧是用於識別被加載到參數寄存器的值所對應的源是什麼,對常量、全局數據、棧地址和存放在棧上的數據有效。如圖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
圖 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"
這個技巧是找出參數寄存器中的值是否被寫入內存。當函數使用/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
圖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指令能夠看到不可變寄存器與參數之間的關係,以下:
使用上面的技能找回 x64 調用棧上的參數的時候,可能會比較耗時和麻煩。CodeMachine 提供了一個 windbg extension,能夠自動完成上述的過程,找回參數,有興趣能夠繼續閱讀相關的文章。