开始学内核的时候,一定会讲从ring3到ring0的调用,但是网上很多的文章讲的模棱两可,这次记录下我对系统调用的研究。。。。。。。。
一个线程由用户态进入内核态的途径有3种典型的方式:
1、 主动通过int 2e(软中断自陷方式)或sysenter指令(快速系统调用方式)调用系统服务函数,主动进入内核
2、 发生异常,被迫进入内核
3、 发生硬件中断,被迫进入内核
现在的cpu调用api进入内核都是通过sysenter指令(AMD的处理器是syscall)进入到ring0,系统在启动的时候会根据cpuid指令来测试是否支持sysenter指令,不能的话就会通过古老的int 0x2e来进行系统调用。。。。。
为了IDT的学习,我把分析的重点放在int 0x2e上
我们来看下r3下api的调用流程
如ReadFile函数调用系统服务函数NtReadFile
Kernel32.ReadFile() //点号前面表示该函数的所在模块
{
//所有Win32 API通过NTDLL中的系统服务存根函数调用系统服务进入内核
NTDLL.NtReadFile();
}
NTDLL.NtReadFile()
{
Mov eax,152 //我们要调用的系统服务函数号,也即SSDT表中的索引,记录在eax中
If(cpu不支持sysenter指令)
{
Lea edx,[esp+4] //用户空间中的参数区基地址,记录在edx中
Int 2e //通过该自陷指令方式进入KiSystemService,‘调用’对应的系统服务
}
Else
{
Lea edx,[esp +4] //用户空间中的参数区基地址,记录在edx中
Sysenter //通过sysenter方式进入KiFastCallEntry,‘调用’对应的系统服务
}
Ret 36 //不管是从int 2e方式还是sysenter方式,系统调用都会返回到此条指令处
Int 2e的内部实现原理:
该指令是一条自陷指令,执行该条指令后,cpu会自动将当前线程的当前栈切换为本线程的内核栈(栈分用户栈、内核栈),保存中断现场,也即那5个寄存器。然后从该cpu的中断描述符表(简称IDT)中找到这个2e中断号对应的函数(也即中断服务例程,简称ISR),jmp 到对应的isr处继续执行,此时这个ISR本身就处于内核空间了,当前线程就进入内核空间了
Int 2e指令可以把它理解为intel提供的一个内部函数,它内部所做的工作如下
Int 2e
{
//这些都是cpu硬件帮我们完成的
Cli //cpu一中断,立马自动关中断
Mov esp, TSS.内核栈地址 //切换为内核栈,TSS中记录了当前线程的内核栈地址
Push SS
Push esp //可能会有疑问,由于 Mov esp, TSS.内核栈地址,这里压入的是内核下的esp,怎么不是用户层的esp,接下来就会讲解原因
Push eflags
Push cs
Push eip //这5项工作保存了中断现场【标志、ip、esp】
Jmp IDT[中断号] //跳转到对应本中断号的isr
}
我们来手工设置一个除0错误的异常,在0号中断处理程序处下个断点来验证我们的猜想(在0x2e下了之后虚拟机系统老中断
#include "stdafx.h" int main(int argc, char* argv[]) { int a=9; int b=0; int c=a/b;//在这里设置一个断点 return 0; }
调试之后记录一下当前的寄存器值
同时我们用windbg在0号中断处理程序处下个断点
kd> !idt 0 Dumping IDT: 00: 8053f23c nt!KiTrap00 kd> bp 8053f23c nt!KiTrap00 ^ Range error in 'bp 8053f23c nt!KiTrap00' kd> bp 8053f23c breakpoint 0 redefined
运行测试程序后,会发现windbg断点命中
Breakpoint 0 hit nt!KiTrap00: 8053f23c 6a00 push 0
我们用dd esp命令查看堆栈来验证int 0x2e所做的操作
kd> dd esp b237adcc 0040103a 0000001b 00010212 0012ff28 b237addc 00000023 00000000 00000000 00000000
由于0号异常是在执行idv这条指令产生的,
所以eip==dword ptr [esp]
cs=dword ptr [esp+4]
eflags第16位做了下改变
用户层esp=dword ptr [esp+c]
ss=dword ptr [esp+0x10]
11: int c=a/b;
00401036 mov eax,dword ptr [ebp-4]
00401039 cdq
0040103A idiv eax,dword ptr [ebp-8]
现在线程已经进入内核态,开始执行int 0x2e的中断处理程序,
我们用windbg来慢慢分析中断处理程序的代码(win xp下)
kd> u 8053e521 l30 nt!KiSystemService: 8053e521 6a00 push 0 8053e523 55 push ebp 8053e524 53 push ebx 8053e525 56 push esi 8053e526 57 push edi 8053e527 0fa0 push fs 8053e529 bb30000000 mov ebx,30h 8053e52e 668ee3 mov fs,bx //使fs指向KPCR结构 8053e531 ff3500f0dfff push dword ptr ds:[0FFDFF000h]//压入当前的exceptionlist,不是很懂 8053e537 c70500f0dfffffffffff mov dword ptr ds:[0FFDFF000h],0FFFFFFFFh//设置新的exceptionlist
kd> dt _KPCR PrcbData. nt!_KPCR +0x120 PrcbData : +0x000 MinorVersion : Uint2B +0x002 MajorVersion : Uint2B +0x004 CurrentThread : Ptr32 _KTHREAD
8053e541 8b3524f1dfff mov esi,dword ptr ds:[0FFDFF124h]//使esi指向ETHREAD线程对象
kd> dt _KTHREAD
nt!_KTHREAD
+0x140 PreviousMode
8053e547 ffb640010000 push dword ptr [esi+140h]//压入线程的上个模式是r3还是r0 8053e54d 83ec48 sub esp,48h//为调试寄存器腾出空间,后面再解释
----------------至此没有push的操作,不过形成了一个trap帧,保存了用户空间的寄存器,用于从r0到r3的恢复----
我画了个堆栈图看下这个来自用户空间构造的这个trap帧
8053e550 8b5c246c mov ebx,dword ptr [esp+6Ch] 8053e554 83e301 and ebx,1 //0环的最低位为0,3环的最低位为1 8053e557 889e40010000 mov byte ptr [esi+140h],bl//通过判断cs的值来设置previousmode了 8053e55d 8bec mov ebp,esp //ebp保存trap帧 8053e55f 8b9e34010000 mov ebx,dword ptr [esi+134h]//+0x134 TrapFrame : Ptr32 _KTRAP_FRAME 8053e565 895d3c mov dword ptr [ebp+3Ch],ebx//将上个TrapFrame放入trap帧 8053e568 89ae34010000 mov dword ptr [esi+134h],ebp//设置当前的TrapFrame 8053e56e fc cld //改变DF标志位 8053e56f 8b5d60 mov ebx,dword ptr [ebp+60h] 8053e572 8b7d68 mov edi,dword ptr [ebp+68h] 8053e575 89550c mov dword ptr [ebp+0Ch],edx//edx指向用户空间的参数 8053e578 c74508000ddbba mov dword ptr [ebp+8],0BADB0D00h 8053e57f 895d00 mov dword ptr [ebp],ebx 8053e582 897d04 mov dword ptr [ebp+4],edi//这几句感觉没啥用 8053e585 f6462cff test byte ptr [esi+2Ch],0FFh //+0x02c DebugActive : UChar
8053e589 0f858dfeffff jne nt!Dr_kss_a (8053e41c)//线程处于调试状态 则跳转
我们来看下nt!Dr_kss_a 的代码//
这是在IDA中看到的,我们大致看一下
还记得上面的sub esp,48吗,如果线程被调试,就会将dr0-dr3 dr6-dr7保存在trap帧
text:004423E0 Dr_kss_a proc near ; CODE XREF: _KiSystemService+62j .text:004423E0 test dword ptr [ebp+70h], 20000h .text:004423E7 jnz short loc_4423F3 .text:004423E7 .text:004423E9 test byte ptr [ebp+6Ch], 1 .text:004423ED jz loc_442546 .text:004423ED .text:004423F3 .text:004423F3 loc_4423F3: ; CODE XREF: Dr_kss_a+7j .text:004423F3 mov ebx, dr0 .text:004423F6 mov ecx, dr1 .text:004423F9 mov edi, dr2 .text:004423FC mov [ebp+18h], ebx .text:004423FF mov [ebp+1Ch], ecx .text:00442402 mov [ebp+20h], edi .text:00442405 mov ebx, dr3 .text:00442408 mov ecx, dr6 .text:0044240B mov edi, dr7 .text:0044240E mov [ebp+24h], ebx .text:00442411 mov [ebp+28h], ecx .text:00442414 xor ebx, ebx .text:00442416 mov [ebp+2Ch], edi .text:00442419 mov dr7, ebx .text:0044241C mov edi, large fs:20h .text:00442423 mov ebx, [edi+2F4h] .text:00442429 mov ecx, [edi+2F8h] .text:0044242F mov dr0, ebx .text:00442432 mov dr1, ecx .text:00442435 mov ebx, [edi+2FCh] .text:0044243B mov ecx, [edi+300h] .text:00442441 mov dr2, ebx .text:00442444 mov dr3, ecx .text:00442447 mov ebx, [edi+304h] .text:0044244D mov ecx, [edi+308h] .text:00442453 mov dr6, ebx .text:00442456 mov dr7, ecx .text:00442459 jmp loc_442546 .text:00442459 .text:00442459 Dr_kss_a endp
8053e58f fb sti//开启中断,至此Trap帧构造完毕
//其实windows有个Trap帧结构,上面的push过程既是按照这个结构来的
//我你们可以看到上面的trap帧构造就是按照这个结构来的
typedef struct _KTRAP_FRAME //Trap现场帧 { ------------------这些是KiSystemService保存的--------------------------- ULONG DbgEbp; ULONG DbgEip; ULONG DbgArgMark; ULONG DbgArgPointer; ULONG TempSegCs; ULONG TempEsp; ULONG Dr0; ULONG Dr1; ULONG Dr2; ULONG Dr3; ULONG Dr6; ULONG Dr7; ULONG SegGs; ULONG SegEs; ULONG SegDs; ULONG Edx;//xy 这个位置不是用来保存edx的,而是用来保存上个Trap帧,因为Trap帧是可以嵌套的 ULONG Ecx; //中断和异常引起的自陷要保存eax,系统调用则不需保存ecx ULONG Eax;//中断和异常引起的自陷要保存eax,系统调用则不需保存eax ULONG PreviousPreviousMode; struct _EXCEPTION_REGISTRATION_RECORD FAR *ExceptionList;//上次seh链表的开头地址 ULONG SegFs; ULONG Edi; ULONG Esi; ULONG Ebx; ULONG Ebp; ---------------------------------------------------------------------------------------- ULONG ErrCode;//发生的不是中断,而是异常时,cpu还会自动在栈中压入对应的具体异常码在这儿 -----------下面5个寄存器是由int 2e内部本身保存的或KiFastCallEntry模拟保存的现场--------- ULONG Eip; ULONG SegCs; ULONG EFlags; ULONG HardwareEsp; ULONG HardwareSegSs;//还记得int 0x2e所做的操作,按照那个操作应该是压入内核层的esp和ss的,但是压入的却是用户层 根据Hardware这个词 我猜想应该是cpu的硬件支持来保存了用户层的esp和ss
---------------以下用于用于保存V86模式的4个寄存器也是cpu自动压入的------------------- ULONG V86Es; ULONG V86Ds; ULONG V86Fs; ULONG V86Gs; } KTRAP_FRAME, *PKTRAP_FRAME;
8053e590 e9d8000000 jmp nt!KiFastCallEntry+0x8d (8053e66d)//跳转到ssdt的表寻找处理函数去了
我们接下来分析一下这个查表的过程
kd> u nt!KiFastCallEntry+0x8d l30 nt!KiFastCallEntry+0x8d: 8053e66d 8bf8 mov edi,eax 8053e66f c1ef08 shr edi,8.//将系统调用号右移8位
//我们需要明白系统调用号的前12位表示要调用哪个API,位12 和位13表示调用哪个ssdt
//这2位如果为00 调用ssdt 如果为01 调用shadowssdt 似乎10 ,11没用到 8053e672 83e730 and edi,30h //判断是属于哪张表ssdt或者shadowssdt edx=0或者0x10 我在想根据ServiceTable获取Shadowssdt的地址的想法是不是根据这个来的 8053e675 8bcf mov ecx,edi
kd> dd nt!KeServiceDescriptorTableShadow 80554060 80502bbc 00000000 0000011c 80503030 80554070 bf99ce00 00000000 0000029b bf99db10
kd> dd nt!KeServiceDescriptorTable
805540a0 80502bbc 00000000 0000011c 80503030
typedef struct _SERVICE_DESCRIPTOR_TABLE {
PULONG ServiceTable;
PULONG CounterTable;
ULONG TableSize;
/*
* Table containing the number of bytes of parameters the handler
* function takes.
*/
PUCHAR ArgumentTable;
} SERVICE_DESCRIPTOR_TABLE, *PSERVICE_DESCRIPTOR_TABLE;
8053e677 03bee0000000 add edi,dword ptr [esi+0E0h]// +0x0e0 ServiceTable 8053e67d 8bd8 mov ebx,eax 8053e67f 25ff0f0000 and eax,0FFFh//判断调用号的后12位,判断用哪个API 8053e684 3b4708 cmp eax,dword ptr [edi+8]//与TableSize做比较 防止调用号越界 8053e687 0f8345fdffff jae nt!KiBBTUnexpectedRange (8053e3d2) 8053e68d 83f910 cmp ecx,10h 8053e690 751a jne nt!KiFastCallEntry+0xcc (8053e6ac)//成立 执行跳转 8053e692 8b0d18f0dfff mov ecx,dword ptr ds:[0FFDFF018h] 8053e698 33db xor ebx,ebx 8053e69a 0b99700f0000 or ebx,dword ptr [ecx+0F70h] 8053e6a0 740a je nt!KiFastCallEntry+0xcc (8053e6ac) 8053e6a2 52 push edx 8053e6a3 50 push eax 8053e6a4 ff15e4405580 call dword ptr [nt!KeGdiFlushUserBatch (805540e4)] 8053e6aa 58 pop eax 8053e6ab 5a pop edx 8053e6ac ff0538f6dfff inc dword ptr ds:[0FFDFF638h]//+0x518 KeSystemCalls : Uint4B 增加调用系统服务次数
8053e6b2 8bf2 mov esi,edx //此时edx指向用户空间的参数块 我们发现在前面的汇编代码从没用过edx 这就是汇编的好处 可以精确的控制寄存器
8053e6b4 8b5f0c mov ebx,dword ptr [edi+0Ch]//ssdt的函数参数表 8053e6b7 33c9 xor ecx,ecx 8053e6b9 8a0c18 mov cl,byte ptr [eax+ebx]//查找参数个数表 8053e6bc 8b3f mov edi,dword ptr [edi] 8053e6be 8b1c87 mov ebx,dword ptr [edi+eax*4]//获取调用号函数的地址 比如NtOpenProcess 8053e6c1 2be1 sub esp,ecx 8053e6c3 c1e902 shr ecx,2 //获取调用号的参数个数 8053e6c6 8bfc mov edi,esp 8053e6c8 3b35d49a5580 cmp esi,dword ptr [nt!MmUserProbeAddress (80559ad4)]//是否在有效用户地址空间 8053e6ce 0f83a8010000 jae nt!KiSystemCallExit2+0x9f (8053e87c) 8053e6d4 f3a5 rep movs dword ptr es:[edi],dword ptr [esi] //复制用户空间的参数到堆栈中 8053e6d6 ffd3 call ebx //调用ssdt的函数
8053e6d8 8be5 mov esp,ebp //ebp指向Trap帧 8053e6da 8b0d24f1dfff mov ecx,dword ptr ds:[0FFDFF124h] 8053e6e0 8b553c mov edx,dword ptr [ebp+3Ch] 8053e6e3 899134010000 mov dword ptr [ecx+134h],edx //恢复上个TrapFrame nt!KiServiceExit: 8053e6e9 fa cli 8053e6ea f7457000000200 test dword ptr [ebp+70h],20000h //网上说检查APC请求 搞不懂 8053e6f1 7506 jne nt!KiServiceExit+0x10 (8053e6f9)
//这是ZwOpenProcess的代码 text:00440B20 mov eax, 0BFh .text:00440B25 lea edx, [esp+ProcessHandle] .text:00440B29 pushf .text:00440B2A push 8 //压入的是8 代替了cs的位置 .text:00440B2C call _KiSystemService
8053e6f3 f6456c01 test byte ptr [ebp+6Ch],1 //判断cs 如果来自内核模式 会进行跳转 8053e6f7 7457 je nt!KiServiceExit+0x67 (8053e750) //来自内核模式执行跳转 8053e6f9 8b1d24f1dfff mov ebx,dword ptr ds:[0FFDFF124h] 8053e6ff c6432e00 mov byte ptr [ebx+2Eh],0 8053e703 807b4a00 cmp byte ptr [ebx+4Ah],0 8053e707 7447 je nt!KiServiceExit+0x67 (8053e750)//也是检查apc的 不管它
//这是一段处理apc的代码 8053e709 8bdd mov ebx,ebp mov dword ptr [ebx+44h],eax
8053e70e c743503b000000 mov dword ptr [ebx+50h],3Bh
8053e715 c7433823000000 mov dword ptr [ebx+38h],23h
8053e71c c7433423000000 mov dword ptr [ebx+34h],23h
8053e723 c7433000000000 mov dword ptr [ebx+30h],0
8053e72a b901000000 mov ecx,1
8053e72f ff15f4864d80 call dword ptr [nt!_imp_KfRaiseIrql (804d86f4)]
8053e735 50 push eax
8053e736 fb sti
8053e737 53 push ebx
8053e738 6a00 push 0
8053e73a 6a01 push 1
8053e73c e8fd02fcff call nt!KiDeliverApc (804fea3e)
8053e741 59 pop ecx
8053e742 ff151c874d80 call dword ptr [nt!_imp_KfLowerIrql (804d871c)]
8053e748 8b4344 mov eax,dword ptr [ebx+44h]
8053e74b fa cli
8053e74c ebab jmp nt!KiServiceExit+0x10 (8053e6f9)
8053e74e 8bff mov edi,edi
*/
8053e750 8b54244c mov edx,dword ptr [esp+4Ch]
8053e754 648b1d50000000 mov ebx,dword ptr fs:[50h]
8053e75b 64891500000000 mov dword ptr fs:[0],edx//恢复exceptionlis
8053e762 8b4c2448 mov ecx,dword ptr [esp+48h]
8053e766 648b3524010000 mov esi,dword ptr fs:[124h] //esi又开始指向ETHREAD了
8053e76d 888e40010000 mov byte ptr [esi+140h],cl
8053e773 f7c3ff000000 test ebx,0FFh
8053e779 7579 jne nt!KiSystemCallExit2+0x17 (8053e7f4)
8053e77b f744247000000200 test dword ptr [esp+70h],20000h
8053e783 0f85eb080000 jne nt!Kei386EoiHelper+0x12c (8053f074)
8053e789 66f744246cf9ff test word ptr [esp+6Ch],0FFF9h
8053e790 0f84b4000000 je nt!KiSystemCallExit2+0x6d (8053e84a)
8053e796 66837c246c1b cmp word ptr [esp+6Ch],1Bh
053e79c 660fba64246c00 bt word ptr [esp+6Ch],0
8053e7a3 f5 cmc
8053e7a4 0f878e000000 ja nt!KiSystemCallExit2+0x5b (8053e838)
8053e7aa 66837d6c08 cmp word ptr [ebp+6Ch],8 //判断cs的值 判断上次的模式是r3还是r0
8053e7af 7405 je nt!KiServiceExit+0xcd (8053e7b6)//来自用户空间的调用不成立
8053e7b1 8d6550 lea esp,[ebp+50h] //从此处开始恢复用户空间的寄存器了
8053e7b4 0fa1 pop fs
//如果是来自内核空间的调用,会跳转到此处
8053e7b6 8d6554 lea esp,[ebp+54h]
8053e7b9 5f pop edi
8053e7ba 5e pop esi
8053e7bb 5b pop ebx
8053e7bc 5d pop ebp //从栈顶恢复寄存器
8053e7bd 66817c24088000 cmp word ptr [esp+8],80h
8053e7c4 0f87c6080000 ja nt!Kei386EoiHelper+0x148 (8053f090) //不懂
8053e7ca 83c404 add esp,4
8053e7cd f744240401000000 test dword ptr [esp+4],1 //判断cs 多次判断参数了
nt!KiSystemCallExitBranch:
8053e7d5 7506 jne nt!KiSystemCallExit2 (8053e7dd) //成立,来自用户空间的调用通过KiSystemCallExit2返回
//来自内核空间的调用执行此处
//这是ZwOpenProcess的代码 text:00440B20 mov eax, 0BFh .text:00440B25 lea edx, [esp+ProcessHandle] .text:00440B29 pushf .text:00440B2A push 8 //压入的是8 代替了cs的位置 .text:00440B2C call _KiSystemService
.text:00440B1D retn 10h
8053e7d7 5a pop edx //按照ZwOpenProcess压入的堆栈框架 此处保存的是调用完KiSystemService后的返回地址
8053e7d8 59 pop ecx
8053e7d9 9d popfd //弹出eflags寄存器
8053e7da ffe2 jmp edx //跳转到 ret 10h指令
----------------------------------------------------------------------------------------------------------
nt!KiSystemCallExit:
//iret的执行代码
//对于实地址模式中断返回,IRET 指令从堆栈将返回指令指针、返回代码段选择器以及 EFLAGS 映像分别弹入 EIP、CS 以及 EFLAGS 寄存器,然后恢复执行中断的程序或过程。如果返回到另一个特权级别,则在恢复程序执行之//前,IRET 指令还从堆栈弹出堆栈指针与 SS
8053e7dc cf iretd //进行中断返回
nt!KiSystemCallExit2:
8053e7dd f644240901 test byte ptr [esp+9],1 //不知道为什么可以判断eflags的TF标志位可以判断是否通过自陷指令进入的
8053e7e2 75f8 jne nt!KiSystemCallExit (8053e7dc)//来自自陷指令时 TF==1 进行跳转
//来自sysenter指令的返回
8053e7e4 5a pop edx
8053e7e5 83c404 add esp,4
8053e7e8 80642401fd and byte ptr [esp+1],0FDh
8053e7ed 9d popfd
8053e7ee 59 pop ecx
8053e7ef fb sti
8053e7f0 0f35 sysexit
//最后总结一下 使用汇编写这个中断处理可以对寄存器达到很好的控制 在调用ssdt之前edx是没有改变的 因为它指向用户空间的参数
//eax在调用ssdt之后也是没有改变的 它包含了返回的结果需要呢
//我们发现形成的TrapFrame中是没有edx和ecx的 edx只是承担着指向用户空间的参数的责任 可以不必push 为啥ecx也不