高libc版本下fini_array劫持探索
在网上偶然发现一种比较底层的控制程序流方法:__libc_csu_fini劫持
利用条件是fini_array为可写状态。利用readelf -S ./pwn | grep .fini_array命令可以查看fini_array位置
然而在我利用本地libc运行demo尝试复现时发现:__libc_start_main函数貌似与题目中不太一样,甚至已经找不到__libc_csu_fini函数了,那么这个利用方法到底能否用于高libc版本的程序呢?我打算一探究竟。
首先过一遍c程序的执行流程:
start函数(text段的起点)调用了libc_start_main
在libc_start_main中调用__libc_start_call_main
__libc_start_call_main调用了main函数
返回__libc_start_call_main中并调用exit退出。
在GLIBC 3.35下,我发现在exit中__libc_csu_fini函数不见了,而取而代之实现对应功能的是(_dl)_call_fini函数
梳理一下libc_start_main流程(动态链接):
__libc_start_main
├── 安全初始化
├── 线程局部存储(TLS)初始化
├── 堆栈保护设置
├── 动态链接器初始化
├── 环境变量设置
├── 调用全局构造函数
│ ├──_init()
│ ├──frame_dummy()[异常处理框架]
│ └──.init_array中的所有函数
↓__libc_start_call_main(GLIBC 2.34+)
│ ↓
│ main() [用户程序]
│ ↓
│ exit()
│ ├── 最后在_dl_call_fini函数内遍历调用fini_array数组
注:在静态链接的程序exit()最后调用到call_fini()
这里需要详细解释一下exit的流程:
exit(status)
↓ 调用__run_exit_handlers(status, &__exit_funcs, true, true)
↓调用
_dl_fini() 进行动态链接器清理
↓函数执行过程
↓遍历linkmap生成模块列表
↓_dl_sort_maps(_dl_loaded, _dl_nloaded, NULL) ← 为模块排序
↓遍历排序后的模块列表
↓对每个模块调用_dl_call_fini
↓调用_IO_cleanup,其中有著名的_IO_flush_all
执行 .fini_array 和 _fini()
这里逆一下_dl_call_fini的源码:
如果在程序执行流中如果有机会通过任意地址写或者数组越界的方式写到fini_array数组的位置,就能实现劫持程序流了?考虑一下题目的两种利用的情景:
1.构造array实现main无限loop
fini_array[0] = __libc_csu_fini
fini_array[1] = main
需要注意控制fini_array长度大于2
通过调试发现存储array长度的数据存在elf的.dynamic段

对.dynamic段的具体解释见:https://blog.csdn.net/qfanmingyiq/article/details/124527430
简单来说.dynamic 段为动态链接器提供了:
- 依赖关系:需要加载哪些共享库
- 符号解析:如何查找和解析符号
- 重定位信息:如何修正地址引用
- 初始化与终结流程:何时调用构造函数与析构函数
- 版本控制:符号版本兼容性
那么fini array的长度和什么东西有关呢。这里先明确一下fini array存在的意义:为了确保资源正确释放
那么什么资源需要释放呢?问ai给出了几种:
| 影响因素 | 对数量的贡献 | 示例 |
|---|---|---|
| 全局 C++ 对象 | +N (每个对象) | MyClass obj1, obj2; |
| destructor 属性 | +N (每个函数) | __attribute__((destructor)) |
| 静态库初始化 | +M (库决定) | 链接的静态库 |
| 编译器生成 | +K (编译器决定) | 异常处理清理等 |
2.构造array实现无限长的ROP链
fini_array[0] = leave_ret
fini_array[1] = ret
fini_array[2]=ROP链子
这个方法利用的前提是在libc_csu_fini中遍历调用fini_array时函数将栈给迁移到了fini_array上,但是在GLIBC3.35以上用的 _dl_call_fini()中栈并没有迁移到这上面,当然也就失效了。
总结一下
综合看来这种利用方法貌似比较过时了(主要局限在fini_array数组的位置不可写),不过这次对于libc_start_main函数的探索也受益良多,本人对于底层的原理尚有很多待探索的。感受较深的就是版本的更迭也就意味攻击手法的更新也需跟上,那么对底层原理的熟练掌握以及积极发动主观能动思考能力也就不可或缺,加油!