深入X64架構(翻譯轉載)(4)之參數獲取

上一節中,通過研究調試器棧回溯輸出的每一個細節解釋了X64的棧是如何工作的。本節中,這些理論會用來獲取X64函數基於寄存器傳遞的參數,不幸的是,沒有任何靈丹妙藥來查找參數,所有的技巧都嚴重依賴於編譯器產生的彙編代碼,如果參數在不可達的內存中,就沒有任何方式可以獲取到它,就算你有棧中函數和模塊的私有符號也無法幫助太多,私有符號告知函數參數的類型和數量,但是並不會告訴你這些參數的值是什麼。

技術概要

本節假設X64 函數均不採用/homeparams選項來編譯。因為如果採用/homeparams來編譯,獲取基於寄存器傳遞的參數以及不那麼重要了,因為這些參數最終會被歸位至棧空間。無論是否開啟/homeparams,第五或者更多的參數會直接採用棧來傳遞,所以獲取基於棧傳遞的參數在任何情況下都不應該存在問題。

現場調試過程中,在函數的開始處設置斷點是獲取函數參數的最簡單的方法,因為在函數的前置指令執行期間,RCX、RDX、R8、R9分別存儲的前四個參數都是有效的。

然而代碼執行到函數體內的時候,參數寄存器的值被改變,導致初始值被複寫了。所以,在函數執行期間去查找基於寄存器傳遞的參數值的時候,必須搞清楚-參數值是從何處讀取的,被寫到什麼地方去了。這些問題的答案可以通過執行調試器的指令序列得到,總結如下:

  • 確認參數是否是從內存載入進入寄存器的,如果是,通過這些內存地址可以發現參數值。
  • 確認參數是否從非易失性寄存器讀取的,並且這些寄存器的值被被調用的函數保存。如果是這樣,被保存的非易失性寄存器的值可以用來決定參數值。
  • 確認參數是否從寄存器保存至內存,如果是這樣,參數可以從內存當中進行獲取。
  • 如果參數被保存入非易失性寄存器,這些寄存器的值被函數保存了。如果是這樣的話,參數的值可以通過被保存的非易失性寄存器的值來決定。

在接下來的幾節中,會通過示例來展示每一種技巧的用法。每一種技術都需要反彙編參數傳遞經過的調用和被調用函數。圖14中,為了找到函數F2的參數,Frame2需要被反彙編來查找參數的來源,而Frame0需要被反彙編來定位參數的去處。

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 Site00 00000000`0029bbf8 000007fe`fdd24d76 ntdll!NtCreateFile01 00000000`0029bc00 00000000`77ac2aad KERNELBASE!CreateFileW+0x2cd02 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,);

對Frame#0的返回地址進行反彙編發現,函數的第三個參數R8寄存器中的值是Rsp+0xc8.前面「kn」命令顯示了Kernelbase!CreateFileW執行中Rsp寄存器的值是00000000`0029bc00。

0:000> ub 000007fe`fdd24d76KERNELBASE!CreateFileW+0x29d:000007fe`fdd24d46 and ebx,7FA7h000007fe`fdd24d4c lea r9,[rsp+88h]000007fe`fdd24d54 lea r8,[rsp+0C8h]000007fe`fdd24d5c lea rcx,[rsp+78h]000007fe`fdd24d61 mov edx,ebp000007fe`fdd24d63 mov dword ptr [rsp+28h],ebx000007fe`fdd24d67 mov qword ptr [rsp+20h],0000007fe`fdd24d70 call qword ptr [KERNELBASE!_imp_NtCreateFile]

通過上面找到的被R8寄存器載入的值,可以重建OBJECT_ATTRIBUTE,如果下面的輸出所示。

0:000> dt ntdll!_OBJECT_ATTRIBUTES 00000000`0029bc00+c8 +0x000 Length : 0x30 +0x008 RootDirectory : (null) +0x010 ObjectName : 0x00000000`0029bcb0 _UNICODE_STRING "??C:WindowsFontsstaticcache.dat" +0x018 Attributes : 0x40 +0x020 SecurityDescriptor : (null) +0x028 SecurityQualityOfService : 0x00000000`0029bc68

非易失寄存器做為參數來源

如果參數寄存器的值來源於非易失性寄存器,而非易失性寄存器的值被保存在了棧上,那麼可以採用下面的技術來定位參數值。

圖16顯示了調用函數(X64caller)和被調用者(X64Callee)的反彙編。X64Caller調用X64Callee前面的指令表明參數寄存器(RCX,RDX,R8,R9)的值來源於非易失性寄存器(RDI,R12,RBX,RBP). 右邊圖中X64Callee的前置指令將非易失性寄存器的值保存至棧上,這些值可以被找到,其實就是原先被寫入參數寄存器的值。

Figure 16 : Non-Volatile Registers as parameter sources

下面的例子採用上面的技術來查找CreateFileW()的第一個參數值。

0:000> kn # Child-SP RetAddr Call Site00 00000000`0029bbf8 000007fe`fdd24d76 ntdll!NtCreateFile01 00000000`0029bc00 00000000`77ac2aad KERNELBASE!CreateFileW+0x2cd02 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當中的函數返回值得到如下指令。參數1對應的寄存器RCX的值來自非易失性寄存器RDI,下一步就是需要確認CreateFileW()是否保存了EDI的值。

0:000> ub 00000000`77ac2aad L Bkernel32!CreateFileWImplementation+0x4a:00000000`77ac2a7a mov rax,qword ptr [rsp+90h]00000000`77ac2a82 mov r9,rsi00000000`77ac2a85 mov r8d,ebp00000000`77ac2a88 mov qword ptr [rsp+30h],rax00000000`77ac2a8d mov eax,dword ptr [rsp+88h]00000000`77ac2a94 mov edx,ebx00000000`77ac2a96 mov dword ptr [rsp+28h],eax00000000`77ac2a9a mov eax,dword ptr [rsp+80h]00000000`77ac2aa1 mov rcx,rdi00000000`77ac2aa4 mov dword ptr [rsp+20h],eax00000000`77ac2aa8 call kernel32!CreateFileW (00000000`77ad2c88)

反彙編CreateFileW可以查看該函數的前置指令。RDI寄存器的值同步「push rdi」保存至棧中,該值和參數寄存器ECX當中的值應該是保持一致的,下一步就是要從棧當中找到RDI的值。

0:000> u KERNELBASE!CreateFileWKERNELBASE!CreateFileW:000007fe`fdd24ac0 mov dword ptr [rsp+18h],r8d000007fe`fdd24ac5 mov dword ptr [rsp+10h],edx000007fe`fdd24ac9 push rbx000007fe`fdd24aca push rbp000007fe`fdd24acb push rsi000007fe`fdd24acc push rdi000007fe`fdd24acd sub rsp,138h000007fe`fdd24ad4 mov edi,dword ptr [rsp+180h]

「.frame /r」命令可以在特定函數執行過程中顯示非易失性寄存器的值,是通過獲取被調用函數前置指令保存在棧中的值來實現的。可以看到當CreateFileWImplementation()調用CreateFileW()時,EDI的值是000000000029beb0。該值可以用來展示傳遞給CreateFile的第一個參數。

0:000> .frame /r 202 00000000`0029bd60 000007fe`fe5b9ebd kernel32!CreateFileWImplementation+0x7drax=0000000000000005 rbx=0000000080000000 rcx=000000000029bc78rdx=0000000080100080 rsi=0000000000000000 rdi=000000000029beb0rip=0000000077ac2aad rsp=000000000029bd60 rbp=0000000000000005 r8=000000000029bcc8 r9=000000000029bc88 r10=0057005c003a0043r11=00000000003ab0d8 r12=0000000000000000 r13=ffffffffb6011c12r14=0000000000000000 r15=00000000000000000:000> du /c 100 000000000029beb000000000`0029beb0 "C:WindowsFontsstaticcache.dat"

確定參數的最終地址

該技術用來確認參數寄存器中的值是否被寫入內存。當函數採用/homeparams來編譯的時候,函數前置指令默認會將參數寄存器的值寫入棧上的參數歸位空間。然而當函數未採用/homeparams來編譯的時候,參數寄存器的內容可能會在函數體內被寫入任何地址。

圖17中函數將寄存器RCX,RDX,R8,R9的值寫入了棧空間。參數值可以通過當前幀的棧指針的值定位的內存空間的值來定位。

Figure 17 : Identifying parameter destinations

下面的例子通過上面的技術來確定DispatchClientMessage的第三和第四個參數。

0:000> kn # Child-SP RetAddr Call Site26 00000000`0029dc70 00000000`779ca01b user32!UserCallWinProcCheckWow+0x1ad27 00000000`0029dd30 00000000`779c2b0c user32!DispatchClientMessage+0xc328 00000000`0029dd90 00000000`77c1fdf5 user32!_fnINOUTNCCALCSIZE+0x3c29 00000000`0029ddf0 00000000`779c255a ntdll!KiUserCallbackDispatcherContinue. . .

DispatchClientMessage的第三和第四個參數分別位於寄存器R8和R9當中,反彙編該函數,找到任何將R8或者R9寄存器內容寫入內存的操作,會發現有如下兩個指令"mov qword ptr [rsp+28h], r9"

and "mov qword ptr [rsp+20h], r8",表明寄存器的值被寫入棧中。這些指令不是函數的前置指令,但卻在整個大函數體中。需要關注的是,R8和R9的值在被寫入寄存器之前是否被修改。雖然對於DispatchClientMessage來說這種情況沒有發生,但是採用這種技術的時候需要時刻關注參數寄存器是否被複寫。

0:000> uf user32!DispatchClientMessageuser32!DispatchClientMessage:00000000`779c9fbc sub rsp,58h00000000`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,eax00000000`779c9fd9 mov qword ptr [rsp+40h],rax00000000`779c9fde cmp edx,113h00000000`779c9fe4 je user32!DispatchClientMessage+0x2a (00000000`779d7fe3)user32!DispatchClientMessage+0x92:00000000`779c9fea lea rax,[rcx+28h]00000000`779c9fee mov dword ptr [rsp+38h],100000000`779c9ff6 mov qword ptr [rsp+30h],rax00000000`779c9ffb mov qword ptr [rsp+28h],r900000000`779ca000 mov qword ptr [rsp+20h],r800000000`779ca005 mov r9d,edx00000000`779ca008 mov r8,r1000000000`779ca00b mov rdx,qword ptr [rsp+80h]00000000`779ca013 mov rcx,r1100000000`779ca016 call user32!UserCallWinProcCheckWow (00000000`779cc2a4)...

從上面」kn」的輸出,發現Frame#27的棧指針(RSP)的值是00000000`0029dd30, 加上偏移得到R8寄存器的是0000000`00000000,這便是DispatchClientMessage()第三個參數的值。

0:000> dp 00000000`0029dd30+20 L100000000`0029dd50 00000000`00000000

同理可以得到R9的值為00000000`0029de70,該值為DispatchClientMessage()的第四個參數。

0:000> dp 00000000`0029dd30+28 L100000000`0029dd58 00000000`0029de70

非易失性寄存器作為參數最終目的地

該技術討論的是參數寄存器的內容被函數被保存至非易失性寄存器中,隨後該寄存器的值被函數保存至棧中的情況。

圖18顯示的是調用者(X64Caller)和被調用者(X64Callee)。基於寄存器的參數的值被傳遞給函數X64Caller.X64Caller包含將參數寄存器(RCX,RDX,R8,R9)的值保存至非易失性寄存器(RDI,RSI,RBX,RBP).X64Callee的前置指令將這些非易失性寄存器的值保存至棧中,這樣的話可以方便的獲取他們的值,再由這些值可以輕鬆的得到參數寄存器的值。

Figure 18 : Non-Volatile Registers as Parameter Destinations

下面的例子採用這個技術來查找CreateFileWImplementation()的四個基於寄存器傳遞的參數。

0:000> kn # Child-SP RetAddr Call Site00 00000000`0029bbf8 000007fe`fdd24d76 ntdll!NtCreateFile01 00000000`0029bc00 00000000`77ac2aad KERNELBASE!CreateFileW+0x2cd02 00000000`0029bd60 000007fe`fe5b9ebd kernel32!CreateFileWImplementation+0x7d03 00000000`0029bdc0 000007fe`fe55dc08 usp10!UniStorInit+0xdd

通過CreateFileWImplementation()的反彙編代碼可以發現,在函數前置指令執行之後,"mov ebx,edx", "mov

rdi,rcx", mov rsi,r9" 和 "mov ebp,r8d"將參數寄存器的值保存入非易失性寄存器。必須檢查到下一個函數調用之前的指令,確保非易失性寄存器的值沒有被複寫。雖然這裡沒有顯示錶明,這條規則已經通過檢查從CreateFileWImplementation()到CreateFileW()的所有代碼來保證。下一步就是通過反彙編CreateFileW()的前置指令來確定這些非易失性寄存器的值是否被保存至棧中。

0:000> uf kernel32!CreateFileWImplementationkernel32!CreateFileWImplementation:00000000`77ac2a30 mov qword ptr [rsp+8],rbx00000000`77ac2a35 mov qword ptr [rsp+10h],rbp00000000`77ac2a3a mov qword ptr [rsp+18h],rsi00000000`77ac2a3f push rdi00000000`77ac2a40 sub rsp,50h00000000`77ac2a44 mov ebx,edx00000000`77ac2a46 mov rdi,rcx00000000`77ac2a49 mov rdx,rcx00000000`77ac2a4c lea rcx,[rsp+40h]00000000`77ac2a51 mov rsi,r900000000`77ac2a54 mov ebp,r8d00000000`77ac2a57 call qword ptr [kernel32!_imp_RtlInitUnicodeStringEx (00000000`77b4cb90)]00000000`77ac2a5d test eax,eax00000000`77ac2a5f js kernel32!zzz_AsmCodeRange_End+0x54ec (00000000`77ae7bc0)...

下面的輸出顯示了CreateFileW()將非易失性寄存器(rbx, rbp, rsi and edi)的值寫入堆棧,使得可以通過「.frame /r」命令去顯示他們的值。

0:000> u KERNELBASE!CreateFileWKERNELBASE!CreateFileW:000007fe`fdd24ac0 mov dword ptr [rsp+18h],r8d000007fe`fdd24ac5 mov dword ptr [rsp+10h],edx000007fe`fdd24ac9 push rbx000007fe`fdd24aca push rbp000007fe`fdd24acb push rsi000007fe`fdd24acc push rdi000007fe`fdd24acd sub rsp,138h000007fe`fdd24ad4 mov edi,dword ptr [rsp+180h]

在包含CreateFileWImplementation() 的frame#2上運行「.frame /r」可以顯示該幀被激活時這些寄存器的值。

0:000> .frame /r 0202 00000000`0029bd60 000007fe`fe5b9ebd kernel32!CreateFileWImplementation+0x7drax=0000000000000005 rbx=0000000080000000 rcx=000000000029bc78rdx=0000000080100080 rsi=0000000000000000 rdi=000000000029beb0rip=0000000077ac2aad rsp=000000000029bd60 rbp=0000000000000005 r8=000000000029bcc8 r9=000000000029bc88 r10=0057005c003a0043r11=00000000003ab0d8 r12=0000000000000000 r13=ffffffffb6011c12r14=0000000000000000 r15=0000000000000000iopl=0 nv up ei pl zr na po nccs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000244kernel32!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提供了一個擴展命令可以自動完成這些操作,該命令嘗試獲取並顯示線程棧當中所有函數的參數。在Usermode調試的時候,如果想獲取指定線程的參數,可以通過"~s"命令來切換線程,這與在Kernel mode調試的時候採用".thread"來切換線程是類似的。

本文涵蓋了X64編譯器的優化功能,這些優化使得其與X86存在著極大差異。介紹了X64的異常處理機制,並詳細解釋了可執行文件格式和數據結構是如何來支持改特性的。討論了X64是如何在運行時建立棧幀的,並利用該理論去獲取函數基於寄存器傳遞的參數值,並最終克服了X64體系的這一頑疾。

參考文獻:

1、 codemachine.com/article

2、 blog.csdn.net/xbgprogra

3、 blog.csdn.net/xbgprogra

4、 blog.csdn.net/woxiaohah

5、 框架指針省略FPO

cnblogs.com/awpatp/arch

(完結)


推薦閱讀:

利用WinDbg本地內核調試器攻陷 Windows 內核

TAG:WinDbg | x8664 | 軟體調試 |