前一篇中提到CALL FAR指令最终跳转的地址是调用门里,今天就要分析一下调用门。首先从调用门的执行流程开始
调用门执行流程
指令格式:CALL CS:EIP(EIP是废弃的)
执行步骤:
- 根据CS的值查GDT表,找到对应的段描述符,这个描述符是一个调用门。
- 在调用门描述符中存储另一个代码段的选择子
- 选择子指向的段的Base+调用门里的Offset,就是真正要执行的地址
光看描述,的确很难懂,结合调用门描述符来分析,会好理解很多
调用门描述符
- 高4字节8~15位:这是和普通段描述符完全一样的8位,其中第12位是判断该描述符是系统段还是数据段或代码段的位置,调用门描述符是系统段,所以此处值一定是0。接下来的Type域,这个根据段描述符那章中也能找到,32位的调用门描述符,Type域为1100,这也是确定的。
- 高4字节的高16位+低4字节的低16位:这两块区域加起来刚好是32位,构成一个Offset,也就是调用门执行流程第三步里Base加上的Offset,那么Base哪里拿呢?
- 低4字节的高16位:这16位是一个段选择子,有段选择子,就可以拆分,于是RPL,TI,Index都能解析出来,这时候就可以根据Index去GDT表里找到段描述符,而这个段,就是调用门跳转的段,因此要用这个段的Base+Offset便可获得真正要执行的地址。
- 高4字节的低5位(第5~7位均为0):这5位的作用是描述调用门传进去的参数,调用门是可以传参的,而参数的个数,决定了这个位置的值
下面,通过代码来验证调用门的执行流程。
无参调用门
调用门分为无参和有参(示例默认都提权)两种情况,这里先用无参调用门进行实验:
构造调用门
因为Windows是不使用调用门的,所以需要自己构造一个调用门:0040EC00 00081010
为什么要这样构造呢?先看最熟悉的那8位,EC = 11101100,P=1,S=0,调用门=1100,DPL为啥选取3呢。首先,调用门的提权在于通过调用门后,新的段选择子会修改CS达到CPL的提权,但是访问调用门描述符还是需要保证CPL=DPL,因此,DPL需要设置为3。由于无参调用门,也就没参数,因此参数位为0,EC00也就解释清了。
接下来看0008,这个也很好理解,段选择子嘛,拆分一下,RPL=0,Index=1,我们去Windbg里看一下就好了
这下就清晰了,指向00cf9b00 0000ffff这个段描述符,拆分一下,Limit=FFFFFFFF,G=1,Base=00000000,是个非一致代码段。看来想要跳转成功,也得是0环的代码段才行。
由于Base为0,那么跳转到的地方就是0+401010,那这个401010是哪来的呢?这得看代码才能说清。
代码实现
1 |
|
来看一下代码,从main函数开始看起,我们自己构造一个CS:EIP(EIP已废弃)的6字节char型数组,然后在汇编中执行CALL FAR调用我们构造的CS:EIP,接着打印部分内容。
可以看出执行调用门的语句嵌入了汇编里,根据上方在构造调用门时的分析可以得出,最终调用门跳转的地址会是401010,那么这个401010是怎么来的呢?其实就是GetValue函数的地址,我们知道通过调用门后会跳转到一个地址,但是如何才能检测成功跳转并提权呢?就得有一个函数来收集这些信息并将其打印出来,也就有了GetValue函数(GetValue地址通过VC下断点查看,然后写入构造的调用门描述符中)GetValue要声明成裸函数,这样堆栈只需自己平衡,可以避免元素访问的位置过远。
根据前一篇文章的内容,如果跨段并提权,堆栈内部大致情况如下
因此,这里采用通过全局变量,来依次读取堆栈不同位置的值,并打印查看结果。
由结果看,ESP,CS,SS均发生了切换,且CPL变为0,ESP的值也变为了一个高2G的数
为了验证结果的正确性,我们可以通过中断再看一下0环的堆栈结构,将裸函数中的汇编代码清除,只留下int 3和长返回语句如下:
1 | _asm { |
然后重新执行,会中断到Windbg(为什么会从虚拟机中断到Windbg,这个到后面中断部分会详细讲解)查看栈顶部分内存
注意:这里的栈顶esp的值和刚刚不一样是因为程序重新执行了,进入0环时,ESP和SS是TSS给的,而TSS内的值是当前线程给的,因为代码修改了,所以重新执行程序时,线程不一样了,所以0环的堆栈也就不一样了。但是3环的数据是没有变的(理论上也是会变的,这里没变是因为编译器的优化),对比刚刚手动读取的结果来看,3环的ESP,CS,SS完全一致,说明刚刚调用门的实验成功提权进入0环。
有参调用门
无参说完了,接下来是有参调用门,有参调用们和无参的区别在于仅仅是参数位的值会有所变动,栈里多了push进去的参数,其余和无参基本上相同。
构造调用门
这里构造的调用门描述符是:0040EC03 00081020,(地址变成了401020,是因为我重启了下虚拟机所以GetValue函数地址变了,并不影响),需要注意的是参数位设置成了3,因为这次计划传入3个参数进去。
代码实现
由于Push了参数进去,所以不确定参数在0环堆栈中位于什么位置,于是先用int 3的方法,在Windbg中查看一下堆栈的情况
图中可以发现,在压入参数的调用门进入0环后,参数位于3环ESP和3环CS中间的位置,堆栈表示大致如下:
这样,就可以来编写代码了,总体和无参的差距不大。
1 |
|
需要注意一点,这里由于push了3个参数,所以长返回的时候要用RETF 0xc来平衡堆栈,否则直接中断到Windbg,要是不处理的话就蓝屏了。
代码执行效果如下,成功在0环堆栈取到了push进入的参数。
总结
- 当通过门,权限不变时,只会PUSH两个值:CS和返回地址,新的CS的值由调用门决定
- 当通过门,权限改变的时候,会PUSH四个值:SS,ESP,CS,返回地址,新的CS由调用门决定,新的SS和ESP由TSS提供
- 通过门调用时,要执行哪行代码由调用门决定,但使用RETF返回时,由堆栈中压入的值决定,也就是说,进门时,只能按指定路线走,出门时,可以翻墙(只要改变堆栈里面的值就可以想去哪去哪)
- 可不可以再建个门出去呢?也就是用Call,当然可以。