Windows内核基础知识-5-调用门(32-Bit Call Gate)
调用门有一个关键的作用,就是用来提权。调用门其实就是一个段。
调用门:
这是段描述符的结构体,里面的s字段用来标记是代码段还是数据段还是系统段,前面解析的时候讲的是 S==1的情况,也就是Code or data的情况,这次的调用门就是当s==0时的情况。
当s==0时,type的内容如下:
那么调用门其实就是 type为12的情况下的一个段寄存器。
32-Bit Call Gate。32位情况下的一个调用门。
就是一个 段描述符下且S==0 && type==12的一个段。
调用门的段描述符:
利用调用门提权
我们可以通过构造段选择子和段描述符来自己添加一个调用门也就是添加一个段来自己使用。
因为段寄存器无非就是拥有一个起始地址然后作为一个段的内容来让你使用。用汇编写过代码的肯定知道如果构造一个段:
sample PROC
ret
sample ENDP
那么我们自己构造一个系统段来执行我们的代码这不就行了吗。段寄存器只是系统默认的方便我们使用,我们自己通过 segment:eip这样来跳转使用不就完事了吗。
段寄存器比如说 ss(stack segment)无法也就是把栈段的内容通过段选择子和段描述符来集成了而已。
所以这里我们直接添加自己的段,也就是这里所谓的调用门。
解析调用门描述符:
我们要自己配一个调用们,肯定需要知道怎么配,段选择子我们知道了,这里还需要知道段描述符:
这个和前面的段寄存器的段描述符大相径庭,只是有一些不一样。
和前面的段描述符一样,上面的是高32位地址,下面的是低32位地址。
字段 | 内容 |
---|---|
offset in segment | 要跳转的函数的地址,或者是要跳转的地址 |
segment selector | 段选择子,要变成的段选择子(提权的关键) |
Param Count | 函数参数个数 |
高位5-7 | 固定的三个0 |
Type | 系统段只能是1100(10进制的12) |
高12地址 | 就是段描述符的S字段,就系统调用的必须是0 |
DPL | 肯定赋值为3呀,这样ring3才能。 |
p | 和段描述符一样表示该段是否有效,当P为0时无效,1时有效。 |
构造段描述符:
segment selector段描述符字段,可以通过WinDbg附加Windows双机调试时查看cs段寄存器的值就行了,因为此时系统正在启动肯定是在r0的情况下的:
所以这里的段选择子就构造为: 0008(因为段选择子是两个字节16位)
然后再根据前面的解析我们目前的构造是这样的:
高32位:
0-3:
0(十六进制)
4-7:
0(十六进制)
8-11:
C(十六进制)
12-15:
E(十六进制)
16-31:
函数地址的高地址:xxxx
低32:
0-15:
函数地址的低地址:xxxx
16-31:
0008
合集:
xxxxEC000008xxxx
目前就是函数地址需要解决,这个函数地址的话其实就是我们的代码的起始地址,然后就处理这个函数的内容了。
我们可以写一个函数不就完了嘛,但是需要修改成固定基址,这样函数地址就不会改变了。需要采用release版本,因为debug版本有个jmp,然后还得修改优化和随机基址:
#include<iostream>
#include<Windows.h>
using namespace std;
void _declspec(naked) test()
{
_asm
{
//这里我们访问一个0环才能访问的地址
//这样就知道是否是拿到了0环的权限
push eax
mov eax,0x80b95040
mov eax,[eax]
pop eax
ret
}
}
int main()
{
printf("%x\n", test);//输出函数的地址
system("pause");
return 0;
}
这里我的函数地址是:00401080。
所以完整的段描述符值为:
0040EC0000081080
将段描述符添加到gdt表:
这个段描述就到了gdt表偏移9的地方了。
使用调用门
前面调用们的段描述符我们已经配置好了。
现在需要使用调用门了,需要学习两个指令:
call far ;跨段调用 长调用
jmp far ;跨段跳转 长跳转
调用门只能采用call far,jmp far无法,因为jmp far无法做到越级这个作用。
然后还要配一个段选择子:
RPL:
00//采用0环
TI:
0
Index
1001
0100 1011
最终的段选择子就是:0x48
那么应该call far的地址为: 0x48:xxxx
但是在内联汇编里不能这样写,只能这样写:
BYTE code[] = {0,0,0,0,0x48,0};
//0,0,0,0 是eip,然后0x0048是段选择子,采用的是小端字节序
//这里用0000是因为这里不会用到
//因为段描述符里面已经指定了函数的地址了会自动跳转,所以写啥都行
_asm
{
call far fwrod ptr code
}
完整代码:
#include<iostream>
#include<Windows.h>
using namespace std;
void _declspec(naked) test()
{
_asm
{
//这里我们访问一个0环才能访问的地址
//这样就知道是否是拿到了0环的权限
push eax
mov eax,0x80b95040
mov eax,[eax]
pop eax
ret
}
}
int main()
{
//跳转
BYTE code[] = {0,0,0,0,0x48,0};
//段选择子为 0000
_asm
{
call far fwrod ptr code
}
system("pause");
return 0;
}
然后拿到虚拟机里运行一下:
直接系统出错WinDbg捕获了并且蓝屏了,但是至少有一个可以确定,就是我们肯定是跑到内核去执行了,不然怎么会导致系统出错呢,r3的应用层代码肯定不会导致系统的问题的。
就出现问题很正常,打几个断点观察下:
#include<iostream>
#include<Windows.h>
using namespace std;
void _declspec(naked) test()
{
_asm
{
//这里我们访问一个0环才能访问的地址
//这样就知道是否是拿到了0环的权限
int 3
push eax
mov eax, 0x80b95040
mov eax, [eax]
pop eax
ret
}
}
int main()
{
//跳转
BYTE code[6] = {0,0,0,0,0x48,0};
//段选择子为0x0048
__asm
{
call far fword ptr code
}
printf("%x\n", test);
system("pause");
return 0;
}
通过单步调试汇编代码发现是,这个ret的原因,call far或者jmp far,需要采用retf来使用。
改成retf:
#include<iostream>
#include<Windows.h>
using namespace std;
void _declspec(naked) test()
{
_asm
{
//这里我们访问一个0环才能访问的地址
//这样就知道是否是拿到了0环的权限
int 3
push eax
mov eax, 0x80b95040
mov eax, [eax]
pop eax
retf
}
}
int main()
{
//跳转
BYTE code[6] = {0,0,0,0,0x48,0};
//段选择子为0x0048
__asm
{
call far fword ptr code
}
printf("%x\n", test);
system("pause");
return 0;
}
这样之后我们函数内的代码是正常执行了,但是还是蓝屏了,通过我的观察,是段寄存器的问题,这里就通过od的观察看进入前和进入后的问题就可以判断是这个问题了。
所以这里我直接不要这个int 3断点了,可能在r0和r3下的int 3断点执行的东西不一样把:
#include<iostream>
#include<Windows.h>
using namespace std;
void _declspec(naked) test()
{
_asm
{
//这里我们访问一个0环才能访问的地址
//这样就知道是否是拿到了0环的权限
push eax
mov eax, 0x80b93040
mov eax, [eax]
pop eax
retf
}
}
int main()
{
//跳转
BYTE code[6] = {0,0,0,0,0x48,0};
//段选择子为0x0048
__asm
{
call far fword ptr code
}
printf("%x\n", test);
system("pause");
return 0;
}
这样我们的程序就可以美美的执行结束了:
小结
段是一个很重要的概念用来进行内存分割,一个段的描述有很多信息保存在段描述符里面,由于段很多所有就有段描述符表,然后这个表呢由一个段选择子来指向获取表内哪一个段描述符,而段寄存器是CPU为了方便使用的一个寄存器,用来保存的是段选择子这个东西。所以这里我们用的是一个系统调用段来执行我们的指令就是操作系统内部的操作所允许也是intel架构的内容,比较复杂,这里的话我们就明白到这里就可以了想深入的可以阅读本博客后面的参考文献。