D1蓝屏、指针和安全的代价
在从Hibernate恢复时,发生D1蓝屏,蓝屏的基本信息为:
DRIVER_IRQL_NOT_LESS_OR_EQUAL (d1)
An attempt was made to access a pageable (or completely invalid) address at an
interrupt request level (IRQL) that is too high. This is usually
caused by drivers using improper addresses.
If kernel debugger is available get stack backtrace.
Arguments:
Arg1: 53646156, memory referenced
Arg2: 00000002, IRQL
Arg3: 00000000, value 0 = read operation, 1 = write operation
Arg4: b9d0beb9, address which referenced memory
这是非常常见的一种蓝屏,直接的错误就是在高IRQL(2,DISPATCH_LEVEL)访问了错误的地址,导致了页错误异常。
对于这个案例,被访问的内存地址是53646156,从表面看这个地址就很有问题,首先它不是一个内核态的地址;再一看,它很不像一个地址,倒像一串ASC代码,使用.FORMATS命令看一下:
kd> .formats 53646156
Evaluate expression:
Hex: 53646156
Decimal: 1399087446
Octal: 12331060526
Binary: 01010011 01100100 01100001 01010110
Chars: SdaV
Time: Sat May 03 07:24:06 2014
Float: low 9.80886e+011 high 0
Double: 6.91241e-315
看来果真是一串ASC码,其内容为SdaV,哦,这岂不是在内核池中分配VAD(Virtual Address Descriptor)结构时使用的分配标记。看来是阴差阳错,谁把这个分配标记当作了地址。
是谁呢?蓝屏的第四个参数就是点这把火的那条指令的地址,即b9d0beb9,使用r命令可以时光倒流回当时的情景:
kd> r
eax=00000002 ebx=8a6b1880 ecx=53646156 edx=8a6b2398 esi=8a6b1888 edi=8984c008
eip=b9d0beb9 esp=aca0d27c ebp=aca0d2a8 iopl=0 nv up ei pl nz na pe nc
cs=0008 ss=0010 ds=0023 es=0023 fs=0030 gs=0000 efl=00010206
psched!MpSend+0x97:
b9d0beb9 833901 cmp dword ptr [ecx],1 ds:0023:53646156=????????
这是一条比较指令,把ECX寄存器所致指向的地址内容与1相比。CPU执行时,需要先读出这个地址的内容,而到这个地址去读的时候,出异常了。
看来是有人把ECX设置错了。反汇编它前面的指令:
kd> ub b9d0beb9
psched!MpSend+0x7b:
b9d0be9d ff158000d1b9 call dword ptr [psched!_imp_KfAcquireSpinLock (b9d10080)]
b9d0bea3 88430c mov byte ptr [ebx+0Ch],al
b9d0bea6 0fb70f movzx ecx,word ptr [edi]
b9d0bea9 8b157c0ad1b9 mov edx,dword ptr [psched!g_WanLinkTable (b9d10a7c)]
b9d0beaf 8b0c8a mov ecx,dword ptr [edx+ecx*4]
b9d0beb2 85c9 test ecx,ecx
b9d0beb4 894df4 mov dword ptr [ebp-0Ch],ecx
b9d0beb7 7405 je psched!MpSend+0x9c (b9d0bebe)
注意观察对ECX寄存器的操作。从下向上追溯,倒数第2行,是将ECX的值赋给EBP-C这个局部变量,观察一下:
kd> dd ebp-0C l1
aca0d29c 53646156
这与出事时的ECX值相符合,看来栈上的数据还都完好。倒数第3行是测试一下ECX是否为零。
倒数第4行是对ECX的赋值语句,意思是把EDX这个数组(结构)的ECX号元素赋给ECX。因为这里的源和目标都使用了ECX寄存器,所以单从这一句是看不出执行这条指令时的ECX值了。好在从向上两行可以看到ECX的值是从EDI寄存器所指向的两字节内存单元中的得到的。因为从这一行到事发时,没有人操作EDI,所以上面r命令显示的EDI值8984c008仍是可信的:
kd> dw 8984c008 l8
8984c008 0061 0064 0073 0061 006d 0070 0008 0045
从此推测,movzx ecx,word ptr [edi]这一句应该是把ECX赋值成了0061。观察上面这串数字,感觉也颇蹊跷,使用db命令显示:
kd> db 8984c008 l10
8984c008 61 00 64 00 73 00 61 00-6d 00 70 00 08 00 45 00 a.d.s.a.m.p...E.
这很像是一个双字节字符串,其内容为adsamp。这样看来这个EDI的指向值得怀疑。那么EDI是如何来的呢?考虑到跳转影响,而且当前代码离入口不远,所以我们还是从入口开始分析,反汇编前30h条指令:
kd> u psched!MpSend l30
psched!MpSend:
01 b9d0be22 8bff mov edi,edi
02 b9d0be24 55 push ebp
03 b9d0be25 8bec mov ebp,esp
04 b9d0be27 83ec20 sub esp,20h
05 b9d0be2a 53 push ebx
06 b9d0be2b 8b5d08 mov ebx,dword ptr [ebp+8]
07 b9d0be2e 56 push esi
08 b9d0be2f 33f6 xor esi,esi
09 b9d0be31 46 inc esi
0a b9d0be32 39b330020000 cmp dword ptr [ebx+230h],esi
0b b9d0be38 7508 jne psched!MpSend+0x20 (b9d0be42)
0c b9d0be3a 39b334020000 cmp dword ptr [ebx+234h],esi
0d b9d0be40 740a je psched!MpSend+0x2a (b9d0be4c)
0e b9d0be42 b8010000c0 mov eax,0C0000001h
0f b9d0be47 e94e0d0000 jmp psched!MpSend+0xd78 (b9d0cb9a)
10 b9d0be4c 83bb8401000003 cmp dword ptr [ebx+184h],3
11 b9d0be53 57 push edi
12 b9d0be54 0f85fb060000 jne psched!MpSend+0x733 (b9d0c555)
13 b9d0be5a 6681bb380200000008 cmp word ptr [ebx+238h],800h
14 b9d0be63 0f8583060000 jne psched!MpSend+0x6ca (b9d0c4ec)
15 b9d0be69 8b450c mov eax,dword ptr [ebp+0Ch]
16 b9d0be6c 8b7008 mov esi,dword ptr [eax+8]
17 b9d0be6f f6460605 test byte ptr [esi+6],5
18 b9d0be73 7405 je psched!MpSend+0x58 (b9d0be7a)
19 b9d0be75 8b7e0c mov edi,dword ptr [esi+0Ch]
1a b9d0be78 eb0b jmp psched!MpSend+0x63 (b9d0be85)
1b b9d0be7a 6a00 push 0
1c b9d0be7c 56 push esi
1d b9d0be7d ff15d801d1b9 call dword ptr [psched!_imp__MmMapLockedPages (b9d101d8)]
1e b9d0be83 8bf8 mov edi,eax
1f b9d0be85 837e140e cmp dword ptr [esi+14h],0Eh
20 b9d0be89 730a jae psched!MpSend+0x73 (b9d0be95)
21 b9d0be8b b8010000c0 mov eax,0C0000001h
22 b9d0be90 e9040d0000 jmp psched!MpSend+0xd77 (b9d0cb99)
23 b9d0be95 8d7308 lea esi,[ebx+8]
24 b9d0be98 8bce mov ecx,esi
25 b9d0be9a 8975f8 mov dword ptr [ebp-8],esi
26 b9d0be9d ff158000d1b9 call dword ptr [psched!_imp_KfAcquireSpinLock (b9d10080)]
27 b9d0bea3 88430c mov byte ptr [ebx+0Ch],al
28 b9d0bea6 0fb70f movzx ecx,word ptr [edi]
29 b9d0bea9 8b157c0ad1b9 mov edx,dword ptr [psched!g_WanLinkTable (b9d10a7c)]
2a b9d0beaf 8b0c8a mov ecx,dword ptr [edx+ecx*4]
2b b9d0beb2 85c9 test ecx,ecx
2c b9d0beb4 894df4 mov dword ptr [ebp-0Ch],ecx
2d b9d0beb7 7405 je psched!MpSend+0x9c (b9d0bebe)
2e b9d0beb9 833901 cmp dword ptr [ecx],1
2f b9d0bebc 740c je psched!MpSend+0xa8 (b9d0beca)
30 b9d0bebe 8ad0 mov dl,al
从#15行可以看到,第二参数(EBP+C)的值赋给了EAX,下一行把EAX结构的偏移8的字段赋给了ESI。通过kv命令显示参数:
kd> kv 1
ChildEBP RetAddr Args to Child
aca0d2a8 ba509985 8a6b1880 87c04418 00000000 psched!MpSend+0x97 (FPO: [Non-Fpo])
可见,第二个参数的值是87c04418,观察它的值:
kd> dd 87c04418
87c04418 ???????? ???????? ???????? ????????
这说明这个地址没有被包含在DUMP文件中。不过这已经不是很重要。分析到这,我们基本可以推断这个蓝屏与调用MPSend的第二个参数密切有关。
MPSend是网络小端口驱动程序中常见的一个发送函数,以下是它的函数原型:
NDIS_STATUS
MPSend(
IN NDIS_HANDLE MiniportAdapterContext,
IN PNDIS_PACKET Packet,
IN UINT Flags
)
MSDN中也有这个函数原型的定义,即:
NDIS_STATUS MiniportSend(
NDIS_HANDLE MiniportAdapterContext,
PNDIS_PACKET Packet,
UINT Flags
);
也就是说,第二个参数是指向NDIS_PACKET结构的指针。MSDN中给出的这个结构的定义是:
typedef struct _NDIS_PACKET {
NDIS_PACKET_PRIVATE Private;
union {
struct {
UCHAR MiniportReserved[2*sizeof(PVOID)];
UCHAR WrapperReserved[2*sizeof(PVOID)];
};
struct {
UCHAR MiniportReservedEx[3*sizeof(PVOID)];
UCHAR WrapperReservedEx[sizeof(PVOID)];
};
struct {
UCHAR MacReserved[4*sizeof(PVOID)];
};
};
ULONG_PTR Reserved[2];
UCHAR ProtocolReserved[1];
} NDIS_PACKET, *PNDIS_PACKET, **PPNDIS_PACKET;
其中,NDIS_PACKET_PRIVATE结构的定义是:
typedef struct _NDIS_PACKET_PRIVATE
{
UINT PhysicalCount; // number of physical pages in packet.
UINT TotalLength; // Total amount of data in the packet.
PNDIS_BUFFER Head; // first buffer in the chain
PNDIS_BUFFER Tail; // last buffer in the chain
// if Head is NULL the chain is empty; Tail doesn't have to be NULL also
PNDIS_PACKET_POOL Pool; // so we know where to free it back to
UINT Count;
ULONG Flags;
BOOLEAN ValidCounts;
UCHAR NdisPacketFlags; // See fPACKET_xxx bits below
USHORT NdisPacketOobOffset;
} NDIS_PACKET_PRIVATE, * PNDIS_PACKET_PRIVATE;
其中的NDIS_BUFFER结构其实就是描述内存分配用的MDL结构:
typedef MDL NDIS_BUFFER, *PNDIS_BUFFER;
终上,有关的访问流程是:
#15行把参数Packet的地址赋给EAX,#16行是把Packet->Head成员赋给ESI。而#19行是把NDIS_BUFFER(MDL )结构的MappedSystemVa成员赋给EDI。而这个EDI所指向的内存数据是有问题的。
kd> dt _MDL
nt!_MDL
+0x000 Next : Ptr32 _MDL
+0x004 Size : Int2B
+0x006 MdlFlags : Int2B
+0x008 Process : Ptr32 _EPROCESS
+0x00c MappedSystemVa : Ptr32 Void
+0x010 StartVa : Ptr32 Void
+0x014 ByteCount : Uint4B
+0x018 ByteOffset : Uint4B
那么是谁向psched模块的MpSend传递了错误的参数呢?首先看一下pshed模块:
kd> lmvm psched
start end module name
b9d03000 b9d13e00 psched (pdb symbols) d:\symbols\psched.pdb\72F13E8E57F04ADA961EFC51F1587E9B1\psched.pdb
Loaded symbol image file: psched.sys
Mapped memory image file: C:\WINDOWS\system32\drivers\psched.sys
Image path: psched.sys
Image name: psched.sys
Timestamp: Wed Aug 04 10:04:16 2004 (41107C60)
CheckSum: 0001B55A
ImageSize: 00010E00
File version: 5.1.2600.2180
Product version: 5.1.2600.2180
File flags: 0 (Mask 3F)
File OS: 40004 NT Win32
File type: 3.6 Driver
File date: 00000000.00000000
Translations: 0409.04b0
CompanyName: Microsoft Corporation
ProductName: Microsoft® Windows® Operating System
InternalName: PSCHED.SYS
OriginalFilename: PSCHED.SYS
ProductVersion: 5.1.2600.2180
FileVersion: 5.1.2600.2180 (xpsp_sp2_rtm.040803-2158)
FileDescription: MS QoS Packet Scheduler
LegalCopyright: © Microsoft Corporation. All rights reserved.
接下来看一下栈回溯:
kd> kvn 100
# ChildEBP RetAddr Args to Child
00 aca0d2a8 ba509985 8a6b1880 87c04418 00000000 psched!MpSend+0x97 (FPO: [Non-Fpo])
01 aca0d2d0 b14c0b50 88eb5428 87c04418 b14bf2c3 NDIS!ndisMSendX+0x1d6 (FPO: [Non-Fpo])
02 aca0d2dc b14bf2c3 87c04418 88eb5428 8984c0b7 ICsrvr+0x5b50 (FPO: [2,0,0])
03 aca0d640 b14beb61 87c04418 8a682130 87f03160 ICsrvr+0x42c3 (FPO: [Non-Fpo])
04 aca0d668 b14c0d7c 87f03160 87f031f0 0000000e ICsrvr+0x3b61 (FPO: [Non-Fpo])
05 aca0d688 ba52be2d 886b1300 87f03160 87f031f0 ICsrvr+0x5d7c (FPO: [Non-Fpo])
06 aca0d6f0 ba51ee54 013d70f8 aca0d710 00000001 NDIS!ethFilterDprIndicateReceivePacket+0x4a8 (FPO: [Non-Fpo])
07 aca0d718 ba50999d 8993c0e4 89963c20 8a56c008 NDIS!ndisMLoopbackPacketX+0x154 (FPO: [Non-Fpo])
08 aca0d734 b17472b1 89a7f830 89963c20 8a56c008 NDIS!ndisMSendX+0x181 (FPO: [Non-Fpo])
09 aca0d758 b1747355 0056c00e faffffef 89963c20 tcpip!ARPSendBCast+0x2a8 (FPO: [Non-Fpo])
0a aca0d784 b174564a 8a56c008 aca0d700 00000001 tcpip!ARPTransmit+0x125 (FPO: [Non-Fpo])
0b aca0d7b4 b174578f 8a3bd758 faffffef 89963c20 tcpip!SendIPPacket+0x193 (FPO: [Non-Fpo])
0c aca0d900 b17499c4 b17839b4 87b97468 88fb3134 tcpip!IPTransmit+0x289e (FPO: [Non-Fpo])
0d aca0d9a0 b174978b 87f221b8 87b97468 885eb8c0 tcpip!UDPSend+0x41b (FPO: [Non-Fpo])
0e aca0d9c4 b17497f1 00a0d9e8 885ebfa0 88fb3174 tcpip!TdiSendDatagram+0xd5 (FPO: [Non-Fpo])
0f aca0d9fc b1748149 885eb8c0 885eb978 885eb8c0 tcpip!UDPSendDatagram+0x4f (FPO: [Non-Fpo])
10 aca0da18 804ee0ef 89a04f18 885eb8c0 884776f8 tcpip!TCPDispatchInternalDeviceControl+0xff (FPO: [Non-Fpo])
11 aca0da28 b1722cc7 b1722798 89a2ef08 885eb8c0 nt!IopfCallDriver+0x31 (FPO: [0,0,0])
WARNING: Stack unwind information not available. Following frames may be wrong.
12 aca0da2c b1722798 89a2ef08 885eb8c0 89a2efc0 FireTDI+0x6cc7
13 aca0da48 b1722bc3 89a2ef08 885e6c07 b1722e47 FireTDI+0x6798
14 aca0db10 b16b3b5e 8872f008 aca0db64 00000085 FireTDI+0x6bc3
15 aca0dc38 805afda5 aca0dcf0 b16b22a2 b16b2728 afd!AfdFastIoDeviceControl+0x2a7 (FPO: [Non-Fpo])
16 aca0dd34 8053cbc8 000020f0 000026e8 00000000 nt!ObReferenceObjectByHandle+0x249 (FPO: [Non-Fpo])
17 aca0dd34 7c90eb94 000020f0 000026e8 00000000 nt!KiFastCallEntry+0xf8 (FPO: [0,0] TrapFrame @ aca0dd64)
18 039cfd14 00000000 00000000 00000000 00000000 0x7c90eb94
在上面的栈回溯中,首先可以明显的看出有两个没有调试符号的模块,一个是ICsrvr,另一个是FireTDI。
kd> lmvm ICsrvr
start end module name
b14bb000 b14de7a0 ICsrvr (no symbols)
Loaded symbol image file: ICsrvr.sys
Mapped memory image file: C:\WINDOWS\system32\drivers\ICsrvr.sys
Image path: ICsrvr.sys
Image name: ICsrvr.sys
Timestamp: Thu Jul 20 12:36:22 2006 (44BF4086)
CheckSum: 0002B6A4
ImageSize: 000237A0
File version: 0.0.0.0
Product version: 0.0.0.0
File flags: 0 (Mask 3F)
File OS: 40004 NT Win32
File type: 3.7 Driver
File date: 00000000.00000000
Translations: 0409.04b0
CompanyName:
ProductName: VPN Client (Windows 2000)
InternalName: icsrvr.sys
OriginalFilename: icsrvr.sys
ProductVersion:
FileVersion:
FileDescription: VPN Client Service Driver
LegalCopyright:
也就是说,ICsrvr是用于VPN(Virutal Private Network)。再看一下另一个:
kd> lmvm FireTDI
start end module name
b171c000 b1742000 FireTDI (no symbols)
Loaded symbol image file: FireTDI.sys
Mapped memory image file: C:\WINDOWS\system32\drivers\FireTDI.sys
Image path: FireTDI.sys
Image name: FireTDI.sys
Timestamp: Wed Feb 28 04:04:54 2007 (45E4D536)
CheckSum: 000184FA
ImageSize: 00026000
File version: 6.0.1.153
Product version: 6.0.1.153
File flags: 0 (Mask 3F)
File OS: 4 Unknown Win32
File type: 3.6 Driver
File date: 00000000.00000000
Translations: 0409.04e4
CompanyName: McAfee, Inc.
ProductName: McAfee Host Intrusion Prevention
InternalName: FireTDI
OriginalFilename: FireTDI.sys
ProductVersion: 6.0.1.153
FileVersion: 6.0.1.153
FileDescription: McAfee HIP Application Firewall Driver
LegalCopyright: Copyright© 2000-2007 McAfee, Inc. All Rights Reserved.
看来这个McAfee公司的一个驱动程序,其功能是所谓的Host Intrusion Prevention,说白了就是检测侵入到主机中的恶意软件,发现和防止它们干坏事。FireTDI中的TDI是Transport Driver Interface 的缩写,是Windows系统定义的网络传输层向外提供服务的接口。
再看看所在的进程吧:
kd> !thread
GetPointerFromAddress: unable to read from 80557fb4
THREAD 88cf8020 Cid 02e8.0d34 Teb: 7ff5d000 Win32Thread: e427a380 RUNNING on processor 0
Not impersonating
GetUlongFromAddress: unable to read from 80557fc4
Owning Process 89838758 Image: svchost.exe
Attached Process N/A Image: N/A
ffdf0000: Unable to get shared data
Wait Start TickCount 4687958
Context Switch Count 29 LargeStack
ReadMemory error: Cannot get nt!KeMaximumIncrement value.
UserTime 00:00:00.000
KernelTime 00:00:00.000
Win32 Start Address 0x74f0742e
Start Address 0x7c810679
Stack Init aca0e000 Current aca0dc50 Base aca0e000 Limit aca0a000 Call 0
Priority 9 BasePriority 8 PriorityDecrement 0 DecrementCount 16
<以下是栈回溯,省略>
看来是某个系统服务发起了网络有关的操作。从栈回溯的中间部分可以看出,这个线程是在发送一个UDP报文。而在发送过程中,调用了两个第三方的驱动程序FireTDI和ICSrvr,把报文交给它们做过滤和检查,理由是为了安全。
安全重要,毋容置疑。但是问题是我们把保证安全这个重任交给了谁?它足够衷心吧?它值得我们信赖吧?它会不会无事生非和节外生枝呢?
今天的计算机世界还没有秩序,处在文明出现前的混乱不堪状态。因此,今天的安全软件和“不安全”软件使用着类似的技术,在几乎平等的条件下相互肉搏。这难为了安全软件。但是,令主人难以接受的是,很多安全软件在黔驴技穷的时候,居然时常在主人那里耍脾气。它们不管主人在干什么,就恣意为所欲为。因此,当你在WORD里执行一个Paste操作,也可能要等待几秒钟:WORD突然失去响应,甚至整个系统都差不多回到了Windows 1.0的年代,移动窗口像搬砖头一样。而安全软件的图标在那里活泼的闪烁,似乎在说:“我在帮你做安全检查呢^-^”。
也有时候,安全检查者走了火,于是直接把系统搞蓝屏了。
也有时候,安全检查者失去了方向,于是所在的进程永远挂在那里,成为著名的杀不掉的进程,因为“安全”检查的人把线程挂在内核态。
也有时候,安全检查者没看清楚,搞错了人,把好人杀掉了,有用的文件没了,甚至系统起不来了。
也有时侯,系统莫名其妙的挂住了,朴实的用户和客户支持都糊里糊涂的以为是中了病毒,其实是......
更难以接受的是,当你无法忍受它的愚蠢,想将其杀掉时,它已经准备好了N种方法。对于它的较低版本,你或许还可以见到它起来时(因为感觉明显的缓慢),使用任务管理器就把那个占CPU最高的进程杀掉。但是它没几天就升级了,想用任务管理器杀掉它不可能了。但是或许可以在系统服务里找到它,将其禁止,不要担心它下次起不来,它有N+1种方法把自己跑起来。但过了些天后,它可能又自动升级了M个版本,更加鲁棒了(robust)。非得用调试器(WinDBG)附加上去,然后关掉WinDBG,让其与调试器同归于尽。这应该是重启和Log Off外最顶级的杀进程的方法了吧。但是没多久,这种方法也被它发现了,不过它也没有什么好方法抵制,但是它使出了“鱼死网破”的方法,一旦你试图把调试器附加上去,整个系统就在秒一级做响应了,估计是把Windows子系统搞混乱了......
于是很能理解,真正深刻理解了今天安全软件的人都不愿意使用它们。安全领域的著名女将Joanna Rutkowska在一次接受记者采访时就说,我从来不安装什么安全软件。但是她承认“MS Kernel Debugger can be very useful tool for system compromise detection.”
还是回到这个蓝屏吧,因为是小型转储,所以没能分析的水落石出。现已经把转储方式改为内核转储......其实一种更好的方式是启用崩溃时中断到调试器,即/CrashDebug(参见《软件调试》P476)。
另外,通过小型转储也可能看出更多线索,我把这个转储文件上传到如下地址:http://advdbg.org/img/dump/d1.dmp。感兴趣的朋友可以试一试,尤其是对网络和防火墙熟悉的高手们。