버퍼 오버플로우(Buffer Overflow)는 가장 고전적이면서도 여전히 강력한 공격 기법이다. 메모리 구조를 이해하고 리턴 주소를 조작하면 공격자가 원하는 코드를 실행할 수 있다. 이 글에서는 GDB를 활용한 프로세스 분석부터 실제 공격 기법, 그리고 이를 막기 위한 대응책까지 다룬다.
GDB로 프로세스 분석하기
GDB란?
**GDB(GNU Project Debugger)**는 프로그램 실행 중 내부 상태를 관찰하거나, 크래시 발생 시 원인을 분석할 수 있는 디버거이다.
GDB로 할 수 있는 주요 기능은 다음과 같다:
- 프로그램 시작 및 동작에 영향을 주는 설정
- 특정 조건에서 프로그램 정지
- 정지 시점의 상태 검사
- 프로그램 내 값을 변경하여 버그 수정 실험
GDB-PEDA
**PEDA(Python Exploit Development Assistance for GDB)**는 GDB의 기능을 확장한 도구이다. SEED VM에는 기본으로 설치되어 있다.
주요 특징:
- 디스어셈블리 코드, 레지스터, 메모리 정보를 컬러로 표시
- 익스플로잇 개발을 위한 추가 명령어 지원 (
peda help로 전체 명령어 확인 가능)
프로세스 메모리 구조
프로세스 메모리는 Text, Data, Heap, Stack 영역으로 구분된다. 버퍼 오버플로우 공격은 주로 Stack 영역을 타겟으로 한다.
버퍼 오버플로우 공격
취약한 프로그램의 구조
다음과 같은 취약한 프로그램을 예로 들어보자:
badfile에서 300바이트 데이터를 읽음- 읽은 내용을 400바이트 크기의
str변수에 저장 str을 인자로func1함수 호출
여기서 핵심은 badfile의 내용을 사용자가 제어할 수 있다는 점이다.
버퍼 오버플로우의 결과
리턴 주소를 임의의 값으로 덮어쓰면 다음과 같은 상황이 발생할 수 있다:
- 잘못된 명령어 실행
- 존재하지 않는 주소 참조
- 접근 권한 위반
- 공격자가 심어놓은 악성 코드 실행
마지막 경우가 공격자가 노리는 시나리오이다.
악성 코드 실행 방법
리턴 주소를 버퍼 내의 악성 코드 주소로 변경하면, 함수가 리턴할 때 악성 코드가 실행된다.
환경 설정
실습을 위해 보안 기능(ASLR, Stack Guard 등)을 비활성화한다.
악성 입력 생성
악성 입력은 다음 요소로 구성된다:
- NOP sled (빈 공간)
- Shellcode (실행할 악성 코드)
- 리턴 주소 (shellcode를 가리키도록 조작)
Task A: 버퍼 베이스 주소와 리턴 주소 간 거리 계산
공격을 성공시키려면 버퍼 시작 주소에서 리턴 주소까지의 offset을 정확히 알아야 한다.
Task B: 악성 코드 주소 찾기
악성 코드는 badfile에 작성되어 함수 인자로 전달된다. GDB로 함수 인자의 주소를 확인할 수 있다.
정확한 주소로 점프할 확률을 높이기 위해 **NOP 명령어(0x90)**로 버퍼를 채우고, 악성 코드는 버퍼 끝에 배치한다. NOP는 아무 동작도 하지 않는 명령어이므로, 대략적인 주소로 점프해도 NOP를 거쳐 악성 코드에 도달한다.
NOP가 없다면? 정확히 악성 코드 시작 주소로 점프해야 하므로 성공 확률이 크게 낮아진다.
주소 선택 시 주의사항
새로운 리턴 주소에 **0x00(널 바이트)**이 포함되면 strcpy()가 복사를 중단한다.
예: 0x7fffffffdd88 + 0x78 = 0x7fffffffde00 - 마지막 바이트가 0x00이므로 사용 불가
컴파일 및 실행
보안 기능을 모두 비활성화하고 컴파일한다:
익스플로잇 실행:
Shellcode
악성 코드의 목표는 시스템 접근 권한 획득이다. 이를 위해 셸 프로그램을 실행하는 코드를 작성한다.
Shellcode 작성 시 주요 과제:
- Loader Issue: 절대 주소 사용 불가
- Zeros in the code: 널 바이트 포함 불가
Shellcode 어셈블리 코드
셸을 실행하는 기계어 코드 구조:
사용되는 레지스터:
$rax: 0x3b (59) -execve()시스템 콜 번호$rdi: "/bin/sh" 문자열 주소$rsi: 인자 배열 주소argv[0]= "/bin/sh" 주소argv[1]= 0 (인자 끝)
$rdx: 0 (환경 변수 없음)
대응책 (Countermeasures)
버퍼 오버플로우를 막기 위한 접근법은 여러 계층에서 이루어진다.
| 계층 | 대응책 | 설명 |
|---|---|---|
| 개발자 | 안전한 함수 사용 | strncpy(), strncat() 등 길이 체크 함수 |
| OS | ASLR | 메모리 주소 무작위화 |
| 컴파일러 | Stack Guard | 카나리 값으로 오버플로우 탐지 |
| 하드웨어 | NX bit | 스택 영역 실행 금지 |
ASLR (Address Space Layout Randomization)
ASLR은 프로그램 실행 시마다 스택, 힙, 라이브러리 주소를 무작위로 배치하여 공격자가 주소를 예측하기 어렵게 만든다.
Linux에서 randomize_va_space 커널 파라미터로 ASLR을 설정한다:
- VDSO: kernel과 user space 사이 영역
- 2: Full address space (Heap 포함)
ASLR 우회하기
32비트 시스템에서는 주소 공간이 작아 브루트 포스로 우회 가능하다:
32비트 Linux에서 약 19분간 스크립트를 실행한 결과 셸 획득에 성공했다:
Stack Guard (Canary)
Stack Guard는 컴파일러 수준의 대응책이다.
동작 원리:
- 함수 시작 시 스택에 **랜덤 시크릿 값(Canary)**을 저장
- 함수 리턴 전 해당 값이 변경되었는지 검사
- 값이 변경되었다면 버퍼 오버플로우 발생으로 판단하고 프로그램 종료
Non-Executable Stack (NX bit)
**NX bit(No-eXecute)**는 CPU에서 지원하는 기능으로, 메모리의 특정 영역(스택 등)을 실행 불가로 표시한다.
그러나 이 대응책은 Return-to-libc 공격으로 우회할 수 있다.
Return-to-libc 공격
Non-Executable Stack 우회
스택에서 코드 실행이 불가능하다면, 이미 존재하는 코드를 활용하면 된다.
libc의 system() 함수 활용
libc 라이브러리에 있는 system(cmd) 함수를 호출하면 임의의 명령을 실행할 수 있다:
취약한 프로그램
환경 설정
공격 개요
공격에 필요한 정보:
- Task A:
system()함수 주소 - 리턴 주소를 덮어쓸 값 - Task B: "/bin/sh" 문자열 주소 -
system()함수의 인자 - Task C: 인자 전달 방법 - 스택에서 인자 위치 결정
Task A: system() 주소 찾기
GDB로 취약한 프로그램을 디버깅하여 system()과 exit() 주소를 확인한다:
ldd 명령으로 관련 라이브러리 목록도 확인할 수 있다.
Task B: "/bin/sh" 문자열 주소 찾기
libc에는 이미 "/bin/sh" 문자열이 포함되어 있다. system() 함수가 내부적으로 셸을 호출할 때 사용하기 때문이다.
환경 변수를 이용한 문자열 삽입
원하는 임의의 문자열을 사용하려면 환경 변수를 활용한다:
주의: 환경 변수의 주소는 프로그램 이름 길이에 따라 달라진다:
Task C: system() 인자 전달
func2()의 리턴 주소를 system() 주소로 덮어쓰면 점프할 수 있다. 하지만 그 전에 인자를 설정해야 한다.
x64 아키텍처에서 첫 번째 인자는 $rdi 레지스터로 전달된다. 따라서 목표는 $rdi를 "/bin/sh" 주소로 설정하는 것이다.
문제는 코드를 직접 실행하지 않고 어떻게 $rdi를 설정할 것인가?
해답은 **Return Oriented Programming (ROP)**이다.
Return-Oriented Programming (ROP)
ROP란?
**ROP(Return-Oriented Programming)**는 Non-Executable Stack이나 DEP(Data Execution Prevention) 같은 보호 기법을 우회하는 고급 공격 기술이다.
핵심 아이디어는 **이미 존재하는 코드 조각(Gadget)**을 활용하는 것이다.
Gadget이란?
Gadget은 다음 특성을 가진 짧은 명령어 시퀀스이다:
- ret(return) 명령어로 끝남
- 프로그램 바이너리나 공유 라이브러리(libc 등)에 존재
- 레지스터 값 로드, 산술 연산, 시스템 콜 등 유용한 동작 수행
ROP 동작 원리
- 리턴 주소를 Gadget 주소로 덮어씀
- Gadget 실행 후 다음 Gadget으로 제어 이동
- Gadget을 체이닝하여 복잡한 기능 구현
새로운 코드를 주입하는 대신, 기존 코드의 제어 흐름을 변조하는 방식이다.
Gadget 체이닝
스택에 Gadget 주소들을 순차적으로 배치한다:
"pop rdi; ret" Gadget 주소 찾기
버퍼와 리턴 주소 간 offset 계산
악성 입력 생성
첫 번째 실행 시도
실패했다.
디버깅: Stack Alignment 문제
문제 원인: Stack Alignment
movaps명령어는 16바이트 정렬된 메모리 주소를 요구한다- 정렬되지 않은 주소 접근 시 Segmentation Fault 발생
$rsp가 16바이트 경계에 정렬되어야 한다
해결책: 추가 ret Gadget을 삽입한다. ret 명령어는 $rsp를 8바이트 이동시키므로, 정렬을 맞출 수 있다.
두 번째 실행 시도
공격 성공이다.
정리
버퍼 오버플로우 공격은 메모리 구조를 이해하고 리턴 주소를 조작하는 것에서 시작한다.
| 공격 기법 | 우회 대상 | 핵심 원리 |
|---|---|---|
| 기본 Buffer Overflow | 보호 없음 | 리턴 주소를 Shellcode 주소로 변경 |
| Return-to-libc | NX bit | libc의 system() 함수 활용 |
| ROP | NX bit + DEP | 기존 코드 조각(Gadget) 체이닝 |
대응책도 계층별로 존재하지만, 각각 우회 방법이 있다. 보안은 공격과 방어의 끊임없는 경쟁이다.
HGU 전산전자공학부 고윤민 교수님의 24-2 컴퓨터 보안 수업을 듣고 작성한 포스트이며, 첨부한 모든 사진은 교수님 수업 PPT의 사진 원본에 필기를 한 수정본입니다.