call指令
call指令其实分为call 和 call far
平时我们看见最多的是call指令,而不是call far,call far指令很复杂,这里也只是介绍其中一种用法
call的执行
回顾一下以前学的call(短调用)干了什么
//call 地址
push 返回地址(call指令的下一行地址)
mov EIP, 地址
--------------
//ret
pop EIP
call 有着另一种形态,长得和jmp far一模一样,call CS:EIP
,我们称之为长调用(call far)
这里的 CS只是一个偏移,不代表最后放入CS寄存器的值
那么call far又是怎么执行的呢
简单了解call far
call CS:EIP(这里的EIP不会被使用到,写什么都可以)
如果将call far写成简单易懂的汇编指令:
- 跨段不提权
//call 段选择子:地址
push CS
push 返回地址(call far指令的下一行地址)
//将 段选择(selector)子 分解,用index查GDT表(TI == 0)
//门描述符中有一段描述的是EIP填什么(offset in segment)
mov EIP, GDT[index].offset
- 跨段提权
push SS //多出了这个
push ESP //和多出了这个
push CS
push 返回地址
mov CS, GDT[index].selector
mov EIP, GDT[index].offset
需要注意的是:这里会将SS:ESP也存入堆栈,这是因为堆栈将要换成其他权限时使用的堆栈,所以保存起来,以便ret的时候能够恢复原来的堆栈
windows并没有使用调用门
如何使用call far
call CS:EIP会使用CS作为GDT表的偏移
使用dq gdtr就能查询到GDT表了
GDT的首地址 + CS(偏移)就是我们要找的调用门描述符地址了
调用门描述符
在了解call far 之前首先要了解什么是调用门,因为跨段提权的call far是通过调用门来进行跨段跳转的
首先我们可以看到原来的S(system)位直接被指定成了0,这也就是说门描述符其实是系统描述符的其中之一
type被指定成1100,这个type = 1100表示这是个门描述符,定义就是如此,记住就行
- P位依旧表示这个描述符是否有效
- DLP位访问这个段需要的特权级别
- Selector表示真正要调用的代码所在的段,也就是说这个选择子等会儿会被写入CS寄存器中
- Offset in Segment所表示的就是真正要执行的代码存放在的位置(GDT[Selector.index].base + Offset 才是等会儿所要执行的代码的位置)
- Param Count 是参数的个数,至于左边那3个0就是保留位咯
手动构造call far
好了,了解了调用门之后,我们就可以手动构造调用门了
首先要获得我们等会儿要跳转的地址是什么,所以先写个小程序等会儿测试用
#include <windows.h>
//naked是告诉编译器这段我自己写(堆栈自己处理),不要加额外的代码,是真正的空白函数
void __declspec(naked) func{
_asm{
int 3
retf
}
}
int main(){
BYTE addrs[6] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };//暂时不知道CS,先写0
//打印地址,填入自己构造的调用门中,以实现跳转到自己写的函数里
printf("function addrs: %x", func);
_asm{
call fword ptr[addrs]
}
return 0;
}
运行,先看函数地址,等会填描述符要用
可以看到这个函数的地址就是等会儿我们要跳过去的位置,当然也可以不是一个函数,这个稍后再演示
构造出一个调用门描述符:0040 E C 00 ` 0008 100a
加粗的是将要跳转的地址;斜体是1 11 0 1100;黄色部分就是一个0环的段选择子,因为调用门存在的意义就是为了提权,不提权辛辛苦苦整这东西干啥,所以我们这里直接写成提权的
我们将构造好的调用门写在一个空白的地方。
记住我们选中的空白位置的偏移(0x8003f048 - 0x8003f000 = 0x48),用来填充 call CS:EIP
中的 CS
上面写那个小程序的时候我已经把这个偏移量写到一个BYTE里了,addrs = {00,00,00,00,0x48,00},这个要逐个倒过来写。0x0048就是等会儿要用来call的值,前面那四个随便写就行,因为指令不会使用前四个值。
g命令继续执行后,再在小程序里继续执行,就会发现,程序停在windbg里了,因为int 3这个东西会优先被windbg调试器拦截下来(稍微了解就行)
这里写的 jmp 00401020
,跳转后就是我们写的int3语句的位置了
首先观察寄存器的变化:
对比一下前后的堆栈,还有CS寄存器的值,都发生了巨大的变化
新的堆栈里也有着奇妙的事情,切换后的堆栈的值恰好存放了SS ESP CS EIP,这是为retf准备的
至于retf是什么,可以看看这个RET指令的不同
使用g命令继续执行,就能跳回来啦~
总结
- jmp far 只能跳转到 相同权限的地方 和 不同权限的一致代码段
- call far 可以调用相同权限的地方(这个可以自己动手试试,并观察堆栈变化),也可以通过调用门进行提升 当前权限 (CPL)
- 权限切换,堆栈也会切换,且CS与SS的权限一定是相同级别的
补充:带参数的调用门
如果对带参数的调用门带有一定的好奇那可以接着往下看
之前提到过,调用门会在跳转时发生堆栈变化。
那么会发生怎样的变化呢?
将之前的调用门的参数位改为3,表示我们将要传3个参数,其实多传几个也行
int main(){
BYTE addrs[6] = {0x00, 0x00, 0x00, 0x00, 0x48, 0x00 };
printf("function addrs: %x", func);
-asm{
push 0x11111111
push 0x22222222
push 0x33333333
call fword ptr[addrs]
}
return 0;
}
这里main里传入三个参数再调用call far,随后观看堆栈
可以看到参数被复制到了0环堆栈,ESP因为多了几条push所以有所变化
记得注意堆栈平衡,否则会蓝屏,蓝屏!!!