<软件调试>30.7节简要的提到了windbg调试上下文的概念:如会话/进程/寄存器等上下文。为了深入了解背后的含义,我翻开windbg帮助文档,发现其对进程/寄存器上下文的解释最为晦涩。只能通过实际的练习,趟着石子过河,最后记录下自己的总结。
windbg中与进程上下文相关的命令是.process,与寄存器上下文相关的命令是.cxr(或.thread命令),我逐一总结这两个命令的作用。
1. .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
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,与预期想去甚远。
记得在调试异常或dump文件时,!analyiz -v的输出中往往会保存发生异常时寄存器上下文的地址,然后用.cxr命令恢复到异常发生时的上下文并查看调用堆栈,以此来分析异常的原因。 这个过程用<软件调试>作者的话叫做穿越回异常发生时的场景。我仔细回味了这个过程若干次,突然回想到一个细节:
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进行分析的环境,并没有修改程序的执行流程。
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
上面2个例子给我的感觉是:.process EPROCESS这种形式的切换,windbg停留在原进程A空间中,借助进程B保存在内存中的页目录表(并没有将页目录表装载到Cr3中),读写B的进程空间;此时,从windbg输出的模块等信息也是B进程的。整个过程就像借用B的躯壳----进程B并没有运行----却读取了B进程的空间(说不定windbg将进程B的页目录表读到调试机上,然后自己实现一套虚拟的页面中断机制,一点点完善B的进程空间)。至于.process /i EPROCESS不仅切换了进程,还向Cr3装载了目标进程的页目录表,所以能直接读取B的进程空间。