天天看點

windbg中所謂的上下文

    <軟體調試>30.7節簡要的提到了windbg調試上下文的概念:如會話/程序/寄存器等上下文。為了深入了解背後的含義,我翻開windbg幫助文檔,發現其對程序/寄存器上下文的解釋最為晦澀。隻能通過實際的練習,趟着石子過河,最後記錄下自己的總結。

    windbg中與程序上下文相關的指令是.process,與寄存器上下文相關的指令是.cxr(或.thread指令),我逐一總結這兩個指令的作用。

1. .cxr指令

    windbg幫助文檔解釋.cxr的作用是獲得/修改線程的寄存器上下文,并最終影響棧回溯的輸出結果。初看幫助文檔,還以為.cxr指令和r指令有相同的地方----都用于修改寄存器的值,然而在windbg幫助文檔<Register Context>節中明确寫道.cxr(及.thread,.trap)指令并不能實際修改硬體寄存器的值,原話如下:

"These commands do not change the values of the CPU registers. Instead, the debugger retrieves the specified register context from its location in memory. Actually, the debugger can retrieve only the
 saved register values. (Other values are set dynamically and are not saved. The saved values are sufficient to re-create a stack trace. "      

    我印象中調試器修改寄存器的值(windbg r指令可能也是這樣實作)是依次調用SuspendThread挂起執行中的線程,然後調用GetThreadContext/SetThreadContext獲得/修改寄存器值,最終調用ResumeThread恢複線程,以影響原程式執行的過程。既然.cxr指令也用于獲得/修改寄存器上下文,我推斷這個指令的實作和上述過程一緻。

    然而,調試的經曆又一次否認了我的認知,以下面一段代碼為例(及部分反彙編代碼):

#include <stdio.h>

int main()
{
  int i=0;
  i++;
  _asm int 3;
  i++;

  printf("%d\n",i);
}      
; 6    :  i++;

  0001f 8b 45 fc   mov   eax, DWORD PTR _i$[ebp]
  00022 83 c0 01   add   eax, 1
  00025 89 45 fc   mov   DWORD PTR _i$[ebp], eax

; 7    :  _asm int 3;

  00028 cc     int   3

; 8    :  i++;

  00029 8b 4d fc   mov   ecx, DWORD PTR _i$[ebp]
  0002c 83 c1 01   add   ecx, 1
  0002f 89 4d fc   mov   DWORD PTR _i$[ebp], ecx      

我的想法是:調試器中斷前,i=1,調試器中斷後,逐條指令單步運作并用.cxr指令修改偏移0x2C處ecx的值為0x09。如果最終printf顯示的值為0x0A(十進制的10),則說明.cxr和r指令等效,否則需要再次研讀windbg幫助文檔以确認其功能。

0:000> g @讓windbg運作到int 3處,觸發中斷
*** WARNING: Unable to verify checksum for cxr.exe
cxr!main+0x28:
00401038 cc              int     3 @到這觸發中斷
0:000> l-t @單步逐指令執行
Source options are 0:
    None
0:000> t @準備執行第二條i++語句
cxr!main+0x29:
00401039 8b4dfc          mov     ecx,dword ptr [ebp-4] ss:0023:0012ff7c=00000001
0:000> t @給ecx指派
ecx=00000001
cxr!main+0x2c:
0040103c 83c101          add     ecx,1
0:000> r ecx
ecx=00000001
0:000> .dvalloc 0x1000 @.cxr /w指令可以将寄存器上下文儲存到指定區域,是以這裡先配置設定這樣一塊區域,用于儲存和修改ecx的值
Allocated 1000 bytes starting at 003f0000 @配置設定到的虛拟位址為 0x3f0000
0:000> dt ntdll!_CONTEXT 003f0000 @在運作.cxr /w簽名儲存寄存器上下文前先看下原始的記憶體值
   +0x000 ContextFlags     : 0
   +0x0ac Ecx              : 0
   +0x0b0 Eax              : 0
0:000> .cxr /w 003f0000 @儲存上下文
Context written to 003f0000
0:000> dt ntdll!_CONTEXT 003f0000
   +0x000 ContextFlags     : 0x1003f
   +0x0ac Ecx              : 1
   +0x0b0 Eax              : 1

0:000> ed 003f0000+0x0ac 0x09 @修改儲存的寄存器上下文中CONTEXT!ECX的值
0:000> .cxr  003f0000 @使用儲存在0x3f0000處的寄存器上下文
eax=00000001 ebx=7ffd4000 ecx=00000009 edx=00430dc0 esi=00f8f7a0 edi=0012ff80
eip=0040103c esp=0012ff30 ebp=0012ff80 iopl=0         nv up ei pl nz na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000202
cxr!main+0x2c:
0040103c 83c101          add     ecx,1
0:000> r ecx 
Last set context: @注意1
ecx=00000009  @使用新的寄存器上下文後,寄存器ecx顯示的值的确變成了0x09
0:000> t @再次單步運作,你會驚奇的發現,此時ecx的值又變成了0x02
eax=00000001 ebx=7ffd4000 ecx=00000002 edx=00430dc0 esi=00f8f7a0 edi=0012ff80
cxr!main+0x2f:
0040103f 894dfc          mov     dword ptr [ebp-4],ecx ss:0023:0012ff7c=00000001
0:000> l+t
Source options are 1:
     1/t - Step/trace by source line
0:000> g      

以上是調試過程,結果不是很明顯,讓我們來看下實際運作的結果截圖,令我震驚的是,盡管我确實修改過ecx在寄存器上下文的值,但最終結果依然是2,與預期想去甚遠。

windbg中所謂的上下文

    記得在調試異常或dump檔案時,!analyiz -v的輸出中往往會儲存發生異常時寄存器上下文的位址,然後用.cxr指令恢複到異常發生時的上下文并檢視調用堆棧,以此來分析異常的原因。 這個過程用<軟體調試>作者的話叫做穿越回異常發生時的場景。我仔細回味了這個過程若幹次,突然回想到一個細節:

1.發生異常時,不切換寄存器上下文,直接kP檢視函數堆棧,調用棧往往顯示的是windows如何進入函數RaiseException或者KeBugCheck以及執行KeBugCheck時寄存器的值;

2.如果使用.cxr指令先切換寄存器上下文然後在調用kP指令檢視調用棧,尚且能觸發異常的函數調用,以及調用這個函數的寄存器。

由此,我猜測.cxr指令的作用僅僅是控制windbg的顯示輸出,并不能 改變程式的執行流程。為了驗證我的猜測,我仍以上面的代碼為例,修改寄存器上下文。這次僅修改Ebp的值以影響windbg的棧回溯輸出(k指令以ebp為棧回溯的起點):

0:000> g @調試運作,使程式觸發代碼中的int3斷點
cxr!main+0x28:
00401038 cc              int     3
0:000> kP @檢視調用棧
ChildEBP RetAddr  
0012ff80 00401229 cxr!main+0x28 [C:\Documents and Settings\Administrator\桌面\studio\cxr.cpp @ 7]
0012ffc0 7c817067 cxr!mainCRTStartup+0xe9 [crt0.c @ 206]
0012fff0 00000000 kernel32!BaseProcessStart+0x23
0:000> r ebp @修改寄存器上下文前,ebp儲存進入main函數時的棧幀
ebp=0012ff80
0:000> dd [ebp] L1
0012ff80  0012ffc0 @0012ffc0儲存前一個函數的棧幀,即mainCRTStartup的棧幀
0:000> dd 0012ffc0 L1
0012ffc0  0012fff0 @0012fff0儲存再前一個函數的棧幀,即kernel32!BaseProcessStart的棧幀
0:000> dd ebp L2
0012ff80  0012ffc0 00401229 @00401229是main函數執行完後的傳回位址,傳回到mainCRTStartup中
0:000> u 00401229 @檢視00401229處的反彙編
cxr!mainCRTStartup+0xe9 [crt0.c @ 206]:
00401229 83c40c          add     esp,0Ch
0040122c 8945e4          mov     dword ptr [ebp-1Ch],eax
0040122f 8b55e4          mov     edx,dword ptr [ebp-1Ch]

0:000> .dvalloc 0x1000 @配置設定用于儲存和修改寄存器上下文的空間
Allocated 1000 bytes starting at 00530000
0:000> .cxr /w 00530000 @将目前寄存器上下文存放到剛配置設定的空間中
Context written to 00530000
0:000> dt ntdll!_CONTEXT 00530000
   +0x000 ContextFlags     : 0x1003f
   +0x0b0 Eax              : 1 @前面代碼中執行過i++,是以eax==1
   +0x0b4 Ebp              : 0x12ff80 @目前函數的棧幀
   +0x0b8 Eip              : 0x401038 
0:000> ed 00530000+0x0b4 0012fff0 @修改_CONTEXT!Ebp的值,使得從[ebp]中取到錯誤的函數棧
0:000> .cxr 00530000 @使用被修改後的寄存器上下文。使得之後windbg的分析結果(注意我的用詞,是分析結果,不是執行結果)都基于現在的寄存器上下文
eip=00401038 esp=0012ff30 ebp=0012fff0 iopl=0         nv up ei pl nz na po nc
cxr!main+0x28:
00401038 cc              int     3
0:000> r ebp
Last set context: @注意此處,"Last set context",windbg提示我們現在的分析是基于修改後的寄存器上下文;正常的r指令是沒有這樣的提示的
ebp=0012fff0
0:000> kP @此時的棧回溯是不正确的,因為中間的函數幀被我跳過了。和前一個指令一樣,windbg提示目前的棧回溯是基于前一次上下文修改的結果
  *** Stack trace for last set context - .thread/.cxr resets it
ChildEBP RetAddr  
0012fff0 00000000 cxr!main+0x28 [C:\Documents and Settings\Administrator\桌面\studio\cxr.cpp @ 7]
0:000> .cxr @将寄存器上下文恢複為預設情況(即真正的上下文)
Resetting default scope
0:000> kP @恢複後,棧回溯的輸出恢複正常
ChildEBP RetAddr  
0012ff80 00401229 cxr!main+0x28 [C:\Documents and Settings\Administrator\桌面\studio\cxr.cpp @ 7]
0012ffc0 7c817067 cxr!mainCRTStartup+0xe9 [crt0.c @ 206]
0012fff0 00000000 kernel32!BaseProcessStart+0x23      

    根據上面我的調試步驟和注釋,可以看到.cxr指令其實是修改windbg分析環境,使得windbg從特定的位置取值分析并輸出,但并不影響windbg的執行環境。為什麼說并不影響執行環境?我們可以看看修改了寄存器上下文後(僞造的函數棧),使windbg從main函數執行傳回的位置。如果修改寄存器上下文影響到windbg的執行環境,那麼從main函數傳回後,windbg應該傳回到0x0000000處,并将ebp的值恢複為12FFF0:

0:000> .cxr 00530000 @前面的調試過程中,将寄存器上下文恢複為正常;現在要重新切換到被僞造的寄存器上下文
eax=00000001 ebx=7ffde000 ecx=00000000 edx=00430dc0 esi=00f8f7a0 edi=0012ff80
eip=00401038 esp=0012ff30 ebp=0012fff0 iopl=0         nv up ei pl nz na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000202
cxr!main+0x28:
00401038 cc              int     3
0:000> kP @僞造的寄存器上下文。如果.cxr對執行流有影響,windbg将從main函數中ret到0x00處,并将ebp恢複成0x12fff0。這顯然會觸發通路非法記憶體的異常
  *** Stack trace for last set context - .thread/.cxr resets it
ChildEBP RetAddr  
0012fff0 00000000 cxr!main+0x28 [C:\Documents and Settings\Administrator\桌面\studio\cxr.cpp @ 7]
0:000> uf cxr!main @查找main函數的傳回位址并準備下斷點
cxr!main [C:\Documents and Settings\Administrator\桌面\studio\cxr.cpp @ 4]:
...
    7 00401038 cc              int     3
    8 00401039 8b4dfc          mov     ecx,dword ptr [ebp-4]
    8 0040103c 83c101          add     ecx,1
    8 0040103f 894dfc          mov     dword ptr [ebp-4],ecx
   10 00401042 8b55fc          mov     edx,dword ptr [ebp-4]
   10 00401045 52              push    edx
   10 00401046 681c004200      push    offset cxr!`string' (0042001c)
   10 0040104b e830000000      call    cxr!printf (00401080)
   10 00401050 83c408          add     esp,8
...
   11 00401063 c3              ret
0:000> bp 00401063 @在main函數傳回處下斷點,用于觀察windbg的執行流是否真的受到.cxr的影響
0:000> g
Breakpoint 0 hit
cxr!main+0x53:
00401063 c3              ret
0:000> l-t @單步逐條指令運作
Source options are 0:
    None
0:000> t @執行ret指令
eip=00401229 esp=0012ff88 ebp=0012ffc0 @執行ret指令後,ebp是0012ffc0,即原來mainCRTStartup函數的函數幀,并且eip不為0x00000000
cxr!mainCRTStartup+0xe9:
00401229 83c40c          add     esp,0Ch
0:000> ln 00401229 @檢視eip附近的符号為mainCRTStartup,由此可見main函數ret後還是進入了mainCRTStartup。并沒有.cxr的影響直接進入00000000
crt0.c(206)+0x19 
(00401140)   cxr!mainCRTStartup+0xe9   |  (00401270)      

如我所猜想,.cxr指令隻是修改windbg進行分析的環境,并沒有修改程式的執行流程。

    由此可以推斷,windbg中所謂的切換上下文的指令,隻是切換windbg分析問題時所處的出發點。如果把windbg比作調查案件的人,切換上下文可以看做調查人以不同的視角去假設問題解釋現象。但不管以怎樣的觀點分析問題,都無法左右程式的實際流程。

2. .process指令

    windbg解釋.process的作用為切換程序上下文。我的第一反應是中止程序A的執行,排程并運作程序B。沒錯,.process是有這樣的功能,但需要其他參數的配合,這個後面再說。預設.process EPROCESS這樣的形式并沒有切換程序的功效,原因聽我緩緩道來:a).x86 CPU切換程序必然會切換Cr3。當.process EPROCESS執行後,windbg的提示确實變了,但Cr3的值沒有改變,是以可以确定程序沒有切換,以切換目标機上的System.exe和calc.exe為例:

kd> !process 0 0
**** NT ACTIVE PROCESS DUMP ****
Failed to get VAD root
PROCESS 89e34830  SessionId: none  Cid: 0004    Peb: 00000000  ParentCid: 0000
    DirBase: 00b1f000  ObjectTable: e1000cc0  HandleCount: 229.
    Image: System

Failed to get VAD root
PROCESS 89888768  SessionId: 0  Cid: 0684    Peb: 7ffd3000  ParentCid: 0688
    DirBase: 109402c0  ObjectTable: e1cf45f8  HandleCount:  44.
    Image: calc.exe
@calc.exe的頁目錄表為109402c0,System的頁目錄表為00b1f000

kd> r cr3
cr3=00b1f000
@切換程序上下文前,先看下目前Cr3的值是00b1f000,即目前程序是System
kd> .process /r /p 89888768  
Implicit process is now 89888768
.cache forcedecodeuser done
Loading User Symbols
..........................
@切換程序上下文到calc.exe

kd> .reload /user /f
Loading User Symbols
....

Press ctrl-c (cdb, kd, ntsd) or ctrl-break (windbg) to abort symbol loads that take too long.
Run !sym noisy before .reload to track down problems loading symbols.

......................
@重新加載符号
kd> lml
start    end        module name
01000000 0101f000   calc       (pdb symbols)          C:\sym\thisPC\calc.pdb\3B7D84101\calc.pdb
...
kd> !lmi calc
Loaded Module Info: [calc] 
         Module: calc
   Base Address: 01000000
     Image Name: calc.exe
   Machine Type: 332 (I386)
     Time Stamp: 3b7d8410 Sat Aug 18 04:52:32 2001
           Size: 1f000
       CheckSum: 2073c
Characteristics: 10f  
Debug Data Dirs: Type  Size     VA  Pointer
             CODEVIEW    19,  160c,     a0c NB10 - Sig: 3b7d8410, Age: 1, Pdb: calc.pdb
     Image Type: MEMORY   - Image read successfully from loaded memory.
    Symbol Type: PDB      - Symbols loaded successfully from image header.
                 C:\sym\thisPC\calc.pdb\3B7D84101\calc.pdb
    Load Report: public symbols , not source indexed 
                 C:\sym\thisPC\calc.pdb\3B7D84101\calc.pdb
@檢視子產品資訊,calc.exe子產品加載在0x01000000處

@一下讀寫calc.exe的記憶體
kd> dd 01000000 L8
01000000  00905a4d 00000003 00000004 0000ffff
01000010  000000b8 00000000 00000040 00000000
kd> da 01000000 L8
01000000  "MZ."
kd> ed 01000000 00000000
kd> dd 01000000 L8
01000000  00000000 00000003 00000004 0000ffff
01000010  000000b8 00000000 00000040 00000000
@上面在calc程序空間做了這麼多讀寫操作,讓我們來确認一下目前程序是哪個?
kd> r cr3
cr3=00b1f000
@目前Cr3的值仍是00b1f000,仍是System,是不是很意外?      

    上面的例子中,我們切換程序上下文到calc.exe中,并讀寫記憶體,一切就像目前程序真的就是calc一樣,直到看到Cr3的值,我們才意識到這個現實。接着我們再看看windbg真正的切換程序,切換後Cr3的值說明目前程序是calc.exe

kd> .process /i /r /p 89888768  
You need to continue execution (press 'g' <enter>) for the context
to be switched. When the debugger breaks in again, you will be in
the new process context.
kd> g
Break instruction exception - code 80000003 (first chance)
nt!RtlpBreakWithStatusInstruction:
80528bdc cc              int     3

@用.process /i EPROCESS做所謂的入侵式切換
kd> r cr3
cr3=109402c0
@切換後再次驗證Cr3的值,Cr3=109402c0,和!process得到的DirBase的值相同
kd> !process 0 0
**** NT ACTIVE PROCESS DUMP ****

Failed to get VAD root
PROCESS 89888768  SessionId: 0  Cid: 0684    Peb: 7ffd3000  ParentCid: 0688
    DirBase: 109402c0  ObjectTable: e1cf45f8  HandleCount:  44.
    Image: calc.exe