作者:吉林小伙
链接:http://zhuanlan.zhihu.com/p/20642841
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
在linux下,栈越界写坏返回地址会导致调用栈无法回溯,这就导致我们直接使用bt没有办法查看崩溃时调用栈,今天我讲一下我最近研究出来的一种方法(虽然是原创,但可能互联网上早有人发布过此种方法,只不过我没有查到而已).
废话少说,步入正题,首先我写了个简单的程序来构造一个栈溢出的情况,为了使效果更加明显,我使用了一些递归来增加调用栈的深度,代码如下:
不要吐槽命名方式,我也知道很丑,栈都能越界的程序,一定漂亮不到哪去,哈哈!
简单描述下这个代码的功能,func2里递归调用了func2,这样能有效增加调用栈深度,然后调用func3的时候,由于func3里写buf越界了,导致栈被破坏了,然后段错误崩溃.崩溃后生成了core文件,我们用gdb打开,输入bt,效果如下:
由于栈破坏有很多种方式,bt也有可能显示出一排??,总之栈破坏很有可能导致bt无法回溯就是了.那我们如何应对呢?我们首先来看一下调用栈的一些知识:
一般情况下,在调用函数之前,(部分)参数会放入栈内,然后执行汇编指令call, 执行call后会自动将返回地址压入栈中,然后执行被调用函数,被调用函数开头的两条汇编指令很可能是:
这两条指令的作用就是把rbp压入栈中,把栈顶指针rsp赋值给rbp,这样在栈内就会形成一个 链表,以便我们回溯调用栈.push rbp mov rbp,rsp
注意:
在开启优化的时候,默认是 -fomit-frame-pointer,这样可能导致很多函数开头不会出现那两条汇编指令,-fno-omit-frame-pointer选项开启后就会生成以上两条指令.
好了,详细的栈资料请大家自行查阅资料学习,我就不再赘述了.你只要知道调用栈在栈内是以链表的方式保存即可.那栈越界写坏的地方我们可以认为是链表的头部,由于链表的头部被写坏了,导致gdb的bt指令无法回溯调用栈了.
既然如此,那我们可以再找一个节点作为链表的头部.只不过回溯的调用栈可能比"完美"的调用栈少那么一两条,不过半个面包总比没有好,说干就干:
上图的代码是gdb的扩展,我扩展了一个bts(backtrace stack)指令,其作用是打印给定addr后的count条内容.gdb中可以使用source指令来加载扩展,也可以在home路径下新建.gdbinit文件,将脚本内容写入,这样在gdb启动时就会自动生效,我们使用source .gdbinit来加载一个这个bts扩展指令.然后在gdb里输入i r rsp指令:
,rsp的值一般情况下是栈顶,不过不排除这个值是不对的,只是不对的概率比较小而已,然后键入指令:
(gdb) set pagination off
(gdb) set loggin on ./log
Copying output to ./log.
(gdb)bts 0x7fff81f7c250 1000
第一条指令的作用是关闭"超过一屏内容等待键入回车"功能,第二条是打开log,这样gdb里的输出就会写入log文件内,第三条就是我们写的扩展指令了,执行一下,等待结果写入到文件中吧.由于我们的测试程序很短,栈也没用多大,所以1000应该就可以了,实际程序中这个1000可能要变得很大,可能要跑几分钟,不过我很享受这几分钟,因为我就喜欢敲入一条命令然后屏幕刷刷滚的感觉,逼格高,哈哈!!
跑题了,好了,输出结束我们去看看./log文件,执行head ./log命令,结果如下:
这正是我们想要的内容,第一列是栈地址,第二列是该地址的内容,既然调用栈在栈内是链表,那我们就可以写个代码把栈内所有的链表都暴力搜出来,然后看下哪个最可能是调用栈.
这是我写的暴力搜索栈中链表的程序,其实大家完全可以用脚本写,比c++方便多了.好了,g++ stack.cpp -o stack编译一下,然后执行:
./stack log 100 > symbol
log就是我们的log文件了,100呢是调用栈的深度,当你指定100的时候,会把所有调用栈深度为100的链表打印出来,由于我们递归超过100次了,所以这里我就指定100了,如果你在实际应用中,100没有结果,那可以尝试逐渐减小这个数值,然后我们看看symbol里的内容:
大概是这样的,还有好多条,我只截取了部分,之所以有info symbol,是因为我要在gdb中加载这个symbol文件,加载后会自动执行info symbol address,作用就是打印出这条地址附近的符号:
(gdb)source ./symbol
此处应该有掌声!!!
从下往上看,依次是__libc_start_main()->main()->func1()->func2()...,这的确是我们程序中的调用栈,只不过丢失了func3而已.
最后总结一下,此片专栏只是提供一种解决方法而已,具体能否成功,要看运气了.我一直觉得调试找bug是要看运气的,尤其是那些偶然出现的crash,在你不知道原因无法重现时只能从core dump里寻找蛛丝马迹了.
经验:一般栈越界很有可能是字符串越界,此时可以查看rsp附近的内存,说不定有很明显的字符串,一下就定位问题了呢.
由于水平有限,文章中有错误在所难免,请大家包含并指教,谢谢!
-----------------------------------------------
更新一下暴力搜索调用栈的那个代码,其中有一处bug可能导致在遍历调用栈的时候死循环.更新后如上图所示,这样就不怕栈里有回路了,还有一处修改,这个地方用sizeof(void*),就兼容32bit 64bit的程序了,以前写的8只支持64bit的程序