应用安全 --- 逆向工程 之 .eh_frame_hdr 和 .eh_frame
.eh_frame_hdr 和 .eh_frame 都是帮助程序“沿着函数调用关系往回找”的数据,但分工不同:
| 节 | 核心作用 | 通俗比喻 |
|---|---|---|
.eh_frame_hdr |
根据当前指令地址,快速找到对应的FDE | 书的目录 |
.eh_frame |
记录怎样恢复调用者的栈、寄存器和返回地址 | 书的正文/操作说明 |
它们主要用于:
- C++异常处理,例如
throw后寻找上层的catch - 生成崩溃调用栈
- 调试器回溯函数调用关系
backtrace()获取调用栈- 分析程序崩溃发生在哪一层函数
一、用一个最小函数理解
假设程序中有:
int add(int a, int b)
{return a + b;
}int main()
{int result = add(10, 20);
}
执行顺序是:
main└──调用 add
在汇编中,可能类似:
main:call add; call的下一条指令地址,就是add执行完后的返回地址add:push rbpmov rbp, rspsub rsp, 0x20; 函数主体leaveret
假设程序正在 add 中间执行时发生异常。
系统现在想知道:
是谁调用了add?
main的返回地址在哪里?
main原来的RSP是多少?
main原来的RBP是多少?
这时就会使用 .eh_frame_hdr 和 .eh_frame。
二、.eh_frame_hdr 的作用
.eh_frame_hdr 是一个快速查询表。
它大致记录:
函数起始地址 对应的FDE地址
--------------------------------
0x401000 0x402260
0x401100 0x402280
0x401243 0x402318
0x4013D3 0x402338
假设当前程序的指令地址是:
RIP = 0x401300
系统在目录中查找后发现:
0𝑥401243≤0𝑥401300<0𝑥4013𝐷30x401243≤0x401300<0x4013D3
所以当前正在执行:
sub_401243
目录同时告诉系统:
sub_401243的FDE位于0x402318
因此:
当前RIP = 0x401300↓
查找.eh_frame_hdr↓
找到函数入口0x401243↓
找到对应FDE:0x402318
.eh_frame_hdr 本身通常不记录怎样恢复栈,它只负责快速找到说明书。
三、.eh_frame 的作用
.eh_frame 保存真正的栈恢复规则,主要由两类记录组成:
| 记录 | 全称 | 作用 |
|---|---|---|
| CIE | Common Information Entry | 多个函数共同使用的基础规则 |
| FDE | Frame Description Entry | 某一个函数自己的栈变化规则 |
可以理解为:
.eh_frame
├── CIE:大家共同遵守的规则
├── FDE:函数A的规则
├── FDE:函数B的规则
├── FDE:sub_401243的规则
└── FDE:main的规则
四、CIE负责什么
CIE记录通用规则,例如:
栈每次移动的基本单位:8字节
返回地址对应的寄存器:RIP
函数刚进入时:CFA = RSP + 8
返回地址位置:[CFA - 8]
其中CFA可以暂时理解为:
调用当前函数之前,调用者原来的栈顶。
刚进入函数时,call 会把返回地址压入栈中。
假设调用前:
RSP = 0x1000
执行 call 后,压入8字节返回地址:
RSP = 0x0FF8
此时:
𝐶𝐹𝐴=𝑅𝑆𝑃+8CFA=RSP+8
代入:
𝐶𝐹𝐴=0𝑥0𝐹𝐹8+8=0𝑥1000CFA=0x0FF8+8=0x1000
所以CFA正好是调用者原来的栈顶。
返回地址位于:
[𝐶𝐹𝐴−8]=[0𝑥0𝐹𝐹8][CFA−8]=[0x0FF8]
五、FDE负责什么
每个函数修改栈的方式可能不同,所以每个函数有自己的FDE。
例如你的 sub_401243:
0x401243 endbr64
0x401247 push rbp
0x401248 mov rbp,rsp
0x40124B sub rsp,20h; 函数主体0x4013D1 leave
0x4013D2 ret
它对应的FDE位于:
0x402318
FDE描述的核心规则是:
刚进入函数:
CFA = RSP + 8执行push rbp后:
CFA = RSP + 16
旧RBP位于[CFA - 16]执行mov rbp,rsp后:
CFA = RBP + 16执行leave后:
CFA = RSP + 8
因此FDE相当于在说:
如果程序停在
sub_401243的不同位置,应该使用不同的公式寻找上一层函数。
六、具体恢复一次调用者
假设 main 中有:
0x401457 call sub_401243
0x40145C mov [rbp-4],eax
因为 call 的下一条指令是 0x40145C,所以:
sub_401243执行完后,应返回0x40145C
假设调用前:
main的RSP = 0x1000
main的RBP = 0x1100
1. 执行call
call 压入返回地址:
RSP = 0x0FF8
[0x0FF8] = 0x40145C
栈中变成:
0x1000 main原来的栈顶
0x0FF8 返回地址0x40145C
2. 执行push rbp
push rbp
得到:
RSP = 0x0FF0
[0x0FF0] = 0x1100
现在的栈:
高地址0x1000 调用者原来的栈顶,也就是CFA
0x0FF8 返回地址0x40145C
0x0FF0 main原来的RBP:0x1100低地址
3. 执行mov rbp,rsp
mov rbp,rsp
得到:
RBP = 0x0FF0
随后即使执行:
sub rsp,0x20
导致 rsp 继续下降,rbp 仍然保持 0x0FF0。
七、如果此时程序崩溃
假设程序在:
RIP = 0x401300
发生崩溃。
首先查询 .eh_frame_hdr:
0x401300属于sub_401243
sub_401243的FDE位于0x402318
然后读取 .eh_frame 中的FDE,得到:
𝐶𝐹𝐴=𝑅𝐵𝑃+16CFA=RBP+16
代入:
𝐶𝐹𝐴=0𝑥0𝐹𝐹0+0𝑥10=0𝑥1000CFA=0x0FF0+0x10=0x1000
恢复调用者的RBP:
𝑅𝐵𝑃调用者=[𝐶𝐹𝐴−16]RBP调用者=[CFA−16]𝑅𝐵𝑃调用者=[0𝑥0𝐹𝐹0]=0𝑥1100RBP调用者=[0x0FF0]=0x1100
恢复调用者的RIP:
