前面的学习过程中,我们了解到程序进入0环后,有一个保存现场的过程,会将3环的各种寄存器都保存到一个叫做_Trap_Frame的结构体中。在3环部分,程序将一个编号存到了eax中,这个编号叫做系统服务号,此外,在保存现场的过程中,程序还让edx指向了3环第一个参数的地址。回忆起这两步,接下来,就可以继续探究执行内核函数的过程了。
系统服务表
在分析代码前,我们先来学习一个结构,系统服务表(System Service Table)
![[]]() 
在Windows XP系统下,系统服务表有两张,这两张表存着内核文件的导出函数(不包括内核文件的所有函数,主要是3环函数常用的内核函数)。第一张表导出的内核函数主要来源于ntoskrl.exe,实现大部分3环函数基本功能;第二张表导出的内核函数主要来源于win32k.sys,主要实现图形界面相关功能(例如GDI32.dll的底层实现)。
系统服务表结构
根据示例图,我们先简单认识一下系统服务表,先从结构看起:
- ServiceTable:指向一个函数地址表,通过系统服务号可以在函数地址表中找到指定的内核函数。
- Count:指当前系统服务表被调用的次数。
- ServiceLimit:函数地址表的大小,即系统服务函数的个数
- ArgmentTable:系统服务函数参数的大小,以字节为单位,每个成员大小为1个字节。
系统服务表位置
这个系统服务表位于KTHREAD结构的0xE0偏移处。这样,在进入0环后我们可以通过fs:[0]找到KPCR结构,然后在KPCR->0x124找到当前线程的KTHREAD结构,再根据KTHREAD->0xE0就可以找到当前线程所拥有的系统服务表。
| 1
 | fs:[0] -> KPCR -> KPCR+0x124 -> KTHREAD -> KTHREAD+0xE0 -> 系统服务表
 | 
系统服务号
系统服务号用来定位所要寻找的系统服务表的函数。
![[]]() 
系统服务号只有低13位是有用的
- 下标12:判断去查服务表,0去查第一张表;1去查第二张表
- 下标0~11:函数地址表的索引
SharedCode分析
在前面保存现场的代码分析中,由于进入0环的方式不同,中断进0环(int 2E)和快速调用(sysenter)保存现场的方式也不一样,但是当这两种方式,将寄存器保存到_Trap_Frame结构中以后(保存现场),便会从同一个地放(KiFastCallEntry+0x8D)开始执行,我们把这一部分共同的代码称作SharedCode(引用Joney的文中的称呼),接下来,我们简要分析一下SharedCode到底做了什么事。
这里还是将代码放在一起分析,更有连贯性,具体细节可以到Windbg中动手实现
| 12
 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
 
 | 804df781 8bf8            mov     edi,eax			//eax保存的是系统服务号(参考3环部分),edi获804df781							//取服务号
 804df783 c1ef08          shr     edi,8				//将edi右移8位,系统服务号只用到13位,这样还
 804df783							//剩下5位
 804df786 83e730          and     edi,30h			//将余下的5位与0x30(0011 0000)进行与
 804df786							//算,得到的结果只有可能是0x10或者0,这里非常巧
 804df786							//妙,因为0x10刚好是16个字节
 804df789 8bcf            mov     ecx,edi			//将与后的结果,赋值给ecx
 804df78b 03bee0000000    add     edi,dword ptr [esi+0E0h]	//esi指向KTHREAD,[esi+0xE]就算获取到第一个系
 804df78b							//统服务表的首地址,然后加上edi。这里如果edi的
 804df78b							//值是0x10,运算后,edi就会指向第二个系统服务
 804df78b							//表的首地址,所以上一步的与运算很巧妙
 804df791 8bd8            mov     ebx,eax			//ebx获取服务号
 804df793 25ff0f0000      and     eax,0FFFh			//对服务号进行与运算,因为上面的步骤已经确定了
 804df793							//从哪一个系统服务表里找,这里将下标12的位置零
 804df793							//,获取在函数地址表里的索引
 804df798 3b4708          cmp     eax,dword ptr [edi+8]		//[edi+8]指向ServiceLimit,这里与系统服务号作
 804df798							//比较,防止系统符服务号越界
 804df79b 0f8341fdffff    jae     nt!KiBBTUnexpectedRange (804df4e2)	//若越界则跳转异常处理
 804df7a1 83f910          cmp     ecx,10h			//这里判断系统服务号属于哪个表(虽然前面的步骤判
 804df7a1							//断过一次了,前面是为了判断服务号是否越界,这
 804df7a1							//里主要是找到合适的处理函数)
 804df7a4 751a            jne     nt!KiFastCallEntry+0xcc (804df7c0)	//若不等,就跳转到处理查找第一个系统服
 804df7a4							//务表的例程
 804df7a6 8b0d18f0dfff    mov     ecx,dword ptr ds:[0FFDFF018h]	//若要去查找第二个系统服务表,从这里走,不作详细
 804df7a6							//分析
 804df7ac 33db            xor     ebx,ebx
 804df7ae 0b99700f0000    or      ebx,dword ptr [ecx+0F70h]
 804df7b4 740a            je      nt!KiFastCallEntry+0xcc (804df7c0)
 804df7b6 52              push    edx
 804df7b7 50              push    eax
 804df7b8 ff1564b25580    call    dword ptr [nt!KeGdiFlushUserBatch (8055b264)]
 804df7be 58              pop     eax
 804df7bf 5a              pop     edx
 804df7c0 ff0538f6dfff    inc     dword ptr ds:[0FFDFF638h]	//如果查第一个表,会跳到这里来 [0xffdff638]
 804df7c0							//KPRCB结构的0x518偏移处,存的是KeSystemCalls
 804df7c0							//,这里自增1(具体用处到APC那块会讲到)
 804df7c6 8bf2            mov     esi,edx			//edx存着3环第一个参数的地址,赋给esi
 804df7c8 8b5f0c          mov     ebx,dword ptr [edi+0Ch]	//ebx获取函数参数表地址
 804df7cb 33c9            xor     ecx,ecx			//清空ecx,之前用来判断寻找哪个系统服务表
 804df7cd 8a0c18          mov     cl,byte ptr [eax+ebx]		//cl获取函数参数个数
 804df7d0 8b3f            mov     edi,dword ptr [edi]		//另edi直接指向函数地址表首地址
 804df7d2 8b1c87          mov     ebx,dword ptr [edi+eax*4]	//ebx获取到0环实现的内核函数
 804df7d5 2be1            sub     esp,ecx			//提升堆栈,ecx里存的是参数个数的总字节
 804df7d7 c1e902          shr     ecx,2				//相当于运算ecx/4,方便rep movsd,因为rep
 804df7d7							//movsd是4字节运算
 804df7da 8bfc            mov     edi,esp			//rep movsd指令用,Copy的目的地
 804df7dc 3b35d40b5680    cmp     esi,dword ptr [nt!MmUserProbeAddress (80560bd4)]//检测三环参数地址范围是否越界
 804df7e2 0f83a8010000    jae     nt!KiSystemCallExit2+0x9f (804df990)	//若越界,进行异常处理
 804df7e8 f3a5            rep movs dword ptr es:[edi],dword ptr [esi]	//将参数复制到堆栈
 804df7ea ffd3            call    ebx				//调用0环函数!
 804df7ec 8be5            mov     esp,ebp
 804df7ee 8b0d24f1dfff    mov     ecx,dword ptr ds:[0FFDFF124h]
 804df7f4 8b553c          mov     edx,dword ptr [ebp+3Ch]
 804df7f7 899134010000    mov     dword ptr [ecx+134h],edx
 
 | 
至此,这块KiFastCallEntry和KiSystemService最终都会执行的一段被我们称作SharedCode的代码段,就分析完了,
SSDT
前文提到了,我们可以通过fs找到KPCR,在通过KPCR找到KTHREAD,然后在KTHREAD+0xE0处找到系统服务表,这里再介绍另一种找到系统服务表的办法,通过SSDT。
SSDT&SSDT Shadow
SSDT(System Services Descriptor Table)系统服务描述符表,在这个结构中包含4个成员,每个成员都是一个系统服务表的结构体,可以在Windbg中通过dd KeServiceDescriptorTable指令进行查看(在程序中可以直接声明全局变量KeServiceDescriptorTable,从而找到找到系统服务表。):
![[]]() 
我们可以看到第一个成员的ServiceTable,Count,ServiceLimit,ArgmentTable字段,Windows Xp只使用了2张表,所以第三个和第四个成员的位置是空的,此外,由于SSDT第二个成员是未导出的,所以第二个成员的位置也是空的。这里介绍一个新的指令,dd KeServiceDescriptorTableShadow,通过全局变量KeServiceDescriptorTableShadow可以查看两张完整的系统服务表。
![[]]() 
但是,全局变量KeServiceDescriptorTableShadow也是未导出的,在实际写程序时,不能通过直接访问win32k.sys导出的第二张系统服务表的函数地址,因为里面的函数地址都是无效的。原因是,win32k.sys导出的第二张系统服务表只有在当前进程访问GDI相关的API时,里面的函数地址表才会挂载到物理页上。如果进程没有用到GDI相关的API,那么第二张系统服务表里面的函数地址表就不会挂载到物理内存,那么里面的函数也无效。
内核函数查找
有了SSDT表,我们查找3环API对应的内核函数就很简单了,拿之前分析过的3环API函数ReadProcessMemory举例,在进入0环之前,给eax赋值了一个系统服务号0xba,那我们就用这个ba来查看这个这个函数在内核的实现。
![[]]() 
通过这张图,可以很清晰的看出来,ReadProcessMemory所实现的功能,在底层是由一个叫做NtReadVirtualMemory完成的。
总结
API函数的调用过程,从3环进入0环,再到找到对应的内核函数,这部分到这就差不多了,当然,真正的调用过程并没有到此结束,因为调用完0环的函数,总得返回3环呀!只是这部分需要用到APC的知识点,因此这里还不能完整实现。此外,在API函数调用这块,还有个小实验,在SSDT表中追加一个函数地址(NtReadVirtualMemory),自己编写API的3环部分调用这个新增的函数(注意:使用2-9-9-12分页,10-10-12会蓝屏),就留到后面补上了
参考教程:https://www.bilibili.com/video/BV1NJ411M7aE?p=41
参考文章:
- https://www.cnblogs.com/joneyyana/p/12585469.html
- https://blog.csdn.net/qq_38474570/article/details/103674271
- https://blog.csdn.net/qq_41988448/article/details/102994374