0x07 소프트웨어 보안 6 (PLT/GOT 익스플로잇과 RELRO)
1. PLT/GOT 익스플로잇 개요
1.1 공유 라이브러리 복습
공유 라이브러리 특성
공유 라이브러리 (Shared Libraries):
• 정적 라이브러리와 대비되는 동적 라이브러리
• Linux: .so 파일 확장자
• Windows: .dll 파일 확장자
메모리 효율성:
• 하나의 물리적 복사본만 메모리에 로드
• 여러 프로세스의 가상 주소 공간에 매핑
• 메모리 사용량 최적화
공유 라이브러리 로딩 과정
로딩 특성:
• 실행 파일에는 공유 라이브러리 내용(.text, .data 등) 미포함
• 프로세스 초기화 시 메모리 공간에 동적 로드
• 런타임에 심볼 주소 결정
메모리 매핑:
Process A Process B
┌───────────┐ ┌───────────┐
│ .text │ │ .text │
├───────────┤ ├───────────┤
│ libc │ ←───→ │ libc │ (동일한 물리적 메모리)
│ (shared) │ │ (shared) │
└───────────┘ └───────────┘
1.2 동적 링킹 메커니즘
동적 링킹의 필요성
컴파일 시점의 문제:
• 공유 라이브러리가 런타임에 로드됨
• printf() 등 함수의 주소를 미리 알 수 없음
• ASLR로 인해 매번 다른 주소에 로드
해결책: 런타임 링킹
• 외부 참조의 로드와 링킹을 런타임까지 연기
• OS 로더는 메인 프로그램만 먼저 로드
• 지연 바인딩 (Lazy Binding): 함수 첫 호출 시 주소 해결
간접 참조 방식
간접 참조의 원리:
• 컴파일 시 알 수 없는 것 → 포인터를 통한 간접 접근
• 예: 함수 포인터 + call rax 방식
• ELF는 런타임 간접 참조를 위한 특별한 구조 제공
ELF의 해결책:
• PLT (Procedure Linkage Table)
• GOT (Global Offset Table)
• 두 구조가 협력하여 런타임 심볼 해결 지원
1.3 PLT와 GOT 상세 동작
PLT와 GOT 정의
PLT (Procedure Linkage Table):
• 공유 라이브러리 함수 호출을 위한 스텁 코드
• 초기에는 .got.plt 섹션을 참조하여 심볼 해결 여부 확인
• 해결되지 않았으면 동적 링커 호출하여 .got.plt 채움
GOT (Global Offset Table):
• 외부 심볼을 위한 오프셋 테이블
• .got.plt: PLT 엔트리 전용 GOT 섹션
• 동적 링커가 첫 번째 함수 호출 시 실제 주소로 채움
동적 링킹 실행 과정
printf() 호출 과정 분석:
1단계: 첫 번째 printf() 호출
main() 함수에서:
0x40137f <+239>: call 0x401040 <printf@plt>
printf@plt 스텁:
0x401040 <+0>: jmp QWORD PTR [rip+0x2fda] # 0x404020 <printf@got.plt>
0x401046 <+6>: push 0x1 # 함수 인덱스 번호
0x40104b <+11>: jmp 0x401020 # 공통 PLT 헤더로 점프
2단계: GOT 엔트리 확인
첫 번째 호출 시 GOT 상태:
gef➤ x/g 0x404020
0x404020 <printf@got.plt>: 0x0000000000401046
해석: printf@plt의 두 번째 명령어 주소가 저장됨
→ 아직 해결되지 않은 상태 → 동적 링커 호출로 이어짐
3단계: 동적 링커 실행
공통 PLT 헤더 (0x401020):
0x401020: push QWORD PTR [rip+0x2fe2] # 0x404008 (링크맵 정보)
0x401026: jmp QWORD PTR [rip+0x2fe4] # 0x404010 (동적 링커 함수)
동적 링커 함수:
gef➤ x/g 0x404010
0x404010: 0x00007ffff7fd8d30 # _dl_runtime_resolve_xsavec
동적 링커가 printf 심볼을 libc에서 찾아 GOT에 저장
4단계: 심볼 해결 완료
printf() 재호출 후 GOT 상태:
gef➤ x/g 0x404020
0x404020 <printf@got.plt>: 0x00007ffff7dcd6f0 # 실제 __printf 주소
이후 printf@plt 호출 시:
jmp QWORD PTR [rip+0x2fda] # 직접 __printf()로 점프 (오버헤드 최소)
동적 링킹 요약
전체 과정 요약:
1. 외부 함수 호출 → PLT 스텁 실행
2. PLT 스텁 → GOT 엔트리 확인
3. 해결 안됨 → 동적 링커 호출
4. 동적 링커 → libc에서 실제 주소 찾아 GOT에 저장
5. 이후 호출 → GOT를 통해 직접 실제 함수로 점프
지연 바인딩의 장점:
• 사용되지 않는 함수는 해결하지 않음
• 초기 프로그램 로딩 시간 단축
• 메모리 효율성 증대
2. PLT/GOT 공격 기법
2.1 공격 벡터 분석
1. 메모리 노출 공격 (Memory Disclosure)
ASLR 우회를 위한 정보 유출:
전제 조건:
• PLT/GOT는 메인 실행 파일 이미지에 위치
• PIE 미적용 시 고정된 알려진 주소
공격 과정:
1. 임의 읽기 취약점으로 채워진 GOT 엔트리 유출
leak = read(printf@got.plt) // 예: 0x7ffff7dcd6f0
2. libc 기본 주소 계산
libc_base = leak - printf_offset_in_libc
3. 다른 함수 주소 계산
system_addr = libc_base + system_offset
execve_addr = libc_base + execve_offset
4. ROP 체인에서 계산된 주소 활용
2. GOT 덮어쓰기 공격 (GOT Overwrite)
함수 호출 리다이렉션:
공격 원리:
printf() 호출 → printf@got.plt 참조 → 공격자 함수 실행
공격 과정:
1. 임의 쓰기 취약점으로 GOT 엔트리 수정
printf@got.plt: libc_printf_addr → malicious_func_addr
2. printf() 호출 시 공격자가 제어하는 함수 실행
3. 시스템 장악 또는 추가 익스플로잇
실제 예시:
• printf@got.plt → system 주소로 덮어쓰기
• printf("sh") 호출 → system("sh") 실행 → 셸 획득
3. ROP 가젯으로 활용 (Return-to-GOT)
GOT 엔트리를 ROP 체인에서 직접 호출:
활용 방법:
• strcpy@got.plt를 ROP 체인에서 호출
• memcpy@got.plt로 메모리 복사 수행
• mprotect@got.plt로 메모리 권한 변경
장점:
• 유용한 libc 함수들의 직접적인 호출 가능
• 함수 주소 계산 없이 즉시 사용 가능
• PLT 스텁을 거치지 않고 직접 함수 호출
💡 추가 정보: PLT/GOT 공격은 동적 링킹의 근본적 특성을 악용하므로 완전히 막기 어렵지만, RELRO(RELocation Read-Only) 보호 기법으로 상당한 방어가 가능합니다.
3. RELRO (RELocation Read-Only) 방어 기법
3.1 RELRO 개념
RELRO (RELocation Read-Only):
• GOT (Global Offset Table) 덮어쓰기 공격 방어
• 동적 링킹 완료 후 재배치 섹션을 읽기 전용으로 설정
• ELF 바이너리의 현대적 메모리 손상 방어 기법 일부
3.2 Partial RELRO
특성과 한계
Partial RELRO 특성:
• 많은 Linux 배포판에서 기본적으로 활성화
• .got 섹션만 읽기 전용으로 설정
• .got.plt는 여전히 쓰기 가능 상태 유지
• .dtors, .ctors, .eh_frame_hdr도 보호됨
보안상 한계:
• GOT 덮어쓰기 공격을 완전히 방지하지 못함
• 공격자는 여전히 .got.plt 엔트리 악용 가능
• ret2plt, GOT 하이재킹 공격 여전히 가능
메모리 보호 상태
Partial RELRO 적용 시:
┌─────────────────┐
│ .text │ ← R-X (읽기, 실행)
├─────────────────┤
│ .got │ ← R-- (읽기 전용) ✓ 보호됨
├─────────────────┤
│ .got.plt │ ← RW- (읽기, 쓰기) ✗ 취약함
├─────────────────┤
│ .data │ ← RW- (읽기, 쓰기)
└─────────────────┘
3.3 Full RELRO
완전한 보호
Full RELRO 특성:
• 전체 GOT (.got.plt 포함) 읽기 전용 설정
• GOT 덮어쓰기 공격 완전 방지
• ret2plt, GOT 하이재킹 공격 차단
• 컴파일러 플래그: -z relro -z now
구현 원리:
• 프로그램 시작 시 모든 심볼을 즉시 해결
• 지연 바인딩 (Lazy Binding) 비활성화
• 모든 재배치 완료 후 GOT를 읽기 전용으로 변경
메모리 보호 상태
Full RELRO 적용 시:
┌─────────────────┐
│ .text │ ← R-X (읽기, 실행)
├─────────────────┤
│ .got │ ← R-- (읽기 전용) ✓ 보호됨
├─────────────────┤
│ .got.plt │ ← R-- (읽기 전용) ✓ 보호됨
├─────────────────┤
│ .data │ ← RW- (읽기, 쓰기)
└─────────────────┘
3.4 RELRO 비교 요약
기능 비교표:
┌─────────────────┬─────────────────┬─────────────────┐
│ Feature │ Partial RELRO │ Full RELRO │
├─────────────────┼─────────────────┼─────────────────┤
│ .got.plt │ Writable │ Read-only │
├─────────────────┼─────────────────┼─────────────────┤
│ Protection │ Partial │ Complete │
├─────────────────┼─────────────────┼─────────────────┤
│ Overhead │ Low │ Slightly higher │
├─────────────────┼─────────────────┼─────────────────┤
│ Compiler Flag │ -z relro │ -z relro -z now │
└─────────────────┴─────────────────┴─────────────────┘
성능 vs 보안 트레이드오프:
• Partial RELRO: 빠른 시작 시간, 부분적 보안
• Full RELRO: 느린 시작 시간, 완전한 GOT 보호
3.5 RELRO 우회 기법
Full RELRO 한계점
여전히 가능한 공격:
1. GOT 이외 다른 함수 포인터 공격
• C++ vtable 포인터
• 콜백 함수 포인터
• 구조체 내 함수 포인터
2. 힙 기반 공격
• Use-After-Free
• 힙 오버플로우
3. 스택 기반 공격
• 반환 주소 덮어쓰기
• ROP 체인 (다른 가젯 활용)
4. C++ Vtable 공격 복습
4.1 가상 함수 테이블 구조
C++ 가상 함수의 구현:
• 런타임 다형성(polymorphism) 지원
• 가상 함수 테이블(Vtable)을 통한 간접 호출
• 각 객체는 해당 클래스의 Vtable 포인터 보유
객체 메모리 레이아웃:
┌─────────────────┐
│ Vtable Pointer │ → Vtable → [func1, func2, func3, ...]
├─────────────────┤
│ Member Data 1 │
├─────────────────┤
│ Member Data 2 │
└─────────────────┘
공격 원리:
• 힙 오버플로우로 Vtable 포인터 덮어쓰기
• 가상 함수 호출 시 공격자 제어 코드 실행
💡 추가 정보: Vtable 공격은 RELRO로 방어할 수 없는 대표적인 공격으로, C++ 프로그램에서 특히 주의해야 합니다. 현대적인 방어 기법으로는 Control Flow Integrity(CFI)가 효과적입니다.
예상 시험문제
1. PLT와 GOT의 동적 링킹 과정
문제: ELF 바이너리에서 printf() 함수가 처음 호출될 때부터 두 번째 호출될 때까지의 PLT/GOT 동작 과정을 단계별로 설명하고, 지연 바인딩(Lazy Binding)의 장점을 서술하시오.
모범답안:
첫 번째 printf() 호출 과정:
1단계: PLT 스텁 호출
main() 함수:
call 0x401040 <printf@plt>
printf@plt 스텁:
0x401040: jmp QWORD PTR [rip+0x2fda] # printf@got.plt 참조
0x401046: push 0x1 # 함수 인덱스
0x40104b: jmp 0x401020 # 공통 PLT 헤더
2단계: GOT 엔트리 확인
첫 호출 시 GOT 상태:
printf@got.plt: 0x0000000000401046 # PLT 스텁의 두 번째 명령어 주소
해석: 아직 해결되지 않았으므로 동적 링커 호출 필요
3단계: 동적 링커 실행
공통 PLT 헤더:
0x401020: push QWORD PTR [rip+0x2fe2] # 링크맵 정보
0x401026: jmp QWORD PTR [rip+0x2fe4] # _dl_runtime_resolve
동적 링커 작업:
1. 심볼 "printf" 검색
2. libc.so에서 실제 __printf 함수 주소 발견
3. printf@got.plt에 실제 주소 저장 (0x7ffff7dcd6f0)
4. __printf 함수로 점프하여 실행
두 번째 printf() 호출 과정:
call 0x401040 <printf@plt>
printf@plt:
0x401040: jmp QWORD PTR [rip+0x2fda] # 이미 해결된 주소로 직접 점프
# 0x7ffff7dcd6f0 (__printf)로 즉시 이동
결과: 동적 링커 오버헤드 없이 직접 함수 실행
지연 바인딩의 장점:
- 프로그램 시작 시간 단축: 사용되지 않는 함수는 해결하지 않음
- 메모리 효율성: 필요한 라이브러리만 로드
- 성능 최적화: 한 번 해결된 후에는 직접 호출
- 모듈화: 런타임에 라이브러리 변경 가능
2. PLT/GOT 공격 기법과 방어
문제: PLT/GOT를 악용한 3가지 주요 공격 기법을 설명하고, 각각에 대한 RELRO의 방어 효과를 분석하시오.
모범답안:
1. 메모리 노출을 통한 ASLR 우회:
공격 과정:
// 1. GOT 엔트리 유출
void* leaked_addr = arbitrary_read(printf@got.plt);
// leaked_addr = 0x7ffff7dcd6f0
// 2. libc 기본 주소 계산
void* libc_base = leaked_addr - 0x64f0; // printf 오프셋
// 3. 다른 함수 주소 계산
void* system_addr = libc_base + 0x50d60; // system 오프셋
RELRO 방어 효과: 효과 없음
- GOT 읽기는 여전히 가능
- 정보 유출 자체를 막지 못함
2. GOT 덮어쓰기 공격:
공격 과정:
// 1. GOT 엔트리 덮어쓰기
arbitrary_write(printf@got.plt, system_addr);
// 2. 함수 호출 리다이렉션
printf("sh"); // 실제로는 system("sh") 실행
RELRO 방어 효과:
- Partial RELRO: 부분적 방어 (.got.plt는 여전히 쓰기 가능)
- Full RELRO: 완전 방어 (GOT 전체가 읽기 전용)
3. Return-to-GOT ROP 가젯:
공격 과정:
ROP 체인에서 GOT 엔트리 직접 호출:
┌─────────────────┐
│ &(pop rdi; ret) │
├─────────────────┤
│ "/bin/sh" addr │
├─────────────────┤
│ system@got.plt │ ← GOT를 ROP 가젯으로 활용
├─────────────────┤
│ exit@got.plt │
└─────────────────┘
RELRO 방어 효과: 효과 없음
- GOT 읽기와 실행은 여전히 가능
- 함수 포인터로서의 활용은 차단하지 못함
3. RELRO 방어 기법 비교
문제: Partial RELRO와 Full RELRO의 차이점을 구체적으로 설명하고, Full RELRO의 한계점과 우회 방법을 서술하시오.
모범답안:
Partial RELRO vs Full RELRO 비교:
Partial RELRO:
보호 범위:
• .got: 읽기 전용 ✓
• .got.plt: 쓰기 가능 ✗
• .dtors, .ctors: 읽기 전용 ✓
특성:
• 지연 바인딩 유지
• 빠른 프로그램 시작
• 부분적 GOT 보호
컴파일 옵션: -z relro
Full RELRO:
보호 범위:
• .got: 읽기 전용 ✓
• .got.plt: 읽기 전용 ✓
• 모든 재배치 섹션: 읽기 전용 ✓
특성:
• 즉시 바인딩 (모든 심볼 사전 해결)
• 느린 프로그램 시작
• 완전한 GOT 보호
컴파일 옵션: -z relro -z now
메모리 레이아웃 차이:
Partial RELRO: Full RELRO:
┌─────────────┐ ┌─────────────┐
│ .text (R-X) │ │ .text (R-X) │
├─────────────┤ ├─────────────┤
│ .got (R--) │ ✓ │ .got (R--) │ ✓
├─────────────┤ ├─────────────┤
│.got.plt(RW-)│ ✗ │.got.plt(R--)│ ✓
├─────────────┤ ├─────────────┤
│ .data (RW-) │ │ .data (RW-) │
└─────────────┘ └─────────────┘
Full RELRO의 한계점:
1. 성능 오버헤드:
문제점:
• 프로그램 시작 시 모든 심볼 해결
• 사용되지 않는 함수도 사전 로드
• 큰 프로그램에서 눈에 띄는 지연
측정 예시:
Normal: 0.5초 → Full RELRO: 2.3초 (4.6배 증가)
2. 보호 범위의 한계:
보호되지 않는 영역:
• 스택의 반환 주소
• 힙의 함수 포인터
• C++ vtable 포인터
• 사용자 정의 콜백 함수
Full RELRO 우회 방법:
1. 힙 기반 공격:
// C++ 객체의 vtable 포인터 공격
class MyClass {
public:
virtual void func();
};
MyClass* obj = new MyClass();
// 힙 오버플로우로 vtable 포인터 덮어쓰기
overflow_heap_buffer(fake_vtable_addr);
obj->func(); // 공격자 코드 실행
2. 스택 기반 공격:
• 반환 주소 덮어쓰기
• ROP 체인 (GOT 이외 가젯 활용)
• 스택 카나리 우회 후 제어 흐름 탈취
3. 데이터 전용 공격:
struct Config {
char buffer[128];
char* plugin_path; // 플러그인 경로
bool is_admin; // 권한 플래그
};
// 버퍼 오버플로우로 중요 데이터 조작
// GOT를 건드리지 않고도 프로그램 논리 변경
4. 다른 함수 포인터 공격:
// 구조체 내 함수 포인터
struct EventHandler {
char data[256];
void (*callback)(int); // RELRO로 보호되지 않음
};
// 힙/스택 오버플로우로 callback 포인터 덮어쓰기
4. 동적 링킹과 보안의 관계
문제: 동적 링킹이 제공하는 이점과 보안상 위험을 분석하고, 정적 링킹과 동적 링킹의 보안 관점에서의 장단점을 비교하시오.
모범답안:
동적 링킹의 이점:
1. 메모리 효율성:
단일 라이브러리 공유:
Process A, B, C → 동일한 libc.so 물리 메모리 공유
절약 효과: N개 프로세스 × 라이브러리 크기 만큼 메모리 절약
2. 업데이트 용이성:
라이브러리 패치:
• 보안 패치 시 라이브러리만 교체
• 모든 프로그램이 자동으로 패치 적용
• 재컴파일 불필요
3. 모듈화:
플러그인 시스템:
• 런타임에 기능 확장 가능
• 조건부 기능 로딩
• 시스템 자원 효율적 사용
동적 링킹의 보안 위험:
1. GOT/PLT 공격 표면:
공격 벡터:
• GOT 덮어쓰기로 함수 호출 리다이렉션
• PLT/GOT 주소 유출로 ASLR 우회
• Return-to-GOT ROP 가젯 활용
2. 런타임 의존성:
위험 요소:
• 라이브러리 경로 조작 (LD_LIBRARY_PATH)
• DLL 하이재킹 (Windows)
• 심볼 인터셉션 (Symbol Interposition)
3. 지연 바인딩 취약점:
타이밍 공격:
• 첫 호출 시 해결 과정에서의 레이스 컨디션
• 동적 링커 자체의 취약점
• PLT 스텁 코드의 복잡성
정적 링킹 vs 동적 링킹 보안 비교:
정적 링킹:
장점:
• GOT/PLT 없음 → 관련 공격 벡터 제거
• 직접 함수 호출 → 간접 참조 위험 없음
• 라이브러리 조작 불가능
• 예측 가능한 메모리 레이아웃
단점:
• 큰 바이너리 크기 → 더 많은 공격 표면
• 라이브러리 패치 어려움
• 메모리 사용량 증가
• 코드 재사용성 감소
동적 링킹:
장점:
• 보안 패치 용이성
• 메모리 효율성
• 모듈화된 보안 정책
• ASLR과 결합 시 높은 보안성
단점:
• GOT/PLT 공격 위험
• 런타임 의존성 문제
• 복잡한 로딩 과정
• 심볼 해결 과정의 취약점
보안 권장 사항:
1. 방어 기법 조합:
권장 설정:
• Full RELRO: -z relro -z now
• PIE: -pie (주소 무작위화)
• Stack Canary: -fstack-protector-strong
• Fortify Source: -D_FORTIFY_SOURCE=2
2. 최소 권한 원칙:
구현 방법:
• 필요한 라이브러리만 링크
• 사용되지 않는 심볼 제거
• 샌드박스 환경에서 실행
3. 하이브리드 접근:
전략:
• 보안이 중요한 핵심 기능: 정적 링킹
• 일반적인 유틸리티 기능: 동적 링킹
• 플러그인 시스템: 격리된 프로세스에서 실행
5. C++ Vtable 공격과 RELRO의 한계
문제: C++ 가상 함수 테이블(Vtable) 공격의 메커니즘을 설명하고, 이 공격이 RELRO 방어를 우회할 수 있는 이유를 서술하시오.
모범답안:
C++ Vtable 공격 메커니즘:
1. C++ 가상 함수 구현 원리:
class Shape {
public:
virtual void draw() = 0; // 순수 가상 함수
virtual void move() {} // 가상 함수
};
class Circle : public Shape {
public:
void draw() override { /* 원 그리기 */ }
void move() override { /* 원 이동 */ }
};
2. 메모리 레이아웃:
Circle 객체 메모리:
┌─────────────────┐
│ Vtable Pointer │ → Circle Vtable
├─────────────────┤ ┌─────────────┐
│ Member Data 1 │ │ &Circle::draw │
├─────────────────┤ ├─────────────┤
│ Member Data 2 │ │ &Circle::move │
└─────────────────┘ └─────────────┘
함수 호출 과정:
obj->draw() → *(obj->vtable + 0) → Circle::draw()
obj->move() → *(obj->vtable + 8) → Circle::move()
3. 공격 과정:
3-1단계: 힙 레이아웃 조작:
// 힙에 연속으로 할당
char* buffer = new char[128];
Circle* obj = new Circle();
힙 레이아웃:
┌─────────────────┐
│ buffer[128] │ ← 오버플로우 시작점
├─────────────────┤
│ Circle object │ ← 공격 대상
│ vtable_ptr │
│ member_data │
└─────────────────┘
3-2단계: Vtable 포인터 덮어쓰기:
// 공격자가 준비한 가짜 vtable
void* fake_vtable[] = {
(void*)evil_function, // 첫 번째 가상 함수
(void*)another_evil // 두 번째 가상 함수
};
// 버퍼 오버플로우로 vtable 포인터 덮어쓰기
strcpy(buffer, "A" * 128 + (char*)&fake_vtable);
3-3단계: 가상 함수 호출 트리거:
obj->draw(); // 실제로는 evil_function() 실행
RELRO 우회 이유 분석:
1. 보호 범위의 차이:
RELRO 보호 대상: Vtable 위치:
• .got 섹션 • 힙 메모리 (동적 할당)
• .got.plt 섹션 • 또는 .rodata 섹션
결론: Vtable은 RELRO 보호 범위 밖에 위치
2. 공격 메커니즘의 차이:
GOT 공격: Vtable 공격:
• 전역 함수 테이블 조작 • 객체별 함수 테이블 조작
• 프로그램 전체에 영향 • 특정 객체에만 영향
• 정적 위치 (PIE 제외) • 동적 할당 (힙)
RELRO는 정적 위치의 GOT만 보호 가능
3. 메모리 권한 차이:
GOT 영역:
• Full RELRO 시 읽기 전용
• 운영체제 수준에서 쓰기 방지
Vtable 영역:
• 힙 메모리는 기본적으로 읽기/쓰기 가능
• 객체 생성/소멸로 인한 정상적인 쓰기 필요
• RELRO로 권한 변경 불가능
추가 공격 시나리오:
1. Use-After-Free와 결합:
Circle* obj = new Circle();
delete obj; // 객체 해제
// ... 다른 할당으로 같은 메모리 재사용 ...
obj->draw(); // 해제된 객체의 가상 함수 호출
2. 타입 컨퓨전과 결합:
Shape* shape = new Circle();
// 타입 컨퓨전으로 Rectangle로 오해석
Rectangle* rect = (Rectangle*)shape;
rect->special_function(); // 잘못된 vtable 엔트리 접근
방어 기법:
1. Control Flow Integrity (CFI):
원리:
• 간접 호출의 대상 주소 검증
• Vtable 포인터의 유효성 확인
• 허용된 함수만 호출 가능하도록 제한
구현:
• 컴파일 타임에 CFI 정보 생성
• 런타임에 호출 대상 검증
2. Vtable Pointer Verification:
// 런타임 검사 예시
void call_virtual_function(Object* obj) {
if (!is_valid_vtable(obj->vtable)) {
abort(); // 잘못된 vtable 감지
}
obj->virtual_func();
}
3. 메모리 안전 언어 사용:
Rust:
• 소유권 시스템으로 메모리 안전성 보장
• 가상 함수 호출도 컴파일 타임에 검증
C++ 대안:
• 스마트 포인터 사용
• RAII 패턴 적용
• 범위 기반 for 루프 활용
핵심 요약
- PLT/GOT 동적 링킹은 지연 바인딩을 통해 효율성을 제공하지만 다양한 공격 벡터를 노출
- PLT/GOT 공격은 메모리 노출(ASLR 우회), GOT 덮어쓰기(함수 리다이렉션), Return-to-GOT(ROP 가젯) 등으로 분류
- RELRO 방어 기법은 Partial(부분적 GOT 보호)과 Full(완전한 GOT 보호)로 나뉘며, Full RELRO가 GOT 공격을 효과적으로 차단
- Full RELRO의 한계로는 성능 오버헤드와 힙/스택 기반 공격에 대한 무력함이 있음
- C++ Vtable 공격은 RELRO 보호 범위 밖의 힙 메모리를 대상으로 하여 완전히 우회 가능
- 동적 링킹 vs 정적 링킹은 각각 보안과 효율성 측면에서 트레이드오프 관계
- 현대적 방어로는 CFI(Control Flow Integrity), Vtable 검증, 메모리 안전 언어 등이 필요
- 다층 방어 전략이 필수적이며, 단일 방어 기법으로는 모든 공격을 막을 수 없음
💡 중요: PLT/GOT 공격과 RELRO는 동적 링킹 환경에서 중요한 공격-방어 메커니즘이지만, 힙 기반 공격과 C++ 특성을 고려한 추가적인 방어 기법이 반드시 필요합니다. 특히 현대 소프트웨어에서는 Control Flow Integrity 같은 하드웨어 지원 방어 기법과 메모리 안전 프로그래밍 패러다임의 도입이 근본적인 해결책으로 주목받고 있습니다.