windows系统调用(二)

简介:这篇接着继续介绍windows系统调用的过程,当填充好之后是如何调用对应的服务函数,参数的传送以及调用完之后的收尾工作有哪些。

前言

上篇我们介绍了windows再x86的CPU架构下,使用sysenter指令,进入内核前后的过程,当通过填充KTRAP_FRAME后,保存好用户层的上下文环境(文章地址:https://daliu.net/posts/20250105/),今天通过这篇来介绍后续的调用过程。

系统服务

我们接着上篇文章介绍的汇编代码,接着向下继续阅读,接下来总该进入系统函数执行的过程了吧,毕竟,进入内核主要要干的事情就是这个呀,首先把后续的汇编代码粘到下面:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
.text:00435449                 mov     edi, eax
.text:0043544B                 shr     edi, 8
.text:0043544E                 and     edi, 10h
.text:00435451                 mov     ecx, edi
.text:00435453                 add     edi, [esi+0BCh]
.text:00435459                 mov     ebx, eax
.text:0043545B                 and     eax, 0FFFh
.text:00435460                 cmp     eax, [edi+8]
.text:00435463                 jnb     _KiEndUnexpectedRange@0 ; KiEndUnexpectedRange()
.text:00435469                 cmp     ecx, 10h
.text:0043546C                 jnz     short loc_435488
.text:0043546E                 mov     ecx, [esi+88h]
.text:00435474                 xor     esi, esi

第一句提到的eax寄存器,从上文分析可以知道,这个eax至今还没有出现过,他是从用户层传入的,具体可以定位到下面这个图片。

1
.text:00435449                 mov     edi, eax

在进入KiFastSystemCall函数前,传入给eax的参数是一个固定的数,而且,在这篇区域,有很多个重复的内容,区别就是每一个调用函数对应传给eax的固定的数是不同的,借此我们可以分析,每一个系统函数对应的一个固定的编号,想要调用哪个就需要给KiFastSystemCall函数传入对应的编号。

2

那么操作系统一定维护了一个这样的表,描述了函数的信息和编号信息的对应关系。

SSDT(系统服务表)

为了方便,可以把刚刚的汇编部分的代码在IDA中用昨天说过的方式引入结构体,看起来会更方便

3

接着看后面的代码:

1
2
3
4
5
6
7
8
// edi向右移动8位,然后又与上10,如果这个编号是小于256的数,此刻这个值就是0了,
// 但如果是大于0x1000的数(两个字节),经过位运算,这个数一定是0x10
.text:0043544B                 shr     edi, 8
.text:0043544E                 and     edi, 10h

// 将这个值又覆给了ecx,然后又把esi一个成员,结合上文这个是线程块结构体中的系统服务表地址
.text:00435451                 mov     ecx, edi
.text:00435453                 add     edi, [esi+_ETHREAD.Tcb.ServiceTable]

继续跟下去:

1
2
3
4
5
// 继续比较服务号,与0xfff也就是获得索引,然后跟edi+8比较,如果超过了就报错。
.text:00435459                 mov     ebx, eax
.text:0043545B                 and     eax, 0FFFh
.text:00435460                 cmp     eax, [edi+8]
.text:00435463                 jnb     _KiEndUnexpectedRange@0 ; KiEndUnexpectedRange()

在这里要简单介绍下,这个系统服务表。从上文汇编代码可以看到,从线程块中可以取到系统服务表的地址,这个服务表不是我们前面提到的序号和函数的对应关系表,这个位置指向的结构体是用来描述对应关系的

这个结构体描述有两种,一种是非UI的函数所对应的描述表,一种是UI相关函数的描述表(比如MessageBox等),非UI的叫做KeServiceDescriptorTable,UI的叫做KeServiceDescriptorTableShadow

具体的结构如下:

1
2
3
4
5
其中有四个成员,每个成员的大小是4个字节,也就是ULONG
第一:内核服务函数地址表【四个字节】
第二:统计API调用次数(其实也不怎么用了,应该算是废弃的感觉)
第三:内核服务函数地址表的函数数量
第四:内核函数参数表【一个字节Byte数组,参数大小】

然后我们再通过windbg查看下实际上面的数据情况:

1
2
3
4
5
6
7
8
9
kd> dd keservicedescriptortable
83f77b00  83e6de3c 00000000 00000191 83e6e484
83f77b10  00000000 00000000 00000000 00000000
// 不跟UI相关,eax< 0x1000

kd> dd keservicedescriptortableshadow
83f77b40  83e6de3c 00000000 00000191 83e6e484
83f77b50  9b2d6000 00000000 00000339 9b2d702c
// 跟UI相关,eax > 0x1000 

可以看到有一些区别,第一行都是一样的,但是后面第二行算是另一个KeServiceDescriptorTable表,某种意义说这是一个结构体的数组,同样数据的不同也是用来区分是UI还是非UI的表,其次,服务号的大小也是,服务号小于0x1000的都是非UI,也就是KeServiceDescriptorTable这个表,而大于的就都是KeServiceDescriptorTableShadow这个表的。

除此,还要说明一点,KeServiceDescriptorTable这个表在32位系统里是导出的,但是在64位系统就不是导出的了。

所以我们书接上文,这句比较,其实就是将当前用户层想要调用的服务编号跟当前服务表里的函数个数进行比较,判断是否越界,如果大于就会跳转。

1
.text:00435460                 cmp     eax, [edi+8]

我们先简单向跳转之后的内容看一下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
.text:00434E30                 cmp     ecx, 10h
.text:00434E33                 jnz     short loc_434E72
.text:00434E35                 push    edx
.text:00434E36                 push    ebx
.text:00434E37                 call    _PsConvertToGuiThread@0 ; PsConvertToGuiThread()
.text:00434E3C                 or      eax, eax
.text:00434E3E                 pop     eax
.text:00434E3F                 pop     edx
.text:00434E40                 mov     ebp, esp
.text:00434E42                 mov     [esi+128h], ebp
.text:00434E48                 jz      loc_435449
.text:00434E4E                 lea     edx, unk_564AD0
.text:00434E54                 mov     ecx, [edx+8]
.text:00434E57                 mov     edx, [edx]
.text:00434E59                 and     eax, 0FFFh
.text:00434E5E                 cmp     eax, ecx
.text:00434E60                 lea     edx, [edx+ecx*4]
.text:00434E63                 jnb     short loc_434E72
.text:00434E65                 add     edx, eax
.text:00434E67                 movsx   eax, byte ptr [edx]
.text:00434E6A                 or      eax, eax
.text:00434E6C                 jle     loc_435536

首先与0x10比较,因为上文我们了解,如果是一个大于0x1000的数(两个字节大小哈),这个ecx一定是0x10,如果等于0x10,结果是为0,会向下走。转到_PsConvertToGuiThread函数,推测切换成UI相关的线程。

1
2
3
4
5
.text:00434E30                 cmp     ecx, 10h
.text:00434E33                 jnz     short loc_434E72
.text:00434E35                 push    edx
.text:00434E36                 push    ebx
.text:00434E37                 call    _PsConvertToGuiThread@0 ; PsConvertToGuiThread()

好的我们再回到跳转前接着向下看:

1
2
3
4
5
6
// 跟0x10比较,判断非UI,也就是不等,然后跳转
.text:00435469                 cmp     ecx, 10h
.text:0043546C                 jnz     short loc_435488

.text:0043546E                 mov     ecx, [esi+_ETHREAD.Tcb.Teb]
.text:00435474                 xor     esi, esi

我们来看非UI跳转后面的处理步骤:

 1
 2
 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
// 系统调用次数 自增1
.text:00435488                 inc     large dword ptr fs:_KPCR.PrcbData.KeSystemCalls

//根据上篇文章的追踪,edx是用户层传入的esp + 8的值,也就是用户层传入的参数的地址
.text:0043548F                 mov     esi, edx

// 清空ecx
.text:00435491                 xor     ecx, ecx

// edi存的是服务表地址,+c 就是服务函数的参数表
.text:00435493                 mov     edx, [edi+0Ch]

// edi存储服务函数的函数表地址
.text:00435496                 mov     edi, [edi]

// eax是编号,也就是函数的索引号
// edx是参数表的起始地址,表中的每个成员是1个字节,用来表示参数的大小
// 所以edx+eax,就是参数大小的值所在的地址
.text:00435498                 mov     cl, [eax+edx]

// edi是函数表其实位置,eax * 4,也就是索引乘以4个字节
// edi + eax * 4 就是函数的地址(函数表每4个字节一个地址,对应的是函数地址)
.text:0043549B                 mov     edx, [edi+eax*4]

// ecx,也就是上面赋值的cl,是参数的大小
// 所以需要将栈顶降低cl大小以方便填入参数
.text:0043549E                 sub     esp, ecx

// 右移2表示除以4,也就是要参数的个数或者是说,需要栈给多少个4字节。
.text:004354A0                 shr     ecx, 2
.text:004354A3                 mov     edi, esp

// ebp存储的是KTRAP_FRAME,+ 0x72是eflags中的某一位
// 查阅资料是VM:
// 虚拟8086模式标志(Virtual 8086 mode flag)是标志寄存器的第17位,
// 当其被设置表示启用虚拟8086模式(在保护模式下模拟实模式),否则退回到保护模式工作。
// 仅供参考哈
.text:004354A5                 test    byte ptr [ebp+72h], 2
.text:004354A9                 jnz     short loc_4354B1

// 查看SegCs,也就是上一阶段的代码段,是用户层还是内核,1为用户,0为内核
.text:004354AB                 test    byte ptr [ebp+6Ch], 1

// 如果为0才跳转
.text:004354AF                 jz      short _KiSystemServiceCopyArguments@0 ; KiSystemServiceCopyArguments()

以上 jnz short loc_4354B1的位置就是这段代码之后的位置,所以接着向下看:

1
2
.text:004354B1                 cmp     esi, ds:_MmUserProbeAddress
.text:004354B7                 jnb     loc_43572E

以上_MmUserProbeAddress到底值什么可以在windbg中查看下:

1
2
3
kd> dd MmUserProbeAddress
83f77854  7fff0000 80741000 0003ffff 80000000
83f77864  85500e2c 85500e24 85500e00 c03fff78

发现该值是7fff0000,我们借此可以推断,esi存放的是用户层传入参数的地址,然后跟MmUserProbeAddress进行比较,应该是判断是不是一个合理的用户空间的地址。

如果比这个地址大的话,就会跳转到下图所示的C0000005h,也就是触发异常了

4

继续向后看:这里就能看到就是在批量拷贝了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 这就是选循环,从esi为起始(源参数地址),以edi(栈顶)为目标
// 以ecx为循环次数,开始复制,地址为递增方式(也就是从栈顶到栈底的方向)
.text:004354BD                 rep movsd


.text:004354BF                 test    byte ptr [ebp+6Ch], 1
.text:004354C3                 jz      short loc_4354DB
.text:004354C5                 mov     ecx, large fs:124h
.text:004354CC                 mov     edi, [esp+0]
.text:004354CF                 mov     [ecx+13Ch], ebx
.text:004354D5                 mov     [ecx+12Ch], edi

这段代码用IDA添加结构体的方式来展示:就比较轻松的知道做了什么

5

继续向下看:

1
2
3
4
5
6
7
8
9
// 把函数地址给ebx,然后再后面调用了ebx
.text:004354DB                 mov     ebx, edx

// 这部分是日志相关,就不做展开了
.text:004354DD                 test    byte ptr ds:dword_531FC8, 40h
.text:004354E4                 setnz   [ebp+_KTRAP_FRAME.Logging]
.text:004354E8                 jnz     loc_4358F4

.text:004354EE                 call    ebx

调用完成之后

首先用结构体方式把代码重新展示下,此法屡试不爽

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
.text:004354F0                 test    byte ptr [ebp+_KTRAP_FRAME.SegCs], 1
.text:004354F4                 jz      short loc_43552A
.text:004354F6                 mov     esi, eax
.text:004354F8                 call    ds:__imp__KeGetCurrentIrql@0 ; KeGetCurrentIrql()
.text:004354FE                 or      al, al
.text:00435500                 jnz     loc_4358BB
.text:00435506                 mov     eax, esi
.text:00435508                 mov     ecx, large fs:_KPCR.PrcbData.CurrentThread
.text:0043550F                 test    [ecx+_ETHREAD.Tcb.ApcStateIndex], 0FFh
.text:00435516                 jnz     loc_4358D9
.text:0043551C                 mov     edx, dword ptr [ecx+_ETHREAD.Tcb.___u26.__s0.KernelApcDisable]
.text:00435522                 or      edx, edx
.text:00435524                 jnz     loc_4358D9

.text:0043552A                 mov     esp, ebp
.text:0043552C                 cmp     [ebp+_KTRAP_FRAME.Logging], 0
.text:00435530                 jnz     loc_435900

.text:00435536                 mov     ecx, large fs:_KPCR.PrcbData.CurrentThread
.text:0043553D                 mov     edx, [ebp+_KTRAP_FRAME._Edx]
.text:00435540                 mov     [ecx+_ETHREAD.Tcb.TrapFrame], edx
1
2
3
// 依然是判断当前调用是从用户层还是内核进入的
.text:004354F0                 test    byte ptr [ebp+_KTRAP_FRAME.SegCs], 1
.text:004354F4                 jz      short loc_43552A

上述代码在整个过程中,这个判断出现多次,为什么会出现这样,事实是因为,通过KiFastCallEntry调用,不止用户层,内核层也会调用。

1
.text:004354F0                 test    byte ptr [ebp+_KTRAP_FRAME.SegCs], 1

但是为什么内核层也会通过这个函数调用,按理说,内核层直接就可以去调用函数本身就可以了,为什么还要通过这个函数来调用呢?

在用户层有两套除了名字外一模一样的函数,ZwXXX系列和NtXXX系列,这两个函数区别只是导出的名称不一样,其他都是一样的。但是在内核层,这两个函数却长得完成不一样,事实上,在内核的ZwXXX系列函数,跟踪下来会发现,他会跳转到KiFastCallEntry函数中的一个位置,这个位置刚刚好是保存完用户层的上线文代码之后的位置,也就是,今天开篇开始的代码。

故此,内核里的Zw函数也是通过系统调用,找到最终要调用的Nt函数,这样看来,干活的还是内核里的Nt函数,这也就说明了,为什么要判断是用户层还是内核层跳转进来的。

假设是从用户层,那就不跳转:

1
2
3
4
5
6
// eax是调用完系统函数之后的结果,做一个保存
.text:004354F6                 mov     esi, eax

// 又调用了一个函数
.text:004354F8                 call    ds:__imp__KeGetCurrentIrql@0 ; KeGetCurrentIrql()
.text:004354FE                 or      al, al
IRQL

IRQL,是中断请求级别的概念,更详细的概念可以看这个链接:https://learn.microsoft.com/zh-cn/windows-hardware/drivers/devtest/irql-annotations-for-drivers

0-31表示irql级别,其中数字越大等级越高,优先级越高 PASSIVE_LEVEL,是0,基本上用户层都是这个 APC_LEVEL,是1, DISPATCH_LEVEL ,是2

中断,是在当前运行的CPU状态下,中断然后做其他事情,而且中断是可以嵌套的, 也就是当一个中端发生,保存状态,然后保存返回位置,进入中断位置,执行中断任务, 中断过程中又被中断,然后接着保存状态,保存返回位置,进入中断,执行中断任务, 。。。。依次类推。

但是如果可以被任意打断,会造成一直在忙于切换而无法完成任务,因而,中断也是有级别的,任何请求中断的级别要高于你当前处在中断任务的中断级别才能去打断该中断或任务,憋得不行一定要上厕所,是很难被旁边要给你讲笑话的人打断的。

1
2
3
4
// 获取当前的IRQL,并判断是否为0,如果不为0跳转
.text:004354F8                 call    ds:__imp__KeGetCurrentIrql@0 ; KeGetCurrentIrql()
.text:004354FE                 or      al, al
.text:00435500                 jnz     loc_4358BB

顺手看一下跳转之后的内容,其实就是kebugcheck函数,记住这个函数,它就是大名鼎鼎的蓝屏函数

6

1
2
3
4
5
6
7
// 这部分是apc相关,进程挂靠的内容,跳转还是蓝屏函数
.text:00435508                 mov     ecx, large fs:_KPCR.PrcbData.CurrentThread
.text:0043550F                 test    [ecx+_ETHREAD.Tcb.ApcStateIndex], 0FFh
.text:00435516                 jnz     loc_4358D9
.text:0043551C                 mov     edx, dword ptr [ecx+_ETHREAD.Tcb.___u26.__s0.KernelApcDisable]
.text:00435522                 or      edx, edx
.text:00435524                 jnz     loc_4358D9
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 判断为当前日志是否开启,如果开启就跳转,然后后面应该就是要关闭这个日志
// 差不多意思就是调用要完成了,日志可以关闭了
.text:0043552A                 mov     esp, ebp
.text:0043552C                 cmp     [ebp+_KTRAP_FRAME.Logging], 0
.text:00435530                 jnz     loc_435900

.text:00435536                 mov     ecx, large fs:_KPCR.PrcbData.CurrentThread

// edx存储的是用户层的参数
.text:0043553D                 mov     edx, [ebp+_KTRAP_FRAME._Edx]
.text:00435540                 mov     [ecx+_ETHREAD.Tcb.TrapFrame], edx

上文最后两行,捣鼓的这下令人不是很理解,事实上这部分是给内核层调用准备的,如果是用户层应该用不到,虽然执行了也没什么意义。可以看下在内核层调用的过程中做的事情:

7

上图可以看到,在内核层开始调用前,就是把线程块的KTRAP_FRAME保存在KTRAP_FRAME的edx里,而执行完,又把它还原给线程块里。

继续向后走:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 关中断
.text:00435546                 cli

// 跟之前一样,还是判断是否为虚拟8086模式,依然不是
.text:00435547                 test    byte ptr [ebp+(_KTRAP_FRAME.EFlags+2)], 2
.text:0043554B                 jnz     short loc_435553

// 依然是判断是否为用户层还是内核层过来的
.text:0043554D                 test    byte ptr [ebp+_KTRAP_FRAME.SegCs], 1
.text:00435551                 jz      short loc_4355B8
// 姑且跳转,继续向下

后面是一段跟计数统计相关以及跟apc相关,这篇文章姑且不详细描述。

 1
 2
 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
.text:00435553                                         ; _KiServiceExit+6F↓j
.text:00435553                 mov     ebx, large fs:_KPCR.PrcbData.CurrentThread
.text:0043555A                 test    byte ptr [ebx+2], 2
.text:0043555E                 jz      short loc_435568
.text:00435560                 push    eax
.text:00435561                 push    ebx
.text:00435562                 call    _KiCopyCounters@4 ; KiCopyCounters(x)
.text:00435567                 pop     eax

.text:00435568                 mov     byte ptr [ebx+3Ah], 0
.text:0043556C                 cmp     byte ptr [ebx+56h], 0
.text:00435570                 jz      short loc_4355B8
.text:00435572                 mov     ebx, ebp
.text:00435574                 mov     [ebx+44h], eax
.text:00435577                 mov     dword ptr [ebx+50h], 3Bh ; ';'
.text:0043557E                 mov     dword ptr [ebx+38h], 23h ; '#'
.text:00435585                 mov     dword ptr [ebx+34h], 23h ; '#'
.text:0043558C                 mov     dword ptr [ebx+30h], 0
.text:00435593                 mov     ecx, 1          ; NewIrql
.text:00435598                 call    ds:__imp_@KfRaiseIrql@4 ; KfRaiseIrql(x)
.text:0043559E                 push    eax
.text:0043559F                 sti
.text:004355A0                 push    ebx
.text:004355A1                 push    0
.text:004355A3                 push    1
.text:004355A5                 call    _KiDeliverApc@12 ; KiDeliverApc(x,x,x)
.text:004355AA                 pop     ecx             ; NewIrql
.text:004355AB                 call    ds:__imp_@KfLowerIrql@4 ; KfLowerIrql(x)
.text:004355B1                 mov     eax, [ebx+44h]
.text:004355B4                 cli
.text:004355B5                 jmp     short loc_435553

恢复阶段

最后到了接下来这段代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// 恢复异常链地址
.text:004355B8                 mov     edx, [esp+_KTRAP_FRAME.ExceptionList]
.text:004355BC                 mov     large fs:_KPCR.___u0.NtTib.ExceptionList, edx

// 恢复线程的先前模式
.text:004355C3                 mov     ecx, [esp+_KTRAP_FRAME.PreviousPreviousMode]
.text:004355C7                 mov     esi, large fs:_KPCR.PrcbData.CurrentThread
.text:004355CE                 mov     [esi+_ETHREAD.Tcb.PreviousMode], cl

//dr7,调试寄存器,判断是否有硬件端点
.text:004355D4                 test    [esp+_KTRAP_FRAME.Dr7], 0FFFF23FFh
.text:004355DC                 jnz     loc_4356A0

// 跳转过去的就是恢复dr寄存器

继续向下,依然用结构体替换内容展示:

 1
 2
 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
// 依然判断是否为虚拟8086模式
.text:004355E2                 test    [esp+_KTRAP_FRAME.EFlags], 20000h
.text:004355EA                 jnz     loc_4367A8

//用户层是1B,与之后是19,内核层是08,与之后是8,都不跳转
.text:004355F0                 test    word ptr [esp+_KTRAP_FRAME.SegCs], 0FFF9h
.text:004355F7                 jz      loc_4356FB

// 用户层是1B,内核层是08,比较
.text:004355FD                 cmp     word ptr [esp+_KTRAP_FRAME.SegCs], 1Bh

// bt指令是 bit test,位测试,测试某位
// bt 操作数1,操作数2
// 将操作数1的第n位的值给CF标志位,n是操作数2
.text:00435603                 bt      word ptr [esp+_KTRAP_FRAME.SegCs], 0

// cmc指令,将CF标志位取反
.text:0043560A                 cmc
// 用户层CF位最后为0,内核层位1,CF标志位和ZF标志位都为0跳转 
// 用户层:cmp之后,ZF为1,cmc之后CF为0 不跳转  内核层也是一样不跳转
.text:0043560B                 ja      loc_4356E4

.text:00435611                 mov     [esp+_KTRAP_FRAME._Eax], eax
.text:00435615                 test    byte ptr [ebp+_KTRAP_FRAME.SegCs], 1
.text:00435619                 jz      short loc_435655
.text:0043561B                 movzx   eax, large byte ptr fs:_KPCR.PrcbData.BpbUserSpecCtrl
.text:00435623                 cmp     large fs:_KPCR.PrcbData.BpbCurrentSpecCtrl, al
.text:0043562A                 jz      short loc_43563B
.text:0043562C                 mov     large fs:_KPCR.PrcbData.BpbCurrentSpecCtrl, al
.text:00435632                 mov     ecx, 48h ; 'H'
.text:00435637                 xor     edx, edx
.text:00435639                 wrmsr

接着经过一些其他的判断(感觉对当前的分析都没有什么意义可以忽略掉),看下面的代码

1
2
3
4
5
6
7
8
9
.text:00435655                 mov     eax, [esp+_KTRAP_FRAME._Eax]

//内核层会直接跳转到下面 
.text:00435659                 cmp     word ptr [ebp+_KTRAP_FRAME.SegCs], 8
.text:0043565E                 jz      short loc_435665

// 恢复fs寄存器了
.text:00435660                 lea     esp, [ebp+_KTRAP_FRAME.SegFs]
.text:00435663                 pop     fs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 然后就是恢复各种寄存器的值
.text:00435665                 lea     esp, [ebp+_KTRAP_FRAME._Edi]
.text:00435668                 pop     edi
.text:00435669                 pop     esi
.text:0043566A                 pop     ebx
.text:0043566B                 pop     ebp

// 这个其实还是在判断是否为用户层还是内核层
.text:0043566C                 add     esp, 4
.text:0043566F                 test    dword ptr [esp+4], 1

在IDA里,最后这部分代码有问题:

8

我们用windbg来看着这部分是做了什么,通过搜索"pop fs"来搜索相似的代码,最终就找到图如下图所示的位置,以下才是实际的代码内容

9

首先判断是否为用户层,用户层跳转。

1
2
83e42c0c f744240401000000 test    dword ptr [esp+4],1
83e42c14 7505            jne     nt!KiSystemCallExit2 (83e42c1b)  Branch

然后判断eflags的第9位是否为1,为1跳转,

1
2
3
nt!KiSystemCallExit2:
83e42c1b f744240800010000 test    dword ptr [esp+8],100h
83e42c23 751e            jne     nt!KiSystemCallExit (83e42c43)  Branch

这个是eflags的TF标志位在 EFLAGS 寄存器中,TF(Trap Flag)是陷阱标志位,用于控制CPU的单步执行模式。当 TF 被设置为1时,CPU将进入单步执行模式,即每执行一条指令后,都会产生一个单步中断请求。这种方式主要用于程序的调试,可以比对下,用户层和内核的值:

内核层:

1
2
3
4
kd> r
eax=00000002 ebx=00000000 ecx=83f72b07 edx=00000002 esi=8fcc7800 edi=99b18298
eip=83e6a1bc esp=83f37b48 ebp=83f37b88 iopl=0         nv up ei pl nz na po nc
cs=0008  ss=0010  ds=0023  es=0023  fs=0030  gs=0000             efl=00000202

用户层:

10

可见,用户层为1,会跳转。

然后接着下面的分支:

1
2
3
4
5
6
nt!KiSystemCallExit:
83e42c43 33c9            xor     ecx,ecx
83e42c45 33d2            xor     edx,edx

// 跳转返回了。
83e42c6b cf              iretd  

这里简单说下这个iretd指令: 这个指令跟 iret一样,在32位下没有区别,这个指令执行的时候会从堆栈依次弹出三个值,分别赋给EIP(当前指令的地址)、cs(代码段选择子)、eflags(标志寄存器),在上文情境中,esp已经指向的就是KTRAP_FRAME中的EIP地址,所以当iretd执行的时候,正好就把初次进入这个函数的时候,保存在KTRAP_FRAME中用用户层的对应值又重新赋给了这三个寄存器

并且,返回地址EIP刚好值_KUSER_SHARED_DATA 中的SystemCallReturn的地址,也就是用户层sysenter指令之后的位置,这在上一篇中有提到过(地址如下:https://daliu.net/posts/20250105/#_kuser_shared_data)。所以,iretd这个指令确保了执行过程的顺畅。

至此,系统调用的内核阶段完成,并成功将结果返回。以上内容都是在x86架构下的调用流程,在64位的操作系统中还会有一些区别,我们后续继续补充介绍来,今天就到此为止哈。

updatedupdated2025-01-062025-01-06