pwn | Exploiting DWARF | 异常处理的DWARF相关利用姿势
pwn | Exploiting DWARF | 异常处理的DWARF相关利用姿势
突然想到了,就深入研究一下这块儿。
丢一个原文PPT,是2011年国外的研究:https://cs.dartmouth.edu/~sergey/battleaxe/hackito_2011_oakley_bratus.pdf
https://cs.dartmouth.edu/~sergey/battleaxe/
边学边记录的,写的比较乱,翻译的不对的地方还希望指正。
同时的参考文章还有:
https://bbs.kanxue.com/thread-271891.htm
https://zhuanlan.zhihu.com/p/302726082 (这个是中文的,对异常处理栈&eh_frame讲的很清晰的一个文章)
总述
所有的GCC编译的二进制都支持使用DWARF进行异常处理,二进制文件中会有一个DWARF字节码解释器,且几乎可以执行所有指令。
DWARF的主要作用:
- 描述栈帧
- 在异常发生时解释执行,恢复栈帧
通过这种方式可以执行代码,并且不会被杀毒软件检测。
题外话,PPT真的很有意思(所以推荐直接看ppt)【矮人->黄金哈哈哈哈哈哈】:
ELF and DWARF
DWARF(Debugging With Attributed Records Format)
http://dwarfstd.org/
ELF调试相关的节,都使用了DWARF格式的信息。
而gcc以及x86_64的ABI使用了.eh_frame
段去描述在异常发生时如何恢复栈帧。
eh_frame
从概念上来说,这个段代表了一个表格,对于每个text段中的地址而言如何去设置寄存器来保存前一个call的栈帧。
值得说明一下的是,这个段的查找应该是根据.eh_frame_hdr
节来查找的(没有验证过),我自己分析的结果如下:
另外,在手动分析eh_frame的时候,我发现似乎不同版本的gcc编译出来的是有差别的,应该使用readelf工具服务数据结构,这样会比较准确(readelf --debug-dump=frames [file])。
eh_frame的表结构示例:
概念解释:
CFA(Canonical Frame Address): 规范栈帧地址。Address other address within the call frame can be relative to.
它的值是在执行(不是执行完)当前函数(callee)的caller的call指令时的RSP值, 例子如下:
caller:
push arg1 --> RSP = 0xFFF8
push arg2 --> RSP = 0xFFF0 (执行call指令时的RSP值在这
call callee --> RSP = 0xFFE8
callee:
push rbp --> CFA = 0xFFF0
在上面的表格中,每一行表示了当前的代码位置如何回溯到之前的栈帧。
如果是按上面的表格来描述的话,会有一个非常大的表格,并不方便,因此才有了DWARF/eh_frame来进行数据的压缩。
接下来是eh_frame的两个重要数据结构。
每个.eh_frame
section 包含一个或多个CFI(Call Frame Information)
记录,记录的条目数量由.eh_frame 段大小决定。每条CFI
记录包含一个CIE(Common Information Entry Record)
记录,每个CIE
包含一个或者多个FDE(Frame Description Entry)
记录:
通常情况下,CIE对应一个文件,FDE对应一个函数。
(更具体一点的可以看这个文档, 可以适当参考):https://refspecs.linuxfoundation.org/LSB_5.0.0/LSB-Core-generic/LSB-Core-generic/ehframechpt.html)
可以注意到,FDE中存在一个CIE_pointer,指向一个CIE结构。
也就是在FDE中,存在DWARF字节码。
下面是我手动分析的CIE和FDE数据结构,以及相对应的readelf解析结果:
手动分析:
readelf读取:
DWARF字节码
DWARF执行的基础是一个栈虚拟机。
它的执行方式就像是一个RPN calculator(逆波兰式计算)
可以访问内存以及修改寄存器。
PPT中也提到了一些使用限制,比如说gcc(4.5.2)版本限制栈空间在64字以下。
有一下几个指令示例:
有一些关键数据结构(还是上面提到的CIE和FDE):
可以用如下指令读取elf中的.eh_frame段(test是我编译的二进制文件名):
也可以用readelf -wF查看相关的信息:
这个eh_frame意味着什么?
本质山意味着,在抛出异常的时候,可以回溯到上一个栈帧,将所有用到的寄存器给还原回去,这也是异常处理的目的(我想应该是这样子的OvO)
PPT中提到了可以使用katana工具来编辑dwarf字节码:
我们可以通过上面的工具来修改CIE和FDE的结构,以及DWARF字节码。
.gcc_except_table
另一个关键的段就是.gcc_except_table。
我在有一篇文章中看到了一个形象的描述:要知道着陆垫在哪里,要使用称为gcc_except_table的东西。
https://blog.csdn.net/wuhui_gdnt/article/details/88737310
Exception Handling Flow 异常处理流
给了一张非常详细的图:
并提出了两个问题:
- libgcc是怎么知道要如何unwind的(恢复现场)
- 一个异常处理是怎么被识别的?
实验
我做出来了哈哈哈哈哈,成功通过dwarf字节码控制了执行,其实里面还是有没搞清楚的东西,之后有机会补上:
我的操作步骤:
- 编写cpp代码,留一个异常
- 使用脚本编译dwarf字节码(使用的是开头那个看雪文章里大师傅的脚本)
- 将cpp编译为.s汇编
- 在抛出异常的地方插入dwarf字节码
- 编译.s文件为二进制文件
- 对二进制文件最后进行一些需求上的patch
本文来自博客园,作者:Mz1,转载请注明原文链接:https://www.cnblogs.com/Mz1-rc/p/17392370.html
如果有问题可以在下方评论或者email:mzi_mzi@163.com