如何突破C++的指针保护

C++对C语言虽然是能够向后兼容,但由于C++在C语言的基础上增加了不少功能,所以这个“向后兼容”有时并不是那么完美,其中一个最典型的例子就是内存保护。C++采用了比C语言更加严格的内存保护机制,指针在C语言中是可以在赋值时完成隐式类型转换的,也就是说用户可以把一个整型直接赋值给一个指针以控制指针指向的地址:

但是如果把这段代码放在C++程序中编译器会毫不留情地抛出一个error,告诉你从int到*int的隐式转换是非法的。也就是说在C++中,指针是无法被直接赋值一个地址的,C++中指针只能通过取地址运算赋值。内存保护更严格对程序员来说固然是一件好事,那么问题就来了,C++的这种指针保护能否被绕过呢?OK,这个当然可以~~至少在Intel的x86和x86_64架构上是可以的。

0x00 先做一些基本说明:

我在整个过程中用到的demo程序长这样:

这个程序无论使用C++编译器还是C语言编译器编译都没有问题,程序本身不违背任何一种的语法规则。但是这个程序实际跑起来会比较有点意思,因为函数f2里面ptr这个指针始终没有赋值,按常规的理解,这个未赋值的ptr应该有一个随机值,接下来把这个ptr的值打印出来,目的是看看这个随机值到底是多少。在编译过程中我没有使用任何额外优化选项,因为这样可能会给目标代码带来不确定因素,而且会受编译器版本,实际平台和代码的影响。

0x01 GCC下编译为64位程序:

GCC的版本是gcc (Ubuntu 5.4.0-6ubuntu1~16.04.4) 5.4.0 20160609,在Ubuntu下编译,文件保存为main.cpp:

8589934593这个值貌似没啥特别的,感觉就像是随机从内存里面读取的一个值一样,不过把它转换为16进制端倪就出来了,8589934593=0x0000000200000001 ~~~对,就是在函数f1里面两个变量a=0x00000001和b=0x00000002拼接起来得到的结果。那么这种情况到底是巧合还是必然呢?(PS:在x86_64里面指针的宽度是64bit,也就是8个字节,而int类型依然兼容于x86下的4字节)

现在把源程序的汇编代码弄出来看看吧:

去掉部分宏之后是这个样子:

其实玄机已经可以看出来了,不过为了更直观,还是调试一波。本人吐槽gdb的哑终端操作风格已经很久了,就我看来基本上等同于反人类,所以这次不用gdb,取而代之是一款山寨Ollydbg的edb:https://github.com/eteran/edb-debugger,总体来说虽然这款edb功能虽然比较简陋,但至少比gdb好操作一些。

OK,把edb跑起来,跟踪进入f1函数:

screenshot-from-2016-11-29-16-51-12

此时RBP的值:

screenshot-from-2016-11-29-18-52-44

再顺着这个RBP看看栈里面的情况:

screenshot-from-2016-11-29-19-08-47

灰色数据表示栈外部,栈内部最低位的就是RSP,此时RSP与RBP相等。这里编译器并没有通过自减RSP的方式给f1函数的变量开辟空间,这是因为f1中两个变量并没有进一步的运算操作,但程序还是把0x00000001和0x00000002两个变量存在了栈里面,PS:原谅我手画的箭头 ( ̄▽ ̄”)。

接着跟进,跳出f1后进入f2:

screenshot-from-2016-11-29-19-15-23

再看看RBP:

screenshot-from-2016-11-29-19-16-39

可以发现这两个函数的RBP是相同的,而接下来程序将[RBP-8]作为了ptr的值,而如果对比一下两个函数的变量操作就能发现问题了:

screenshot-from-2016-11-29-19-19-32

这里两个函数不同的本地变量实际上是存在于同一地址,因为小端存储的关系,最终ptr的值就是0x0000000200000001 !而以十进制打印出来就是8589934593。实际上这里就已经成功绕开了C++的指针保护,把一个任意的整型数赋值给了一个指针,从而可以让指针指向该用户空间的任意一个内存地址!

0x02 GCC下编译为32位程序:

要编译为32位程序需要对源码做一点改动,因为32位下指针的宽度只有4字节,所以最终的打印不能以long为单位,而应该以int为单位。这里只变f2函数就好:

可以预测一下结果会是什么,以上面64位的结论类推,GCC下本地变量入栈的顺序与声明顺序相反,那么输出应该是f1中变量b的值,也就是2对吧~ 现在跑一下看看:

呵呵~~结果并不是 ( ̄▽ ̄”)。为啥呢?请看反汇编的结果:

screenshot-from-2016-11-29-19-43-19

对比之前64位的情况来看应该就会发现32位下的诡异之处(PS:额。。。其实按常理来说64位下的情况才是诡异)。这里由调用关系可以看出来函数f1和函数f2的EBP寄存器值也依然是相同的。但是函数f2在为ptr分配空间的时候跳过了f1已经用过的空间,f1使用了[EBP-8]和[EBP-4]两个DWORD空间,而f2中ptr直接去使用了[EBP-0xc],刚好将f1用过的两个DWORD跳过。那么输出的这个134513851是在那里设置的呢。OK,跟进函数f1,在把两个变量入栈之后检查一下栈里面的情况:

screenshot-from-2016-11-29-19-56-29

那个0x080484bb就是最终输出的134513851,它是一个initial代码中的返回地址。好吧~至此已经知道GCC在编译Intel x86程序的时候会有这种目测应该是刻意保护的机制,导致绕过失败。

0x03 CL下编译为32位程序:

接下来看看在Windows下的表现,我的环境是Windows 7 SP1 64bit,编译器是Visual Studio 2010 Express自带的cl编译器,版本16.00.30319.01。因为Express版本的VS没有64位编译器,所以Windows下我就只测试了32位程序。代码依旧是GCC下编译32位程序所用的代码:

这里警告了ptr没有初始化,不过对编译并没有影响,忽略之~ 运行:

这里又得到了一个可控的“随机”结果,ptr的值等于函数f1里面的第一个变量,接下来按惯例,调试一下,这里我用的是Ollydbg。和之前Linux下的例子一样,跟进f1:

screenshot-from-2016-11-29-20-41-20

栈里面可以看到已经入栈的本地变量a=1和b=2:

screenshot-from-2016-11-29-20-41-36

接下来跟进f2:

screenshot-from-2016-11-29-20-47-24

好了,出问题了,cl编译器不知怎么的在函数开始后除了保护EBP之外还压入了ECX (°□°;)。不过恰好ECX此时的值也是1,所以并没有影响我们设想的运行结果。

screenshot-from-2016-11-29-20-51-45
screenshot-from-2016-11-29-20-47-34

实际上刚才的实验只是一次巧合而已,为了避免入栈的ECX带来的影响,我们可以把f2函数再改一下:

再次进入f2:

screenshot-from-2016-11-29-21-00-19

额。。。这次又不知道怎么的,编译器不压入ECX了,不过也刚刚好,让ptr的地址落在f1中变量a的位置上:

screenshot-from-2016-11-29-21-02-05

这样,就成功通过f1中的变量a控制了f2中ptr的值,成功绕过!

0x04 总结一个:

这种绕过指针保护的思路本质上是利用了栈帧切换的规则:函数返回后栈顶指针寄存器也会还原,从而利用EBP或者RBP寄存器在连续几次函数调用过程中的相对差值来控制某个函数内部悬空指针的值。当然,在上面演示的例子中,EBP或者RBP寄存器的值在两个函数调用的过程中都没有变化,这是因为我的例子中的两个函数都没有传入参数,如果有传入参数那么EBP/RBP的值就可能会不同,比如下面这个例子:

比起之前的例子,稍微变了一点,两个函数都有传入参数,这样的话两个函数内部RBP的值就会不一样了。但是两个RBP的相对差值依然是不变的,还是可以在这个程序的基础上再改动一点,从而控制ptr的值。从避免这种绕过的角度上来说GCC的32位模式有考解决方案,就是跳过上一个函数用过的区域,但是GCC的64位模式和Windows下的CL并没有考虑这个问题。

好了,就说这么多,纯粹的脑洞使然罢了 (´・ω・)ノ

Leave a Reply

Your email address will not be published. Required fields are marked *