Winodws x64系统服务调用的那头4个参数

http://blog.youkuaiyun.com/muy/article/details/46742083


起因

        前几天有朋友遇到一个问题来问我。他有一个Windows 7 x64下的minidump文件(http://pan.baidu.com/s/1dD524hz),经过初步分析,知道系统崩溃最初是因用户态调用了NtDeviceIoControlFile造成的,KeBugCheckEx调用已经位于多重函数调用的深处,问如何找到最初NtDeviceIoControlFile的参数,尤其是头4个参数。经过一番努力,我总算是把问题解决了,其中得到一些领悟,想趁热记录,也想趁这个机会将内核服务调用的基本技术点梳理一下。

        在开始针对问题开始讨论之前,我们先要补习一下Windows x64下函数调用的基本知识。

 

x64函数调用堆栈

        x64下,不再使用x86下以“pushebp/mov ebp, esp”为特征的堆栈帧格式。取而代之的是如下图所示格局:


        函数调用的头4个参数虽然在堆栈中预留了位置,但参数值并不真的存储在那里。实际上从第1个参数到第4个参数是依次是使用rcx、rdx、r8、r9(浮点数情况下依次对应:xmm0、xmm1、xmm2、xmm3)。几乎(注意是:几乎)所有函数在头几条指令范围内就通过sub rsp, xxh的形式准备好了本函数用到的堆栈空间,之后直到函数返回,此函数不再进行改变rsp的任何操作。也就是说函数的堆栈是“一步到位”的。这样一步到位的好处是避免堆栈指针的反复调整,从而有效提高代码执行效率。其副作用是:头4个参数不在堆栈上,不利于追踪;没有特殊的ebp堆栈基址(rbp不再使用),堆栈回溯较难。在函数调用时,rdi、rsi、rbx、rbx、rbp、r12、r13、r14、r15是所谓的非容失性(nonvolatile)寄存器。也就是说,在函数调用过程中,被调用函数必须保证这些寄存器在函数返回时和进入函数时是一样的。

        某处代码在调用某个函数时,会将头4个参数分别存入相应寄存器,如果超过4个参数,会在位于rsp+20h的地方存入第5个参数。随后call指令会将返回地址压入堆栈,造成rsp减8。此时第1个参数对应位置是rsp+8,第4个参数对应位置是rsp+20h。随后,一般的函数会一次性sub rsp, xxh,其中xxh不同函数有所不同。通过IDA Pro进行观察会发现,函数内部代码对局部变量和参数的访问,都翻译成[rsp+xxh+yyh]和形式,其中yyh是数据相对于函数第1条指令将要执行而尚未执行时rsp的指针位置(当然返回地址位置是rsp+0)。

        补习完基础知识,我们就可以开始研究问题了。当然在开始之前,要启动我们的Windbg,装入dump文件,下载并装入相应符号。

 

第5个及以后的参数

        首先NtDeviceIoControlFile的原型是:

[cpp]  view plain  copy
  1. NTSTATUS WINAPI NtDeviceIoControlFile(  
  2.   _In_  HANDLE                    FileHandle,  
  3.   _In_  HANDLE                    Event,  
  4.   _In_  PIO_APC_ROUTINE           ApcRoutine,  
  5.   _In_  PVOID                     ApcContext,  
  6.   _Out_ PIO_STATUS_BLOCK           IoStatusBlock,  
  7.   _In_  ULONG                     IoControlCode,  
  8.   _In_  PVOID                     InputBuffer,  
  9.   _In_  ULONG                     InputBufferLength,  
  10.   _Out_ PVOID                      OutputBuffer,  
  11.   _In_  ULONG                     OutputBufferLength  
  12. );  

  毫无疑问,第5个及以后的参数存储在堆栈中。我们使用kv来观察堆栈:

[plain]  view plain  copy
  1. 1:kd> kv  
  2. Child-SP       RetAddr        : Args to Child                                      : Call Site  
  3. fffff880`0253ff58 fffff800`03f577ab : 00000000`0000001e ffffffff`c0000005 fffff880`0254001000000000`00000000 : nt!KeBugCheckEx  
  4. fffff880`0253ff60 fffff800`03f16118 : 00000000`00000001 00000000`00000000 fffffa80`03306b30fffff880`02540920 : nt!KipFatalFilter+0x1b  
  5. fffff880`0253ffa0 fffff800`03eee89c : 00000000`00000000 fffffa80`03ee0da0 fffff8a0`00262450fffff8a0`00262140 : nt! ?? ::FNODOBFM::`string'+0x83d  
  6. fffff880`0253ffe0 fffff800`03eee31d : fffff800`0400f30c fffff880`02541610 00000000`00000000fffff800`03e50000 : nt!_C_specific_handler+0x8c  
  7. fffff880`02540050 fffff800`03eed0f5 : fffff800`0400f30c fffff880`025400c8 fffff880`02540f38fffff800`03e50000 : nt!RtlpExecuteHandlerForException+0xd  
  8. fffff880`02540080 fffff800`03efe081 : fffff880`02540f38 fffff880`02540790 fffff880`0000000000000000`00000000 : nt!RtlDispatchException+0x415  
  9. fffff880`02540760 fffff800`03ec20c2 : fffff880`02540f38 fffffa80`02884010 fffff880`02540fe000000000`00000010 : nt!KiDispatchException+0x135  
  10. fffff880`02540e00 fffff800`03ec0c3a : 00000000`00000001 00000000`00000018 00000000`00000000fffffa80`02884010 : nt!KiExceptionDispatch+0xc2  
  11. fffff880`02540fe0 fffff880`01374092 : fffff880`0512bf79 fffffa80`03f99a80 fffff880`0254127000000000`00000000 : nt!KiPageFault+0x23a (TrapFrame @ fffff880`02540fe0)  
  12. fffff880`02541178 00000000`00000087 : fffff800`04057580 fffffa80`03fb9ba0 fffff880`0512bee9fffffa80`03fb9ba0 : Ntfs!NtfsIterateMft+0x192  
  13. fffff880`02541218 fffff800`04057580 : fffffa80`03fb9ba0 fffff880`0512bee9 fffffa80`03fb9ba000000000`00000000 : 0x87  
  14. fffff880`02541220 fffffa80`03fb9ba0 : fffff880`0512bee9 fffffa80`03fb9ba0 00000000`00000000fffffa80`02884010 : nt!NonPagedPoolDescriptor  
  15. fffff880`02541228 fffff880`0512bee9 : fffffa80`03fb9ba0 00000000`00000000 fffffa80`02884010fffff800`03ffb44e : 0xfffffa80`03fb9ba0  
  16. fffff880`02541230 fffff880`0512bc1b : fffffa80`03fb9ba0 00000000`00000000 fffffa80`03fc0080fffff800`0406da28 : afd!AfdTLBindComplete2+0xb9  
  17. fffff880`025412d0 fffff880`016777b9 : 00000000`00000000 fffffa80`00000000 fffffa80`03f99a8000000000`00000000 : afd!AfdTLBindComplete+0x3b  
  18. fffff880`02541370 fffff880`016774aa : 00000000`00000002 fffffa80`03f99a80 00000000`0000000200000000`00000000 : tcpip!TcpBindEndpointInspectComplete+0x249  
  19. fffff880`02541410 fffff880`016782ff : fffffa80`025c9100 fffff880`00000000 fffffa80`03f99a80fffffa80`025c9100 : tcpip!TcpBindEndpointRequestInspectComplete+0x27a  
  20. fffff880`025414f0 fffff880`016780c7 : fffffa80`03e17240 fffff880`0512bbe0 fffffa80`02884010fffffa80`02884010 : tcpip!TcpBindEndpointWorkQueueRoutine+0x8f  
  21. fffff880`02541540 fffff880`01678168 : fffff880`025417f0 fffff880`0174e160 fffffa80`03f99a80fffff880`02541810 : tcpip!TcpBindEndpoint+0x87  
  22. fffff880`02541570 fffff880`0167706c : 00000000`00000000 fffff880`02541720 00000000`0000000100000000`00004800 : tcpip!TcpIoControlEndpoint+0x68  
  23. fffff880`025415b0 fffff800`03ecf3d8 : fffff880`02541ca0 fffff800`04102540 00000000`00000000fffff880`02541870 : tcpip!TcpTlEndpointIoControlEndpointCalloutRoutine+0x1c  
  24. fffff880`02541610 fffff880`016771e0 : fffff880`01677050 fffff800`03ecf3d8 00000000`00000000fffff800`03ecfd01 : nt!KeExpandKernelStackAndCalloutEx+0xd8  
  25. fffff880`025416f0 fffff880`051404a4 : fffffa80`03f99a80 fffff880`01677170 fffffa80`03fb9ba0fffff880`025417f0 : tcpip!TcpTlEndpointIoControlEndpoint+0x70  
  26. fffff880`02541760 fffff880`0512efc5 : 00000000`00000000 fffff880`01677170 fffffa80`03fb9ba000000000`00000018 : afd! ?? ::GFJBLGFE::`string'+0xa1c0  
  27. fffff880`025417d0 fffff880`051404cb : fffffa80`03f99a80 fffff880`01677170 fffffa80`03fb9ba0fffff880`025418e0 : afd!AfdTLBind+0x75  
  28. fffff880`02541850 fffff880`0512bdf9 : 00000000`00000010 fffff880`02541ca0 fffffa80`0288401000000000`00000000 : afd! ?? ::GFJBLGFE::`string'+0xa1e7  
  29. fffff880`025418c0 fffff880`05114639 : fffffa80`03f7f560 fffffa80`00000000 00000000`00000001fffff880`02541ca0 : afd!AfdTLBindSecurity+0x139  
  30. fffff880`02541940 fffff800`041de8d7 : fffffa80`02884010 fffff880`00000003 fffffa80`0000001000000000`00000000 : afd!AfdBind+0x399  
  31. fffff880`02541a10 fffff800`041df136 : fffff683`ff7e6eb8 00000000`00000188 00000000`0000000000000000`00000000 : nt!IopXxxControlFile+0x607  
  32. fffff880`02541b40 fffff800`03ec1cd3 : fffffa80`03fbf530 0000007f`ffffffff fffff880`02541bc800000980`00000000 : nt!NtDeviceIoControlFile+0x56  
  33. fffff880`02541bb0 00000000`776adc4a : 00000000`00000000 00000000`00000000 00000000`0000000000000000`00000000 : nt!KiSystemServiceCopyEnd+0x13 (TrapFrame @fffff880`02541c20)  
  34. 00000000`0054f228 00000000`00000000 : 00000000`00000000 00000000`00000000 00000000`0000000000000000`00000000 : 0x776adc4a  
        通过Call Site对应列的显示内容可以看出,异常确实起源于NtDeviceIoControlFile。对NtDeviceControlFile的调用源自符号KiSystemServiceCopyEnd附近(那里是系统从用户态转换到核心态,并查找调用系统服务例程的地方)。Call Site列基本显示出函数的调用关系,因此我们也引用它来表示“当前”某个函数。

        nt!KiSystemServiceCopyEnd+0x13的Child-SP为fffff880`02541bb0我们使用下面的命令观察:

[plain]  view plain  copy
  1. 1:kd> dq fffff880`02541bb0 L4  
  2. fffff880`02541bb0  fffffa80`03fbf530 0000007f`ffffffff  
  3. fffff880`02541bc0  fffff880`02541bc8 00000980`00000000  
  4. 1:kd> dqs fffff880`02541bb0-8 L1  
  5. fffff880`02541ba8 fffff800`03ec1cd3 nt!KiSystemServiceCopyEnd+0x13  
        地址fffff880`02541bb0是KiSystemServiceCopyEnd所处函数所使用的堆栈顶端。也就是说,这个位置是调用NtDeviceControlFile第1个参数的位置,此处之后4个QWORD值与kv命令显示的nt!NtDeviceIoControlFile+0x56这一行的Args to Child相同。这个位置减去8,是NtDeviceControlFile的返回地址RetAddr,也就是nt!KiSystemServiceCopyEnd+0x13。那么第5个参数应当位于fffff880`02541bb0+0x20处:

[plain]  view plain  copy
  1. 1:kd> dq fffff880`02541bb0+0x20 L6  
  2. fffff880`02541bd0  00000000`0054f2c0 000007fe`00012003  
  3. fffff880`02541be0  00000000`0054f2e8 00000000`00000014  
  4. fffff880`02541bf0  00000000`0054f2e8 00000000`00000010  
        这样我们就得出第5至第10个参数的值依次为:

[cpp]  view plain  copy
  1. IoStatusBlock = 00000000`0054f2c0  
  2. IoControlCode = 0x12003  
  3. InputBuffer = 00000000`0054f2e8  
  4. InputBufferLength = 0x14  
  5. OutputBuffer = 00000000`0054f2e8  
  6. OutputBufferLength = 0x10  

第2、3、4个参数

        用kv命令看到的NtDeviceIoControlFile前4个参数明显不对,它只是堆栈上的随机值罢了。实际上前4个参数存储在当时的rcx、rdx、r8、r9寄存器中。我开始猜想,既然nt!NeDeviceIoControlFile是从ntdll!NtDeviceIoControlFile调用过来的,系统应该在切换处理器状态时,将这4个寄存器存入创建的_KTRAP_FRAME结构中。于是在kv命令中,我们看到这个:

[plain]  view plain  copy
  1. nt!KiSystemServiceCopyEnd+0x13(TrapFrame @ fffff880`02541c20)  
        这里有个疑问——我无法访问当前线程。查看gs:188h处内存,会得到:

[plain]  view plain  copy
  1. 1:kd> dq gs:188 L1  
  2. 002b:00000000`00000188  ????????`????????  
        查看gs寄存器基址(x64下GDT已废弃不用,命令rM100已派不上用场):

[plain]  view plain  copy
  1. 1:kd> rdmsr c0000101  
  2. nosuch msr  
        使用命令!pcr:

[plain]  view plain  copy
  1. 1:kd> !pcr  
  2. Cannot get PRCB address  
        但是使用命令.thread、!thread或r $thread,却能看到当前线程对象为fffffa80`03fbf530。用dt _KTHREAD TrapFrame fffffa80`03fbf530命令查看到的TrapFrame指针与kv命令显示的最底层TrapFrame相同,都是fffff880`02541c20。于是:

[plain]  view plain  copy
  1. 1:kd> dt _KTRAP_FRAME fffff880`02541c20  
  2. nt!_KTRAP_FRAME  
  3. ……  
  4.    +0x038 Rcx              : 0x14  
  5.    +0x040 Rdx              : 0x10  
  6.    +0x048 R8               : 1  
  7.    +0x050 R9               : 0  
  8. ……  
        但这里第3个参数ApcRoutine为1,使我深深怀疑它的准确性。

        既然TrapFrame数据不正确,那我们只能顺着代码追一追,看看以后的代码是否会将前4个参数存到我们可以访问到的地方。逆向nt!NtDeviceIoControlFile:

[plain]  view plain  copy
  1. DeviceIoControlFile proc near  
  2.           sub     rsp,68h  
  3.           mov     eax, dword ptr[rsp+68h+arg_48]  
  4.           mov     byte ptr [rsp+68h+var_18], 1  
  5.           mov     [rsp+68h+var_20], eax  
  6.           mov     rax, [rsp+68h+arg_40]  
  7.           mov     [rsp+68h+Src], rax  
  8.           mov     eax, [rsp+68h+arg_38]  
  9.           mov     [rsp+68h+var_30], eax  
  10.           mov     rax, qword ptr[rsp+68h+arg_30]  
  11.           mov     qword ptr [rsp+68h+var_38], rax  
  12.           mov     eax, dword ptr[rsp+68h+arg_28]  
  13.           mov     dword ptr [rsp+68h+var_40], eax  
  14.           mov     rax, [rsp+68h+arg_20]  
  15.           mov     [rsp+68h+var_48], rax  
  16.           call    IopXxxControlFile  
  17.           add     rsp,68h  
  18.           retn  
  19. NtDeviceIoControlFile endp  
        可以看出,这个函数没有改变前4个参数所在的寄存器值,而只是复制其余参数,然后调用了IopXxxControlFile。逆向IopXxxControlFile看到头4条指令是:

[plain]  view plain  copy
  1. IopXxxControlFile proc near  
  2.           mov     r11, rsp  
  3.           mov     [r11+20h], r9  
  4.           mov     [r11+18h], r8  
  5.           mov     [r11+10h], rdx  
        原来这个函数一上来就将第2至第4个参数保存到相应预留的位置。那么在异常发生时,这3个值依然在那里。kv命令显示,nt!IopXxxControlFile+0x607行的这3个参数是:

[cpp]  view plain  copy
  1. Event = 0x188  
  2. ApcRoutine = 0  
  3. ApcContext = 0  


那个难缠的第1个参数

        顺着这条路向下,发现很难追到rcx的踪迹了。于是转变思路,看一看NtDeviceIoControlFile是如何从用户态切换到核心态的,也许能发现什么蛛丝马迹。

        在ntdll中NtDeviceIoControlFile是这样子的:

[plain]  view plain  copy
  1. NtDeviceIoControlFile proc near  
  2.           mov     r10, rcx  
  3.           mov     eax,4  
  4.           syscall  
  5.           retn  
  6. NtDeviceIoControlFile endp  
        ntdll!NtDeviceIoControlFile被调用时,无疑参数传递也遵循x64的函数调用约定。第1句用r10临时存储rcx,因为随后的syscall调用会用到rcx。eax中是NtDeviceIoControlFile所对应的系统服务号。在syscall调用之前,rsp是返回地址,rsp+8h是第1个参数位置,以此类推rsp+28h是第5个参数位置。根据AMD64手册,syscall指令所做操作大致为:

  1)  下一条指令——即retn——的地址存入rcx;

  2)  将rflags内容存入r11;

  3)  将MSR_LSTAR内容存入rip;

  4)  设置cs;

  5)  设置ss;

  6)  改变当前CPL(当前权限等级)为0;

  7)  设置rflags。

        我们可以在一个正常的被调试的Windowsx64系统中(不是在当前加载dump文件时)看到:

[plain]  view plain  copy
  1. 1:kd> rdmsr c0000082  
  2. Msr[c0000082]= fffff800`03cd3640  
  3. 1:kd> ln fffff800`03cd3640  
  4. (fffff800`03cd3640)nt!KiSystemCall64  
  5. ……  
        MSR_LSTAR的内容是内核地址KiSystemCall64:

[plain]  view plain  copy
  1. KiSystemCall64  proc near  
  2. (1)      swapgs  
  3. (2)      mov     gs:10h, rsp  
  4. (3)      mov     rsp, gs:1A8h  
  5.           push    2Bh  
  6.           push    qword ptr gs:10h  
  7.           push    r11  
  8.           push    33h  
  9.           push    rcx  
  10.           mov     rcx, r10  
  11.           sub     rsp,8  
  12.           push    rbp  
  13.           sub     rsp,158h  
  14.           lea     rbp, [rsp+190h+var_110]  
  15. (4)      mov     [rbp+0C0h],rbx  
  16.           mov     [rbp+0C8h], rdi  
  17.           mov     [rbp+0D0h], rsi  
  18.           mov     byte ptr [rbp-55h], 2  
  19. (5)      mov     rbx, gs:188h  
  20.           prefetchw byte ptr [rbx+1D8h]  
  21.           stmxcsr dword ptr [rbp-54h]  
  22.           ldmxcsr dword ptr gs:180h  
  23. (6)      cmp     byte ptr [rbx+3],0  
  24.           mov     word ptr [rbp+80h],0  
  25.           jz      loc_140071B50  
  26.           mov     [rbp-50h], rax  
  27. (7)      mov     [rbp-48h], rcx  
  28.           mov     [rbp-40h], rdx  
  29.           test    byte ptr [rbx+3],3  
  30.           mov     [rbp-38h], r8  
  31.           mov     [rbp-30h], r9  
  32.           ……  
        指令(1)将gs的基址与MSR[c0000102]内容互换,设置过后gs:0指向内核处理器控制域_KPCR。指令(2)将用户态堆栈指针保存到_KPCR+0x010的成员UserRsp里。指令(3)使用_KPCR+0x1A8的成员RspBase设置当前rsp,这个成员存储了当前线程核心态的堆栈指针。一路执行下来,到达指令(4), rsp与设置伊始相比共减少了0x190字节。期间恢复了rcx,在堆栈上保存了一些值。使用下面的命令,我们可以看出,0x190正是_KTRAP_FRAME的大小:

[plain]  view plain  copy
  1. 1:kd> ??sizeof(_KTRAP_FRAME)  
  2. unsigned int64 0x190  
        现在rsp指向_KTRAP_FRAME的起始地址,rbp指向_KTRAP_FRAME+0x80的位置。我们惊奇地发现指令(7)处及以下若干条指令将rcx、rdx、r8、r9存入了_KTRAP_FRAME当中,其中rbp-48h相当于rsp+80h-48h即rsp+38h。但为什么我们之前分析_KTRAP_FRAME时得到的是错误的数据呢?再仔细看看!指令(6)处将rbx+3处的字节与0进行了比较,如果是0则不会保存那4个寄存器。而通过指令(5),我们知道rbx存储了当前线程对象的起始地址:

[plain]  view plain  copy
  1. 1:kd> dt _KTHREAD -b  
  2. nt!_KTHREAD  
  3.    +0x000 Header           : _DISPATCHER_HEADER  
  4.       +0x000 Type             : UChar  
  5.    ……  
  6.       +0x003 DebugActive      : UChar  
  7.       ……  
        可见只有在当前线程的DebugActive为非0时,系统才会在_KTRAP_FRAME中存储系统服务调用的头4个参数。绝望了吗?继续向下看,代码跳转至loc_140071B50后:

[plain]  view plain  copy
  1. loc_140071B50:  
  2.           sti  
  3.           mov     [rbx+1E0h], rcx  
  4.           mov     [rbx+1F8h], eax  
        上帝终于看不下去出手拯救了我们:

[plain]  view plain  copy
  1. 1:kd> dt _KTHREAD fffffa80`03fbf530  
  2. nt!_KTHREAD  
  3. ……  
  4.    +0x1e0 FirstArgument    : 0x00000000`0000018c Void  
  5. ……  
        于是我们得到了第1个参数的值:

[cpp]  view plain  copy
  1. FileHanle = 0x18c  
        到这里似乎问题已得到了解答。但我们还想看一看,系统服务是如何被调用的,参数又是如何从用户态堆栈转移到核心态堆栈的。

 

系统服务的调用

        沿着KiSystemCall64的执行路径继续向下,到达了KiSystemServiceStart:

[plain]  view plain  copy
  1. KiSystemServiceStart proc near  
  2.           mov     [rbx+1D8h], rsp  
  3.           mov     edi, eax  
  4.           shr     edi,7  
  5.           and     edi,20h  
  6.           and     eax,0FFFh  
        这里_KTHREAD+0x1D8h处就是TrapFrame。之后,系统取得了存于eax中的服务号。这个服务号的最高位,即第13个比特位(Bit12,最低位为Bit0),表示服务的类型是属于执行体的还是图形界面的。如果这一位置位,则结果是rdi为0x20,否则就为0。服务号的其余低位是真正的服务索引。在往后,到达了KiSystemServiceRepeat:

[plain]  view plain  copy
  1. KiSystemServiceRepeat proc near  
  2.           lea     r10,KeServiceDescriptorTable  
  3.           lea     r11,KeServiceDescriptorTableShadow  
  4. (1)      test    dword ptr [rbx+100h],80h  
  5. (2)      cmovnz  r10, r11  
  6. (3)      cmp     eax, [rdi+r10+10h]  
  7.           jnb     loc_140071E82  
  8. (4)      mov     r10, [rdi+r10]  
  9. (5)      movsxd  r11, dword ptr [r10+rax*4]  
  10.           mov     rax, r11  
  11.           sar     r11,4  
  12.           add     r10, r11  
  13.           cmp     edi,20h  
  14. (6)      jnz     short loc_140071C00  
        指令(1)比较了_KTHREAD+0x100处的GuiThread比特位是否置位,置位则表示当前线程有图形界面。r10作为服务描述符表的基址,语句(2)决定了在无图形界面时这个基址是KeServiceDescriptorTable,有图形界面时这个基址是KeServiceDescriptorTableShadow。语句(3)比较当前服务号是否超出了服务分派表内服务数量的上限。为什么会把rdi设置为0或0x20的作用这里体现出来了,这个服务描述符表可能包含2个元素,每个元素描述了一个服务分派表,而每个元素的大小恰好是0x20。语句(4)根据服务号的最高位选择使用哪一个服务分派表。系统维护的两个服务分派表,一个指向ntoskrnl.exe内的服务例程nt!Nt*,一个指向wind32k.sys内的win32k!Nt*。指令(5)取得服务分派表内与服务号相对应的元素并存入r11并复制了一份在rax当中。之后使用下面的公式计算服务例程地址,并把它放入r10:

[plain]  view plain  copy
  1. 服务例程地址 = 服务分派表基址 + ( 服务分派表元素>> 4 )  
        这里我们先不去管GUI线程,由指令(6)跳转至loc_140071C00:

[plain]  view plain  copy
  1. loc_140071C00:  
  2. (1)      and     eax, 0Fh  
  3.           jz      KiSystemServiceCopyEnd  
  4. (2)      shl     eax, 3  
  5. (3)      lea     rsp, [rsp-70h]  
  6. (4)      lea     rdi, [rsp+18h]  
  7. (5)      mov     rsi, [rbp+100h]  
  8. (6)      lea     rsi, [rsi+20h]  
  9.           test    byte ptr [rbp+0F0h],1  
  10.           jz      short loc_140071C40  
  11.           cmp     rsi, cs:MmUserProbeAddress  
  12.           cmovnb  rsi, cs:MmUserProbeAddress  
  13.           nop     dword ptr [rax+00000000h]  
  14. loc_140071C40:  
  15. (7)      lea     r11, KiSystemServiceCopyEnd  
  16.           sub     r11, rax  
  17.           jmp     r11  
        指令(1)比较服务分派表元素低4位是否为0。之后指令(2)将这低4位乘以8。指令(3)预留了足够多的堆栈空间以便复制参数。如果我们假定rdi当前指向第1个参数的位置,那么指令(4)将rdi定位在了第4个参数的位置上。注意,这是精心设计的。指令(5)将rsi设置为_KTRAP_FRAME+0x180位置的值,即用户态的rsp。还记得syscall之前它的位置吗?它指向ntdll!NtDeviceIoControlFile的返回地址,于是rsi+8就是ntdll!NtDeviceIoControlFile第1个参数的位置,那么指令(6)的rsi+20h就是第4个参数的位置。这样,rsi与rdi分别指向了用户态和核心态的第4个参数的位置。看来是为复制参数做好了准备。从指令(7)开始的3条指令,可以看出服务分派表元素低4位原来是需要复制参数的个数,它的数据为m-4,其中m为服务调用的参数个数。如果这个服务有5个参数,则这个服务对应元素的低4位就应当是1。如果要复制的参数个数为n(n=m-4),那么代码会跳转至KiSystemServiceCopyEnd之前的n*8个字节的位置执行。KiSystemServiceCopyEnd前后的代码揭示了这个精巧设计的秘密:

[plain]  view plain  copy
  1. ……  
  2.           48 8B 46 10     mov     rax, [rsi+10h]  
  3.           48 89 47 10     mov     [rdi+10h], rax  
  4.           48 8B 46 08     mov     rax, [rsi+8]  
  5.           48 89 47 08     mov     [rdi+8], rax  
  6. KiSystemServiceCopyEnd proc near  
  7.                           test   cs:dword_140207348,40h  
  8.                           jnz     loc_140071F20  
  9.                           call    r10  
        原来KiSystemServiceCopyEnd之前是复制参数的指令,每复制一个参数需要8个字节的指令。还记得rsi和rdi的位置吗?它们指向第4个参数位置,于是加上8就是第5个参数。复制是从第5个参数开始的,头4个参数在寄存器中。之后的代码就无需做过多解释了。

 
 

 

结论

        我们为了找到系统服务调用NtDeviceIoControlFile的参数做出了很大的努力,比较自然地找到了第5个及以后的参数,费力地找到了第1个参数,比较幸运地找到了第2、3、4个参数——因为系统比较偶然的将它们存入了堆栈。对于第2、3、4个参数来说,无法保证系统一直会这样处理它们,实际上有许多的系统服务例程没有这样做,比如说NtCreateThread,所以也就没有通用可靠的方法在系统出现崩溃时去找到它们的值。当然系统服务例程应该有责任避免崩溃的发生,但却难以避免正在开发的驱动程序在其深处崩溃。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值