pwn | Exploiting DWARF | 异常处理的DWARF相关利用姿势

Mz1 / 2023-05-14 / 原文

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的栈帧。

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对应一个函数。

可以注意到,FDE中存在一个CIE_pointer,指向一个CIE结构。
也就是在FDE中,存在DWARF字节码。

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. 一个异常处理是怎么被识别的?

【之后补一个实验,主要是还没搞太清楚,所以没办法弄呜呜呜太菜了】