欢迎来到 嗅灵易学

零基础也能上手的脚本技术课,一对一答疑带你入门

[原创]记一次复杂的debug过程

[原创]记一次复杂的debug过程

记一次复杂的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-

注意:上传附件及图片大小不得大于30M。

⚠️ 版权声明:
本博客所有内容(含教程、源码、工具)仅供个人技术学习与研究交流使用,严禁商用、倒卖、二次分发及非法用途
未经作者书面授权,任何组织或个人不得转载、复制或用于其他平台,违者将追究相关责任。

0 0 0 举报
复制成功