-
-
[原创]记一次复杂的debug过程
-
发表于:
2016-12-6 14:17
5867
-
记一次复杂的debug过程
笔者一个tdi驱动,造成部分应用层程序卡死,抓包发现在服务器无响应或失去连接时可能重现。
review代码发现发送和接受逻辑里都加了超时判断,如果超时会置错误值并完成该IRP,看似没有问题。
内核调试查看三层挂起的线程信息,用到的命令如下:
!process 0 0
.process /i /p 8154d020
g
!process
.reload
!cs -l
!thread 814a7020
0 Id: 55c.520 Suspend: 1 Teb: 7ffdf000 Unfrozen
ChildEBP RetAddr Args to Child
0012f194 7c92da2c 719ce0d8 000004c0 0012f1e0 ntdll!KiFastSystemCallRet (FPO: [0,0,0])
0012f198 719ce0d8 000004c0 0012f1e0 0012f1d4 ntdll!NtRemoveIoCompletion+0xc (FPO: [5,0,0])
0012f1d8 719d80b4 719cd65f 518ec7c8 0021fa98 mswsock!SockIsSocketConnected+0x60 (FPO: [Non-Fpo])
0012f21c 71a309e4 000008b0 000301da 00000373 mswsock!WSPAsyncSelect+0x145 (FPO: [Non-Fpo])
*** ERROR: Symbol file could not be found. Defaulted to export symbols for Common.dll -
0012f244 51708a38 000008b0 000301da 00000373 ws2_32!WSAAsyncSelect+0x53 (FPO: [Non-Fpo])
......
可见挂在了NtRemoveIoCompletion上,NtRemoveIoCompletion是完成端口相关的函数,在《windows内和原理与实现》中对完成端口有描述。
可这里为什么挂起了,是个问题。
出错程序网络操作比较多分析不便,于是试图手写一个用到WSAAsyncSelect的demo.
MSDN查看该函数,搜得示例代码,然后纠结于一个问题,connect与WSAAsyncSelect谁先谁后;
测试先WSAAsyncSelect后connect的话,并不会重现卡死的问题,
IDA mswsock发现*(_DWORD *)(CompletionKey + 0x8C)为真的时候才会去NtRemoveIoCompletion;
CompletionKey + 0x8C是什么?
结合NT4源码和WRK以及调试,跟踪socket的创建过程,
发现CompletionKey + 0x8C就是 SOCKET_INFORMATION 里的 AsyncConnectContext;
而 AsyncConnectContext 是在 connect 里赋值的;
说明要重现卡死的问题,connect 要在 WSAAsyncSelect 前面,至此问题可以重现了。
NtRemoveIoCompletion为什么没有返回?
查阅《windows内和原理与实现》可知,完成端口在消息来时插入一个KQUEUE,而NtRemoveIoCompletion是取KQUEUE里消息的函数,
NtRemoveIoCompletion未返回说明KQUEUE是空的,那么就需要追踪插入KQUEUE的过程,
插入 KQUEUE 是在完成 IRP 时做的,这里为什么没有做?
if ( FileObject )
{
CompletionContext = FileObject->CompletionContext;
if ( CompletionContext )
{
Port = (int)CompletionContext->Port;
Key = CompletionContext->Key;
}
}
if ( UserApcRoutine )
{
KeInitializeApc(
Apc,
thread,
2,
IopUserCompletion,
IopUserRundown,
UserApcRoutine,
(char)Apc[-1].ApcListEntry.Blink,
Apc[-1].SystemArgument1);
KeInsertQueueApc(Apc, Apc[-1].RundownRoutine, 0, 2);
}
else if ( Port && pIrp->Overlay.AllocationSize._s0.HighPart )
{
*(_DWORD *)&Apc->Type = Key;
pIrp->Tail.Overlay.PacketType = 0;
KeInsertQueue(Port, (int)&pIrp->Tail.Overlay.ListEntry);
}
在代码里判断超时后调用IoCompleteRequest的地方下断,跟进去,
IoCompleteRequest->IopfCompleteRequest->IopCompleteRequest,
需要断在IopCompleteRequest,可是IopCompleteRequest操作很频繁,
这里需要IopCompleteRequest的参数,是我们的代码调用的时候则断,于是用条件断点:
bu MySys!MyFunc+0x666 "r @$t0=poi(ecx+0x64);gc;" ---这里是调用IoCompleteRequest的地方!
bp IopCompleteRequest ".if(@$t0==poi(poi(esp+0x10))){}.else{gc;}"
断下后,跟IopCompleteRequest,最后发现会检查Irp-Flags,决定是否会往完成端口投递消息。
解决方案之一是在完成IRP之前往 flags 置 pending,就是一句话IoMarkIrpPending(pIrp);
-End-
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课