0x06 소프트웨어 보안 2 (Software Security - History)
1. 소프트웨어 익스플로잇 기초
1.1 소프트웨어 버그
소프트웨어 버그:
• 소프트웨어의 실수와 허점
• 프로그램이 잘못된 작업 수행
• 확률적으로 "크래시" 발생
크래시 예시:
• Segmentation Fault (메모리 접근 오류)
• Invalid Opcode (코드가 임의 위치로 점프)
1.2 소프트웨어 익스플로잇
익스플로잇 과정:
Input → [BUG] → 악의적 트리거 → Access Secret
정의: 버그를 악의적으로 트리거하여 프로그램 동작 제어
결과: 버그가 익스플로잇 가능하면 → 취약점(Vulnerability)
1.3 메모리 손상과 제어 흐름 하이재킹
메모리 손상의 결과
메모리 손상 → 프로그램의 정의되지 않은 동작
프로그램 실행이 의도된 프로그래머 로직을 벗어남
실행 동작이 원본 소스 코드와 전혀 다름
제어 흐름 하이재킹
정의: Control-Flow Hijacking
소프트웨어 버그를 사용하여 프로그램 상태나 실행을 장악하고
임의의 작업을 실행하는 행위
목표: 버그 트리거 후 프로그램 상태를 제어하여 악의적 행동 수행
2. 런타임 소프트웨어 공격 완화 기법
2.1 기본 가정과 목표
가정: 프로그램에 익스플로잇 가능한 버그가 있을 수 있음
목표: 익스플로잇을 불가능하게 하거나 매우 어렵게 만듦
방법: OS, 컴파일러, 런타임 소프트웨어를 활용한 공격 어려움 증가
2.2 현대 방어 기법들
현대 컴퓨터 시스템의 다층 공격 완화 기법:
• DEP (Data Execution Prevention)
• ASLR (Address Space Layout Randomization)
• Canaries (Stack Canaries)
• 기타 방어 기법들
특징: 대부분 기본적으로 활성화됨
2.3 메모리에서의 영원한 전쟁
"Eternal War in Memory"
공격자와 방어자 간의 지속적인 군비 경쟁:
공격 기법 개발 → 방어 기법 개발 → 방어 우회 기법 → 새로운 방어 기법 → ...
3. 스택 버퍼 오버플로우
3.1 스택 버퍼 오버플로우 개념
스택 프레임 구조:
┌─────────────────┐ ← 높은 주소
│ Parameter 2 │
├─────────────────┤
│ Parameter 1 │
├─────────────────┤
│ Return Address │ ← 중요! 공격 대상
├─────────────────┤
│ Saved EBP │
├─────────────────┤
│ Buffer[256] │ ← 버퍼 오버플로우 시작점
└─────────────────┘ ← 낮은 주소
버퍼 오버플로우 발생 과정
void vulnerable_function(char *input) {
char buffer[256];
strcpy(buffer, input); // 길이 검사 없음!
return;
}
문제점: 입력이 256바이트를 초과하면 스택의 다른 데이터 덮어씀
3.2 공격 시나리오
정상 상황:
buffer[256] → Saved EBP → Return Address → Parameters
오버플로우:
AAAA...AAAA → 0x41414141 → 0x41414141 → 공격자 제어
결과: Return Address 변조 → 임의 코드 실행
4. 스택 기반 코드 주입 공격
4.1 코드 주입 공격 개념
스택 기반 코드 주입 공격:
• 스택 기반 소프트웨어 공격의 "위대한 할아버지"
• 스택에 직접 셸코드 주입하여 실행
• 셸코드: 셸(예: /bin/sh)을 실행하는 최소 코드
4.2 셸코드 (Shellcode)
셸코드 정의
셸코드: 공격에서 주입되는 코드
이름의 유래: 가장 일반적인 주입 코드가 "/bin/sh" 실행이기 때문
셸코드 예시
xor %eax,%eax ; EAX = 0
push %eax ; NULL 종료자 푸시
push $0x68732f2f ; "hs//" 푸시
push $0x6e69622f ; "nib/" 푸시 → "/bin//sh"
mov %esp,%ebx ; 문자열 주소를 EBX에
push %eax ; NULL 푸시
push %ebx ; 문자열 포인터 푸시
mov %esp,%ecx ; argv 배열 주소
mov $0xb,%al ; execve 시스템 콜 번호
int $0x80 ; 시스템 콜 실행
바이트코드 변환
char shellcode[] =
"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e"
"\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80";
4.3 셸코드 작성 기법
제약 조건
C 문자열 관련 API 제약:
• NULL 바이트 (\x00)에서 읽기 중단
• 개행 문자 (\x0a)에서 읽기 중단
• 길이 제한: 짧은 셸코드가 항상 더 좋음
최적화 기법
; 나쁜 예 (NULL 바이트 포함)
mov eax, 0 ; B8 00 00 00 00
; 좋은 예 (NULL 바이트 없음)
xor eax, eax ; 31 c0
고급 셸코드 예시
Linux/x86 파일 리더 셸코드 (65바이트):
• 파일을 열고 내용을 읽어 출력
• 시스템 콜 활용: open, read, write, exit
• NULL 바이트 회피 기법 적용
4.4 공격 실행 과정
스택 레이아웃 조작
공격 전:
┌─────────────────┐
│ Return Address │
├─────────────────┤
│ Buffer[256] │
└─────────────────┘
공격 후:
┌─────────────────┐
│ 셸코드 주소 │ ← Return Address 덮어씀
├─────────────────┤
│ 셸코드 │ ← Buffer에 셸코드 주입
│ 셸코드 │
│ ... │
└─────────────────┘
4.5 스택 주소 변화 대응
문제점
함수 호출 순서에 따른 스택 주소 변화:
funcA() → funcB() → funcC() → funcVuln() // 주소1
funcA() → funcB() → funcVuln() // 주소2 (다름)
도전: funcVuln의 buf[32] 정확한 주소 필요
해결책: NOP Sled
NOP 명령어:
nop (\x90):
• No-Operation의 약자
• 아무것도 하지 않음
• 공격 페이로드의 공간 채우기용
존재 이유:
• 공간 채우기
• 코드/데이터를 캐시 라인에 정렬
NOP Sled 공격:
공격 페이로드 구조:
┌─────────────────┐
│ 대략적 주소 │ ← Return Address
├─────────────────┤
│ \x90\x90\x90 │ ← NOP Sled
│ \x90\x90\x90 │
│ \x90\x90\x90 │
├─────────────────┤
│ 셸코드 │ ← 실제 실행할 코드
└─────────────────┘
원리: 정확한 주소를 모르더라도 NOP 영역에 착지하면 셸코드로 슬라이드
4.6 공격 결과
공격 성공 시:
• root 권한으로 실행 중인 프로세스 → root 셸 획득
• 서비스로 실행 중인 프로세스 → 원격 서버 셸 획득
실제 피해:
• 시스템 완전 장악
• 권한 상승
• 데이터 유출
💡 추가 정보: 스택 기반 코드 주입 공격은 1988년 Morris Worm에서 처음 대규모로 악용되었으며, 이후 수많은 공격의 기초가 되었습니다.
5. 데이터 실행 방지 (DEP)
5.1 DEP 개발 배경
1997년: Alexander Peslyak이 Linux 커널용 방어 기법 제안
목표: 스택 기반 코드 주입 공격 방어
핵심 아이디어: W ⊕ X (Write XOR Execute) 정책
5.2 W⊕X 정책
W ⊕ X 정책:
• 쓰기 가능한 메모리 페이지는 실행 불가능
• 실행 가능한 페이지는 쓰기 불가능
목적:
• 코드 주입 공격 방지
• 코드와 데이터의 명확한 분리
5.3 하드웨어 지원
초기 구현 문제
x86 아키텍처의 한계:
• 원래 x86는 Read/Write 권한만 존재
• Execute 권한이 별도로 없음
질문: W⊕X를 어떻게 구현할까?
NX 비트 도입
64비트 x86 프로세서의 DEP 하드웨어 지원:
NX (No-eXecute) 비트 도입
페이지 테이블 엔트리 플래그:
• P bit: 페이지 접근 가능 여부
• R/W bit: 페이지 수정 가능 여부
• XD bit: 페이지 코드 실행 가능 여부
5.4 운영체제 구현
메모리 영역별 권한
메모리 레이아웃과 권한:
0x00000000
├─ .text : R-X (읽기, 실행 가능)
├─ .data : RW- (읽기, 쓰기 가능)
├─ .bss : RW- (읽기, 쓰기 가능)
├─ heap : RW- (읽기, 쓰기 가능)
└─ stack : RW- (읽기, 쓰기 가능)
0xFFFFFFFF
핵심: 데이터 영역(.data, .bss, heap, stack)은 더 이상 실행 불가능
예외 사항
W⊕X 정책의 예외:
• JIT (Just-In-Time Compilation): JavaScript 등
• 동적 코드 생성이 필요한 특수한 경우
역사적 도입
Microsoft: Windows XP Service Pack 2부터 DEP 포함
Intel: 페이지 테이블 엔트리에 "executable" 플래그 제공
x86 아키텍처: 최초로 코드와 데이터 구분
5.5 DEP의 효과
코드 주입 공격 차단
DEP 적용 전:
스택에 셸코드 주입 → 실행 → 공격 성공
DEP 적용 후:
스택에 셸코드 주입 → 실행 시도 → Segmentation Fault
결과: 스택 기반 코드 주입 공격 완전 차단
6. 스택 카나리 (Stack Canary)
6.1 스택 카나리 개념
스택 카나리 (StackGuard):
함수 프롤로그/에필로그에 카나리 값 추가로 스택 오버플로우 탐지
함수 구조:
FuncUnderAttack:
카나리 설정
// 작업 수행
if (Canary == OrigCanaryValue)
return; // 정상 반환
else
crash; // 프로그램 종료
6.2 스택 레이아웃
보호된 스택 프레임:
┌─────────────────┐
│ Parameter 2 │
├─────────────────┤
│ Parameter 1 │
├─────────────────┤
│ Return Address │
├─────────────────┤
│ Canary │ ← 새로 추가된 보호값
├─────────────────┤
│ Saved EBP │
├─────────────────┤
│ Buffer[256] │
└─────────────────┘
6.3 구현 세부사항
어셈블리 코드 분석
echo:
sub $0x18,%rsp
mov %fs:0x28,%rax ; 카나리 값 로드
mov %rax,0x8(%rsp) ; 스택에 카나리 저장
xor %eax,%eax
; ... 함수 본체 ...
mov 0x8(%rsp),%rax ; 카나리 값 확인
xor %fs:0x28,%rax ; 원본과 비교
je 400768 ; 같으면 정상 종료
callq __stack_chk_fail ; 다르면 크래시
동작 과정
카나리 설정:
mov %fs:0x28,%rax // 카나리 값을 RAX에 로드
mov %rax,-0x8(%rbp) // 스택에 카나리 배치
카나리 검사:
mov -0x8(%rbp),%rcx // 스택에서 카나리 로드
xor %fs:0x28,%rcx // 원본 카나리와 XOR
je <정상 종료> // 0이면 정상
callq <__stack_chk_fail> // 아니면 크래시
6.4 카나리 우회 기법
스택 카나리 우회 방법:
• 상황별로 매우 다름
• 카나리는 프로세스 생성 시마다 무작위 생성
우회 기법:
1. 무차별 대입: 프로그램을 크래시시키지 않고 지속 공격 가능한 경우
(예: 웹서버 스레드)
2. 메모리 공개 버그: 카나리 값을 먼저 유출
3. 기타 기법들...
💡 추가 정보: 스택 카나리는 탄광의 카나리아에서 이름이 유래되었습니다. 탄광에서 유독가스를 감지하기 위해 카나리아를 사용했던 것처럼, 스택 오버플로우를 감지하는 역할을 합니다.
예상 시험문제
1. 스택 버퍼 오버플로우 분석
문제: 다음 코드의 취약점을 분석하고, 스택 레이아웃을 그려 공격 시나리오를 설명하시오.
void vulnerable_function(char *user_input) {
char buffer[128];
strcpy(buffer, user_input);
printf("Echo: %s\n", buffer);
}
모범답안:
취약점 분석:
strcpy()함수는 길이 검사를 하지 않음user_input이 128바이트를 초과하면 버퍼 오버플로우 발생- 스택의 다른 데이터 (Saved EBP, Return Address) 덮어쓰기 가능
스택 레이아웃:
공격 전: 공격 후:
┌─────────────────┐ ┌─────────────────┐
│ Return Address │ │ 0x41414141 │ ← 공격자가 제어
├─────────────────┤ ├─────────────────┤
│ Saved EBP │ │ 0x41414141 │ ← 덮어씀
├─────────────────┤ ├─────────────────┤
│ buffer[128] │ │ AAAA...AAAA │ ← 128바이트 초과 입력
└─────────────────┘ └─────────────────┘
공격 시나리오:
- 공격자가 128바이트 초과 입력 전송
strcpy()가 버퍼 경계 넘어서 데이터 복사- Return Address가 공격자가 원하는 주소로 변경
- 함수 종료 시 공격자가 지정한 주소로 점프
- 공격자가 준비한 코드 실행
2. 셸코드 작성 원리
문제: 셸코드 작성 시 고려해야 할 제약 조건들을 설명하고, 다음 어셈블리 코드에서 문제점을 찾아 수정하시오.
mov eax, 0 ; B8 00 00 00 00
push eax
mov ebx, '/bin/sh'
push ebx
모범답안:
셸코드 제약 조건:
- NULL 바이트 금지: 문자열 처리 함수들이
\x00에서 중단 - 길이 제한: 버퍼 크기에 맞춰야 함
- 위치 독립성: 메모리 주소에 의존하지 않아야 함
- 시스템 콜 사용: 직접 라이브러리 함수 호출 불가
문제점과 수정:
문제점:
mov eax, 0 ; B8 00 00 00 00 ← NULL 바이트 포함
수정안:
xor eax, eax ; 31 C0 ← NULL 바이트 없음
push eax
push 0x68732f2f ; "hs//"
push 0x6e69622f ; "nib/" → "/bin//sh"
mov ebx, esp ; 문자열 주소
최적화 기법:
- XOR 연산으로 레지스터 초기화 (NULL 바이트 방지)
- 문자열을 스택에 직접 구성
- 시스템 콜 번호와 인자 설정
3. DEP(Data Execution Prevention) 분석
문제: DEP의 W⊕X 정책을 설명하고, 이 정책이 기존 코드 주입 공격을 어떻게 방어하는지 메모리 레이아웃 관점에서 설명하시오.
모범답안:
W⊕X 정책:
Write ⊕ Execute = 배타적 OR 관계
• 쓰기 가능한 메모리 페이지는 실행 불가능
• 실행 가능한 페이지는 쓰기 불가능
• 둘 다 가능한 페이지는 존재하지 않음
메모리 레이아웃 변화:
DEP 적용 전:
0x00000000
├─ .text : R-X (읽기, 실행)
├─ .data : RW- (읽기, 쓰기)
├─ .bss : RW- (읽기, 쓰기)
├─ heap : RWX (읽기, 쓰기, 실행) ← 문제!
└─ stack : RWX (읽기, 쓰기, 실행) ← 문제!
0xFFFFFFFF
DEP 적용 후:
0x00000000
├─ .text : R-X (읽기, 실행)
├─ .data : RW- (읽기, 쓰기)
├─ .bss : RW- (읽기, 쓰기)
├─ heap : RW- (읽기, 쓰기) ← 실행 불가
└─ stack : RW- (읽기, 쓰기) ← 실행 불가
0xFFFFFFFF
방어 효과:
- 코드 주입 방지: 스택/힙에 주입된 코드 실행 불가
- Segmentation Fault: 실행 시도 시 즉시 프로그램 종료
- 공격 비용 증가: 공격자가 새로운 기법 개발 필요
4. 스택 카나리 동작 원리
문제: 스택 카나리의 동작 원리를 설명하고, 다음 어셈블리 코드에서 카나리 관련 부분을 찾아 설명하시오.
function_entry:
sub $0x20,%rsp
mov %fs:0x28,%rax
mov %rax,-0x8(%rbp)
; ... function body ...
mov -0x8(%rbp),%rcx
xor %fs:0x28,%rcx
je normal_exit
call __stack_chk_fail
normal_exit:
add $0x20,%rsp
ret
모범답안:
스택 카나리 동작 원리:
- 함수 시작: 스택에 카나리 값 배치
- 함수 실행: 일반적인 작업 수행
- 함수 종료: 카나리 값 검증
- 결과: 변경되었으면 프로그램 종료
어셈블리 코드 분석:
카나리 설정 (함수 프롤로그):
mov %fs:0x28,%rax ; 시스템의 카나리 값을 RAX에 로드
mov %rax,-0x8(%rbp) ; 스택 프레임에 카나리 저장
카나리 검증 (함수 에필로그):
mov -0x8(%rbp),%rcx ; 스택에서 카나리 값 로드
xor %fs:0x28,%rcx ; 원본 카나리와 XOR 연산
je normal_exit ; 결과가 0이면 정상 (값이 같음)
call __stack_chk_fail ; 결과가 0이 아니면 공격 탐지
보호 메커니즘:
- 버퍼 오버플로우 시 카나리 값도 덮어씀
- 함수 종료 전 카나리 검사로 공격 탐지
- 공격 시도 즉시 프로그램 종료
5. NOP Sled 기법
문제: NOP Sled 기법의 필요성과 동작 원리를 설명하고, 공격 페이로드 구조를 그려서 설명하시오.
모범답안:
NOP Sled 필요성:
문제: 스택 주소의 가변성
• 함수 호출 순서에 따라 스택 위치 변화
• 정확한 셸코드 주소 예측 어려움
• 작은 주소 오차로도 공격 실패
해결: NOP Sled 기법
• 대략적인 주소만 알아도 공격 성공
• 넓은 착지 영역 제공
NOP 명령어 특성:
NOP (\x90):
• No Operation - 아무것도 하지 않음
• 다음 명령어로 단순히 진행
• 연속 실행 시 "슬라이드" 효과
공격 페이로드 구조:
메모리 레이아웃:
┌─────────────────┐ ← Return Address가 가리키는 대략적 위치
│ \x90 \x90 \x90 │ ← NOP Sled (100-200바이트)
│ \x90 \x90 \x90 │ 어디에 착지해도 셸코드로 슬라이드
│ \x90 \x90 \x90 │
│ \x90 \x90 \x90 │
├─────────────────┤
│ 셸코드 │ ← 실제 실행할 악성 코드
│ \x31\xc0\x50 │ (execve("/bin/sh") 등)
│ \x68\x2f\x2f │
│ ... │
└─────────────────┘
스택 오버플로우:
┌─────────────────┐
│ 0xbffff700 │ ← Return Address (대략적 NOP 영역)
├─────────────────┤
│ 오버플로우 데이터 │
└─────────────────┘
공격 성공 시나리오:
- Return Address를 NOP Sled 중간 어딘가로 설정
- 함수 반환 시 NOP Sled 영역에 착지
- NOP 명령어들을 순차 실행하며 슬라이드
- 최종적으로 셸코드에 도달하여 실행
6. 공격과 방어의 진화
문제: "Eternal War in Memory"의 의미를 설명하고, 스택 기반 공격에 대한 방어 기법들의 진화 과정을 시간순으로 정리하시오.
모범답안:
"Eternal War in Memory" 의미:
메모리에서의 영원한 전쟁:
• 공격자와 방어자 간의 지속적인 군비 경쟁
• 새로운 공격 기법 → 새로운 방어 기법 → 방어 우회 → 강화된 방어
• 메모리 관리의 근본적 취약성으로 인한 지속적 투쟁
방어 기법 진화 과정:
1단계: 무방비 시대 (1980년대)
• 스택 실행 가능
• 보안 메커니즘 없음
• 코드 주입 공격 쉽게 성공
2단계: DEP 도입 (1990년대 후반)
1997: Alexander Peslyak의 W⊕X 제안
효과: 스택/힙에서 코드 실행 방지
우회: Return-to-libc 공격 등장
3단계: 스택 카나리 (2000년대)
StackGuard 개발: 버퍼 오버플로우 탐지
효과: 스택 오버플로우 공격 탐지
우회: 카나리 우회 기법, 힙 기반 공격
4단계: ASLR 도입 (2000년대 중반)
주소 공간 무작위화: 메모리 레이아웃 예측 방지
효과: 공격 주소 예측 어려움
우회: 메모리 누수, ROP 기법
5단계: 현대적 방어 (2010년대 이후)
• Control Flow Integrity (CFI)
• Intel CET (Control-flow Enforcement Technology)
• ARM Pointer Authentication
• 하드웨어 기반 보안 기능 강화
미래 전망:
• 메모리 안전 언어로의 전환 (Rust 등)
• 하드웨어 보안 기능 강화
• 새로운 공격 벡터 등장 (IoT, AI 등)
핵심 요약
- 스택 버퍼 오버플로우는 가장 기본적이고 위험한 메모리 손상 공격
- 코드 주입 공격은 스택에 셸코드를 주입하여 임의 코드 실행
- NOP Sled 기법으로 주소 예측의 부정확성 극복
- **DEP (W⊕X 정책)**은 코드와 데이터를 분리하여 코드 주입 방지
- 스택 카나리는 함수 프롤로그/에필로그에서 오버플로우 탐지
- 공격과 방어의 진화는 지속적인 군비 경쟁 양상
- 하드웨어 지원이 소프트웨어 보안 강화에 중요한 역할
💡 중요: 각 방어 기법은 특정 공격을 막지만 완벽하지 않으며, 공격자들은 지속적으로 우회 기법을 개발합니다. 따라서 다층 방어(Defense in Depth) 전략이 필수적입니다.