admin 管理员组

文章数量: 887021

开始学内核的时候,一定会讲从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帧

8
053e550 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也不

 

 

转载于:https://wwwblogs/Reverser/p/4442351.html

本文标签: 学习笔记 系统