Skip to main content

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바이트 초과 입력
└─────────────────┘ └─────────────────┘

공격 시나리오:

  1. 공격자가 128바이트 초과 입력 전송
  2. strcpy()가 버퍼 경계 넘어서 데이터 복사
  3. Return Address가 공격자가 원하는 주소로 변경
  4. 함수 종료 시 공격자가 지정한 주소로 점프
  5. 공격자가 준비한 코드 실행

2. 셸코드 작성 원리

문제: 셸코드 작성 시 고려해야 할 제약 조건들을 설명하고, 다음 어셈블리 코드에서 문제점을 찾아 수정하시오.

mov eax, 0              ; B8 00 00 00 00
push eax
mov ebx, '/bin/sh'
push ebx

모범답안:

셸코드 제약 조건:

  1. NULL 바이트 금지: 문자열 처리 함수들이 \x00에서 중단
  2. 길이 제한: 버퍼 크기에 맞춰야 함
  3. 위치 독립성: 메모리 주소에 의존하지 않아야 함
  4. 시스템 콜 사용: 직접 라이브러리 함수 호출 불가

문제점과 수정:

문제점:

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

방어 효과:

  1. 코드 주입 방지: 스택/힙에 주입된 코드 실행 불가
  2. Segmentation Fault: 실행 시도 시 즉시 프로그램 종료
  3. 공격 비용 증가: 공격자가 새로운 기법 개발 필요

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

모범답안:

스택 카나리 동작 원리:

  1. 함수 시작: 스택에 카나리 값 배치
  2. 함수 실행: 일반적인 작업 수행
  3. 함수 종료: 카나리 값 검증
  4. 결과: 변경되었으면 프로그램 종료

어셈블리 코드 분석:

카나리 설정 (함수 프롤로그):

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 영역)
├─────────────────┤
│ 오버플로우 데이터 │
└─────────────────┘

공격 성공 시나리오:

  1. Return Address를 NOP Sled 중간 어딘가로 설정
  2. 함수 반환 시 NOP Sled 영역에 착지
  3. NOP 명령어들을 순차 실행하며 슬라이드
  4. 최종적으로 셸코드에 도달하여 실행

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) 전략이 필수적입니다.