登录  | 立即注册

游客您好!登录后享受更多精彩

查看: 124|回复: 0

WindowsX86内核14.调用门实现系统调用

[复制链接]

90

主题

9

回帖

407

积分

管理员

积分
407
发表于 2024-12-15 11:00:25 | 显示全部楼层 |阅读模式
系统调用(system call) sys call
  • 系统调用就是调用系统的功能,但是对于开发者来说系统调用就是调API,但是从操作系统角度来说系统调用不是API,API不一定要进系统权限,但是对于3环开发者来说,他不需要关心,所有函数有都是API,他们也不需要去了解要不要进内核
  • 所有我们说的这个系统调用就是必须要有0环权限才能做到
  • 3环调用需要进0换的API有: VirtualAlloc (需要改页表) CreateFile (操作磁盘[硬件]) ..... 等
  • 正常流程: 用户层 => 系统调用 => 切换权限到ring0 => 执行系统调用 => 切换权限ring3
  • 这里就涉及到权限切换 ring3 => 切换 => ring0 权限的切换有CPU 负责 ,因为代码是由 CPU 执行的,CPU会提一些方案给操作系统
切换权限的方法
  • 中断 : 只要产生中断CPU自动进入0环权限
  • 异常 : 只要产生异常CPU自动进入0环权限 (自陷: 故意产生一条指令的异常,通常是 int 3 )
  • 门 :只要用了门就自动进入0环

    • 调用门(GDT) 不是中断,所以不管 IF 标志位,没有屏蔽信号一说
    • 中断门(IDT)EFLAG(IF = 0)(CLI) : IF = 0 是 cli这条指令造成的,1表示不屏蔽中断 0表示屏蔽中断,cli 就是改这个标志位,但是只有0环可以改,3环改不了
    • 陷阱门(IDT):用的比较少,因为用中断门比用陷阱门好,因为中断门解决了重复中断(一个中断没处理完又来一个)的问题

  • 们到底要提供什么呢: 我们进入0环,首先对CPU来说就是API的地址在哪,所以中断门也好,调用门也好,都是提供一个函数调用的地址
调用门实现自己的API
  • 做系统调用不需要跟用户通信,只要在软件启动的时候安装这个系统调用
  • 因为要实现系统调用,那么函数的实现代码一定会让他在分页内存中,因为API如果没人用放到磁盘上,马上会被换回来,API时时有人调
门描述符
1664181182177-443cb973-e739-4ba9-8708-92879a36a1ff.png
  • 描述门格式
1664181336039-96f4d149-8d0f-472e-a1fb-62c7660ee52b.png

    • 跟GDT相比 保留的 8位(0~7)中 5位(0~4)用于放参数数量,3位保留
  • 做门就是做一个描述符,然后早GDT中找一个为空的把他填进去就可以了

注册和卸载调用门
  • 驱动安装的是调用注册调门函数,卸载时调用卸载注册门函数
  • 在GDT中找空的可以通过 r gdate 查看gdtr的值 然后dq gtdr的值查看,找到为空的一项(第0项不能用)
  • 多核CPU每一核都得注册和卸载


  1. //12 1 1 0 0 32-Bit Call Gate
  2. //门描述符
  3. struct GateDes {
  4.   unsigned offset1 : 16;     //偏移低16位
  5.   unsigned selector : 16;    //代码段选择子
  6.   unsigned param : 5;        //参数数量
  7.   unsigned res:3;            //保留
  8.   unsigned type : 4;         //类型
  9.   unsigned s : 1;            //描述符类型 (0 = 系统段; 1 = 存储段)
  10.   unsigned dpl : 2;          //描述符特权级 ring0~ring3  
  11.   unsigned p : 1;            //存在位   0 段不在    1 存在
  12.   unsigned offset2 : 16;     //偏移高16位
  13. };

  14. #pragma pack(1)
  15. struct DTR {
  16.   unsigned short limit;
  17.   unsigned int base;
  18. };


  19. //要调用的函数
  20. void  syscall() {
  21.   DbgPrint("[51asm] syscall\n");
  22. }



  23. //注册调用门
  24. void RegisterGate() {
  25.   struct GateDes gate;
  26.   gate.offset1 = (unsigned)&syscall & 0xffff;    //函数地址的低16位
  27.   gate.offset2 = (unsigned)&syscall >> 16;       //函数地址的高16位(无符号改为自动补0)
  28.   gate.selector = 8;                                  //函数地址在0环的CS段
  29.   gate.param = 0;                                     //参数数量
  30.   gate.res = 0;                                       //保留
  31.   gate.s = 0;                                         //系统段
  32.   gate.type = 12;                                     //类型:12调用门
  33.   gate.dpl = 3;                                       //0和3环都可以用                                    
  34.   gate.p = 1;                                         //有效

  35.   KAFFINITY  mask = KeQueryActiveProcessors();
  36.   DbgPrint("mask:%x\n", mask);

  37.   KAFFINITY   shift = 1;
  38.   struct DTR gdt = { 0 };
  39.   //多核的话每一核都得去改
  40.   while (mask) {
  41.     KeSetSystemAffinityThread(shift);
  42.     __asm {
  43.       sgdt gdt;    //获取寄存器GDTR 的值
  44.     }
  45.     DbgPrint("base:%p limit:%p\n", (void*)gdt.base, (void*)gdt.limit);

  46.     //修改GDT
  47.     struct GateDes* pGate = (struct GateDes*)gdt.base;
  48.     pGate[9] = gate; //放在GDT表的第9项  3环的话选择子是0x4b,0环是48

  49.     shift <<= 1;
  50.     mask >>= 1;
  51.   }
  52. }


  53. //卸载调用门
  54. void UnRegisterGate() {
  55.   KAFFINITY  mask = KeQueryActiveProcessors();
  56.   DbgPrint("mask:%x\n", mask);

  57.   KAFFINITY   shift = 1;
  58.   struct DTR gdt = { 0 };
  59.   //每一核都得卸载
  60.   while (mask) {
  61.     KeSetSystemAffinityThread(shift);
  62.     __asm {
  63.       sgdt gdt;
  64.     }
  65.     DbgPrint("base:%p limit:%p\n", (void*)gdt.base, (void*)gdt.limit);

  66.     //修改GDT
  67.     struct GateDes* pGate = (struct GateDes*)gdt.base;
  68.     struct GateDes gate = {0};  //改回0
  69.     pGate[9] = gate;   //3环的话选择子是0x4b,0环是48

  70.     shift <<= 1;
  71.     mask >>= 1;
  72.   }
  73. }
复制代码



3环使用调用门
1664182897430-64f6f2c9-7eea-4543-aa35-0bc369a15d5d.png
  • 因为JMP回不来,所以我们一般用CALL,把返回值压入栈里面,这样我们肺癌能 ret 回来
  • 调用门只能用汇编来写


  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. int main()
  4. {
  5.   __asm {   
  6.     call far 0x4b:0; //调用调用门,端的偏移不用写,在描述符里面有,所以这里随便写也没事
  7.   }
  8.   return 0;
  9. }
复制代码



  • 此时编译会提示没有 call 0x4b:0 这条指令,但是我们查CPU手册可以看到这条指令是存在
1664183421421-1c5a530c-1173-4ec8-9ef5-3f0364efdbfc.png
  • 既然确定这条指令存在 我们可以直接用 call far 0x4b:0 的二进制字符 9a 00 00 00 00 4b 00


  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. int main()
  4. {
  5.    __asm {
  6.    
  7.     _emit 9ah
  8.     _emit 00h
  9.     _emit 00h
  10.     _emit 00h
  11.     _emit 00h
  12.     _emit 4bh
  13.     _emit 00h
  14.   }

  15.   printf("call syscall ok\n");
  16.   system("pause");
  17.   return 0;
  18. }
复制代码



  • 那怎么从0环回去3环呢, 我们权限已经切到0环,那么很多寄存器环境需要切,栈还得切,要把3环的栈换成0环的,栈要切换会涉及到2个寄存器 SS 和 ESP
1664184020671-eb008700-5f5b-4e45-92ee-1a6cfd692139.png
  • 微软会把3环的寄存器都保存到0环的栈里面,他会把参数拷贝到0换的栈里面
  • 0环的SS 和ESP是从 TSS中来的,0环的CS 和EIP 是保存在了 门描述符里面
  • 回去就是把 SS ESP CS EIP 四个寄存器还原,不能用ret,因为对于CPU来是 ret 并不会切换特权等级,并且栈顶的返回值放到EIP, 所以要用 retf 返回,他会在切成3环执行代码之前,他会把栈里的四个寄存器还原
  • 说明我们驱动里面的函数只能用裸函数去写,因为得控制寄存器环境,所以系统调用函数不可能用C来写


  1. __declspec(naked) void  syscall() {
  2.   __asm{
  3.       //int 3;
  4.   }  
  5.    
  6.   DbgPrint("[51asm] syscall\n");
  7.    
  8.   __asm{
  9.       retf;
  10.   }  
  11. }
复制代码




  • 在0环和3环代码都下一个断点,这个我们可以从3环调到0环
  • 3环没有能力改门的下标,但是还是存在缺陷
  • 我们安装好驱动之后,运行3环代码.发现没有暂停,.调试发现在调 printf 时候 崩了,调试可以发现 fs 寄存器发生了改变 ,在操作系统里面 fs 有作用,但cpu 不负责切着个寄存器,所以得我们自己切,但是前面我们没做(WinDbg在调试的时候发现fs不对会改掉),所以一旦用到TEB,就会崩
  • 所以函数的代码要改


  1. __declspec(naked) void  syscall() {
  2.   __asm{
  3.       //int 3;
  4.       push fs;
  5.       mov ax,30h;
  6.       mov fs,ax;
  7.   }  
  8.    
  9.   DbgPrint("[51asm] syscall\n");
  10.    
  11.   __asm{
  12.       pop fs;
  13.       retf;
  14.   }  
  15. }
复制代码





  • 但是这个函数不能调,因为调的话调试器会把 fs的值改了,push的时候 fs的值就不对,pop之后 回到3环 他又 把 fs值改了,所以3环一旦用到 fs 就崩
  • fs 的保存恢复 都应该由系统调用完成,不应该有3环来完成,因为3环代码可以改,因此有被攻击的危险
  • 我们研究函数调用在以后不管是反调试还是shellcode 漏洞方面都有应用
  • 如果3环随意写调用用代码,会导致3环崩,0环没事,因此CPU查表没有这一项,,而且不是门因此不会让3环调,所以只能是 0x4b
  • 3环不能控制代码的流程,这就保证了3环只能用,不能控制0环 ,而且权限切换都是CPU完成的,当调用结束,环境恢复,3环就没权限了
  • 现在还有大量API调用和传参问题了,0环和3环用的栈是不一样,传参0环自己会拷贝
多API和传参
  • 每个API都得提供调用门,因为从3环拷参数到0环需要在门描述符里面描述
  • 参数数量是 5位 ,因此一个API最多 可以传 2^5 = 32 个参数,但是不同的APi参数可能是不一样的
  • 因为传参,所以我们需要 保存和恢复 ebp


  1. __declspec(naked) void  syscall() {
  2.   __asm{
  3.       //int 3;
  4.       push fs;
  5.       mov ax,30h;
  6.       mov fs,ax;
  7.   }  
  8.    
  9.   DbgPrint("[51asm] syscall\n");
  10.    
  11.   __asm{
  12.       pop fs;
  13.       retf;
  14.   }  
  15. }
  16. __declspec(naked) void  syscall1(int  p1) {
  17.   __asm{
  18.       //int 3;
  19.       push ebp;
  20.       mov ebp,esp;
  21.       push fs;
  22.       mov ax,30h;
  23.       mov fs,ax;
  24.   }  
  25.    
  26.   DbgPrint("[51asm] syscall1 p1 = %d\n",p1);
  27.    
  28.   __asm{
  29.       
  30.       pop fs;
  31.       mov esp,ebp;
  32.       pop ebp;
  33.       retf;
  34.   }  
  35. }

  36. __declspec(naked) void  syscall2(int p1 ,int p2) {
  37.   __asm{
  38.        //int 3;
  39.       push ebp;
  40.       mov ebp,esp;
  41.       push fs;
  42.       mov ax,30h;
  43.       mov fs,ax;
  44.   }  
  45.    
  46.   DbgPrint("[51asm] syscall2 p1 = %d  p2 = %d\n",p1,p2);
  47.    
  48.   __asm{
  49.       pop fs;
  50.       mov esp,ebp;
  51.       pop ebp;
  52.       retf;
  53.   }  
  54. }
复制代码




  • 我们也要为 syscall1 和 syscall2 定义一个门,因为参数不一样,但是如果有1万个API,啊呢么就需要一万个门,但是gdtr表 最多只能放 8192项 ,明显不够用,那该如何解决呢
  • 因为参数数量不同会导致我们要做很多门,但是如果三叔我们自己从3环拷贝到0环,这样门就可以固定,参数数量是0,但是数量该怎么解决呢,还有不同的API EIP 不一样.这个我们可以通过参数来解决

  1. //参数 api序号   参数数量  
  2. __declspec(naked) void SysCallProxy(int ApiNo ,int ParamCount) {
  3.   __asm{
  4.        //int 3;
  5.       push ebp;
  6.       mov ebp,esp;
  7.       push fs;
  8.       mov ax,30h;
  9.       mov fs,ax;
  10.   }  

  11.   //拷贝参数  我们不知道3换的栈在哪,所以可以让3环通过寄存器edx传过来
  12.   //这样我们就可以通过栈拷贝了
  13.   mencpy(esp,edx,ParamCount*4)

  14.   switch(ApiNo){
  15.     case 0:  syscall();
  16.       break;
  17.     case 1:  syscall1();
  18.       break;
  19.     case 2:  syscall2();
  20.       break;
  21.   }
  22.   DbgPrint("[51asm] SysCallProxy\n");
  23.    
  24.   __asm{
  25.       pop fs;
  26.       mov esp,ebp;
  27.       pop ebp;
  28.       retf;
  29.   }  
  30. }


  31. void  syscall() {
  32.   DbgPrint("[51asm] syscall\n");
  33. }
  34. void  syscall1(int  p1) {  
  35.   DbgPrint("[51asm] syscall1 p1 = %d\n",p1);
  36. }

  37. void  syscall2(int p1 ,int p2) {     
  38.   DbgPrint("[51asm] syscall2 p1 = %d  p2 = %d\n",p1,p2);
  39. }
复制代码



  • 通过上面的设计 系统调用代理函数,我们一个门就可以了,而且我们的API 就不必用汇编写了,因为我们可以通过汇编把参数处理好,函数正常ret就可以了,由代理函数 retf,这样就解决了参数转换的问题
  • 但是上面那样写 switch 太多,因此 可以通过 表来解决,这样3换调用哪个api额外传一个参数就可以了,因为API调用很频繁,而且把参数入栈我们还需要拷贝,因此我们可以把参数通过寄存器传递,而且 ParamCount 也不需要用了,因为API几个字节3环知道,0环也知道,因为API是他提供的
多API
  • 下面先不考参数数情况,只考虑多函数

  1. -----------3环----------------
  2. #include <stdio.h>
  3. #include <stdlib.h>
  4. int main()
  5. {
  6.    __asm {
  7.     mov eax,0;  //调用第0个api,想调第几个给对应的下标就可以了
  8.     _emit 9ah
  9.     _emit 00h
  10.     _emit 00h
  11.     _emit 00h
  12.     _emit 00h
  13.     _emit 4bh
  14.     _emit 00h
  15.   }

  16.   printf("call syscall ok\n");
  17.   system("pause");
  18.   return 0;
  19. }

  20. -----------0环----------------

  21. typedef void (*SYSCALL)();  //定义函数指针
  22. SYSCALL g_SysCallTable[] = {&syscall1,(SYSCALL)&syscall2, (SYSCALL)&syscall3 };  //系统服务表


  23. __declspec(naked) void SysCallProxy(int ApiNo ,int ParamCount) {
  24.   __asm{
  25.        //int 3;

  26.       push fs;
  27.           push ebx;
  28.       mov bx,30h;
  29.       mov fs,bx;

  30.       //判断调用函数下标是否存在   length g_SysCallTable 求数组的项数
  31.       cmp eax, length g_SysCallTable
  32.       jae EXIT

  33.       call dword ptr [g_SysCallTable + eax * 4]  //调用对应函数,如果函数是C调用约定,我们的帮他平栈,否则栈会不平衡,而我们也没办法帮他平,不知道要平多少,所以API要设计成 stdcall调用约定,

  34. EXIT:
  35.       pop ebx;
  36.       pop fs;
  37.       retf;
  38.   }  
  39. }

  40. //因为没用到参数,所以不用考虑ebp的情况
复制代码



  • 这样加函数的话 级表就行了,代理函数不用改,这样维护性提升了一个档次,并且实现函数只需要正常写,门只需要一个
传参

  1. -----------3环----------------
  2. #include <stdio.h>
  3. #include <stdlib.h>
  4. int main()
  5. {
  6.    __asm {
  7.     mov eax,0;    //调用第0个api,想调第几个给对应的下标就可以了
  8.     mov edx,esp;  //把当前栈地址通过edx传给0环
  9.     _emit 9ah
  10.     _emit 00h
  11.     _emit 00h
  12.     _emit 00h
  13.     _emit 00h
  14.     _emit 4bh
  15.     _emit 00h


  16.     mov eax,1;    //调用第1个api,想调第几个给对应的下标就可以了
  17.     push 1;       //传参
  18.     mov edx,esp;  //把当前栈地址通过edx传给0环
  19.     _emit 9ah
  20.     _emit 00h
  21.     _emit 00h
  22.     _emit 00h
  23.     _emit 00h
  24.     _emit 4bh
  25.     _emit 00h
  26.     add esp 4 //平栈

  27.     mov eax,1;    //调用第1个api,想调第几个给对应的下标就可以了
  28.     push 2;       //传参 第二个
  29.     push 1;       //传参 第一个
  30.     mov edx,esp;  //把当前栈地址通过edx传给0环
  31.     _emit 9ah
  32.     _emit 00h
  33.     _emit 00h
  34.     _emit 00h
  35.     _emit 00h
  36.     _emit 4bh
  37.     _emit 00h
  38.     add esp 8 //平栈
  39.   }

  40.   printf("call syscall ok\n");
  41.   system("pause");
  42.   return 0;
  43. }

  44. -----------0环----------------

  45. void __stdcall syscall1() {
  46.   DbgPrint("[51asm] syscall1\n");
  47. }

  48. void __stdcall syscall2(int p1) {
  49.   DbgPrint("[51asm] syscall2  p1:%d\n", p1);
  50. }

  51. void __stdcall syscall3(int p1, int p2) {
  52.   DbgPrint("[51asm] syscall3 p1:%d p2:%d\n", p1, p2);
  53. }


  54. //系统服务表
  55. typedef void (*SYSCALL)();
  56. SYSCALL g_SysCallTable[] = {&syscall1,(SYSCALL)&syscall2, (SYSCALL)&syscall3 };

  57. //函数参数大小表
  58. unsigned char  g_SysCallParam[] = { 0, 4,  8 };

  59. __declspec(naked) void SysCallProxy() {
  60.   __asm {
  61.     //int 3
  62.     push ebp
  63.     mov  ebp, esp
  64.     //保存环境
  65.     push fs
  66.     push ebx
  67.     push ecx
  68.     push esi
  69.     push edi

  70.     mov bx, 30h
  71.     mov fs, bx

  72.     cmp eax, length g_SysCallTable
  73.     jae EXIT

  74.     //从用户栈拷贝参数到内核栈
  75.     movzx ecx, byte ptr g_SysCallParam[eax]  //获取参数总大小
  76.     sub esp, ecx     //抬栈
  77.     mov esi, edx
  78.     mov edi, esp
  79.     rep movsb
  80.     //调用函数
  81.     call dword ptr [g_SysCallTable + eax * 4]

  82. EXIT:
  83.     //恢复环境
  84.     pop edi
  85.     pop esi
  86.     pop ecx
  87.     pop ebx
  88.     pop fs
  89.     mov esp, ebp   //平栈
  90.     pop ebp
  91.     retf
  92.   }
  93. }
复制代码




  • 这样3环用的也很舒服,类的就是系统,如果有一万个API,那么代理函数也要写一万个,所以一般操作系统作者会给编译器加一个宏,让他自动生成代理函数,因为有规律
  • 0环的两个表不一定要做成2个数组,做成结构体也可以
  • 上面代码还有问提,例如参数是个指针,用户故意传一个空指针,这样就会导致蓝屏,因此在函数里需要先验证一下参事是否是有效的
  • 如果攻击者知道我们系统调用的实现原理,他有可能不调 ntdll.dll,直接内联汇编进内核,这样调API进内核的出号就是调试器的API断点将失效,并且IDA逆向软件你将会看到导入表是空的,导出表是空的,因为这些系统调用可以直接内联汇编来调,直接进内核,不需要经过系统,反汇编代码如下:
  • 我们看到的汇编代码只有上面的3行,这就是加密
  • 这种加密方式只有我们了解windwos 的系统调用怎么实现的才能模仿
  • windows 里面没有用调用门,调用门一般内核漏洞 shellcode 经常用,系统不用,攻击者可以用
完整源码0环 内核驱动
  1. #include <ntddk.h>

  2. //12 1 1 0 0 32-Bit Call Gate
  3. //门描述符
  4. struct GateDes {
  5.   unsigned offset1 : 16;     //偏移低16位
  6.   unsigned selector : 16;    //代码段选择子
  7.   unsigned param : 5;        //参数数量
  8.   unsigned res:3;            //保留
  9.   unsigned type : 4;         //类型
  10.   unsigned s : 1;            //描述符类型 (0 = 系统段; 1 = 存储段)
  11.   unsigned dpl : 2;          //描述符特权级 ring0~ring3  
  12.   unsigned p : 1;            //存在位   0 段不在    1 存在
  13.   unsigned offset2 : 16;     //偏移高16位
  14. };

  15. #pragma pack(1)
  16. struct DTR {
  17.   unsigned short limit;
  18.   unsigned int base;
  19. };

  20. void __stdcall syscall1() {
  21.   DbgPrint("[51asm] syscall1\n");
  22. }

  23. void __stdcall syscall2(int p1) {
  24.   DbgPrint("[51asm] syscall2  p1:%d\n", p1);
  25. }

  26. void __stdcall syscall3(int p1, int p2) {
  27.   DbgPrint("[51asm] syscall3 p1:%d p2:%d\n", p1, p2);
  28. }


  29. //系统服务表
  30. typedef void (*SYSCALL)();
  31. SYSCALL g_SysCallTable[] = {&syscall1,(SYSCALL)&syscall2, (SYSCALL)&syscall3 };

  32. //函数参数大小表
  33. unsigned char  g_SysCallParam[] = { 0, 4,  8 };

  34. __declspec(naked) void SysCallProxy() {
  35.   __asm {
  36.     //int 3
  37.     push ebp
  38.     mov  ebp, esp
  39.     //保存环境
  40.     push fs
  41.     push ebx
  42.     push ecx
  43.     push esi
  44.     push edi

  45.     mov bx, 30h   //0环 fs的值
  46.     mov fs, bx

  47.     cmp eax, length g_SysCallTable
  48.     jae EXIT

  49.     //从用户栈拷贝参数到内核栈
  50.     movzx ecx, byte ptr g_SysCallParam[eax]  //获取参数总大小
  51.     sub esp, ecx     //抬栈
  52.     mov esi, edx
  53.     mov edi, esp
  54.     rep movsb
  55.     //调用函数
  56.     call dword ptr [g_SysCallTable + eax * 4]

  57. EXIT:
  58.     //恢复环境
  59.     pop edi
  60.     pop esi
  61.     pop ecx
  62.     pop ebx
  63.     pop fs
  64.     mov esp, ebp   //平栈
  65.     pop ebp
  66.     retf
  67.   }
  68. }
  69.    
  70. //卸载调用门
  71. void UnRegisterGate() {
  72.   KAFFINITY  mask = KeQueryActiveProcessors();
  73.   DbgPrint("mask:%x\n", mask);

  74.   KAFFINITY   shift = 1;
  75.   struct DTR gdt = { 0 };
  76.   //每一核都得卸载
  77.   while (mask) {
  78.     KeSetSystemAffinityThread(shift);
  79.     __asm {
  80.       sgdt gdt;
  81.     }
  82.     DbgPrint("base:%p limit:%p\n", (void*)gdt.base, (void*)gdt.limit);

  83.     //修改GDT
  84.     struct GateDes* pGate = (struct GateDes*)gdt.base;
  85.     struct GateDes gate = {0};  //改回0
  86.     pGate[9] = gate;   //3环的话选择子是0x4b,0环是48

  87.     shift <<= 1;
  88.     mask >>= 1;
  89.   }
  90. }

  91. //注册调用门
  92. void RegisterGate() {
  93.   struct GateDes gate;
  94.   gate.offset1 = (unsigned)&SysCallProxy & 0xffff;    //函数地址的低16位
  95.   gate.offset2 = (unsigned)&SysCallProxy >> 16;       //函数地址的高16位(无符号改为自动补0)
  96.   gate.selector = 8;                                  //函数地址在0环的CS段
  97.   gate.param = 0;                                     //参数数量
  98.   gate.res = 0;                                       //保留
  99.   gate.s = 0;                                         //系统段
  100.   gate.type = 12;                                     //类型:12调用门
  101.   gate.dpl = 3;                                       //0和3环都可以用               
  102.   gate.p = 1;                                         //有效

  103.   KAFFINITY  mask = KeQueryActiveProcessors();
  104.   DbgPrint("mask:%x\n", mask);

  105.   KAFFINITY   shift = 1;
  106.   struct DTR gdt = { 0 };
  107.   //多核的话每一核都得去改
  108.   while (mask) {
  109.     KeSetSystemAffinityThread(shift);
  110.     __asm {
  111.       sgdt gdt;    //获取寄存器GDTR 的值
  112.     }
  113.     DbgPrint("base:%p limit:%p\n", (void*)gdt.base, (void*)gdt.limit);

  114.     //修改GDT
  115.     struct GateDes* pGate = (struct GateDes*)gdt.base;
  116.     pGate[9] = gate; //放在GDT表的第9项  3环的话选择子是0x4b,0环是48

  117.     shift <<= 1;
  118.     mask >>= 1;
  119.   }
  120. }


  121. /*驱动卸载函数 clean_up*/
  122. VOID Unload(__in struct _DRIVER_OBJECT* DriverObject)
  123. {
  124.   DbgPrint("[51asm] Unload! DriverObject:%p\n", DriverObject);

  125.   UnRegisterGate();
  126. }


  127. /*1.驱动入口函数*/
  128. NTSTATUS DriverEntry(
  129.   __in struct _DRIVER_OBJECT* DriverObject,
  130.   __in PUNICODE_STRING  RegistryPath)
  131. {
  132.   UNREFERENCED_PARAMETER(RegistryPath);

  133.   DbgPrint("[51asm] DriverEntry DriverObject:%p\n", DriverObject);

  134.   DriverObject->DriverUnload = Unload;    //注册卸载函数

  135.   RegisterGate();   //注册调用门

  136.   return STATUS_SUCCESS;
  137. }
复制代码



3环 用户调用

  1. #include <stdio.h>
  2. #include <stdlib.h>


  3. __declspec(naked) void __stdcall CallGate() {
  4.   __asm {
  5.     //int 3
  6.      lea edx, [esp + 8]   //多了一个返回值
  7.     _emit 9ah
  8.     _emit 00h
  9.     _emit 00h
  10.     _emit 00h
  11.     _emit 00h
  12.     _emit 4bh
  13.     _emit 00h
  14.     ret
  15.   }
  16. }

  17. void (__stdcall* g_SysCall)() = &CallGate;   //加这个可以防止修改金0环的方式而导致后面大改,这样只需要修改 CallGate() 函数就可以了

  18. //ntdll.dll
  19. __declspec(naked) void __stdcall Syscall1() {
  20.   __asm {
  21.     mov eax, 0  //syscall1
  22.     call g_SysCall
  23.     retn
  24.   }
  25. }
  26. __declspec(naked) void __stdcall Syscall2(int p1) {
  27.   __asm {
  28.     mov eax, 1  //syscall2
  29.     call g_SysCall
  30.     retn 4
  31.   }
  32. }

  33. __declspec(naked) void __stdcall Syscall3(int p1, int p2) {
  34.   __asm {
  35.     mov eax, 2  //syscall3
  36.     call g_SysCall
  37.     retn 8
  38.   }
  39. }


  40. int main()
  41. {
  42.   Syscall1();
  43.   Syscall2(1);
  44.   Syscall3(1, 2);
  45.   printf("call syscall1 ok\n");

  46.   system("pause");

  47. /*
  48.   //内联汇编进内核  
  49.   __asm {
  50.     mov eax, 1  //syscall2
  51.     call g_SysCall
  52.     retn 4
  53.   }
  54. */
  55.    
  56.   return 0;
  57. }
复制代码


系统调用
  • 系统调用就是调用系统的功能,英文为system call 缩写 sys call,对于开发者来说系统调用就是调API

    • 但是从操作系统的角度来说,系统调用不是API,因为API并不一定要进内核,所以我们说的系统调用就是调这个API必须要用0环权限才能完成,这才叫系统调用

    • 在3环下比如申请内存,创建文件这些API都得进内核
  • 提供系统调用功能

正常情况下是: 用户层发起系统调用 ==> 切换权限到ring0 ==> 执行系统调用 ==> 切换权限到ring3 中间还有寄存器的环境要改变,大的过程就是这样
这里就涉及到如何权限切换:首先就是ring3如何切换到ring0 权限的切换是由CPU来提供,代码是由CPU执行的,而CPU并不知道有没有权限执行,是操作系统告诉CPU的;所以检测 者就是CPU,切换权限就是CPU来切换的,CPU会提供一些方案来告诉操作系统怎么切换 1.中断:只要产生中断,CPU自动进入0环权限 2.异常:产生异常也会自动进入0环权限 3.自陷:自陷就是故意产生异常,最简单的自陷就是int 3;这就是故意的就是为了切换权限,自陷也算异常 4.门:只要用了门就会自动进入0环权限;中断门(IDT),陷阱门(IDT),调用门(GDT)等 陷阱门用的比较少,中断门就是IF = 0 也就是CLI指令那种 一般都是调用门或者中断门这两种. 门提供的就是进入0环了,对于CPU来说就要知道去哪里执行代码,也就是API的地址在哪,中断门也好调用门也好提 供的都是系统调用的地址
做系统调用是不需要和3环通讯的,只要在软件启动的时候安装一下,然后最后卸载
  • 自做API
这个函数代码一定要是在分页内存中,因为API没人调的话给它交换到磁盘上是不合理的... 编写好提供的API后,就要做一个调用门了 可以在CPU手册里查看门描述符格式,按照格式来完成调用门; 描述符格式其实可以参考GDT表格式.但是它中间的保留位门描述符用了5位,用于参数数量,只保留了3位
//门描述符
  1. //门描述符
  2. struct GateDes {
  3.     unsigned offset1  : 16; //偏移低16位
  4.     unsigned selector : 16; //代码段选择子
  5.     unsigned param    : 5; //参数数量
  6.     unsigned res      : 3; //保留的3位
  7.     unsigned type     : 4; //类型
  8.     unsigned s        : 1; //描述符类型 (0 = 系统段; 1 = 存储段)
  9.     unsigned dpl      : 2; //描述符特权级 ring0~ring3  
  10.     unsigned p        : 1; //存在位   0 段不在    1 存在
  11.     unsigned offset2  : 16;//偏移高16位
  12. };
复制代码



门的做法就是:做一项这样的描述符,然后在GDT表里找一项空的填进去所以就写俩函数,注册门和卸载门注册门函数里就做一个门,然后把它写到GDT表里就行了卸载门函数就把对应项再置为0就行了这样0环的API就写完了,可以开始编写3环的使用程序了3环首先要检查CPL(当前特权级),RPL(描述符特权级),DPL(段寄存器的DPL)3环使用的话就是call或者jmp,但是一般使用的都是call,因为jmp过去就回不来了所以用call,把返回值压入栈里,这样才能return回来;        -但是CPL要小于等于DPL,这是合理的.注册的时候DPL写的是3环,所以调用者是3环2环1环0环都可以        -RPL要小于等于DPL也就是段寄存器里面的选择子的权限要小于等于DPL因为只能用call和jmp,所以3环也只有内联汇编了,但是内联汇编的话 call 0x4b:0这条指令无法编译通过,所以需要自己手动去写2进制的call指令去调用调用门


    • 切换流程
  • 虽然权限已经切换到0环了,但是栈也得切,要把3环的栈切换成0环的栈.切换栈的话就涉及到SS堆栈段和ESP. ESP要切换成ESP0. 为了调用完毕能回到3环,把3环的寄存器都保存到0环的栈里了(SS ESP CS EIP),并且还把3环的参数也拷贝到0 环栈里了. 也就是它和3环call函数的时候是不一样的,3环call一个函数就压一个返回地址(EIP)就行了,而调用门则额外多 压入了3个寄存器. 至于0环的SS和ESP从TS里来的,0环的CS和EIP就从门描述符里拿,所以它只要从TS里拿一共SS和ESP就可以切换 了. 回去的话就把这四个寄存器还原就行了,不能用ret(ret对于CPU来说是特权级不切换,并且是把栈顶的返回写入 EIP),现在是有四个寄存器都得还原,所以要用retf来返回. retf第一步就是把特权级切换成3环,接下来会把栈里的四个寄存器还原了.
  • 所以0环所提供的API函数只能用裸函数写了,因为要控制寄存器环境.

    • 至此门已经设计好了,3环除了调用它就没有别的操作了,但是它还是有缺陷的.
  • 通过调试时观察寄存器发现,当切换到0环的时候 eip,cs,esp,ss这些寄存器都改变了,这是对的,CPU是会切换 在Windows操作系统里FS是有作用的,但是CPU不负责切换这个寄存器,所以FS得自己切 使用WinDbg的时候因为要用dt命令!process命令,这些命令都会用到KPCR,而KPCR的值就是从FS里拿的,如果 FS不对的话,所有的命令都会出错,所以WinDbg会检测,只要发现FS不对,它就会自己改掉 但是从0环执行完毕,切换回去的时候WinDbg就不管了. 也就是调试的时候进0环它会自动把FS改成30,回3环它会自动改成0,但是不是断点调试的话FS是不会动的
  • 所以就要自己手动保存FS,而且需要注意的是不能下断点,下断点就会被WinDbg自己改了.就会造成保存失败.所以 系统调用不能调也不可调. FS的保存和恢复都应该由系统调用来完成

    • 代码示例

  1. /*********************0环程序****************************************/
  2. #pragma pack(1)
  3. struct DTR {
  4.     unsigned short limit;
  5.     unsigned int base;
  6. };
  7. //提供的第一个API
  8. __declspec(naked) void Syscall1() {

  9.     __asm {
  10.         //int 3
  11.         push fs //进入0环就把3环的fs压入栈里

  12.         mov ax,30h
  13.         mov fs,ax //然后把fs改成0环用的30h
  14.         
  15.     }
  16.     DbgPrint("[WANG] Syscall1\n");

  17.     __asm {
  18.         pop fs //返回的时候再把fs弹出栈
  19.         retf
  20.     }
  21. }
  22. //卸载门
  23. void UnRegisterGate() {
  24.     //卸载的时候就直接填0就行了

  25.     KAFFINITY  mask = KeQueryActiveProcessors();
  26.     DbgPrint("mask:%x\n", mask);
  27.     KAFFINITY   shift = 1;
  28.     struct DTR gdt = { 0 };
  29.     while (mask) {
  30.         KeSetSystemAffinityThread(shift);
  31.         __asm {
  32.             sgdt gdt;
  33.         }
  34.         DbgPrint("base:%p limit:%p\n", (void*)gdt.base, (void*)gdt.limit);
  35.         //修改GDT
  36.         struct GateDes* pGate = (struct GateDes*)gdt.base;
  37.         struct GateDes gate = { 0 };
  38.         pGate[9] = gate; //004B

  39.         shift <<= 1;
  40.         mask >>= 1;
  41.     }
  42. };
  43. //注册门
  44. void RegisterGate() {
  45.     struct GateDes gate;
  46.     //门要描述的就是那个API地址
  47.     gate.offset1 = (unsigned)&Syscall1 & 0xffff;
  48.     gate.offset2 = (unsigned)&Syscall1 >> 16;
  49.     //再就是其他的一些成员了
  50.     gate.selector = 8;//选择子,肯定要给0环的段,0环的段只有一个就是cs
  51.     gate.param = 0;
  52.     gate.res = 0;
  53.     gate.s = 0;//描述符类型 0 = 系统段
  54.     gate.type = 12; //类型;12就是调用门(32位系统的),
  55.     gate.dpl = 3; //这个门3环可以用
  56.     gate.p = 1; //有效
  57.     //门做好后就要写到gdt表里
  58.     KAFFINITY  mask = KeQueryActiveProcessors();
  59.     DbgPrint("mask:%x\n", mask);
  60.     KAFFINITY   shift = 1;
  61.     struct DTR gdt = { 0 };
  62.     while (mask) {
  63.         KeSetSystemAffinityThread(shift);
  64.         __asm {
  65.             sgdt gdt;
  66.         }
  67.         DbgPrint("base:%p limit:%p\n", (void*)gdt.base, (void*)gdt.limit);

  68.         //修改GDT
  69.         struct GateDes* pGate = (struct GateDes*)gdt.base;
  70.         pGate[9] = gate; //选择子给3环用的应该是0x4b
  71.         //每一核都改一下
  72.         shift <<= 1;
  73.         mask >>= 1;
  74.     }
  75. };
  76. /*驱动卸载函数 clean_up*/
  77. VOID Unload(__in struct _DRIVER_OBJECT* DriverObject)
  78. {
  79.     DbgPrint("[WANG] Unload! DriverObject:%p\n", DriverObject);
  80.     //卸载门
  81.     UnRegisterGate();
  82. }


  83. /*1.驱动入口函数*/
  84. NTSTATUS DriverEntry(
  85.     __in struct _DRIVER_OBJECT* DriverObject,
  86.     __in PUNICODE_STRING  RegistryPath)
  87. {
  88.     UNREFERENCED_PARAMETER(RegistryPath);
  89.     DbgPrint("[WANG] DriverEntry DriverObject:%p\n", DriverObject);
  90.     //注册门
  91.     RegisterGate();
  92.     DriverObject->DriverUnload = Unload;
  93.     return STATUS_SUCCESS;
  94. }

  95. /*********************3环程序*****************************************/
  96. int main()
  97. {

  98.     __asm {
  99.         //第一个参数选择段描述符的选择子,第二个参数写段的偏移,但是不用写,因为偏移在描述符里已经有
  100.         //了所以写0也行
  101.         //call 0x4b:0;//调用调用门;这么写编译器不会编译通过的,得自己手写2进制了
  102.         _emit 9ah
  103.         _emit 00h
  104.         _emit 00h
  105.         _emit 00h
  106.         _emit 00h
  107.         _emit 4bh
  108.         _emit 00h //自己写2进制的call指令 ,这些代码是固定的.

  109.     }

  110.     printf("call syscall1 ok\n");
  111.     system("pause");
  112.     return 0;
  113. }
复制代码




    • 还要考虑有大量的API时的情况,以及传递参数的问题
  • 每个API都需要提供一个调用门,要从3环拷贝参数到0环需要在描述符里描述数量,一个参数4个字节是固定了,描述符 里数量给的是5位,也就是32个参数. 不同的API数量也是不一样的,因为带了参数,所以0环提供的API里就得保存ebp. 而且还得为这个API写一个调用门,但是如果数量太多的话,GDT表就不够用了.


      • 大量API时的解决方法
    • 因为参数数量不同,所以导致要做很多个门,解决方法就是自己拷贝,自己从3环的栈拷贝到0环.自己拷贝的话,门就可以写固定的了,参数数量直接写0但是门写固定的话调用API就无法区分了,所以要多两个参数,也就是API的下标和参数数量增加一个API代理函数,传递这两个参数,门的话就写这个函数的地址,这么设计的话门只需要一个就够了,而且提供的API也不需要用汇编写了,可以通过汇编把参数处理好,然后它正常返回到代理函数里.代理函数里再retf这个函数就叫系统调用代理函数.来解决参数转换问题在这个函数里就通过API下标这个参数来判断调用那个函数,其他API需要参数的话就可以自己通过参数数量来拷贝参数,但是拷贝的话不知道3环的栈在哪里,因为已经被切走了.所以可以在3环程序里先把它的栈保存到一个寄存器里.这样0环这个代理函数里就能用了$但是这么写的话如果函数过多的话,switch也得写很多,所以还是很麻烦.为了解决它可以增加一个函数指针表,利用传递的API下标直接调用表里对应的项$//定义一个函数指针typedef void(*SYSCALL)();//再写一个函数数组用于决定调那个函数SYSCALL g_SysCallTable[] = { &Syscall1,(SYSCALL)&Syscall2 };//直接调用就行了g_SysCallTable[apiNum]();$这样3环想调那个API的话额外传递一个参数就行了,而且apiNum这个参数可以通过寄存器来传递.$也就是3环想调那个API直接用寄存器说明就行了,比如:mov eax,0 //调用第0个API,0就是API编号0环调用的话就直接call dword ptr [g_SysCallTable + eax * 4]这样加API就直接加g_SysCallTable这个表就行了,而且API也可以正常写了,不用裸函数了,门也只需要一个了eax做返回值,也不需要保存了.$但是还要注意的是提供的API必须要是__stdcall约定的,这样在代理函数里调用API就不需要平栈了$


    • 参数问题解决
  • 首先3环在传递API编号的时候可以顺便把3环的栈通过寄存器传递给0环 这样0环就可以通过寄存器知道3环的栈在哪里了;但是API有几个参数还是不知道. 可以在做一个表,就是参数信息表,因为0环它自己提供的API,它知道每个API有几个字节的参数,所以提供参数信息 表,每一项就是对应API的参数字节信息. 然后3环已经把它的栈通过寄存器传递过来了,这样就可以按照字节数拷贝到0环的栈里,这样调用参数问题就可以了

    • 虽然0环和3环的代码已经写完了,但是3环调API还是用的是汇编,这样就很不通用了,所以3环得封装一个函数来方便调用,这就是Windows里面Ntdll.dll的功能,而kernel32就是检查参数的

    • 代码示例


  1. /**********************************0环************************/
  2. //提供的第一个API
  3. void __stdcall Syscall1() {


  4.     DbgPrint("[WANG] Syscall1\n");
  5. }
  6. //提供的第二个API
  7. void __stdcall Syscall2(int p1) {

  8.     DbgPrint("[WANG] Syscall2 p1:%d\n",p1);
  9. }

  10. //提供的第三个API
  11. void __stdcall Syscall3(int p1,int p2) {

  12.     DbgPrint("[WANG] Syscall3 p1:%d p2:%d\n", p1, p2);
  13. }

  14. //定义一个函数指针
  15. typedef void(*SYSCALL)();
  16. //再写一个函数数组用于决定调那个函数 (它叫系统服务表)
  17. SYSCALL g_SysCallTable[] = { &Syscall1,(SYSCALL)&Syscall2 ,(SYSCALL)&Syscall3 };
  18. //参数信息表
  19. unsigned char g_SysCallParam[] = { 0,4,8 };

  20. //代理函数,这个代理函数就写在门里.
  21. __declspec(naked) void SysCallProxy() {
  22.     //这里的代码是固定的,
  23.     __asm {
  24.         //int 3
  25.         push ebp
  26.         mov ebp,esp

  27.         push fs //进入0环就把3环的fs压入栈里
  28.         push ebx
  29.         push ecx
  30.         push esi
  31.         push edi

  32.         mov bx, 30h
  33.         mov fs, bx //然后把fs改成0环用的30h

  34.         //判断一下传过来的下标是否正确
  35.         cmp eax ,length g_SysCallTable //对比一下eax和数组下标
  36.         jae EXIT //大于数组下标就直接退出 ;使用jae避免负数的情况

  37.         //从用户栈拷贝参数到内核栈,
  38.         movzx ecx,byte ptr g_SysCallParam[eax]//获取API的参数字节
  39.         sub esp,ecx //抬0环的栈
  40.         mov esi,edx
  41.         mov edi,esp
  42.         rep movsb //然后从3环的栈里拷贝到0环的栈里

  43.         //根据API下标参数判断调用那个API;eax里就是3环传递的API下标
  44.         call dword ptr [g_SysCallTable + eax * 4]

  45. EXIT:
  46.         pop edi
  47.         pop esi
  48.         pop ecx
  49.         pop ebx
  50.         pop fs
  51.         mov esp,ebp
  52.         pop ebp

  53.         retf
  54.     }
  55. }
  56. /*注册卸载还是一样的,没有改动*/

  57. /*************************3环代码*********************************/
  58. //调用的代理函数
  59. __declspec(naked) void __stdcall CallGate() {
  60.     //相同的就写一个就行了
  61.     __asm {
  62.         //把3环的栈通过edx传递给0环//用edx来保存3环的栈,而且edx是不会切换的,这样0环代理函数就
  63.         //能用3环的栈了
  64.         lea edx, [esp + 8]//多了一个返回值入栈,所以要+8
  65.         _emit 9ah
  66.         _emit 00h
  67.         _emit 00h
  68.         _emit 00h
  69.         _emit 00h
  70.         _emit 4bh
  71.         _emit 00h //自己写2进制的call指令
  72.         ret
  73.     }

  74. }

  75. //以后进0环的方式可能会改变,为了避免代码大改,所以可以用函数指针,下面这些函数调用的时候就调g_SysCall就行了
  76. void(__stdcall* g_SysCall)() = &CallGate;


  77. //封装的第一个函数
  78. __declspec(naked) void __stdcall SysCall1() {
  79.     __asm {
  80.         mov eax, 0//调用第0个API syscall1
  81.         call g_SysCall //调用代理函数
  82.         ret
  83.     }
  84. }
  85. //第2个API的封装
  86. __declspec(naked) void __stdcall SysCall2(int p1) {
  87.     __asm {
  88.         mov eax, 1//调用第1个API syscall2
  89.         //push 1    //传递一个参数 封装了就不用push了,参数已经在栈里了
  90.         call g_SysCall //调用代理函数
  91.         ret 4
  92.     }
  93. }
  94. //第3个API的封装
  95. __declspec(naked) void __stdcall SysCall3(int p1,int p2) {
  96.     __asm {
  97.         mov eax, 2//调用第3个API syscall3
  98.         //push 2    //传递第二个参数
  99.         //push 1    //传递第一个参数
  100.         call g_SysCall //调用代理函数
  101.         retn 8
  102.     }
  103. }
  104. int main()
  105. {
  106.     SysCall1();
  107.     printf("call syscall1 ok\n");
  108.     SysCall2(1);
  109.     printf("call syscall2 ok\n");
  110.     SysCall3(1,2);
  111.     printf("call syscall3 ok\n");

  112.     system("pause");
  113.     return 0;
  114. }
复制代码


您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

QQ|Archiver|手机版|小黑屋|断点社区 |网站地图

GMT+8, 2025-1-18 15:59 , Processed in 0.079603 second(s), 32 queries .

Powered by XiunoBBS

Copyright © 2001-2025, 断点社区.

快速回复 返回顶部 返回列表