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的主要作用:

  1. 描述栈帧
  2. 在异常发生时解释执行,恢复栈帧

通过这种方式可以执行代码,并且不会被杀毒软件检测。

题外话,PPT真的很有意思(所以推荐直接看ppt)【矮人->黄金哈哈哈哈哈哈】:
image

ELF and DWARF

DWARF(Debugging With Attributed Records Format)
http://dwarfstd.org/

ELF调试相关的节,都使用了DWARF格式的信息。
image

而gcc以及x86_64的ABI使用了.eh_frame段去描述在异常发生时如何恢复栈帧。

eh_frame

从概念上来说,这个段代表了一个表格,对于每个text段中的地址而言如何去设置寄存器来保存前一个call的栈帧。
值得说明一下的是,这个段的查找应该是根据.eh_frame_hdr节来查找的(没有验证过),我自己分析的结果如下:
image
另外,在手动分析eh_frame的时候,我发现似乎不同版本的gcc编译出来的是有差别的,应该使用readelf工具服务数据结构,这样会比较准确(readelf --debug-dump=frames [file])。

eh_frame的表结构示例:
image

概念解释:
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)记录:
image

通常情况下,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解析结果:
手动分析:
image

readelf读取:
image

DWARF字节码

DWARF执行的基础是一个栈虚拟机。
它的执行方式就像是一个RPN calculator(逆波兰式计算)
可以访问内存以及修改寄存器。

PPT中也提到了一些使用限制,比如说gcc(4.5.2)版本限制栈空间在64字以下。

有一下几个指令示例:
image

有一些关键数据结构(还是上面提到的CIE和FDE):
image

可以用如下指令读取elf中的.eh_frame段(test是我编译的二进制文件名):
image

也可以用readelf -wF查看相关的信息:
image

这个eh_frame意味着什么?
本质山意味着,在抛出异常的时候,可以回溯到上一个栈帧,将所有用到的寄存器给还原回去,这也是异常处理的目的(我想应该是这样子的OvO)

PPT中提到了可以使用katana工具来编辑dwarf字节码:
image

image

我们可以通过上面的工具来修改CIE和FDE的结构,以及DWARF字节码。

.gcc_except_table

另一个关键的段就是.gcc_except_table。
我在有一篇文章中看到了一个形象的描述:要知道着陆垫在哪里,要使用称为gcc_except_table的东西。 https://blog.csdn.net/wuhui_gdnt/article/details/88737310

Exception Handling Flow 异常处理流

给了一张非常详细的图:
image

并提出了两个问题:

  1. libgcc是怎么知道要如何unwind的(恢复现场)
  2. 一个异常处理是怎么被识别的?

实验

我做出来了哈哈哈哈哈,成功通过dwarf字节码控制了执行,其实里面还是有没搞清楚的东西,之后有机会补上:
image

我的操作步骤:

  1. 编写cpp代码,留一个异常
  2. 使用脚本编译dwarf字节码(使用的是开头那个看雪文章里大师傅的脚本)
  3. 将cpp编译为.s汇编
  4. 在抛出异常的地方插入dwarf字节码
  5. 编译.s文件为二进制文件
  6. 对二进制文件最后进行一些需求上的patch
posted @ 2023-05-13 21:53  Mz1  阅读(586)  评论(0)    收藏  举报