系统调用(system call) sys call- 系统调用就是调用系统的功能,但是对于开发者来说系统调用就是调API,但是从操作系统角度来说系统调用不是API,API不一定要进系统权限,但是对于3环开发者来说,他不需要关心,所有函数有都是API,他们也不需要去了解要不要进内核
- 所有我们说的这个系统调用就是必须要有0环权限才能做到
- 3环调用需要进0换的API有: VirtualAlloc (需要改页表) CreateFile (操作磁盘[硬件]) ..... 等
- 正常流程: 用户层 => 系统调用 => 切换权限到ring0 => 执行系统调用 => 切换权限ring3
- 这里就涉及到权限切换 ring3 => 切换 => ring0 权限的切换有CPU 负责 ,因为代码是由 CPU 执行的,CPU会提一些方案给操作系统
切换权限的方法- 们到底要提供什么呢: 我们进入0环,首先对CPU来说就是API的地址在哪,所以中断门也好,调用门也好,都是提供一个函数调用的地址
调用门实现自己的API- 做系统调用不需要跟用户通信,只要在软件启动的时候安装这个系统调用
- 因为要实现系统调用,那么函数的实现代码一定会让他在分页内存中,因为API如果没人用放到磁盘上,马上会被换回来,API时时有人调
门描述符注册和卸载调用门- 驱动安装的是调用注册调门函数,卸载时调用卸载注册门函数
- 在GDT中找空的可以通过 r gdate 查看gdtr的值 然后dq gtdr的值查看,找到为空的一项(第0项不能用)
- 多核CPU每一核都得注册和卸载
- //12 1 1 0 0 32-Bit Call Gate
- //门描述符
- struct GateDes {
- unsigned offset1 : 16; //偏移低16位
- unsigned selector : 16; //代码段选择子
- unsigned param : 5; //参数数量
- unsigned res:3; //保留
- unsigned type : 4; //类型
- unsigned s : 1; //描述符类型 (0 = 系统段; 1 = 存储段)
- unsigned dpl : 2; //描述符特权级 ring0~ring3
- unsigned p : 1; //存在位 0 段不在 1 存在
- unsigned offset2 : 16; //偏移高16位
- };
- #pragma pack(1)
- struct DTR {
- unsigned short limit;
- unsigned int base;
- };
- //要调用的函数
- void syscall() {
- DbgPrint("[51asm] syscall\n");
- }
- //注册调用门
- void RegisterGate() {
- struct GateDes gate;
- gate.offset1 = (unsigned)&syscall & 0xffff; //函数地址的低16位
- gate.offset2 = (unsigned)&syscall >> 16; //函数地址的高16位(无符号改为自动补0)
- gate.selector = 8; //函数地址在0环的CS段
- gate.param = 0; //参数数量
- gate.res = 0; //保留
- gate.s = 0; //系统段
- gate.type = 12; //类型:12调用门
- gate.dpl = 3; //0和3环都可以用
- gate.p = 1; //有效
- KAFFINITY mask = KeQueryActiveProcessors();
- DbgPrint("mask:%x\n", mask);
- KAFFINITY shift = 1;
- struct DTR gdt = { 0 };
- //多核的话每一核都得去改
- while (mask) {
- KeSetSystemAffinityThread(shift);
- __asm {
- sgdt gdt; //获取寄存器GDTR 的值
- }
- DbgPrint("base:%p limit:%p\n", (void*)gdt.base, (void*)gdt.limit);
- //修改GDT
- struct GateDes* pGate = (struct GateDes*)gdt.base;
- pGate[9] = gate; //放在GDT表的第9项 3环的话选择子是0x4b,0环是48
- shift <<= 1;
- mask >>= 1;
- }
- }
- //卸载调用门
- void UnRegisterGate() {
- KAFFINITY mask = KeQueryActiveProcessors();
- DbgPrint("mask:%x\n", mask);
- KAFFINITY shift = 1;
- struct DTR gdt = { 0 };
- //每一核都得卸载
- while (mask) {
- KeSetSystemAffinityThread(shift);
- __asm {
- sgdt gdt;
- }
- DbgPrint("base:%p limit:%p\n", (void*)gdt.base, (void*)gdt.limit);
- //修改GDT
- struct GateDes* pGate = (struct GateDes*)gdt.base;
- struct GateDes gate = {0}; //改回0
- pGate[9] = gate; //3环的话选择子是0x4b,0环是48
- shift <<= 1;
- mask >>= 1;
- }
- }
复制代码
3环使用调用门- 因为JMP回不来,所以我们一般用CALL,把返回值压入栈里面,这样我们肺癌能 ret 回来
- 调用门只能用汇编来写
- #include <stdio.h>
- #include <stdlib.h>
- int main()
- {
- __asm {
- call far 0x4b:0; //调用调用门,端的偏移不用写,在描述符里面有,所以这里随便写也没事
- }
- return 0;
- }
复制代码
- 此时编译会提示没有 call 0x4b:0 这条指令,但是我们查CPU手册可以看到这条指令是存在
- 既然确定这条指令存在 我们可以直接用 call far 0x4b:0 的二进制字符 9a 00 00 00 00 4b 00
- #include <stdio.h>
- #include <stdlib.h>
- int main()
- {
- __asm {
-
- _emit 9ah
- _emit 00h
- _emit 00h
- _emit 00h
- _emit 00h
- _emit 4bh
- _emit 00h
- }
- printf("call syscall ok\n");
- system("pause");
- return 0;
- }
复制代码
- 那怎么从0环回去3环呢, 我们权限已经切到0环,那么很多寄存器环境需要切,栈还得切,要把3环的栈换成0环的,栈要切换会涉及到2个寄存器 SS 和 ESP
- 微软会把3环的寄存器都保存到0环的栈里面,他会把参数拷贝到0换的栈里面
- 0环的SS 和ESP是从 TSS中来的,0环的CS 和EIP 是保存在了 门描述符里面
- 回去就是把 SS ESP CS EIP 四个寄存器还原,不能用ret,因为对于CPU来是 ret 并不会切换特权等级,并且栈顶的返回值放到EIP, 所以要用 retf 返回,他会在切成3环执行代码之前,他会把栈里的四个寄存器还原
- 说明我们驱动里面的函数只能用裸函数去写,因为得控制寄存器环境,所以系统调用函数不可能用C来写
- __declspec(naked) void syscall() {
- __asm{
- //int 3;
- }
-
- DbgPrint("[51asm] syscall\n");
-
- __asm{
- retf;
- }
- }
复制代码
- 在0环和3环代码都下一个断点,这个我们可以从3环调到0环
- 3环没有能力改门的下标,但是还是存在缺陷
- 我们安装好驱动之后,运行3环代码.发现没有暂停,.调试发现在调 printf 时候 崩了,调试可以发现 fs 寄存器发生了改变 ,在操作系统里面 fs 有作用,但cpu 不负责切着个寄存器,所以得我们自己切,但是前面我们没做(WinDbg在调试的时候发现fs不对会改掉),所以一旦用到TEB,就会崩
- 所以函数的代码要改
- __declspec(naked) void syscall() {
- __asm{
- //int 3;
- push fs;
- mov ax,30h;
- mov fs,ax;
- }
-
- DbgPrint("[51asm] syscall\n");
-
- __asm{
- pop fs;
- retf;
- }
- }
复制代码
- 但是这个函数不能调,因为调的话调试器会把 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
- __declspec(naked) void syscall() {
- __asm{
- //int 3;
- push fs;
- mov ax,30h;
- mov fs,ax;
- }
-
- DbgPrint("[51asm] syscall\n");
-
- __asm{
- pop fs;
- retf;
- }
- }
- __declspec(naked) void syscall1(int p1) {
- __asm{
- //int 3;
- push ebp;
- mov ebp,esp;
- push fs;
- mov ax,30h;
- mov fs,ax;
- }
-
- DbgPrint("[51asm] syscall1 p1 = %d\n",p1);
-
- __asm{
-
- pop fs;
- mov esp,ebp;
- pop ebp;
- retf;
- }
- }
- __declspec(naked) void syscall2(int p1 ,int p2) {
- __asm{
- //int 3;
- push ebp;
- mov ebp,esp;
- push fs;
- mov ax,30h;
- mov fs,ax;
- }
-
- DbgPrint("[51asm] syscall2 p1 = %d p2 = %d\n",p1,p2);
-
- __asm{
- pop fs;
- mov esp,ebp;
- pop ebp;
- retf;
- }
- }
复制代码
- 我们也要为 syscall1 和 syscall2 定义一个门,因为参数不一样,但是如果有1万个API,啊呢么就需要一万个门,但是gdtr表 最多只能放 8192项 ,明显不够用,那该如何解决呢
- 因为参数数量不同会导致我们要做很多门,但是如果三叔我们自己从3环拷贝到0环,这样门就可以固定,参数数量是0,但是数量该怎么解决呢,还有不同的API EIP 不一样.这个我们可以通过参数来解决
- //参数 api序号 参数数量
- __declspec(naked) void SysCallProxy(int ApiNo ,int ParamCount) {
- __asm{
- //int 3;
- push ebp;
- mov ebp,esp;
- push fs;
- mov ax,30h;
- mov fs,ax;
- }
- //拷贝参数 我们不知道3换的栈在哪,所以可以让3环通过寄存器edx传过来
- //这样我们就可以通过栈拷贝了
- mencpy(esp,edx,ParamCount*4)
- switch(ApiNo){
- case 0: syscall();
- break;
- case 1: syscall1();
- break;
- case 2: syscall2();
- break;
- }
- DbgPrint("[51asm] SysCallProxy\n");
-
- __asm{
- pop fs;
- mov esp,ebp;
- pop ebp;
- retf;
- }
- }
- void syscall() {
- DbgPrint("[51asm] syscall\n");
- }
- void syscall1(int p1) {
- DbgPrint("[51asm] syscall1 p1 = %d\n",p1);
- }
- void syscall2(int p1 ,int p2) {
- DbgPrint("[51asm] syscall2 p1 = %d p2 = %d\n",p1,p2);
- }
复制代码
- 通过上面的设计 系统调用代理函数,我们一个门就可以了,而且我们的API 就不必用汇编写了,因为我们可以通过汇编把参数处理好,函数正常ret就可以了,由代理函数 retf,这样就解决了参数转换的问题
- 但是上面那样写 switch 太多,因此 可以通过 表来解决,这样3换调用哪个api额外传一个参数就可以了,因为API调用很频繁,而且把参数入栈我们还需要拷贝,因此我们可以把参数通过寄存器传递,而且 ParamCount 也不需要用了,因为API几个字节3环知道,0环也知道,因为API是他提供的
多API
- -----------3环----------------
- #include <stdio.h>
- #include <stdlib.h>
- int main()
- {
- __asm {
- mov eax,0; //调用第0个api,想调第几个给对应的下标就可以了
- _emit 9ah
- _emit 00h
- _emit 00h
- _emit 00h
- _emit 00h
- _emit 4bh
- _emit 00h
- }
- printf("call syscall ok\n");
- system("pause");
- return 0;
- }
- -----------0环----------------
- typedef void (*SYSCALL)(); //定义函数指针
- SYSCALL g_SysCallTable[] = {&syscall1,(SYSCALL)&syscall2, (SYSCALL)&syscall3 }; //系统服务表
- __declspec(naked) void SysCallProxy(int ApiNo ,int ParamCount) {
- __asm{
- //int 3;
- push fs;
- push ebx;
- mov bx,30h;
- mov fs,bx;
- //判断调用函数下标是否存在 length g_SysCallTable 求数组的项数
- cmp eax, length g_SysCallTable
- jae EXIT
- call dword ptr [g_SysCallTable + eax * 4] //调用对应函数,如果函数是C调用约定,我们的帮他平栈,否则栈会不平衡,而我们也没办法帮他平,不知道要平多少,所以API要设计成 stdcall调用约定,
- EXIT:
- pop ebx;
- pop fs;
- retf;
- }
- }
- //因为没用到参数,所以不用考虑ebp的情况
复制代码
- 这样加函数的话 级表就行了,代理函数不用改,这样维护性提升了一个档次,并且实现函数只需要正常写,门只需要一个
传参
- -----------3环----------------
- #include <stdio.h>
- #include <stdlib.h>
- int main()
- {
- __asm {
- mov eax,0; //调用第0个api,想调第几个给对应的下标就可以了
- mov edx,esp; //把当前栈地址通过edx传给0环
- _emit 9ah
- _emit 00h
- _emit 00h
- _emit 00h
- _emit 00h
- _emit 4bh
- _emit 00h
- mov eax,1; //调用第1个api,想调第几个给对应的下标就可以了
- push 1; //传参
- mov edx,esp; //把当前栈地址通过edx传给0环
- _emit 9ah
- _emit 00h
- _emit 00h
- _emit 00h
- _emit 00h
- _emit 4bh
- _emit 00h
- add esp 4 //平栈
- mov eax,1; //调用第1个api,想调第几个给对应的下标就可以了
- push 2; //传参 第二个
- push 1; //传参 第一个
- mov edx,esp; //把当前栈地址通过edx传给0环
- _emit 9ah
- _emit 00h
- _emit 00h
- _emit 00h
- _emit 00h
- _emit 4bh
- _emit 00h
- add esp 8 //平栈
- }
- printf("call syscall ok\n");
- system("pause");
- return 0;
- }
- -----------0环----------------
- void __stdcall syscall1() {
- DbgPrint("[51asm] syscall1\n");
- }
- void __stdcall syscall2(int p1) {
- DbgPrint("[51asm] syscall2 p1:%d\n", p1);
- }
- void __stdcall syscall3(int p1, int p2) {
- DbgPrint("[51asm] syscall3 p1:%d p2:%d\n", p1, p2);
- }
- //系统服务表
- typedef void (*SYSCALL)();
- SYSCALL g_SysCallTable[] = {&syscall1,(SYSCALL)&syscall2, (SYSCALL)&syscall3 };
- //函数参数大小表
- unsigned char g_SysCallParam[] = { 0, 4, 8 };
- __declspec(naked) void SysCallProxy() {
- __asm {
- //int 3
- push ebp
- mov ebp, esp
- //保存环境
- push fs
- push ebx
- push ecx
- push esi
- push edi
- mov bx, 30h
- mov fs, bx
- cmp eax, length g_SysCallTable
- jae EXIT
- //从用户栈拷贝参数到内核栈
- movzx ecx, byte ptr g_SysCallParam[eax] //获取参数总大小
- sub esp, ecx //抬栈
- mov esi, edx
- mov edi, esp
- rep movsb
- //调用函数
- call dword ptr [g_SysCallTable + eax * 4]
- EXIT:
- //恢复环境
- pop edi
- pop esi
- pop ecx
- pop ebx
- pop fs
- mov esp, ebp //平栈
- pop ebp
- retf
- }
- }
复制代码
- 这样3环用的也很舒服,类的就是系统,如果有一万个API,那么代理函数也要写一万个,所以一般操作系统作者会给编译器加一个宏,让他自动生成代理函数,因为有规律
- 0环的两个表不一定要做成2个数组,做成结构体也可以
- 上面代码还有问提,例如参数是个指针,用户故意传一个空指针,这样就会导致蓝屏,因此在函数里需要先验证一下参事是否是有效的
- 如果攻击者知道我们系统调用的实现原理,他有可能不调 ntdll.dll,直接内联汇编进内核,这样调API进内核的出号就是调试器的API断点将失效,并且IDA逆向软件你将会看到导入表是空的,导出表是空的,因为这些系统调用可以直接内联汇编来调,直接进内核,不需要经过系统,反汇编代码如下:
- 我们看到的汇编代码只有上面的3行,这就是加密
- 这种加密方式只有我们了解windwos 的系统调用怎么实现的才能模仿
- windows 里面没有用调用门,调用门一般内核漏洞 shellcode 经常用,系统不用,攻击者可以用
完整源码0环 内核驱动
- #include <ntddk.h>
- //12 1 1 0 0 32-Bit Call Gate
- //门描述符
- struct GateDes {
- unsigned offset1 : 16; //偏移低16位
- unsigned selector : 16; //代码段选择子
- unsigned param : 5; //参数数量
- unsigned res:3; //保留
- unsigned type : 4; //类型
- unsigned s : 1; //描述符类型 (0 = 系统段; 1 = 存储段)
- unsigned dpl : 2; //描述符特权级 ring0~ring3
- unsigned p : 1; //存在位 0 段不在 1 存在
- unsigned offset2 : 16; //偏移高16位
- };
- #pragma pack(1)
- struct DTR {
- unsigned short limit;
- unsigned int base;
- };
- void __stdcall syscall1() {
- DbgPrint("[51asm] syscall1\n");
- }
- void __stdcall syscall2(int p1) {
- DbgPrint("[51asm] syscall2 p1:%d\n", p1);
- }
- void __stdcall syscall3(int p1, int p2) {
- DbgPrint("[51asm] syscall3 p1:%d p2:%d\n", p1, p2);
- }
- //系统服务表
- typedef void (*SYSCALL)();
- SYSCALL g_SysCallTable[] = {&syscall1,(SYSCALL)&syscall2, (SYSCALL)&syscall3 };
- //函数参数大小表
- unsigned char g_SysCallParam[] = { 0, 4, 8 };
- __declspec(naked) void SysCallProxy() {
- __asm {
- //int 3
- push ebp
- mov ebp, esp
- //保存环境
- push fs
- push ebx
- push ecx
- push esi
- push edi
- mov bx, 30h //0环 fs的值
- mov fs, bx
- cmp eax, length g_SysCallTable
- jae EXIT
- //从用户栈拷贝参数到内核栈
- movzx ecx, byte ptr g_SysCallParam[eax] //获取参数总大小
- sub esp, ecx //抬栈
- mov esi, edx
- mov edi, esp
- rep movsb
- //调用函数
- call dword ptr [g_SysCallTable + eax * 4]
- EXIT:
- //恢复环境
- pop edi
- pop esi
- pop ecx
- pop ebx
- pop fs
- mov esp, ebp //平栈
- pop ebp
- retf
- }
- }
-
- //卸载调用门
- void UnRegisterGate() {
- KAFFINITY mask = KeQueryActiveProcessors();
- DbgPrint("mask:%x\n", mask);
- KAFFINITY shift = 1;
- struct DTR gdt = { 0 };
- //每一核都得卸载
- while (mask) {
- KeSetSystemAffinityThread(shift);
- __asm {
- sgdt gdt;
- }
- DbgPrint("base:%p limit:%p\n", (void*)gdt.base, (void*)gdt.limit);
- //修改GDT
- struct GateDes* pGate = (struct GateDes*)gdt.base;
- struct GateDes gate = {0}; //改回0
- pGate[9] = gate; //3环的话选择子是0x4b,0环是48
- shift <<= 1;
- mask >>= 1;
- }
- }
- //注册调用门
- void RegisterGate() {
- struct GateDes gate;
- gate.offset1 = (unsigned)&SysCallProxy & 0xffff; //函数地址的低16位
- gate.offset2 = (unsigned)&SysCallProxy >> 16; //函数地址的高16位(无符号改为自动补0)
- gate.selector = 8; //函数地址在0环的CS段
- gate.param = 0; //参数数量
- gate.res = 0; //保留
- gate.s = 0; //系统段
- gate.type = 12; //类型:12调用门
- gate.dpl = 3; //0和3环都可以用
- gate.p = 1; //有效
- KAFFINITY mask = KeQueryActiveProcessors();
- DbgPrint("mask:%x\n", mask);
- KAFFINITY shift = 1;
- struct DTR gdt = { 0 };
- //多核的话每一核都得去改
- while (mask) {
- KeSetSystemAffinityThread(shift);
- __asm {
- sgdt gdt; //获取寄存器GDTR 的值
- }
- DbgPrint("base:%p limit:%p\n", (void*)gdt.base, (void*)gdt.limit);
- //修改GDT
- struct GateDes* pGate = (struct GateDes*)gdt.base;
- pGate[9] = gate; //放在GDT表的第9项 3环的话选择子是0x4b,0环是48
- shift <<= 1;
- mask >>= 1;
- }
- }
- /*驱动卸载函数 clean_up*/
- VOID Unload(__in struct _DRIVER_OBJECT* DriverObject)
- {
- DbgPrint("[51asm] Unload! DriverObject:%p\n", DriverObject);
- UnRegisterGate();
- }
- /*1.驱动入口函数*/
- NTSTATUS DriverEntry(
- __in struct _DRIVER_OBJECT* DriverObject,
- __in PUNICODE_STRING RegistryPath)
- {
- UNREFERENCED_PARAMETER(RegistryPath);
- DbgPrint("[51asm] DriverEntry DriverObject:%p\n", DriverObject);
- DriverObject->DriverUnload = Unload; //注册卸载函数
- RegisterGate(); //注册调用门
- return STATUS_SUCCESS;
- }
复制代码
3环 用户调用
- #include <stdio.h>
- #include <stdlib.h>
- __declspec(naked) void __stdcall CallGate() {
- __asm {
- //int 3
- lea edx, [esp + 8] //多了一个返回值
- _emit 9ah
- _emit 00h
- _emit 00h
- _emit 00h
- _emit 00h
- _emit 4bh
- _emit 00h
- ret
- }
- }
- void (__stdcall* g_SysCall)() = &CallGate; //加这个可以防止修改金0环的方式而导致后面大改,这样只需要修改 CallGate() 函数就可以了
- //ntdll.dll
- __declspec(naked) void __stdcall Syscall1() {
- __asm {
- mov eax, 0 //syscall1
- call g_SysCall
- retn
- }
- }
- __declspec(naked) void __stdcall Syscall2(int p1) {
- __asm {
- mov eax, 1 //syscall2
- call g_SysCall
- retn 4
- }
- }
- __declspec(naked) void __stdcall Syscall3(int p1, int p2) {
- __asm {
- mov eax, 2 //syscall3
- call g_SysCall
- retn 8
- }
- }
- int main()
- {
- Syscall1();
- Syscall2(1);
- Syscall3(1, 2);
- printf("call syscall1 ok\n");
- system("pause");
- /*
- //内联汇编进内核
- __asm {
- mov eax, 1 //syscall2
- call g_SysCall
- retn 4
- }
- */
-
- return 0;
- }
复制代码
系统调用 正常情况下是: 用户层发起系统调用 ==> 切换权限到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后,就要做一个调用门了 可以在CPU手册里查看门描述符格式,按照格式来完成调用门; 描述符格式其实可以参考GDT表格式.但是它中间的保留位门描述符用了5位,用于参数数量,只保留了3位 //门描述符
- //门描述符
- struct GateDes {
- unsigned offset1 : 16; //偏移低16位
- unsigned selector : 16; //代码段选择子
- unsigned param : 5; //参数数量
- unsigned res : 3; //保留的3位
- unsigned type : 4; //类型
- unsigned s : 1; //描述符类型 (0 = 系统段; 1 = 存储段)
- unsigned dpl : 2; //描述符特权级 ring0~ring3
- unsigned p : 1; //存在位 0 段不在 1 存在
- unsigned offset2 : 16;//偏移高16位
- };
复制代码
门的做法就是:做一项这样的描述符,然后在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的保存和恢复都应该由系统调用来完成
- /*********************0环程序****************************************/
- #pragma pack(1)
- struct DTR {
- unsigned short limit;
- unsigned int base;
- };
- //提供的第一个API
- __declspec(naked) void Syscall1() {
- __asm {
- //int 3
- push fs //进入0环就把3环的fs压入栈里
- mov ax,30h
- mov fs,ax //然后把fs改成0环用的30h
-
- }
- DbgPrint("[WANG] Syscall1\n");
- __asm {
- pop fs //返回的时候再把fs弹出栈
- retf
- }
- }
- //卸载门
- void UnRegisterGate() {
- //卸载的时候就直接填0就行了
- KAFFINITY mask = KeQueryActiveProcessors();
- DbgPrint("mask:%x\n", mask);
- KAFFINITY shift = 1;
- struct DTR gdt = { 0 };
- while (mask) {
- KeSetSystemAffinityThread(shift);
- __asm {
- sgdt gdt;
- }
- DbgPrint("base:%p limit:%p\n", (void*)gdt.base, (void*)gdt.limit);
- //修改GDT
- struct GateDes* pGate = (struct GateDes*)gdt.base;
- struct GateDes gate = { 0 };
- pGate[9] = gate; //004B
- shift <<= 1;
- mask >>= 1;
- }
- };
- //注册门
- void RegisterGate() {
- struct GateDes gate;
- //门要描述的就是那个API地址
- gate.offset1 = (unsigned)&Syscall1 & 0xffff;
- gate.offset2 = (unsigned)&Syscall1 >> 16;
- //再就是其他的一些成员了
- gate.selector = 8;//选择子,肯定要给0环的段,0环的段只有一个就是cs
- gate.param = 0;
- gate.res = 0;
- gate.s = 0;//描述符类型 0 = 系统段
- gate.type = 12; //类型;12就是调用门(32位系统的),
- gate.dpl = 3; //这个门3环可以用
- gate.p = 1; //有效
- //门做好后就要写到gdt表里
- KAFFINITY mask = KeQueryActiveProcessors();
- DbgPrint("mask:%x\n", mask);
- KAFFINITY shift = 1;
- struct DTR gdt = { 0 };
- while (mask) {
- KeSetSystemAffinityThread(shift);
- __asm {
- sgdt gdt;
- }
- DbgPrint("base:%p limit:%p\n", (void*)gdt.base, (void*)gdt.limit);
- //修改GDT
- struct GateDes* pGate = (struct GateDes*)gdt.base;
- pGate[9] = gate; //选择子给3环用的应该是0x4b
- //每一核都改一下
- shift <<= 1;
- mask >>= 1;
- }
- };
- /*驱动卸载函数 clean_up*/
- VOID Unload(__in struct _DRIVER_OBJECT* DriverObject)
- {
- DbgPrint("[WANG] Unload! DriverObject:%p\n", DriverObject);
- //卸载门
- UnRegisterGate();
- }
- /*1.驱动入口函数*/
- NTSTATUS DriverEntry(
- __in struct _DRIVER_OBJECT* DriverObject,
- __in PUNICODE_STRING RegistryPath)
- {
- UNREFERENCED_PARAMETER(RegistryPath);
- DbgPrint("[WANG] DriverEntry DriverObject:%p\n", DriverObject);
- //注册门
- RegisterGate();
- DriverObject->DriverUnload = Unload;
- return STATUS_SUCCESS;
- }
- /*********************3环程序*****************************************/
- int main()
- {
- __asm {
- //第一个参数选择段描述符的选择子,第二个参数写段的偏移,但是不用写,因为偏移在描述符里已经有
- //了所以写0也行
- //call 0x4b:0;//调用调用门;这么写编译器不会编译通过的,得自己手写2进制了
- _emit 9ah
- _emit 00h
- _emit 00h
- _emit 00h
- _emit 00h
- _emit 4bh
- _emit 00h //自己写2进制的call指令 ,这些代码是固定的.
- }
- printf("call syscall1 ok\n");
- system("pause");
- return 0;
- }
复制代码
- 还要考虑有大量的API时的情况,以及传递参数的问题
每个API都需要提供一个调用门,要从3环拷贝参数到0环需要在描述符里描述数量,一个参数4个字节是固定了,描述符 里数量给的是5位,也就是32个参数. 不同的API数量也是不一样的,因为带了参数,所以0环提供的API里就得保存ebp. 而且还得为这个API写一个调用门,但是如果数量太多的话,GDT表就不够用了.
- 因为参数数量不同,所以导致要做很多个门,解决方法就是自己拷贝,自己从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就是检查参数的
- /**********************************0环************************/
- //提供的第一个API
- void __stdcall Syscall1() {
- DbgPrint("[WANG] Syscall1\n");
- }
- //提供的第二个API
- void __stdcall Syscall2(int p1) {
- DbgPrint("[WANG] Syscall2 p1:%d\n",p1);
- }
- //提供的第三个API
- void __stdcall Syscall3(int p1,int p2) {
- DbgPrint("[WANG] Syscall3 p1:%d p2:%d\n", p1, p2);
- }
- //定义一个函数指针
- typedef void(*SYSCALL)();
- //再写一个函数数组用于决定调那个函数 (它叫系统服务表)
- SYSCALL g_SysCallTable[] = { &Syscall1,(SYSCALL)&Syscall2 ,(SYSCALL)&Syscall3 };
- //参数信息表
- unsigned char g_SysCallParam[] = { 0,4,8 };
- //代理函数,这个代理函数就写在门里.
- __declspec(naked) void SysCallProxy() {
- //这里的代码是固定的,
- __asm {
- //int 3
- push ebp
- mov ebp,esp
- push fs //进入0环就把3环的fs压入栈里
- push ebx
- push ecx
- push esi
- push edi
- mov bx, 30h
- mov fs, bx //然后把fs改成0环用的30h
- //判断一下传过来的下标是否正确
- cmp eax ,length g_SysCallTable //对比一下eax和数组下标
- jae EXIT //大于数组下标就直接退出 ;使用jae避免负数的情况
- //从用户栈拷贝参数到内核栈,
- movzx ecx,byte ptr g_SysCallParam[eax]//获取API的参数字节
- sub esp,ecx //抬0环的栈
- mov esi,edx
- mov edi,esp
- rep movsb //然后从3环的栈里拷贝到0环的栈里
- //根据API下标参数判断调用那个API;eax里就是3环传递的API下标
- call dword ptr [g_SysCallTable + eax * 4]
- EXIT:
- pop edi
- pop esi
- pop ecx
- pop ebx
- pop fs
- mov esp,ebp
- pop ebp
- retf
- }
- }
- /*注册卸载还是一样的,没有改动*/
- /*************************3环代码*********************************/
- //调用的代理函数
- __declspec(naked) void __stdcall CallGate() {
- //相同的就写一个就行了
- __asm {
- //把3环的栈通过edx传递给0环//用edx来保存3环的栈,而且edx是不会切换的,这样0环代理函数就
- //能用3环的栈了
- lea edx, [esp + 8]//多了一个返回值入栈,所以要+8
- _emit 9ah
- _emit 00h
- _emit 00h
- _emit 00h
- _emit 00h
- _emit 4bh
- _emit 00h //自己写2进制的call指令
- ret
- }
- }
- //以后进0环的方式可能会改变,为了避免代码大改,所以可以用函数指针,下面这些函数调用的时候就调g_SysCall就行了
- void(__stdcall* g_SysCall)() = &CallGate;
- //封装的第一个函数
- __declspec(naked) void __stdcall SysCall1() {
- __asm {
- mov eax, 0//调用第0个API syscall1
- call g_SysCall //调用代理函数
- ret
- }
- }
- //第2个API的封装
- __declspec(naked) void __stdcall SysCall2(int p1) {
- __asm {
- mov eax, 1//调用第1个API syscall2
- //push 1 //传递一个参数 封装了就不用push了,参数已经在栈里了
- call g_SysCall //调用代理函数
- ret 4
- }
- }
- //第3个API的封装
- __declspec(naked) void __stdcall SysCall3(int p1,int p2) {
- __asm {
- mov eax, 2//调用第3个API syscall3
- //push 2 //传递第二个参数
- //push 1 //传递第一个参数
- call g_SysCall //调用代理函数
- retn 8
- }
- }
- int main()
- {
- SysCall1();
- printf("call syscall1 ok\n");
- SysCall2(1);
- printf("call syscall2 ok\n");
- SysCall3(1,2);
- printf("call syscall3 ok\n");
- system("pause");
- return 0;
- }
复制代码
|