Windows XP 系统调用(一)
ʕ •ᴥ•ʔ ɔ:
1 Windows API
Application Programming Interface
,简称 API 函数。- 主要是存放在
C:\WINDOWS\system32
下面所有的dll导出的函数。 - 几个重要的DLL
- Kernel32.dll:最核心的功能模块,比如管理内存、进程和线程相关的函数等。
- User32.dll:是Windows用户界面相关应用程序接口,如创建窗口和发送消息等。
- GDI32.dll:全称是Graphical Device Interface(图形设备接口),包含用于画图和显示文本的函数。比如要显示一个程序窗口,就调用了其中的函数来画这个窗口。
- Ntdll.dll:大多数API都会通过这个DLL进入内核(0环)。
R3下无论如何调用,均无法绕过SSDT HOOK,R0下调用Nt*可以绕过SSDT HOOK。Rtl* 函数是windows ddk提供的编写驱动的函数。
2 3环进0环
2.1 分析ReadProcessMemory3环调用过程
分析kernel32.dll导出函数ReadProcessMemory(x,x,x,x)
调用流程。
1 | .text:7C8021C6 ; --------------------------------------------------------------------------- |
双击查看.text:7C8021E5 call ds:__imp__NtReadVirtualMemory@20 ; NtReadVirtualMemory(x,x,x,x,x)
,结果如下:
1 | .idata:7C801418 ; NTSTATUS __stdcall NtReadVirtualMemory(HANDLE ProcessHandle, |
__imp__
表示这里的NtReadVirtualMemory
函数是从导入表导入的,在IDA导入表中查看可得NtReadVirtualMemory
:位于ntdll.dll。
分析得如下结论:
ReadProcessMemory
:位于kernel32.dllNtReadVirtualMemory
:位于ntdll.dll
1 | BOOL __stdcall ReadProcessMemory( |
进入到ntdll.dll
的导出表查看NtReadVirtualMemory
函数,发现其调用了0x7FFE0300
处的函数。
1 | .text:7C92D9E0 ; __stdcall NtReadVirtualMemory(x, x, x, x, x) |
可以看到在这里调用了一个地址:0x7FFE0300
(其中eax存的值为系统服务号,即一个0环函数的编号),具体该地址是什么函数需要学习下一节。
2.2 _KUSER_SHARED_DATA结构
_KUSER_SHARED_DATA结构为一块3环和0环共享的内存块(共享同一个物理页)。- 在 User 层和 Kernel 层分别定义了一个
_KUSER_SHARED_DATA
结构区域,用于 User 层和 Kernel 层共享某些数据。 - 它们使用固定的地址值映射,
_KUSER_SHARED_DATA
结构区域在 User 和 Kernel 层地址分别为:- User 层地址为:0x7FFE0000 只读
- Kernnel 层地址为:0xFFDF0000 可读可写
特别说明:虽然指向的是同一个物理页,但在User 层是只读的,在Kernnel层是可读可写的。同一个物理页映射了两个线性地址,这两个线性地址的对应的访问权限不同。(PDE、PTE的R/W位),使用dd 0x7FFE0000
和dd 0xFFDF0000
里面的数据是一样的。
该结构如下:
1 | kd> dt _KUSER_SHARED_DATA 0x7FFE0000 |
在偏移0x300
的地方即为NtReadVirtualMemory
函数调用的0x7FFE0300
处的函数,为一个SystemCall
(系统调用函数)。该函数但汇编代码为:
1 | kd> u 0x7c92e4f0 |
可以看到该函数使用了指令sysenter
进入到0环,同时EDX指向栈顶。该指令具体的实现请看下一节。
2.3 SystemCall
SystemCall是两个能从3环进入到0环的函数的名称。系统调用有中断调用和快速调用两种方式,中断调用是通过中断门进0环,此过程需要查IDT表和TSS表;快速调用则是使用sysenter指令进0环,这种方式不需要查内存,而是直接从CPU的MSR寄存器中获取所需数据,所以称为快速调用。
怎么判断系统是否支持快速调用?
当通过 eax = 1 来执行cpuid
指令时,处理器的特征信息被放在 ecx 和 edx 寄存器中,其中edx包含了一个SEP位(EDX值的第12位,下标为11),该位指明了当前处理器知否支持sysenter/sysexit
指令(SEP == 1,支持)
- SEP == 1,支持。SystemCall为:
ntdll.dll!KiFastSystemCall()
- SEP == 0,不支持。SystemCall为:
ntdll.dll!KiIntSystemCall()
1 | ntdll!KiFastSystemCall: |
实验:
将eax = 1,ecx = 0、edx = 0,执行cpuid
指令前:
执行cpuid
指令后如下图,edx = 0x1F8BFBFF,B = 1011,第12位为1,说明支持快速调用。
关于KiFastSystemCall()
和KiIntSystemCall()
这两个函数:
- 这两个函数都叫做SystemCall。如果系统支持快速调用,就会把KiFastSystemCall()函数的地址写到0x7FFE0300处。不支持就会把KiIntSystemCall()的函数地址写到0x7FFE0300处。
- 这两个函数的相同点:都是找新的CS、SS、ESP、EIP。不同点:找的方式不同。
2.4 3环进0环需要更改的4个寄存器
3环进0环需要更改哪些寄存器?
- CS的权限由3变为0 ,意味着需要新的CS。
- SS与CS的权限永远一致,需要新的SS。
- 权限发生切换的时候,堆栈也一定会切换,需要新的ESP。
- 进0环后代码的位置,需要新的EIP。
简单复习一下,中断门进0环时,我们在IDT表里填的中断门描述符,然后根据中断门描述符中的段选择子找到一个代码段描述符去提权,包含了0环的CS和EIP,而SS和0环的ESP是在TSS里存储的,当时我们还有一个结论,Windows里不使用任务,所以TSS的唯一作用就是提权时提供ESP0、SS0和EIP。
现在,我们知道了进0环需要更改的4个寄存器,接下来分析 KiFastSystemCall 和 KiIntSystemCall 时,只要明白一点,这两个函数做的事情就是更改这4个寄存器。
2.4 重写ReadProcessMemory和WriteProcessMemory
重写API最重要的六点:
- 进0环前函数服务号。
- 进0环的方式(中断门/快速调用)。
- 进0环前寄存器的值(EAX、EDX)。
- 进0环前堆栈的值。
- 中断门进入0环时:中断门本身会将SS3、ESP3、EFLAG3、CS3、返回地址压栈,故不需要考虑堆栈恢复(被调用的API会自己POP返回地址)。
- 快速调用进入0环时:需要自己压栈返回地址,且该返回地址要紧挨着原来的参数,EBP指向的位置应该是第二个返回地址的坑,需要先将里面的值保存起来,还要保存起来原来的ESP,实际上就是在模拟中断门保存ESP和返回地址。
- 进0环前返回地址。
- 大部分API调用约定都是
__stdcall
,被调用的API会自己平衡压入参数的堆栈。
- 服务号:函数服务号 = 0xBA;
- 进0环的方式:
- 中断门
int 0x2e
:EAX = 服务号 = 0xBA、EDX = 第一个参数的地址。 - 快速调用
sysenter
:EAX = 服务号 = 0xBA。EDX = 返回地址的地址。
- 中断门
- 寄存器的值:第2点中所述。
- 进0环前堆栈的值:本函数仅需注意函数的返回地址。
- 返回地址:中断门无需提供返回地址(返回地址被压栈)、快速调用需要自己压栈返回地址,且该返回地址要紧挨着原来的参数,EBP指向的坑需要放入当前的返回地址,原来的值需要先保存起来,还要保存起来原来的ESP,实际上就是在模拟中断门保存ESP和返回地址。。
- 注入函数的堆栈平衡。
重写ReadProcessMemory,分析:
先观察原来API调用的堆栈变化:
可以看到,在函数ReadProcessMemory(x,x,x,x)
中重新把自己的参数压栈了一次,目的是调用NtReadVirtualMemory(x,x,x,x,x)
,然后调用一次函数压栈一次返回地址,总共调用了两次函数。所以自己重写API时,要让两个返回地址紧挨着堆栈里的参数。
如下代码:
1 | __asm |
进入上面的函数时,堆栈情况如下:故当前EBP指向的位置(坑)需要放入第二个返回地址,需要先将里面的值保存起来,从0环返回时恢复。由于快速调用返回时ESP会变化,故ESP的值也需要保存起来(有必要时EBP的值也需要保存起来)。
1 | Debug模式下该函数的反汇编代码: |
总结有:快速调用需要保存原来的ESP、返回地址(模拟中断门调用)。
注意,VC、VS内联汇编不支持sysenter
指令,可以用_emit
代替。
对WriteProcessMemory
逆向分析可知,会先调用NtProtectVirtualMemory(x,x,x,x,x)
对要写的内存进行权限检查等工作,如果可写就会调用__imp__NtWriteVirtualMemory@20 ; NtWriteVirtualMemory(x,x,x,x,x)
进行内存写入。
1 | .text:7C92DF90 ; __stdcall NtWriteVirtualMemory(x, x, x, x, x) |
重写实现代码如下:
1 |
|
3 分析 INT 0x2E 和 sysenter
3.1 中断门进0环
EAX保存系统调用号,内核函数编号。
EDX保存的指针,指向函数参数的起始地址。
然后通过中断门进入内核。且中断号都是0x2E。
分析:INT 0x2E进0环。
步骤一:在IDT表中找到0x2E号门描述符。
步骤二:分析CS/SS/ESP/EIP的来源。
步骤三:分析EIP是什么。
查看IDT表中的中断门描述符看一下提权跳转过程:
0x2E = 46D,即第47项。8054ee00`00082451
可以看到:
0xE = 1110:P = 1,DPL = 11,S = 0(系统段)。
Type = 0xE = 1110: 为32位中断门。
代码段选择子:0008 = 1 0 00
- RPL = 00
- TI = 0,查GDT
- Index = 1,第二项,查得00cf9b00`0000ffff,代码段Base = 0x0。
中断门偏移:0x80542451
中断门跳转执行地址 = Code.Base + Interrupt.Offset = 0x0 + 0x80542451 = 0x80542451。该地址已经为高2G(0环)。
得到EIP = 0x80542451。这个是内核模块的
KiSystemService
函数。1
2
3
4
5
6
7
8
9
10kd> u 0x80542451
nt!KiSystemService:
80542451 6a00 push 0
80542453 55 push ebp
80542454 53 push ebx
80542455 56 push esi
80542456 57 push edi
80542457 0fa0 push fs
80542459 bb30000000 mov ebx,30h
8054245e 668ee3 mov fs,bx
综上,已经知道CS0来自调用门中的代码段选择子,EIP = Code.Base + Interrupt.Offset = 0x80542451。接下来还要分析来自TSS的SS0和ESP0。
获取TR的值。(任务段执行流程:3环CALL任务段选择子 –> 根据段选择子到GDT找到对应任务段描述符 –> 将任务段描述符加载到TR寄存器,同时根据任务段中的Base找到TSS内存块的起始地址 –> 根据TSS中的EIP去执行代码 –> IRETD返回。)
1
2kd> r tr
tr=00000028可以得到TR寄存器选择子的值为0x28 = 00101 0 00,Index = 5,查GDT表如下。获取TSS任务段描述符。
1
2
3
4
5
6kd> r gdtr
gdtr=8003f000
kd> dq 8003f000
8003f000 00000000`00000000 00cf9b00`0000ffff
8003f010 00cf9300`0000ffff 00cffb00`0000ffff
8003f020 00cff300`0000ffff 80008b04`200020ab得到GDT描述符 = 80008b04`200020ab,为32位TSS任务段,TSS.Base = 0x80042000。
可以得到:ESP0 =0xaabe9de0,SS0 = 0x0010。
1
2
3
4kd> dd 0x80042000
80042000 0c458b24 aabe9de0 8b080010 758b0855
80042010 eac14008 ffe68110 030000ff 06e400a0
80042020 e1750855 08458b5e 0310e8c1 c25d0845
3.2 快速调用进0环
在执行sysenter指令之前,操作系统必须指定0环的CS段、SS段、EIP以及ESP,SS = CS+8。操作系统会提前将CS/SS/ESP/EIP的值存储在MSR寄存器中,sysenter指令执行时,CPU会将MSR寄存器中的值直接写入相关寄存器,没有读内存的过程,所以叫快速调用,本质是一样的!
MSR | 地址 | 含义 |
---|---|---|
IA32_SYSENTER_CS | 174H | 低16位值指定了特权级0的代码段和栈段的段选择符 |
IA32_SYSENTER_ESP | 175H | 内核栈指针的32 位偏移 |
IA32_SYSENTER_EIP | 176H | 目标例程的32位偏移 |
可以通过RDMSR/WRMST
来进行读写(操作系统使用WRMST写该寄存器):
1 | kd> rdmsr 174 |
操作系统启动的时候就已经把这些值写入到MSR寄存器中了。
3.3 总结
API通过中断门进0环:
1) 固定中断号为0x2E
2) CS/EIP由门描述符提供,ESP/SS由TSS提供
3) 进入0环后执行的内核函数:NT!KiSystemService
API通过sysenter指令进0环:
1) CS/ESP/EIP由MSR寄存器提供(SS是算出来的)
2) 进入0环后执行的内核函数:NT!KiFastCallEntry
内核模块:ntoskrnl.exe/ntkrnlpa.exe
(10-10-12/2-9-9-12)
4 进入0环后分析
之前的课程,我们学习了API系统调用在3环部分做的事情,有两种方式进0环,分别是中断门int 0x2e
和快速调用sysenter
。通过中断门进入0环后,会调用函数KiSystemService
、通过快速调用进入0环后会调用函数KiFastCallEntry
。
分析之前,思考四个问题:
- 进0环后,原来的寄存器存在哪里?
- 如何根据系统调用号(eax中存储)找到要执行的内核函数?
- 调用时参数是存储到3环的堆栈,如何传递给内核函数?
- 2种调用方式是如何返回到3环的?
要分析 KiSystemService 和 KiFastCallEntry ,我们需要先了解几个结构体,_Trap_Frame,_ETHREAD,_KTHREAD,_KPCR,_NT_TIB 和 _KPRCB。
接下来,按照 KiSystemService 代码执行顺序,依次介绍涉及到的结构体。
4.1 分析KiSystemService
本节通过分析中断门int 0x2e
进入0环后,调用函数KiSystemService
的过程,解释线程进0环后,原来的寄存器存在哪里?
使用IDA Pro打开ntkrnlpa.exe,并连接远程ntkrnlpa.pdb文件(Windows XP 驱动开发(一)第二章节)。
Alt+T搜索_IDT。
双击后如下。
找到
0x2e
对应的KiSystemService
函数然后双击。待会儿分析push 0
的含义。保存3环的通用寄存器。
- 从3环进入0环的时候,CPU会往0环的堆栈压5个值:eip、cs、eflag、esp、ss。
- 但是操作系统会将3环的寄存器都保存起来,方便从0环返回,于是操作系统就维护了一个
_Trap_Frame
结构来保存这些3环的寄存器。
4.2 _Trap_Frame结构
首先复习一下TSS表,一个CPU只有一张TSS表,但是系统里有成百上千的线程,线程进0环时,假设使用中断门,0环的ESP和SS从TSS表获取,怎么保证每个线程都有自己的堆栈,不互相冲突呢?答案是,线程切换时会修改TSS表,确保每个线程执行时,TSS里的ESP,SS都对应当前线程。
_Trap_Frame
是线程在0环的堆栈结构,由操作系统维护。
1 | kd> dt _KTrap_Frame -v |
一个线程一个_Trap_Frame
结构,每个线程有一个自己的ESP0,该ESP0指示该线程0环下的堆栈(_Trap_Frame
结构)在哪。
其中:
- 高4个成员仅在虚拟8086模式下使用。
- 从TSS中获取的ESP0一开始指向0x7C。(从TSS中获取ESP0后才能知道0环的堆栈在哪)
- 从0x68~0x78由中断门进入0环时CPU自己填充。
- 一个线程一个
_Trap_Frame
结构。 - 一个CPU只有一块TSS内存块,每次线程切换时,TSS中的值都被更新为当前线程对应的值。
结合_Trap_Frame
结构分析函数KiSystemService
分析:
1 | push 0 //push ErrCode |
可以看到段寄存器FS的在GDT表中对应的段描述符为ffc093df`f0000001,是一个数据段,FS.Base = 0xFFDFF000。
此时的FS有了新的含义,在3环,FS[0]指向TEB结构。但是在0环FS段寄存器被重新赋值,也有了新的意义,0环的FS指向一个KPCR的结构体。
4.3 _KPCR结构
KPCR是个结构体,用来描述CPU状态。叫CPU控制区(Processor Control Region)。
CPU也有自己的控制块,每一个CPU有一个,叫KPCR。
0环下FS段寄存器的基址,即FS[0]指向KPCR结构,这里为0xFFDFF000
。
1 | kd> dt _KPCR |
查看CPU数量(这里是单核):
1 | kd> dd KeNumberProcessors |
_KPCR结构中比较重要的两个结构(第一个和最后一个):_NT_TIB、_KPRCB
4.3.1 _NT_TIB结构
先分析第一个结构_NT_TIB:
1 | kd> dt _NT_TIB |
第一个成员_EXCEPTION_REGISTRATION_RECORD
是一个异常链表,存储的是异常的处理函数。
接着分析KiSystemService
函数和_Trap_Frame
结构、_KPCR
结构:
1 | push dword ptr ds:0FFDFF000h |
PUSH的作用就是保存老的异常链表,然后将新的ExceptionList
设为-1。
4.3.2 _KPRCB结构
下面列举该结构的成员:
1 | kd> dt _KPRCB |
查看KPRCB地址(KiProcessorBlock),这里是单核,所以只有一个值。
1
2kd> dd KiProcessorBlock L2
8055d5a0 ffdff120 00000000ffdff120
这个地址减去0x120就是KPCR的地址。查看CPU数量(KeNumberProcessors )。
1
2
3
4kd> dd KeNumberProcessors
80556a60 00000001 00000006 00008e0a a0013fff
80556a70 806ceec0 00000000 00000000 00000061
80556a80 8003f0e0 00000000 00000000 00000000
接着分析KiSystemService
函数代码:
这里的FS:124
为_KPRCB的第3个成员CurrentThread,是当前CPU所执行线程的_ETHREAD
。_ETHREAD
部分成员如下:
1 | kd> dt _ETHREAD |
第一个成员为_KTHREAD
,_KTHREAD
部分成员如下:
1 | kd> dt _KTHREAD |
0FFDFF124h+140h就是PreviousMode
,意为先前模式。
保存老的先前模式,因为一段代码如果是0环执行和3环执行是不一样的,记录调用这段代码之前是0环的还是3环的。
接着分析代码:
1 | sub esp, 48h |
将esp指向_Trap_Frame
的起始位置。
1 | mov ebx, [esp+68h+arg_0] |
这里arg_0是4
那么就是将esp+6c位置上的值赋给ebx,并与上1。esp+6c位置就是cs的值,主要是为了判断权限的问题,如果是3环来的,cs的值是11,如果是0环来的,cs的值是8。所以与后如果是0,那么就是0环来的,如果是1,那么就是3环来的。
1 | mov [esi+140h], bl |
esi是KTHREAD,esi+140偏移的地方正好是先前模式,将值赋给先前模式,“新的先前模式”。
1 | mov ebp, esp |
esp和ebp此时都指向Trap_Frame的首地址。
1 | mov ebx, [esi+134h] |
esi+134h是KTHREAD+134h偏移又是TrapFrame,这里也可以验证TrapFrame结构体实际上是一个线程一份,因为在KTHREAD这个线程结构体中有一份,而KTHREAD是每个线程一份。
1 | mov [ebp+3Ch], ebx |
将老的TrapFrame的值存在一个位置上,然后将新的TrapFrame放到esi+134h这个位置。
1 | mov ebx, [ebp+60h] |
将原来三环的ebp放到ebx里,将三环eip放到了edi中
1 | mov [ebp+0Ch], edx |
进0环的时候,edx保存的是参数的地址,这里赋值给TrapFrame+0Ch偏移正好是DbgArgPointer
。
1 | mov [ebp+0], ebx |
TrapFrame+0h 和TrapFrame+4h分别是DbgEbp和DbgEip。
1 | test byte ptr [esi+2Ch], 0FFh |
判断是否处于调试状态(DebugActive),如果是处于调试状态,才会把cr0-cr7存到TrapFrame结构体中,如果没处于调试状态就不存cr0-cr7的值。那么就是将esp+6c位置上的值赋给ebx,并与上1。esp+6c位置就是cs的值,主要是为了判断权限的问题,如果是3环来的,cs的值是11,如果是0环来的,cs的值是8。所以与后如果是0,那么就是0环来的,如果是1,那么就是3环来的。
1 | mov [esi+140h], bl |
esi是KTHREAD,esi+140偏移的地方正好是先前模式,将值赋给先前模式,“新的先前模式”。
1 | mov ebp, esp |
esp和ebp此时都指向Trap_Frame的首地址。
1 | mov ebx, [esi+134h] |
esi+134h是KTHREAD+134h偏移又是TrapFrame,这里也可以验证TrapFrame结构体实际上是一个线程一份,因为在KTHREAD这个线程结构体中有一份,而KTHREAD是每个线程一份。
1 | mov [ebp+3Ch], ebx |
将老的TrapFrame的值存在一个位置上,然后将新的TrapFrame放到esi+134h这个位置。
1 | mov ebx, [ebp+60h] |
将原来三环的ebp放到ebx里,将三环eip放到了edi中
1 | mov [ebp+0Ch], edx |
进0环的时候,edx保存的是参数的地址,这里赋值给TrapFrame+0Ch偏移正好是DbgArgPointer
。
1 | mov [ebp+0], ebx |
TrapFrame+0h 和TrapFrame+4h分别是DbgEbp和DbgEip。
1 | test byte ptr [esi+2Ch], 0FFh |
判断是否处于调试状态(DebugActive),如果是处于调试状态(DebugActive != 0xFF
),才会把Dr0-Dr7存到TrapFrame结构体中,如果没处于调试状态就不存Dr0-Dcr7的值。
通过对函数KiSystemService
的分析,已经知道如下的问题一和问题三。原来的寄存器存储到了 _Trap_Frame 结构体里,3环API参数指针通过EDX传给0环。
- 进0环后,原来的寄存器存在哪里?
- 如何根据系统调用号(eax中存储)找到要执行的内核函数?
- 调用时参数是存储到3环的堆栈,如何传递给内核函数?
- 2种调用方式是如何返回到3环的?
4.4 完整分析KiSystemService
1 | .text:0046A451 ; =============== S U B R O U T I N E ======================================= |
5 完整分析KiFastCallEntry
从3环KiFastSystemCall通过sysenter
进入0环后,会调用函数KiFastCallEntry,其中CS0、EIP、SS0 = CS0 + 8从MSR寄存器中来,ESP0从TSS中获取。
与中断门进入0环后调用KiSystemService函数中前部分是不太一样,快速调用进来0环的SS3、ESP3、EFLAG3、CS3、EIP3由操作系统来压栈(中断门进入由CPU自己压栈)。
KiFastCallEntry也需要将_KTRAP_FRAME
结构填满。
5.1 进入0环前的堆栈与寄存器
通过正常的系统调用从3环进入0环都会从Ntdll.dll
里面的函数调用KiFastSystemCall
进去R0。
那么这里就来分析一下,一个 API 函数在调用Ntdll.dll
前都会做一些什么工作?这里以Kernel32.dll
中的函数ReadProcessMemory
来进行举例说明。
函数ReadProcessMemory
进入0环前,在3环的堆栈以及寄存器的值如下:
这里查看一下IA32_SYSENTER_EIP
是到0环的哪个地方执行:
1 | kd> rdmsr 176 |
可以看到是0环的KiFastCallEntry
函数。
5.2 KiFastCallEntry函数分析
注意事项:
快速调用进入0环后,CPU并不会像中断门调用进入0环那样自动将寄存器SS、ESP3、Eflags3、CS、EIP3压入0环堆栈(TrapFrame)。
进入0环时,edx 寄存器并非直接指向3环 API 的参数。
1
add edx, 8 //此时edx指向3环API参数。这也说明在3环时edx+8才指向API的参数
使用
int 0x2e
进入0环后,当TrapFrame
的成员都初始化完成后,会在函数_KiSystemService
中会设置KTRAP_FRAME._Edx = Currentthread->TrapFrame
:1
2
3
4.text:0046A473 mov esi, large fs:_KPCR.PrcbData.CurrentThread
...
.text:0046A492 mov ebx, [esi+_KTHREAD.TrapFrame]
.text:0046A498 mov [ebp+_KTRAP_FRAME._Edx], ebx但是在
KiFastCallEntry
中并没有发现会将当前线程的TrapFrame
保存起来。但是:在函数
KiFastCallEntry
、NtRaiseException
等这些函数中都会有如下代码:1
2.text:0046A625 mov edx, [ebp+_KTRAP_FRAME._Edx]
.text:0046A628 mov [ecx+_KTHREAD.TrapFrame], edx用来恢复
TrapFrame
。尽管在KiFastCallEntry
没有看到使用KTRAP_FRAME._Edx
保存TrapFrame
,但是从其他函数逆向分析来看,**“假装”KiFastCallEntry
函数会像函数_KiSystemService
中一样设置KTRAP_FRAME._Edx = Currentthread->TrapFrame
**。
- 先处理段寄存器
- FS = 0x30
- DS = 0x23
- ES = 0x23
esp0 = TSS.ESP0
,找到当前线程的栈顶
进入系统服务函数之前,KiFastCallEntry
堆栈与寄存器图:
1 | .text:0046A520 // =============== S U B R O U T I N E ======================================= |
总结:可以看到无论是从函数KiSystemService,还是从函数KiFastCallEntry往下分析,都会调用地址0046A5AF
,只是在调用该地址前初始化工作有些不同。
进入地址0046A5AF
前,eax保存系统服务号,edx都指向3环API的参数起始地址。
往下的代码段 将会放到下一节开始分析余下的两个问题:
- 如何根据系统调用号(eax中存储)找到要执行的内核函数?
- 调用时参数是存储到3环的堆栈,如何传递给内核函数?
可参考:
6 从0环回3环
6.1 中断返回
6.2 快速调用返回
sysenter指令执行步骤如下:
- 将
IA32_SYSENTER_CS
保存到 CS中。 - 将
IA32_SYSENTER_EIP
保存到 EIP 中。 - 将
IA32_SYSENTER_CS + 8
保存到 SS 中。 - 将
IA32_SYSENTER_ESP
保存到 ESP 寄存器中。 - 切换到 R0 级别。
- 如果 EFLAGS 中的 VM 标志被设定,那么清 0 该标志。
- 开始执行 R0 代码。
sysexit指令执行前需要如下准备工作:设置EDX为ring3下要执行的指令的首地址。设置ECX为ring3下的栈指针。sysexit
指令的执行步骤如下:
- 将
IA32_SYSENTER_CS + 16
保存到 CS 中。(R3下代码段) - 将 EDX 赋值给 EIP。
- 将
IA32_SYSENTER_CS + 24
保存到 SS 中。 - 将 ECX 赋值给 ESP。
- 切换到 R3 下继续执行 R3 代码。
指令 | 寄存器的值 | 寄存器填装 | 对应GDT段描述符 |
---|---|---|---|
sysenter | CS = 0x1B、SS = 0x23、FS = 0x3B DS = 0x23、ES = 0x23、GS = 0 |
CS0 = IA32_SYSENTER_CS SS0 = IA32_SYSENTER_CS + 8 ESP0 = IA32_SYSENTER_ESP(临时) EIP0 = IA32_SYSENTER_EIP |
R0: CS0 = 0x8,00001 0 00,Index = 1,GDT_CS = 00cf9b00`0000ffff。 SS0 = GDT_CS + 8,–> Index = 2,GDT_SS0 = 00cf9300`0000ffff。 |
sysexit | CS = 0x8、SS = 0x10、FS = 0x30 DS = 0x23、ES = 0x23、GS = 0 |
CS3 = IA32_SYSENTER_CS + 16 SS3 = IA32_SYSENTER_CS + 24 ESP3 = ECX EIP3 = EDX |
R3: CS3 = GDT_CS + 16,–> Index = 3,GDT_CS3 = 00cffb00`0000ffff。 SS3 = GDT_CS + 24,–> Index = 4,GDT_SS3 = 00cff300`0000ffff。 |
1 | kd> r gdtr |
1、需要解释下快速调用缺少的如下在KiSystemService
的两句:
1 | .text:0046A492 mov ebx, [esi+_KTHREAD.TrapFrame] |
2、快速调用返回函数KiFastSystemCallRet
是紧跟在KiFastSystemCall
之后的:
1 | kd> u 0x7c92e4f0 |
3、好好解释一下快速返回KiFastSystemCallRet
的ret
实际就是弹出NtReadVirtualMemory
的返回地址去执行。
6.3 0环的返回3环寄存器
从0环返回三环,最重要的是找到 EIP3、ESP3、EBP3的位置。
两种返回方式:iretd、sysexit。
- 常规方式 :系统调用返回
进入0环时
ESP3:TRAP_FRAME.HardwareSegEsp = edx,第二个返回地址(KiFastCallEntry)
EIP3:KTRAP_FRAME.Eip = _KUSER_SHARED_DATA.SystemCallReturn(KiFastCallEntry)
EBP3:TRAP_FRAME.Ebp = ebp (KiFastCallEntry)
1
2
3
4
5.text:0046A539 push edx //HardwareSegEsp = esp3 == 3环的第二个返回地址
...//eip3
.text:0046A548 push dword ptr ds:0FFDF0304h
...//ebp3
.text:0046A550 push ebp
退出0环时
ESP3:ecx
EIP3:edx
EBP3:ebp
1
2
3
4
5
6//KiSystemCallExit2
.text:0046A732 pop ecx //ecx = TrapFrame.HardwareSegEsp, sysexit --> esp3 = ecx
...//KiSystemCallExit2
.text:0046A728 pop edx //edx = TrapFrame.Eip, sysexit --> eip3 = edx
...//KiServiceExit
.text:0046A700 pop ebp //ebp = TrapFrame.Ebp, sysexit 不改变Ebp
- APC返回
- 进入0环时:使用常规调用
- 退出0环前:在执行到常规退出之前,修改TrapFrame
- ESP3:在
KiFastSystemCallRet
函数里构造sizeof(CONTEXT) + 0x10 == 0x2DC
,然后将这时3环的地址赋给TrapFrame.HardwareSegEsp(KiInitializeUserApc)。 - EIP3:TrapFrame.Eip = KeUserApcDispatcher(KiInitializeUserApc)。
- EBP3:在KiInitializeUserApc中没有处理,使用默认值。
- ESP3:在
- 退出0环时:使用常规调用
- 用户异常返回(使用sysenter方式,非中断进入0环)
- 进入0环时:使用常规调用
- 退出0环前:在执行到常规退出之前,修改TrapFrame
- ESP3:在
KiDispatchException--KiEspToTrapFrame
函数里赋值给TrapFrame.HardwareSegEsp,此时的Eip3指向KiUserExceptionDispatcher
函数的栈顶(参数1、2,EXCEPTION_RECORD,CONTEXT)。 - EIP3:TrapFrame.Eip = KeUserExceptionDispatcher(KiDispatchException)。
- EBP3:在KiDispatchException中没有处理,使用默认值。
- ESP3:在
- 退出0环时:使用常规调用
7 API前缀
21 22 23 24 25 26 年
25 26 27 28 29 30
b- b- b- b b- b-
15 20 30
45 60
20*1.3 = 26