长调用与短调用
短调用
指令格式
CALL 立即数 / 寄存器 / 内存
堆栈变化
发生改变的寄存器
ESP EIP
长调用(跨段不提权)
指令格式
CALL CS:EIP(如果是通过调用门则 EIP 是废弃的,真正的 EIP 存储在门中)
堆栈变化
发生改变的寄存器
ESP EIP CS
验证
测试代码如下:
#include<stdio.h>
void test(){
puts("This is test.");
}
int main(){
unsigned char buf[]={0,0,0,0,0x48,0};
*(unsigned int*)(&buf[0])=(unsigned int)&test;
__asm {
call fword ptr ds:[buf]
}
return 0;
}
在 0x90 处构造 DPL = 3 的非一致代码段。
kd> dq gdtr
8003f000 00000000`00000000 00cf9b00`0000ffff
8003f010 00cf9300`0000ffff 00cffb00`0000ffff
8003f020 00cff300`0000ffff 80008b04`200020ab
8003f030 ffc093df`f0000001 0040f300`00000fff
8003f040 0000f200`0400ffff 00000000`00000000
8003f050 80008955`23800068 80008955`23e80068
8003f060 00009302`2f40ffff 0000920b`80003fff
8003f070 ff0092ff`700003ff 80009a40`0000ffff
kd> eq 8003f048 00cffb00`0000ffff
kd> dq gdtr
8003f000 00000000`00000000 00cf9b00`0000ffff
8003f010 00cf9300`0000ffff 00cffb00`0000ffff
8003f020 00cff300`0000ffff 80008b04`200020ab
8003f030 ffc093df`f0000001 0040f300`00000fff
8003f040 0000f200`0400ffff 00cffb00`0000ffff
8003f050 80008955`23800068 80008955`23e80068
8003f060 00009302`2f40ffff 0000920b`80003fff
8003f070 ff0092ff`700003ff 80009a40`0000ffff
运行代码,调用前栈顶指针 esp 指向 0x12FF2C 处。
调用后栈顶指针 esp 指向 0x12FF24 处,并将原来的 cs 和 返回地址压栈,请求的 0 环权限获取,CPL 仍然为 3 。
继续执行后出错,但是 puts 函数成功调用。根据报错位置 checkesp.c
可以确定是堆栈不平衡导致。即 test 函数的 ret 只将返回值从栈中弹出,未将之前压入的 cs 弹出。
将代码做如下修改:将 test 更改为裸函数并将函数返回改为 retf 。由于 puts 函数调用会将 cs 修改为 0x1B ,因此将 puts 去掉。
#include<stdio.h>
void __declspec(naked) test(){
__asm{
retf
}
}
int main(){
unsigned char buf[]={0,0,0,0,0x48,0};
*(unsigned int*)(&buf[0])=(unsigned int)&test;
__asm {
call fword ptr ds:[buf]
}
return 0;
}
返回前的栈状态(图中多出来的跳转与调试状态有关,对结果无影响):
返回后栈中的返回地址和 cs 均被弹出,cs 被修改为正确的值。
如果将 retf 改为 ret 4
同样可以平衡堆栈,但是返回后 cs 没有修复。同样,也可以在函数调用后手动 add esp, 4
来平衡堆栈。
长调用(跨段并提权)
指令格式:CALL CS:EIP(EIP是废弃的)
调用阶段堆栈变化
返回阶段堆栈变化
发生改变的寄存器
ESP EIP CS SS
验证
运行如下代码:
#include<stdio.h>
#include<stdlib.h>
int val = 0;
void __declspec(naked) test() {
__asm {
int 3
mov ebx, 0x10
mov val, ebx
retf
}
}
int main() {
unsigned char buf[] = {0,0,0,0,0x48,0};
printf("%X\n",&test);
system("pause");
__asm{
call fword ptr ds:[buf]
}
return 0;
}
首先打印出 test 函数的地址 0x40100A:
构造调用门提权(稍后会介绍调用门):
kd> dq gdtr
8003f000 00000000`00000000 00cf9b00`0000ffff
8003f010 00cf9300`0000ffff 00cffb00`0000ffff
8003f020 00cff300`0000ffff 80008b04`200020ab
8003f030 ffc093df`f0000001 0040f300`00000fff
8003f040 0000f200`0400ffff 00cffb00`0000ffff
8003f050 80008955`23800068 80008955`23e80068
8003f060 00009302`2f40ffff 0000920b`80003fff
8003f070 ff0092ff`700003ff 80009a40`0000ffff
kd> eq 8003f048 0040ec00`0008100a
kd> dq gdtr
8003f000 00000000`00000000 00cf9b00`0000ffff
8003f010 00cf9300`0000ffff 00cffb00`0000ffff
8003f020 00cff300`0000ffff 80008b04`200020ab
8003f030 ffc093df`f0000001 0040f300`00000fff
8003f040 0000f200`0400ffff 0040ec00`0008100a
8003f050 80008955`23800068 80008955`23e80068
8003f060 00009302`2f40ffff 0000920b`80003fff
8003f070 ff0092ff`700003ff 80009a40`0000ffff
继续运行代码,成功在 test 函数断下来,并且 cs 和 ss 均已被提升为 0 环权限。
观察栈结构发现 0 环的栈上已被依次压入了 3 环的 ss,esp,cs,eip。
执行 retf 时完成了 eip,cs,esp,ss值的修复。
kd> p
0040105c cb retf
kd> dd esp
b0f58dd0 004010b2 0000001b 0012ff2c 00000023
b0f58de0 00000000 00000000 00000000 00000000
b0f58df0 0000027f 7c930000 00000000 00000000
b0f58e00 00000000 00000000 00001f80 23222120
b0f58e10 27262524 00380178 00380188 00000002
b0f58e20 37363534 3b3a3938 00380000 003823a0
b0f58e30 003804e8 0012f8f0 00380178 0012f8f0
b0f58e40 7c930981 00380608 000000bc 00000040
kd> p
001b:004010b2 33c0 xor eax,eax
kd> dd esp
0012ff2c 00241fe4 0012f7bc 7ffd6000 cccccccc
0012ff3c cccccccc cccccccc cccccccc cccccccc
0012ff4c cccccccc cccccccc cccccccc cccccccc
0012ff5c cccccccc cccccccc cccccccc cccccccc
0012ff6c cccccccc cccccccc cccccccc 00000000
0012ff7c cccc0048 0012ffc0 004012e9 00000001
0012ff8c 00430e70 00430da0 00241fe4 0012f7bc
0012ff9c 7ffd6000 00000006 b0f58d04 0012ff94
kd> r cs
cs=0000001b
kd> r ss
ss=00000023
探究 RETF 指令是否能提权
前面实验发现:retf 指令会将保存在栈中的 cs 和 ss 赋值给 cs 和 ss 寄存器,使得权限由 3 环转变为 0 环。因此于是考虑尝试设定栈中的值使得 retf 指令可以提权。
执行如下代码:
#include<stdio.h>
#include<stdlib.h>
int val = 0;
void __declspec(naked) test() {
__asm {
int 3
mov ebx, 0x10
mov val, ebx
retf
}
}
int main() {
printf("%X\n",&test);
system("pause");
__asm{
push 0x10
push 0x12ff2c
push 0x48
lea eax, test
push eax
retf
}
return 0;
}
在执行 retf 之前,栈中设置了合理的提权数据。
继续运行触发异常,说明 retf 只能在同级上跳转或者降低权限,不能利用 retf 提权。
总结
-
跨段调用时,一旦有权限切换,就会切换堆栈
-
CS的权限一旦改变,SS的权限也要随着改变,CS与SS的等级必须一样
-
如果不是一致代码段或者利用 TSS ,JMP FAR 只能同级跳转,但CALL FAR可以通过调用门提权,提升CPL的权限
-
RETF,IRETD 只能在同级跳转,或者只能降低权限
-
SS 与 ESP 的值来自与 TSS 段
-
RETF 指令根据 CS 指向的段描述符是否为们以及是否提权来确定调用时是压入的 4 个值还是 2 个值
调用门
指令格式
CALL CS:EIP(EIP是废弃的)
执行步骤
- 根据CS的值查GDT表,找到对应的段描述符 这个描述符是一个调用门
- 在调用门描述符中存储另一个代码段的段选择子
- 段选择子指向的段 段.Base + 偏移地址 就是真正要执行的地址
门描述符
结构图:
注意:
- S位(第12位)必须为0,只有当S位为0时,段描述符才是系统段描述符;此时,当Type域为1100时,该描述符是门描述符
- 低四字节的16~31位是决定 调用的代码存在于哪个段 的 段选择子
- 当长调用执行时:真正要执行的代码地址 = = = 门描述符中段选择子所指向的代码段的Base + + + 门描述符高四字节的 16 ∼ 31 16\sim31 16∼31 位 + + + 门描述符低四字节的 0 ∼ 15 0\sim15 0∼15 位
关于调用门不稳定问题
在前面长调用提权实验中,构造的调用门在返回时会出现异常,定位到错误原因是 FS 寄存器的段选择子变为 0 导致。
通过调试发现,在经过调用门提权后 fs 寄存器的值也提权为 0x30,而 retf 返回时并没有恢复 fs 寄存器,而是把 fs 寄存器置为 0 。
kd> g
Break instruction exception - code 80000003 (first chance)
00401050 cc int 3
kd> p
00401051 bb10000000 mov ebx,10h
kd> p
00401056 891db8374200 mov dword ptr ds:[4237B8h],ebx
kd> p
0040105c cb retf
kd> p
001b:004010b2 33c0 xor eax,eax
kd> r fs
fs=00000000
kd> g
Break instruction exception - code 80000003 (first chance)
00401050 cc int 3
kd> p
00401051 bb10000000 mov ebx,10h
kd> p
00401056 891db8374200 mov dword ptr ds:[4237B8h],ebx
kd> p
0040105c cb retf
kd> r fs
fs=00000030
kd> p
001b:004010b2 33c0 xor eax,eax
kd> r fs
fs=00000000
因此在调用门提权前后需要手动保存和恢复 fs 寄存器的值。
__asm{
push fs
call fword ptr ds:[buf]
pop fs
}
另外,调用门不稳定的另一个原因是因为操作系统经常修改 GDT 表,这会导致构造出的门被操作系统修改造成错误,因此最好用 GDT 表中偏移 0x90 以上的元素构造门。
堆栈变化(含参数)
将 0x48 位置的门描述符的参数个数修改为 4 。
kd> dq gdtr
8003f000 00000000`00000000 00cf9b00`0000ffff
8003f010 00cf9300`0000ffff 00cffb00`0000ffff
8003f020 00cff300`0000ffff 80008b04`200020ab
8003f030 ffc093df`f0000001 0040f300`00000fff
8003f040 0000f200`0400ffff 0040ec00`0008100a
8003f050 80008955`23800068 80008955`23e80068
8003f060 00009302`2f40ffff 0000920b`80003fff
8003f070 ff0092ff`700003ff 80009a40`0000ffff
kd> eq 8003f048 0040ec04`0008100a
kd> dq gdtr
8003f000 00000000`00000000 00cf9b00`0000ffff
8003f010 00cf9300`0000ffff 00cffb00`0000ffff
8003f020 00cff300`0000ffff 80008b04`200020ab
8003f030 ffc093df`f0000001 0040f300`00000fff
8003f040 0000f200`0400ffff 0040ec04`0008100a
8003f050 80008955`23800068 80008955`23e80068
8003f060 00009302`2f40ffff 0000920b`80003fff
8003f070 ff0092ff`700003ff 80009a40`0000ffff
执行如下代码:
#include<stdio.h>
#include<stdlib.h>
int val = 0;
void __declspec(naked) test() {
__asm {
int 3
mov ebx, 0x10
mov val, ebx
retf 0x10
}
}
int main() {
unsigned char buf[] = {0,0,0,0,0x4B,0};
printf("%X\n",&test);
system("pause");
__asm{
push fs
push 1
push 2
push 3
push 4
call fword ptr ds:[buf]
pop fs
}
return 0;
}
成功在 test 函数断下来。
kd> g
00401050 cc int 3
kd> uf eip
00401050 cc int 3
00401051 bb10000000 mov ebx,10h
00401056 891db8374200 mov dword ptr ds:[4237B8h],ebx
0040105c ca1000 retf 10h
kd> dd esp
b0cacdc0 0040d73c 0000001b 00000004 00000003
b0cacdd0 00000002 00000001 0012ff18 00000023
b0cacde0 00000000 00000000 00000000 00000000
b0cacdf0 0000027f 7c930000 00000000 00000000
b0cace00 00000000 00000000 00001f80 23222120
b0cace10 27262524 00380178 00380188 00000002
b0cace20 37363534 3b3a3938 00380000 003823a0
b0cace30 003804e8 0012f8f0 00380178 0012f8f0
可以看出,调用门在传参后栈的结构为下图所示:
retf 指令后面跟的数字指定了参数的总字节数,从而确保返回时能用正确的平衡堆栈。
注意:retf 平衡的是用户态的堆栈,即回到用户态后将 esp 减去 参数个数
×
\times
× 4 ,使得进入调用门前压入用户态的堆栈的参数弹出,而内核态的堆栈的 esp 每次调用都利用 TSS 中保存的值初始化,因此返回用户态前不需要平衡堆栈。
权限检查
经过反复试验,总结出如下规律:
- 指令中 RPL 不参与整个过程
- 最终的 CPL 取决于代码段的 DPL
- CPL ≤ \le ≤ 门的 DPL,否则没有权限使用门
- 门的 RPL ≤ \le ≤ 代码段的 DPL,否则蓝屏
注意:上述规律可能会受环境不同的影响,比如第 4 条在我的环境中不成立
总结
- 当通过门,权限不变的时候,只会PUSH两个值:CS 和 返回地址,新的CS的值由调用门决定
- 当通过门,权限改变的时候,会PUSH四个值:SS、ESP、CS、返回地址,新的CS的值由调用门决定,新的SS和ESP由TSS提供
- 通过门调用时,要执行哪行代码由调用门决定;但使用RETF返回时,由堆栈中压入的值决定(只要改变堆栈里面的值就可以想去哪去哪)
- 可以在调用门中再建个门出去呢,也就是再用CALL出去
利用调用门在3环调0环函数
首先在 Windbg 查看要调用的 0 环函数 RtlInitAnsiString 的地址
kd> uf RtlInitAnsiString
nt!RtlInitAnsiString:
804da26f 57 push edi
804da270 8b7c240c mov edi,dword ptr [esp+0Ch]
804da274 8b542408 mov edx,dword ptr [esp+8]
804da278 c70200000000 mov dword ptr [edx],0
804da27e 897a04 mov dword ptr [edx+4],edi
804da281 0bff or edi,edi
804da283 741e je nt!RtlInitAnsiString+0x34 (804da2a3) Branch
nt!RtlInitAnsiString+0x16:
804da285 83c9ff or ecx,0FFFFFFFFh
804da288 33c0 xor eax,eax
804da28a f2ae repne scas byte ptr es:[edi]
804da28c f7d1 not ecx
804da28e 81f9ffff0000 cmp ecx,0FFFFh
804da294 7605 jbe nt!RtlInitAnsiString+0x2c (804da29b) Branch
nt!RtlInitAnsiString+0x27:
804da296 b9ffff0000 mov ecx,0FFFFh
nt!RtlInitAnsiString+0x2c:
804da29b 66894a02 mov word ptr [edx+2],cx
804da29f 49 dec ecx
804da2a0 66890a mov word ptr [edx],cx
nt!RtlInitAnsiString+0x34:
804da2a3 5f pop edi
804da2a4 c20800 ret 8
在 0x48 处构造调用门,然后执行如下代码
#include<stdlib.h>
typedef struct _STRING {
unsigned short Length;
unsigned short MaximumLength;
char* Buffer;
} STRING;
typedef void(__stdcall* RtlInitAnsiString)(STRING* DestinationString,const char* SourceString);
RtlInitAnsiString initStrFunction=(RtlInitAnsiString)0x804da26f;
STRING str;
const char* bufstr="123456789";
void __declspec(naked) test() {
__asm {
int 3
pushad
pushfd
push fs
mov ax,0x30
mov fs,ax
push bufstr
lea eax,str
push eax
call initStrFunction
pop fs
popfd
popad
retf
}
}
int main() {
unsigned char buf[]={0,0,0,0,0x48,0};
printf("%X\n",&test);
system("pause");
__asm{
push fs
pushad
pushfd
call fword ptr ds:[buf]
popfd
popad
pop fs
}
return 0;
}
在 test 函数中断下来后用 WinDbg 调试发现成功调用 RtlInitAnsiString 函数并返回 3 环。
kd> p
0040d3d9 ff15941b4200 call dword ptr ds:[421B94h]
kd> dd esp
b1609da0 004227c8 004200f0 00000030 00000202
b1609db0 0012ff80 0012f7bc 0012ff80 b1609dd0
b1609dc0 7ffde000 003808dc 00000000 00000000
b1609dd0 0040b4a6 0000001b 0012ff04 00000023
b1609de0 00000000 00000000 00000000 00000000
b1609df0 0000027f 7c930000 00000000 00000000
b1609e00 00000000 00000000 00001f80 23222120
b1609e10 27262524 00380178 00380188 00000002
kd> dt _STRING 004227c8
nt!_STRING
"" +0x000 Length : 0
+0x002 MaximumLength : 0
+0x004 Buffer : (null)
kd> p
0040d3df 0fa1 pop fs
kd> dt _STRING 004227c8
nt!_STRING
"123456789"
+0x000 Length : 9
+0x002 MaximumLength : 0xa
+0x004 Buffer : 0x004200f0 "123456789"
kd> p
0040d3e1 9d popfd
kd> p
0040d3e2 61 popad
kd> p
0040d3e3 cb retf
kd> p
001b:0040b4a6 9d popfd
kd> dt _STRING 004227c8
nt!_STRING
"123456789"
+0x000 Length : 9
+0x002 MaximumLength : 0xa
+0x004 Buffer : 0x004200f0 "123456789"