深刻理解Windows X64調試

隨着64位操做系統的普及,都開始大力進軍x64,X64下的調試機制也發生了改變,與x86相比,添加了許多本身的新特性,以前學習了Windows x64的調試機制,這裏本着「拿來主義」的原則與你們分享。php

本文屬於譯文,英文原文連接:http://www.codemachine.com/article_x64deepdive.htmlhtml

翻譯原文地址:深刻Windows X64 調試express

在正式開始這篇譯文以前,譯者先定義下面兩個關於棧幀的翻譯:windows

  • 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)。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平臺,一些寄存器的用法已經發生變化,這些變化能夠按以下分組:佈局

  1. 不可變寄存器是那些在函數調用過程當中,值被保存起來的寄存器。X64平臺擁有一個擴展的不可變寄存器集合,在這個集合中,之前x86平臺下原有的不可變寄存器也包含在內,新增的寄存器是從R12到R15,這些寄存器對於函數參數的恢復很重要。
  2. Fastcall寄存器用於傳遞函數參數。Fastcall是X64平臺上默認的調用約定,前4個參數經過RCX, RDX, R8, R9傳遞。
  3. RBP再也不用做棧幀寄存器。如今RBP和RBX,RCX同樣都是通用寄存器,調試前再也不使用RBP來回溯調用棧。
  4. 在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

相關文章
相關標籤/搜索