最近学完了滴水逆向线上的系统调用部分,但是视频中使用的是32位的xp系统,所以我自己研究了一下win10/11 64位的系统调用过程。在这篇文章中我会解释系统调用表是如何被调用的(主要研究非gui的系统调用),主要包括:win10中系统调用表中的函数参数个数如何确定;windows如何根据系统调用表和系统调用号找到0环的函数。
并且由于在win10中系统调用表变为未导出对象了,在本文章中也会给出win7/10/11的确定系统调用表的特征码。
在3环,所有的非gui的win32 api最终都会进入ntdll,这个dll包含了一系列进入0环的入口函数,以syscall指令结尾(系统调用号存储在r10中),这类函数最终就会调用0环的函数从而完成某些目的。

intel白皮书中对syscall指令的描述如下
也就是说这个指令通过IA32_LSTAR这个msr寄存器来选择进入0环的地址。在白皮书介绍msr的章节则可以查找到这个寄存器的编号

为0xc0000082,那么我们可以在kd中查看这个寄存器的值与对应的符号。
我们可以发现IA32_LSTAR寄存器的值存储的是一个叫做nt!kiSystemCall64Shadow的函数,这个函数未导出,并且由于未知原因,ntoskrnl载入时并不会完全不变,使用这个偏移在静态文件中找到对应函数是不可能的。但是,我们可以使用特征码搜索(u命令反编译或者dq,然后在ida中搜索bytes)找到这个函数。直接在这个函数开头下断点并调试是不可能的,会导致蓝屏,然而在其偏移0x3c处下断就能正常停止(非常玄学,我也不知道为什么)。


在这里下断以后,我们可以继续调试,这个函数最终会把我们带到一个叫做KiSystemServiceStart的地方,这个地方同样需要通过特征码搜索在ida中找到。不过我先暂停分析过程,在前面我们下的断点是不区分调用号的,这会导致无论是什么系统调用都会断下,就难以跟踪想要研究的系统调用号,因此这里我说明一下如何跟踪特定的系统调用号。
我们要分析特定的系统调用号,可以使用kd的
命令,这个命令可以使用kd对3环进程下断并调试,而且调试过程中是会冻结整个系统的。要分析哪个系统调用号,就写一个3环程序调用那个api,使用kd在对应的ntdll函数中下3环断点,然后先不要下KiSystemCallShadow64函数断点,而是先步进运行到syscall指令处,再对KiSystemCallShadow64函数下断点,然后步进,此时就会断在指定的系统调用号上。
从KiSystemCall64Shadow开始运行,我们最终会来到一个叫做KiSystemServiceStart的地方,下面紧跟着KiSystemServiceRepeat(这里的名字是我手动编辑的,实际上ida不会自动识别,只能自己用特征码找出来然后命名)

前面的代码其实都不太重要,是做一些准备工作,但是从这里开始,就是确定系统调用函数以及函数参数的关键代码了。首先KiSystemServiceStart开头往edi中存储了系统服务号第12位左移4的结果,然后将1~11位存入eax中。
然后在serviceRepeat中往r10、r11中存了两张表,KeServiceDescriptorTable与前表的shadow版本。在win10及其后的版本,服务表结构就不是导出的了,但是我们在32位的表结构基础上推测,仍然能推测出对应的表结构
KeServiceDescriptorTable和KeServiceDescriptorTableShadow在windbg中都是有对应符号的,我们分别看一下两个表的结构
这两个表的第一个_SERVICE_DESCRIPTOR_TABLE结构都是一样的,但是第二个不一样,shadow版本的第二个表是有填上的,也就是说shadow版本的表是带win32k.sys的。正常的用户启动进程最终使用的是shadow这个版本。
在KiSystemServiceRepeat下有一个分支用来选择使用的表,会根据kthread的0x78偏移处的成员ThreadFlag来选择所需的表,简单来讲,若ThreadFlag的GuiThread位标记为1,则选择shadow版本,而若RestrictGuiThread位标记为1,则选择另一张叫做KeServiceDescriptorTableFilter的表。若对选择逻辑感兴趣可自行研究。

之后会来到上图左边的分支中根据系统调用号(存储在eax中)确定所要调用的函数,不过ServiceTableBase中存储的实际上并不是系统函数,而是系统函数相对TableBase的最大不超过DWORD大小的偏移量,我们看它对取到的值的运算就能看出来
也就是说调用的函数实际上为:tableBase+tableBase[系统调用号]>>4,这里我们先看看tableBase中每个项是怎么样的。
至于为什么要右移4位,是因为最后4位存储的是函数参数个数的信息,下面我们来看这最后4位是怎么用的。
不同于xp32位中使用单独的一张表来确定每个系统调用的参数,win10 64位使用了tableBase的最后4位来确定参数个数。上面的分支中有一个jnz跳转,是用来区分普通的系统调用与win32k.sys的系统调用(edi为调用号12位),这里我们讨论的是普通的系统调用,因此这个跳转最终会执行,我们来到这个地方

我们上面的代码中已经把函数偏移项存入rax中了,所以这里&0xf之后,eax中只剩下最后4位。
若eax==0,也就是函数只使用直接寄存器传参或无参,我们会来到KiSystemServiceCopyEnd,
传播安全知识、拓宽行业人脉——看雪讲师团队等你加入!
最后于 2025-8-29 22:11
被nstlgst134编辑
,原因: 勘误与补充说明