<格蠹汇编>_第十章_转储分析双误谜团

何谓双误

         双误就是CPU是报告一个比较严重的异常后,在异常处理过程中,又发生了一个严重的异常(CPU无法处理)。

蛛丝马迹

         利用!process查看当前进程的信息,其中有一项为DirBase(页目录基地址)

         利用r CR3命令来查看CR3寄存器内容,CR寄存器一共存在四个:CR0-CR3。保存着全局性和任务无关性。其中CR3保存的内容是进程的页目录物理地址。理论上CR3的值和DirBase是一致的,在切换任务时,CR3寄存器的内容随之改变。

初现端倪

         我们在接到警告的时候,发现CR3寄存器的值和通过!process查看的DirBase的值不一样,在理论上这是不应该发生的。但是怎么就发生了呢?

         在发生异常之后,CPU在内部,使用硬件相关的线程切换技术,切换CR3寄存器,但是系统却没有来得及及时更新已经发生改变的页目录基地址。造成CR3和DirBase数值不同。

         在CPU控制区(FS指向的段),保存着每个CPU的当前线程结构。在那个线程结构中,保存的是所属进程的进程信息。利用如下方法获取当前进程信息

1
2
mov eax,dword ptr fs:[00000124H]
mov eax,dword ptr [eax+1E0H]

时光倒流

         任务门,就是登记在IDT表中的一种特殊地址。CPU可以根据这个门所指向的任务状态段(TSS)来切换指定的任务线程。例如IDT中的8号表项处的任务门来切换处理双误的新线程。利用!pcr命令得到当前线程的TSS,观察其内容。

         Backlink成员是前一个任务的段选择子。利用.tts [序号]可以切换到前一个线程了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
ntdll!_KTSS
+0x000 Backlink : 0xc45 <---- 为前一个任务的TSS段选择子
+0x002 Reserved0 : 0x4d8a
+0x004 Esp0 : 0xf649bde0 <---- 0 级的 Esp 值,这指向一个 KTRAP_FRAME 结构 V86Es 成员
+0x008 Ss0 : 0x10
+0x00a Reserved1 : 0xb70f
+0x00c NotUsed1 : [4] 0x5031ff00
+0x01c CR3 : 0x8b55ff8b
+0x020 Eip : 0xc75ffec
+0x024 EFlags : 0xe80875ff
+0x028 Eax : 0xfffffbdd
+0x02c Ecx : 0x1b75c084
+0x030 Edx : 0x8b184d8b
+0x034 Ebx : 0x7d8b57d1
+0x038 Esp : 0x2e9c110
+0x03c Ebp : 0xf3ffc883
+0x040 Esi : 0x83ca8bab
+0x044 Edi : 0xaaf303e1
+0x048 Es : 0xeb5f
+0x04a Reserved2 : 0x6819
+0x04c Cs : 0x24fc
+0x04e Reserved3 : 0x44
+0x050 Ss : 0x75ff
+0x052 Reserved4 : 0xff18
+0x054 Ds : 0x1475
+0x056 Reserved5 : 0x75ff
+0x058 Fs : 0xff10
+0x05a Reserved6 : 0xc75
+0x05c Gs : 0x75ff
+0x05e Reserved7 : 0xe808
+0x060 LDT : 0
+0x062 Reserved8 : 0xffff
+0x064 Flags : 0
+0x066 IoMapBase : 0x20ac
+0x068 IoMaps : [1] _KiIoAccessMap
+0x208c IntDirectionMap : [32] "???"

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
3: kd> !pcr
KPCR for Processor 3 at 8ae9f000:
Major 1 Minor 1
NtTib.ExceptionList: 8aea81a0
NtTib.StackBase: 00000000
NtTib.StackLimit: 00000000
NtTib.SubSystemTib: 8aea3270
NtTib.Version: 00ab16c4
NtTib.UserPointer: 00000008
NtTib.SelfTib: 7fb4f000
SelfPcr: 8ae9f000
Prcb: 8ae9f120
Irql: 0000001f
IRR: 00000000
IDR: 00000000
InterruptMode: 00000000
IDT: 8aea8d00
GDT: 8aea8900
TSS: 8aea55e0
CurrentThread: 87de6140
NextThread: 00000000
IdleThread: 8aea5320
DpcQueue:
3: kd> dt _KTSS 8aea55e0
nt!_KTSS
+0x000 Backlink : 0x28
+0x002 Reserved0 : 0
+0x004 Esp0 : 0x8aea86c0
+0x008 Ss0 : 0x10
+0x00a Reserved1 : 0
+0x00c NotUsed1 : [4] 0
+0x01c CR3 : 0x185000
+0x020 Eip : 0x81f223bb
+0x024 EFlags : 0
+0x028 Eax : 0
+0x02c Ecx : 0
+0x030 Edx : 0
+0x034 Ebx : 0
+0x038 Esp : 0x8aea86c0
+0x03c Ebp : 0
+0x040 Esi : 0
+0x044 Edi : 0
+0x048 Es : 0x23
+0x04a Reserved2 : 0
+0x04c Cs : 8
+0x04e Reserved3 : 0
+0x050 Ss : 0x10
+0x052 Reserved4 : 0
+0x054 Ds : 0x23
+0x056 Reserved5 : 0
+0x058 Fs : 0x30
+0x05a Reserved6 : 0
+0x05c Gs : 0
+0x05e Reserved7 : 0
+0x060 LDT : 0
+0x062 Reserved8 : 0
+0x064 Flags : 0
+0x066 IoMapBase : 0x20ac
+0x068 IoMaps : [1] _KiIoAccessMap
+0x208c IntDirectionMap : [32] "@???"

宝贵的内核态栈

         多数线程都有两个栈,一个用户态栈,一个内核态栈,用户态栈大小为1M,一般不会造成溢出,但是内核态栈只有十几千字节到几十千字节,相对来说也是比较容易造成溢出的。

课后实验

         是一个dump文件,我们使用命令!analyse -v分析一下蓝屏产生的原因。可见注释的地方,产生了一个停止码。而带来的异常是double_fault。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
3: kd> !analyze -v
*******************************************************************************
* *
* Bugcheck Analysis *
* *
*******************************************************************************
UNEXPECTED_KERNEL_MODE_TRAP (7f) //停止码
This means a trap occurred in kernel mode, and it's a trap of a kind
that the kernel isn't allowed to have/catch (bound trap) or that
is always instant death (double fault). The first number in the
bugcheck params is the number of the trap (8 = double fault, etc)
Consult an Intel x86 family manual to learn more about what these
traps are. Here is a *portion* of those codes:
If kv shows a taskGate
use .tss on the part before the colon, then kv.
Else if kv shows a trapframe
use .trap on that value
Else
.trap on the appropriate frame will show where the trap was taken
(on x86, this will be the ebp that goes with the procedure KiTrap)
Endif
kb will then show the corrected stack.
Arguments:
Arg1: 00000008, EXCEPTION_DOUBLE_FAULT //双误异常
Arg2: 8aea3270
Arg3: 00000000
Arg4: 00000000
Debugging Details:
------------------
WARNING: Process directory table base C7F45840 doesn't match CR3 00185000 //不同
WARNING: Process directory table base C7F45840 doesn't match CR3 00185000

         由于windbg给出的警告说明了CR3和DirBase值不相同,利用r CR3!prcess 0 0查看对应的数据。发现两者数据确实不同。原因见<初见端倪>一节。

1
2
3
4
5
6
7
3: kd> r CR3
cr3=00185000
3: kd> !process
PROCESS 85ad72c0 SessionId: 1 Cid: 0cfc Peb: 7fb4c000 ParentCid: 08ac
DirBase: c7f45840 ObjectTable: 8a127040 HandleCount: <Data Not Accessible>
Image: ImBuggy.exe

         当使用!process 0 0遍历系统所有进程,发现system进程的Dirbase数据为00185000。这时候,我们大致了解到了事情的经过,程序ImBuggy.exe是发生双误异常后,CPU内部利用硬件机制将页目录基地址改变为00185000,但是OS并没有及时作出调整,产生了error,但是双误是如何产生的呢?

1
2
3
4
5
3: kd> !process 4 0 //!process 0 0 system
Searching for Process with Cid == 4
PROCESS 84ad78c0 SessionId: none Cid: 0004 Peb: 00000000 ParentCid: 0000
DirBase: 00185000 ObjectTable: 8b203000 HandleCount: <Data Not Accessible>
Image: System

         先使用.sympath c:\gedu\xxx设置符号路径,然后加载符号(提前需要设置系统符号命令)。

         使用knvL命令回溯栈。n:显示行号,v:显示调用约定,L:省略源代码。0号栈是产生蓝屏的系统函数,1号栈是处理双误异常的异常处理函数KiTrap08(陷阱处理函数,08代表双误异常)。剩下的是RealBug!StackOverflow是产生双误的函数。

1
2
3
4
5
6
7
8
9
10
3: kd> kvnL
# ChildEBP RetAddr Args to Child
00 8aea869c 81f22433 0000007f 00000008 8aea3270 nt!KiBugCheck2
01 8aea869c b0e013e4 0000007f 00000008 8aea3270 nt!KiTrap08+0x78 (FPO: TSS 28:0)
02 cf6ce004 b0e013e9 0000065e cf6ce01c b0e013e9 RealBug!StackOverflow+0x14 (FPO: [Non-Fpo]) (CONV: stdcall)
03 cf6ce010 b0e013e9 0000065f cf6ce028 b0e013e9 RealBug!StackOverflow+0x19 (FPO: [Non-Fpo]) (CONV: stdcall)
04 cf6ce01c b0e013e9 00000660 cf6ce034 b0e013e9 RealBug!StackOverflow+0x19 (FPO: [Non-Fpo]) (CONV: stdcall)
05 cf6ce028 b0e013e9 00000661 cf6ce040 b0e013e9 RealBug!StackOverflow+0x19 (FPO: [Non-Fpo]) (CONV: stdcall)
06 cf6ce034 b0e013e9 00000662 cf6ce04c b0e013e9 RealBug!StackOverflow+0x19 (FPO: [Non-Fpo]) (CONV: stdcall)
.......

         但是,windbg并没有显示完全栈回溯,使用kvnL 1000回溯1000次调用。

1
2
3
4
5
6
7
8
9
10
11
12
3c9 cf6d0d58 b0e013e9 00000a25 cf6d0d70 b0e013e9 RealBug!StackOverflow+0x19 (FPO: [Non-Fpo]) (CONV: stdcall)
3ca cf6d0d64 b0e013e9 00000a26 cf6d0d98 b0e01b16 RealBug!StackOverflow+0x19 (FPO: [Non-Fpo]) (CONV: stdcall)
3cb cf6d0d70 b0e01b16 00000a27 cf6d0f24 a8e2e888 RealBug!StackOverflow+0x19 (FPO: [Non-Fpo]) (CONV: stdcall)
3cc cf6d0d98 b0e01d50 85cc2b68 00000001 87ab4780 RealBug!RealBugDeviceControl+0xb6 (FPO: [Non-Fpo]) (CONV: stdcall)
3cd cf6d0de4 81fa807d 84ee91f8 85994f68 00000100 RealBug!RealBugDispatch+0x90 (FPO: [Non-Fpo]) (CONV: stdcall)
3ce cf6d0dfc 82066baf 85994ffc 85994f68 00000004 nt!IofCallDriver+0x3d (FPO: [Non-Fpo])
3cf cf6d0e50 82066622 84ee91f8 00000000 a8e31901 nt!IopSynchronousServiceTail+0x10a (FPO: [Non-Fpo])
3d0 cf6d0ef0 8206624f 00000001 85994f68 00000000 nt!IopXxxControlFile+0x3b7 (FPO: [Non-Fpo])
3d1 cf6d0f24 81f200bc 000000b4 00000000 00000000 nt!NtDeviceIoControlFile+0x2a (FPO: [Non-Fpo])
3d2 cf6d0f24 77c85ee4 000000b4 00000000 00000000 nt!KiFastCallEntry+0x12c (FPO: [0,3] TrapFrame @ cf6d0f54)
WARNING: Frame IP not in any known module. Following frames may be wrong.
3d3 0013f568 00000000 00000000 00000000 00000000 0x77c85ee4

         在上面的栈回溯,可以得出,1.#3d2的返回地址接近用户态。2.根据栈回溯:程序递归调用了StackOverflow。

         在栈回溯,可以看到双误处理函数的TSS:28,TSS:28表示的是前一个线程的TSS段选择子是28,利用.tss 28可以查看蓝屏前的线程信息。可以发生是StackOverflow函数发生了双误异常。接下来,我们看一下发生异常的原因是什么。

1
2
3
4
5
6
3: kd> .tss 28
eax=0000065e ebx=0000010e ecx=0000065e edx=00000a28 esi=84ee91f8 edi=85cc2b68
eip=b0e013e4 esp=cf6ce000 ebp=cf6ce004 iopl=0 nv up ei pl nz na po nc
cs=0008 ss=0010 ds=0023 es=0023 fs=0030 gs=0000 efl=00010202
RealBug!StackOverflow+0x14:
b0e013e4 e8e7ffffff call RealBug!StackOverflow (b0e013d0)

         如下图,在StackOverflow中递归调用了StackOverflow函数,会开辟处12个字节的栈空间。

         根据栈回溯信息,发现从#002到#3CB一共调用了969次。产生了969*12=11628个字节大小栈空间。

         利用!thread查看线程的栈空间信息。最后一行:栈空间大小:cf6d1000-cfce000=3000,所需空间不够,在看current:cf6d0d8c明显不在cf6d1000-cfce000区间内,判断是栈溢出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
3: kd> !thread
THREAD 87de6140 Cid 0cfc.1778 Teb: 7fb4f000 Win32Thread: 80cb9008 RUNNING on processor 3
IRP List:
85994f68: (0006,0094) Flags: 00060030 Mdl: 00000000
Not impersonating
DeviceMap ba46c160
Owning Process 85ad72c0 Image: ImBuggy.exe
Attached Process N/A Image: N/A
Wait Start TickCount 304716315 Ticks: 0
Context Switch Count 1554 IdealProcessor: 0
UserTime 00:00:00.078
KernelTime 00:00:00.093
Win32 Start Address 0x0040285e
Stack Init cf6d0fe0 Current cf6d0d8c Base cf6d1000 Limit cf6ce000 Call 00000000