커널 하드닝 (Kernel Hardening)
리눅스 커널의 방어 심층(defense-in-depth) 전략을 종합합니다. KASLR, KPTI, kCFI/FineIBT, SMEP/SMAP, Stack Protector, FORTIFY_SOURCE, W^X 정책, SLAB 하드닝, GCC 보안 플러그인, Lockdown LSM, Spectre/Meltdown 하드웨어 취약점(Vulnerability) 완화, 그리고 프로덕션 환경별 하드닝 체크리스트까지 다룹니다.
핵심 요약
- 하드웨어 보호 — SMEP, SMAP, CET, NX/XD, MTE 등으로 사용자 공간 코드 실행을 차단합니다.
- 주소 은닉 — KASLR, FKASLR, KPTI로 커널 주소를 랜덤화하고 페이지 테이블을 분리합니다.
- 제어 흐름 보호 — kCFI, FineIBT, Shadow Call Stack으로 JOP/ROP 공격을 차단합니다.
- 메모리 무결성 — Stack Protector, FORTIFY_SOURCE, W^X로 오버플로와 코드 주입을 탐지합니다.
- 힙 하드닝 — SLAB freelist 랜덤화/하드닝으로 힙 스프레이와 UAF 익스플로잇을 방지합니다.
- 접근 제어 — Lockdown LSM, Seccomp으로 커널 자체 수정과 공격 표면을 축소합니다.
| 방어 계층 | 대표 기술 | 차단 대상 | 성능 영향 |
|---|---|---|---|
| 하드웨어 보호 | SMEP, SMAP, CET, NX/XD, MTE | ret2usr, 사용자 공간(User Space) 코드 실행 | 무시 가능 |
| 주소 은닉 | KASLR, FKASLR, KPTI | 주소 예측 ROP, Meltdown | 1~5% |
| 제어 흐름 보호 | kCFI, FineIBT, Shadow Call Stack, GCS | JOP, COOP, ROP | ~1% |
| 메모리 무결성(Integrity) | Stack Protector, FORTIFY_SOURCE, W^X | 스택/힙 오버플로, 코드 주입 | <1% |
| 힙 하드닝 | SLAB freelist 랜덤화/하드닝, RANDOM_KMALLOC | 힙 스프레이, UAF 익스플로잇 | <2% |
| 접근 제어(Access Control) | Lockdown LSM, Seccomp, LSM | 커널 자체 수정, 공격 표면 축소 | 없음~최소 |
단계별 이해
- 문제 인식: 커널에 버그가 있으면 공격자가 루트 권한을 탈취할 수 있습니다. 하드닝은 버그가 있어도 익스플로잇을 실패시키는 전략입니다.
- 주소 은닉 (KASLR): 커널 코드 위치를 랜덤화하여, 공격자가 "어디를 공격할지" 알 수 없게 만듭니다.
- 페이지(Page) 격리 (KPTI): 사용자 공간에서 커널 메모리를 볼 수 없게 페이지 테이블을 분리합니다.
- 하드웨어 차단 (SMEP/SMAP): CPU가 커널 모드에서 사용자 공간 코드 실행/데이터 접근을 하드웨어적으로 차단합니다.
- 제어 흐름 검증 (CFI): 함수 포인터를 변조해도 컴파일러가 삽입한 해시(Hash) 검증으로 잘못된 함수를 호출할 수 없게 합니다.
- 메모리 보호 (Stack Protector, FORTIFY_SOURCE): 스택/힙 오버플로를 런타임에 탐지하여 즉시 차단합니다.
- 종심 방어: 각 계층이 독립적으로 동작하므로, 한 계층이 우회되어도 다음 계층이 공격을 차단합니다.
하드닝 계층 개요
리눅스 커널은 다양한 컴파일 시점 및 런타임 보안 옵션을 제공합니다. 이러한 하드닝 옵션은 취약점이 존재하더라도 공격의 성공 확률을 크게 낮추는 방어 심층(defense-in-depth) 전략의 핵심입니다.
KASLR (Kernel Address Space Layout Randomization)
커널 코드, 모듈, 물리 메모리(Physical Memory) 매핑(Mapping)의 베이스 주소를 부팅 시 랜덤으로 배치하여, 메모리 레이아웃을 예측할 수 없게 합니다. 공격자가 커널 심볼(Kernel Symbol)의 정확한 주소를 알아야 하는 ROP/JOP 공격을 근본적으로 어렵게 만드는 1차 방어선입니다.
KASLR은 x86_64에서 커널 3.14, arm64에서 4.6부터 지원됩니다. CONFIG_RANDOMIZE_BASE=y로 활성화하며, 배포판별 기본 활성화 정책은 참고자료에서 최신 상태를 확인하세요.
KASLR의 랜덤화 대상 영역
| 영역 | CONFIG 옵션 | 랜덤화 범위 (x86_64) | 설명 |
|---|---|---|---|
| 커널 텍스트 | CONFIG_RANDOMIZE_BASE | 1GB 범위 내 | vmlinux 로드 주소를 2MB 정렬로 랜덤 배치 |
| 모듈 영역 | CONFIG_RANDOMIZE_BASE | 1GB 범위 내 | 모듈 로드 베이스 주소 랜덤화 |
| 물리 메모리 매핑 | CONFIG_RANDOMIZE_MEMORY | 1TB 범위 내 | direct map, vmalloc, vmemmap 영역 랜덤화 |
| 스택 | CONFIG_RANDOMIZE_KSTACK_OFFSET | 엔트리마다 | syscall 진입 시 스택 오프셋(Offset)을 랜덤 조정 |
KASLR 부팅 시 랜덤화 과정
/* arch/x86/boot/compressed/kaslr.c */
/* 부트로더 → 압축 해제기(decompressor)에서 KASLR 처리 */
/* 1단계: 엔트로피 소스 수집 */
static unsigned long get_boot_seed(void)
{
/* RDTSC (CPU 타임스탬프 카운터) */
/* RDRAND/RDSEED (하드웨어 RNG, 지원 시) */
/* i8254 타이머 카운터 */
/* 부트로더가 전달한 시드 (EFI RNG Protocol 등) */
}
/* 2단계: 사용 가능한 물리 메모리 슬롯 탐색 */
/* e820 메모리 맵에서 커널 크기를 수용할 수 있는 영역 목록 생성 */
/* 이미 사용된 영역(initrd, 부트 파라미터 등)은 제외 */
/* 3단계: 랜덤 슬롯 선택 → 커널 재배치 */
static unsigned long find_random_virt_addr(
unsigned long minimum,
unsigned long image_size)
{
/* slots = 사용 가능 슬롯 수 */
/* random_addr = minimum + (kaslr_get_random_long() % slots) * alignment */
/* x86_64: 2MB 정렬, arm64: 64KB 또는 2MB 정렬 */
}
FKASLR (Function-Granular KASLR)
기본 KASLR은 커널 전체를 하나의 블록으로 이동하므로, 하나의 주소만 유출되면 나머지 모든 심볼의 오프셋을 계산할 수 있습니다. FKASLR은 함수 단위로 재배치(Relocation)하여 이 한계를 극복합니다.
/* FKASLR: CONFIG_FG_KASLR (x86_64, 실험적) */
/* 컴파일 시 -ffunction-sections 플래그로 함수별 별도 섹션 생성 */
/* 부팅 시 각 함수 섹션을 독립적으로 랜덤 재배치 */
/* 장점: 단일 주소 유출로 다른 함수 위치 추론 불가 */
/* 단점: 부팅 시간 증가, 캐시 지역성 저하, 코드 크기 증가 */
/* /proc/kallsyms 심볼 순서가 매 부팅마다 달라짐 */
KASLR 우회 공격과 대응
| 우회 기법 | 원리 | 대응 기술 |
|---|---|---|
| 커널 정보 누출 | 초기화 안 된 메모리에서 커널 포인터 유출 | CONFIG_INIT_ON_ALLOC_DEFAULT_ON, HARDENED_USERCOPY |
| /proc/kallsyms | 심볼 주소 직접 읽기 | kptr_restrict=1/2로 비루트 사용자 차단 |
| dmesg 주소 유출 | %pK 대신 %p로 포인터 출력 | dmesg_restrict=1, %pK 포맷 사용 강제 |
| 타이밍 사이드채널 | 캐시(Cache)/TLB 타이밍으로 주소 추론 | KPTI, 사이트 격리 |
| Spectre 변종 | 투기적 실행(Speculative Execution)으로 커널 메모리 읽기 | Retpoline, IBRS, STIBP |
| 하드웨어 DMA | IOMMU 없이 DMA로 커널 메모리 접근 | IOMMU(Intel VT-d, AMD-Vi) 필수 활성화 |
KASLR 상태 확인 및 관련 sysctl
# KASLR 상태 확인
cat /proc/cmdline | grep -o 'nokaslr\|kaslr'
dmesg | grep "KASLR"
# KASLR enabled (커널 기본 활성화)
# 커널 포인터 보호 수준
cat /proc/sys/kernel/kptr_restrict
# 0: 모든 사용자에게 심볼 주소 노출 (위험)
# 1: 비루트 사용자에게 0x0000000000000000으로 마스킹
# 2: 루트 포함 모든 사용자에게 마스킹 (권장)
# dmesg 접근 제한
sysctl kernel.dmesg_restrict=1
# 커널 텍스트 오프셋 확인 (디버깅 용도, 루트 필요)
# /proc/kallsyms에서 _text 심볼 주소 확인
grep " _text$" /proc/kallsyms
# 매 부팅마다 다른 주소가 출력됨
# 스택 오프셋 랜덤화 확인
cat /proc/sys/kernel/randomize_kstack_offset
# 1 = 활성화 (syscall 진입 시 스택 오프셋 랜덤)
디버깅(Debugging) 시 KASLR을 비활성화하면 커널 주소가 고정되어 심볼 디버깅이 쉬워집니다. 하지만 프로덕션 환경에서는 절대 사용하지 마십시오. QEMU/GDB 커널 디버깅 시에만 nokaslr을 사용하는 것이 일반적입니다.
KPTI (Kernel Page Table Isolation)
Meltdown (CVE-2017-5754) 취약점 완화 기술입니다. 사용자 공간에서 커널 페이지 테이블을 분리하여, 사용자 모드에서 커널 메모리를 투기적 실행으로 읽을 수 없게 합니다.
KPTI는 사용자 모드와 커널 모드에서 별도의 페이지 테이블을 사용합니다. 사용자 공간 페이지 테이블에는 커널 주소 공간(Address Space)이 최소한만 매핑(트램폴린 코드, 인터럽트(Interrupt) 핸들러(Handler) 엔트리)되어, Meltdown 류 사이드 채널로 커널 메모리를 읽을 수 없게 합니다.
/* arch/x86/mm/pti.c — KPTI 페이지 테이블 분리 */
/* 커널 진입 시 CR3 레지스터를 전환하여 전체 커널 매핑을 활성화 */
/* 사용자 모드 PGD: 최소 커널 매핑만 포함 */
static void pti_clone_entry_text(void)
{
/* entry_SYSCALL_64, 인터럽트 핸들러 등
* 트램폴린 코드만 사용자 PGD에 복사 */
pti_clone_pgtable((unsigned long)__entry_text_start,
(unsigned long)__entry_text_end,
PTI_CLONE_PMD);
}
/* CR3 전환: 비트 12(PCID)로 커널/사용자 PGD 구분 */
/* SWITCH_TO_KERNEL_CR3: CR3 비트 12 클리어 → 커널 PGD */
/* SWITCH_TO_USER_CR3: CR3 비트 12 설정 → 사용자 PGD */
#define PTI_USER_PGTABLE_BIT PAGE_SHIFT
#define PTI_USER_PGTABLE_MASK (1 << PTI_USER_PGTABLE_BIT)
# KPTI 상태 확인
dmesg | grep "page tables isolation"
# Kernel/User page tables isolation: enabled
cat /sys/devices/system/cpu/vulnerabilities/meltdown
# Mitigation: PTI
KPTI 성능 영향: 시스템 콜(System Call) 집중 워크로드에서 1~5% 오버헤드(Overhead)가 발생합니다. PCID(Process Context Identifier)를 지원하는 CPU에서는 TLB 플러시(Flush)를 줄여 오버헤드가 최소화됩니다. 최신 Intel/AMD CPU(Meltdown에 영향받지 않는 세대)에서는 KPTI가 자동으로 비활성화됩니다.
SMEP/SMAP
| 기술 | 설명 | 방어 대상 |
|---|---|---|
| SMEP (Supervisor Mode Execution Prevention) | Ring 0에서 사용자 공간 코드 실행 차단 | ret2usr 공격 |
| SMAP (Supervisor Mode Access Prevention) | Ring 0에서 사용자 공간 메모리 접근 차단 | 커널이 의도치 않게 사용자 데이터 읽기 |
/* SMAP 일시 비활성화 (사용자 데이터 복사 시) */
stac(); /* Set AC flag → SMAP 일시 해제 */
copy_from_user(kbuf, ubuf, len);
clac(); /* Clear AC flag → SMAP 재활성화 */
/* copy_from_user/copy_to_user 내부에서 자동 처리 */
SMEP은 Intel Ivy Bridge(2012), AMD Zen(2017)부터 지원됩니다. SMAP은 Intel Broadwell(2014), AMD Zen(2017)부터 지원됩니다. 두 기능 모두 CR4 레지스터(Register)의 비트로 제어되며, 커널이 부팅 시 CPU 기능을 감지하여 자동 활성화합니다.
# SMEP/SMAP 지원 확인
grep -o 'smep\|smap' /proc/cpuinfo | sort -u
# smep
# smap
# CR4 레지스터 비트 확인 (디버깅 환경)
# CR4.SMEP = bit 20, CR4.SMAP = bit 21
SMEP/SMAP 내부 구현과 우회 방어
SMEP(Supervisor Mode Execution Prevention)과 SMAP(Supervisor Mode Access Prevention)은 CR4 레지스터의 특정 비트를 통해 제어됩니다. 커널 모드(Ring 0)에서 사용자 공간 메모리에 대한 실행(SMEP)과 접근(SMAP)을 하드웨어 수준에서 차단하여, ret2usr 공격을 원천 봉쇄합니다.
CR4 핀닝과 우회 방어
공격자가 커널 코드 실행 권한을 얻은 후 mov cr4, rax로 SMEP/SMAP 비트를 클리어하는 우회 기법이 있었습니다. 커널 4.0+에서는 CR4 핀닝(pinning)으로 이를 방어합니다.
/* arch/x86/kernel/cpu/common.c — CR4 핀닝 */
static unsigned long cr4_pinned_bits __read_mostly;
void __init setup_cr_pinning(void)
{
unsigned long mask;
mask = (X86_CR4_SMEP | X86_CR4_SMAP | X86_CR4_UMIP);
cr4_pinned_bits = this_cpu_read(cpu_tlbstate.cr4) & mask;
}
/* native_write_cr4()에서 핀된 비트가 클리어되면 복원 */
void native_write_cr4(unsigned long val)
{
unsigned long bits_missing = cr4_pinned_bits & ~val;
if (unlikely(bits_missing)) {
/* 경고 출력 후 핀된 비트 강제 복원 */
pr_warn_once("pinned CR4 bits changed: 0x%lx!?\n",
bits_missing);
val |= bits_missing;
}
__write_cr4(val);
}
ARM64 PAN (Privileged Access Never)
ARM64에서 SMAP에 해당하는 기능은 PAN(Privileged Access Never)입니다. ARMv8.1부터 하드웨어로 지원되며, 이전 프로세서에서는 소프트웨어 PAN(CONFIG_ARM64_SW_TTBR0_PAN)으로 에뮬레이션합니다.
| 기능 | x86_64 | ARM64 | RISC-V |
|---|---|---|---|
| 커널→유저 실행 차단 | SMEP (CR4.20) | PXN (Page eXecute Never) | Smepmp (PMP) |
| 커널→유저 접근 차단 | SMAP (CR4.21) | PAN (PSTATE.PAN) | Smepmp (PMP) |
| 일시 해제 명령 | stac / clac | uaccess_enable / disable | N/A |
| 지원 시작 | Ivy Bridge / Broadwell | ARMv8.0(PXN) / v8.1(PAN) | Sifive U74+ |
/* arch/arm64/include/asm/uaccess.h — ARM64 PAN 제어 */
static inline void __uaccess_enable_hw_pan(void)
{
/* PSTATE.PAN = 0 → 유저 메모리 접근 허용 */
asm(".arch_extension pan\nmsr panauthkeys, %0"
:: "r"(0));
}
static inline void __uaccess_disable_hw_pan(void)
{
/* PSTATE.PAN = 1 → 유저 메모리 접근 차단 (기본 상태) */
asm(".arch_extension pan\nmsr panauthkeys, %0"
:: "r"(1));
}
/* 소프트웨어 PAN (ARMv8.0 프로세서용) */
/* TTBR0_EL1을 빈 페이지 테이블로 교체하여 유저 매핑 제거 */
/* copy_from_user() 시에만 원래 TTBR0 복원 */
UMIP (User-Mode Instruction Prevention)
UMIP(CR4 비트 11)은 사용자 모드에서 SGDT, SIDT, SLDT, SMSW, STR 명령을 실행하면 #GP 예외를 발생시킵니다. 이 명령들은 커널 주소 정보를 유출할 수 있어 KASLR 우회에 악용됩니다.
/* UMIP: 사용자 모드에서 차단되는 명령 */
/*
* SGDT — GDT 베이스 주소 → 커널 주소 유출
* SIDT — IDT 베이스 주소 → 커널 주소 유출
* SLDT — LDT 셀렉터
* SMSW — Machine Status Word (CR0 일부)
* STR — Task Register
*
* CONFIG_X86_UMIP=y (4.15+)
* 커널은 #GP 에뮬레이션으로 0을 반환하여 레거시 앱 호환성 유지
*/
grep -oE 'smep|smap|umip' /proc/cpuinfo | sort -u
# 출력: smep, smap, umip
세 기능 모두 /proc/cpuinfo의 flags에서 확인 가능합니다. 비활성화하려면 커널 커맨드라인에 nosmep, nosmap을 추가하지만, 프로덕션에서는 절대 사용하지 마세요.
ARM64 MTE (Memory Tagging Extension)
MTE(Memory Tagging Extension)는 ARMv8.5-A에서 도입된 하드웨어 기반 메모리 안전성 기능입니다. 모든 메모리 포인터와 메모리 영역에 4비트 태그를 부여하고, 접근 시 태그 불일치를 하드웨어가 탐지합니다. Use-After-Free, 버퍼 오버플로(Buffer Overflow), 더블 프리 등의 메모리 버그를 하드웨어 수준에서 탐지합니다.
커널 MTE 지원
/* arch/arm64/include/asm/mte-kasan.h — 커널 MTE 통합 */
/* KASAN과 MTE 통합: CONFIG_KASAN_HW_TAGS */
/* MTE를 하드웨어 KASAN 백엔드로 사용 */
static inline u8 mte_get_ptr_tag(void *ptr)
{
/* 포인터의 비트 [59:56]에서 4-bit 태그 추출 */
return ((u64)ptr >> 56) & 0xF;
}
static inline void *mte_set_mem_tag_range(
void *addr, size_t size, u8 tag)
{
/* STG 명령으로 [addr, addr+size) 영역에 태그 설정 */
/* 16바이트 단위(granule)로 태그 저장 */
do {
asm("stg %0, [%0]" : "+r"(addr));
addr += 16; /* MTE granule size */
size -= 16;
} while (size);
return addr;
}
/* SLUB 할당자 MTE 통합 */
/* kmalloc() 반환 시: IRG로 랜덤 태그 생성 → STG로 영역 태깅 */
/* kfree() 시: 새 태그로 재태깅 → stale 포인터 접근 시 태그 불일치 */
| MTE 모드 | CONFIG 옵션 | 오버헤드 | 탐지 정밀도 | 사용 환경 |
|---|---|---|---|---|
| Sync (동기) | kasan.mode=sync | ~15% | 정확한 위치 | 개발/디버깅 |
| Async (비동기) | kasan.mode=async | ~3% | 대략적 위치 | 프로덕션 |
| Asymm (비대칭) | kasan.mode=asymm | ~5% | 쓰기=정확, 읽기=대략 | 프로덕션 (권장) |
MTE 지원 하드웨어: Google Pixel 8 (Tensor G3), Samsung Galaxy S24+ (Exynos 2400), Qualcomm Snapdragon 8 Gen 2+, 그리고 Arm Neoverse N2/V2 서버 CPU에서 MTE를 지원합니다. Android 14+에서 앱별 MTE 활성화가 가능하며, 커널에서는 CONFIG_KASAN_HW_TAGS=y로 하드웨어 KASAN을 활성화합니다.
CFI (Control Flow Integrity)
간접 호출(함수 포인터)의 목적지를 검증하여 ROP/JOP 공격을 차단합니다. 리눅스 커널은 수천 개의 함수 포인터(file_operations, vm_operations_struct 등)를 사용하므로, 함수 포인터 오염을 통한 제어 흐름 탈취는 커널 익스플로잇의 핵심 기법입니다. CFI는 이러한 공격을 런타임에 탐지하여 차단합니다.
Forward-edge vs Backward-edge CFI: Forward-edge CFI는 간접 호출(call *%rax)과 간접 점프의 목적지를 검증합니다 (JOP/COP 방어). Backward-edge CFI는 함수 리턴 주소를 보호합니다 (ROP 방어). 완전한 제어 흐름 보호를 위해 둘 다 필요합니다.
CFI 기술 비교
| 기술 | 커널 버전 | 아키텍처 | 유형 | 설명 |
|---|---|---|---|---|
| Clang CFI (kCFI) | 6.1+ | x86_64, arm64 | Forward-edge (SW) | 간접 호출 시 함수 시그니처 해시 검증. CONFIG_CFI_CLANG |
| FineIBT | 6.2+ | x86_64 (Intel) | Forward-edge (HW+SW) | Intel CET IBT + kCFI 결합. 하드웨어 기반 CFI. CONFIG_X86_KERNEL_IBT |
| Shadow Call Stack | 5.8+ (arm64) | arm64 | Backward-edge | 별도 스택에 리턴 주소 보관. CONFIG_SHADOW_CALL_STACK |
| Intel CET Shadow Stack | 6.6+ | x86_64 (Intel) | Backward-edge (HW) | 하드웨어 Shadow Stack으로 리턴 주소 보호. CONFIG_X86_USER_SHADOW_STACK |
kCFI (Kernel Control Flow Integrity) 동작 원리
kCFI는 Clang 컴파일러가 모든 간접 호출 대상 함수에 타입 해시를 프리픽스로 삽입하고, 호출 직전에 해시를 검증하는 소프트웨어 기반 CFI입니다.
/* kCFI 컴파일러 계측 예시 (개념적) */
/* 원본 코드 */
struct file_operations fops = {
.read = my_read,
.write = my_write,
};
filp->f_op->read(filp, buf, count, pos);
/* kCFI 계측 후 (어셈블리 수준에서 발생하는 동작) */
/*
* 1. 컴파일러가 각 함수 앞에 4바이트 타입 해시 삽입:
* my_read:
* .long 0xDEAD1234 ← 함수 시그니처 해시 (ssize_t(*)(struct file*, char*, size_t, loff_t*))
* push %rbp ← 실제 함수 시작
* ...
*
* 2. 간접 호출 직전에 해시 검증 코드 삽입:
* mov f_op(%rdi), %rax ← 함수 포인터 로드
* movl -4(%rax), %ecx ← 대상 함수 앞의 해시 로드
* cmpl $0xDEAD1234, %ecx ← 기대 해시와 비교
* jne __cfi_failure ← 불일치 시 → BUG()/panic
* call *%rax ← 일치 시 정상 호출
*/
커널 5.13에서 도입된 초기 Clang CFI는 간접 호출을 점프 테이블로 치환하는 방식이었으나, 크로스 모듈 호출 처리가 복잡하고 LTO(Link-Time Optimization)가 필수였습니다. 커널 6.1의 kCFI는 해시 기반 검증으로 전환하여 LTO 없이도 동작하고, 모듈과의 호환성이 크게 개선되었습니다.
FineIBT (Forward-Edge CFI with Intel IBT)
FineIBT는 Intel CET(Control-flow Enforcement Technology)의 IBT(Indirect Branch Tracking)와 kCFI를 결합한 하드웨어 가속 CFI입니다. IBT만 단독 사용 시 모든 ENDBR64 명령어가 유효한 분기 대상이 되어 보호가 약하지만, kCFI의 타입 해시 검증을 결합하면 강력한 forward-edge 보호가 가능합니다.
- 함수 프롤로그에
endbr64(IBT 유효 분기 대상 표시) +subl $hash, %r10d(해시 검증) 삽입 - 간접 호출 사이트에서
movl $hash, %r10d로 기대 해시를 전달 후call *%rax - 해시 불일치 시
ud2→ #UD 예외 → BUG() - 활성화 조건:
CONFIG_X86_KERNEL_IBT=y+CONFIG_CFI_CLANG=y+ CET-IBT 지원 CPU (Intel 12세대+, AMD Zen 4+)
FineIBT는 커널 부팅 시 CPU의 CET-IBT 지원 여부를 자동 감지합니다. 지원하는 CPU에서는 FineIBT(HW+SW), 미지원 CPU에서는 순수 kCFI(SW only)로 자동 폴백합니다. cfi= 커널 파라미터로 동작을 제어할 수 있습니다: cfi=kcfi (소프트웨어만), cfi=fineibt (하드웨어+소프트웨어), cfi=off (비활성화).
Shadow Call Stack (arm64)
Shadow Call Stack(SCS)은 리턴 주소를 일반 스택과 분리된 별도의 그림자 스택에 저장하여, 스택 버퍼 오버플로로 인한 리턴 주소 변조를 방지하는 backward-edge CFI 기법입니다.
/* Shadow Call Stack 동작 (arm64) */
/*
* arm64에서 x18 레지스터를 Shadow Call Stack 포인터로 예약
* (x18은 플랫폼 레지스터, 일반 코드에서 사용 금지)
*
* [함수 프롤로그]
* str x30, [x18], #8 ← 리턴 주소(LR)를 SCS에 push
* stp x29, x30, [sp, #-16]! ← 일반 스택에도 저장 (프레임 포인터)
*
* [함수 에필로그]
* ldr x30, [x18, #-8]! ← SCS에서 리턴 주소 복원
* ldp x29, x30, [sp], #16
* ret ← SCS의 주소로 리턴
*
* 공격자가 일반 스택의 리턴 주소를 변조해도,
* 실제 리턴은 SCS의 원본 주소를 사용하므로 ROP 공격 실패
*/
/* SCS 메모리 할당 (per-task) */
/* arch/arm64/kernel/scs.c */
#define SCS_SIZE (1 << 10) /* 1KB (128개 리턴 주소) */
#define SCS_GFP (GFP_KERNEL | __GFP_ZERO)
/* 태스크 생성 시 SCS 할당, 종료 시 해제 */
/* vmalloc 영역에 할당하여 guard page로 오버플로 감지 */
AArch64 GCS — Guarded Control Stack (커널 6.13+)
커널 6.13에서 AArch64 GCS(Guarded Control Stack) 유저스페이스 지원이 추가되었습니다. GCS는 Armv9.4-A의 하드웨어 기능으로, Shadow Call Stack의 소프트웨어 구현을 하드웨어로 대체합니다.
| 구분 | Shadow Call Stack (기존) | GCS (v6.13+) |
|---|---|---|
| 구현 | 컴파일러 삽입 코드 (x18 레지스터) | CPU 하드웨어 (별도 GCS 메모리) |
| 보호 대상 | 커널 코드만 | 유저스페이스 + 커널 모두 |
| 변조 방지 | 소프트웨어 기반 (우회 가능) | 하드웨어 강제 (일반 store 명령으로 GCS 메모리 쓰기 불가) |
| 오버헤드 | <1% | ~0% (하드웨어 지원) |
prctl(PR_SET_SHADOW_STACK_STATUS)로 스레드별 GCS를 활성화합니다. GCS는 x86의 Intel CET Shadow Stack과 유사한 개념이며, 함수 호출 시 리턴 주소가 별도의 하드웨어 보호 스택에 자동 저장됩니다.
CFI 위반 처리와 디버깅
# CFI 위반 시 커널 로그 예시
# CFI failure at some_function+0x42/0x100 (target: 0xffffffff81234567)
# kernel BUG at arch/x86/kernel/cfi.c:NN!
# CFI 모드 확인
dmesg | grep -i "cfi\|fineibt\|IBT"
# x86/cfi: Switching to FineIBT CFI
# 또는: x86/cfi: Using kCFI
# 모듈에서 CFI 지원 확인
modinfo some_module | grep cfi
# CFI가 활성화된 커널에서 비-CFI 모듈 로드 시
# 간접 호출이 검증 실패할 수 있음
# CONFIG_CFI_PERMISSIVE=y 사용 시 위반을 경고만 출력 (디버깅용)
# 프로덕션에서는 반드시 permissive 비활성화
kCFI가 활성화된 커널에서 서드파티/out-of-tree 모듈을 사용하려면 해당 모듈도 동일한 Clang 버전과 CFI 옵션으로 빌드해야 합니다. GCC로 빌드된 모듈은 kCFI 해시가 없으므로 간접 호출 시 CFI 위반이 발생합니다. DKMS 모듈이나 NVIDIA 드라이버 등은 CFI 환경에서 호환성 문제가 있을 수 있습니다.
스택 프로텍터 (Stack Protector)
스택 프로텍터는 스택 버퍼 오버플로를 런타임에 탐지하는 컴파일러 기반 방어 기술입니다. 함수 진입 시 리턴 주소와 지역 변수 사이에 카나리(canary) 값을 삽입하고, 함수 반환 직전에 카나리가 변조되었는지 검증합니다. 변조가 감지되면 __stack_chk_fail()이 호출되어 커널 패닉(또는 프로세스 종료)을 발생시킵니다.
CONFIG 옵션 비교
| 옵션 | GCC 플래그 | 보호 대상 | 오버헤드 |
|---|---|---|---|
CONFIG_STACKPROTECTOR | -fstack-protector | 8바이트 이상 char 배열이 있는 함수만 | 최소 (~0.1%) |
CONFIG_STACKPROTECTOR_STRONG | -fstack-protector-strong | 지역 배열, 주소 참조 변수 포함 함수 | 낮음 (~0.5%) |
CONFIG_STACKPROTECTOR_ALL | -fstack-protector-all | 모든 함수 | 높음 (~5%) |
CONFIG_STACKPROTECTOR_STRONG을 기본으로 사용합니다. -fstack-protector-strong은 -fstack-protector보다 보호 범위가 넓으면서도 -fstack-protector-all보다 성능 영향이 적어 최적의 균형점입니다.
커널 구현 세부
/* 커널 스택 카나리 초기화 — init/main.c, arch 코드 */
/* per-CPU 또는 per-task 카나리 값 */
/* x86_64: GS 세그먼트의 고정 오프셋에 저장 */
#ifdef CONFIG_STACKPROTECTOR
unsigned long __stack_chk_guard __read_mostly;
EXPORT_SYMBOL(__stack_chk_guard);
#endif
/* 부팅 초기 카나리 설정 */
static void __init boot_init_stack_canary(void)
{
unsigned long canary;
/* 하드웨어 RNG 또는 get_random_bytes()로 생성 */
get_random_bytes(&canary, sizeof(canary));
/* 최하위 바이트를 0으로 설정 (문자열 종료 방지) */
canary &= CANARY_MASK;
/* current→stack_canary에 저장 */
current->stack_canary = canary;
}
/* 컴파일러가 함수 에필로그에 삽입하는 검증 코드 (개념) */
void __noreturn __stack_chk_fail(void)
{
panic("Kernel stack is corrupted in: %pS\n",
__builtin_return_address(0));
}
switch_to()에서 카나리가 교체되므로, 한 태스크(Task)의 카나리 유출이 다른 태스크에 영향을 미치지 않습니다. x86_64에서는 %gs:0x28 오프셋에 저장됩니다.
컴파일러 생성 프롤로그/에필로그 코드
__stack_chk_fail()은 개발자가 직접 호출하는 함수가 아닙니다. GCC(또는 Clang)가 컴파일 시점에 보호 대상 함수의 진입부(프롤로그)와 반환부(에필로그)에 카나리 검증 코드를 자동 삽입합니다. 다음은 x86_64에서 -fstack-protector-strong 적용 시 GCC가 생성하는 실제 어셈블리입니다.
; ── 함수 프롤로그 (GCC 자동 삽입) ──────────────────────
push %rbp
mov %rsp, %rbp
sub $0x50, %rsp ; 지역 변수 공간 할당
mov %gs:0x28, %rax ; per-task 카나리를 GS 세그먼트에서 로드
mov %rax, -0x8(%rbp) ; 스택 프레임(RBP 바로 아래)에 카나리 저장
xor %eax, %eax ; 레지스터에서 카나리 흔적 즉시 제거
; ... 함수 본문 (지역 변수, 버퍼 연산 등) ...
; ── 함수 에필로그 (GCC 자동 삽입) ──────────────────────
mov -0x8(%rbp), %rax ; 스택에서 저장된 카나리 읽기
xor %gs:0x28, %rax ; 원본 카나리와 XOR 비교
je .Lok ; 결과가 0이면(일치) 정상 반환
call __stack_chk_fail ; 불일치 → 카나리 변조 감지 → panic()
.Lok:
leave
ret
핵심은 xor %gs:0x28, %rax 명령입니다. XOR 연산 결과가 0이 아니면(카나리가 변조됨) je 분기를 타지 않고 바로 call __stack_chk_fail로 진행합니다. 이 함수는 __noreturn 속성을 가지므로 호출 시 복귀하지 않으며, 커널에서는 panic()으로 시스템을 즉시 정지시킵니다.
objdump -d vmlinux | grep -A30 "<함수명>:"으로 특정 함수의 프롤로그/에필로그에 삽입된 카나리 검증 코드를 확인할 수 있습니다. 또는 개별 오브젝트 파일을 gcc -S -fstack-protector-strong으로 컴파일하여 어셈블리 출력을 검토할 수도 있습니다.
-fstack-protector-strong 함수 선택 기준
GCC는 모든 함수에 카나리를 삽입하지 않습니다. -fstack-protector-strong은 다음 조건 중 하나라도 만족하는 함수만 보호 대상으로 선택합니다.
| 조건 | 예시 코드 | 설명 | 적용 플래그 |
|---|---|---|---|
| 지역 배열 (모든 타입) | int arr[4]; | 배열 크기·타입 무관, char 외 포함 | -strong, -all |
| 지역 변수 주소 참조 | foo(&local); | 포인터를 통한 간접 변조 가능성 | -strong, -all |
alloca() 호출 | alloca(n); | 런타임 동적 스택 할당 | -strong, -all |
| 가변 길이 배열 (VLA) | char buf[n]; | 런타임 크기 결정 | -strong, -all |
| 8바이트 이상 char 배열 | char buf[8]; | 기본 -fstack-protector의 유일한 조건 | -basic, -strong, -all |
| 위 조건 없는 함수 | int add(int a, int b) | -fstack-protector-all만 보호 | -all 전용 |
-fstack-protector-strong은 GCC 4.9에서 Han Shen(Google)이 도입했습니다. 취약 함수의 약 85%를 ~0.5% 오버헤드로 보호하여, 현재 모든 주요 리눅스 배포판과 Android 커널의 기본 설정입니다.
실제 커널 소스 참조
앞서 보여준 코드는 개념 수준의 단순화입니다. 실제 리눅스 커널에서 스택 프로텍터 관련 코드가 위치하는 주요 파일은 다음과 같습니다.
| 파일 경로 | 역할 | 비고 |
|---|---|---|
kernel/panic.c | __stack_chk_fail() 정의 | __noreturn, panic() 호출 |
arch/x86/include/asm/stackprotector.h | x86 카나리 초기화·교체 | %gs:0x28 (fixed_percpu_data) 기반 |
arch/x86/kernel/process.c | 컨텍스트 스위치 시 카나리 교체 | __switch_to() 내부에서 호출 |
include/linux/stackprotector.h | 아키텍처 공통 인터페이스 | boot_init_stack_canary() 선언 |
arch/arm64/include/asm/stackprotector.h | ARM64 카나리 초기화 | current->stack_canary 직접 접근 |
kernel/panic.c의 __stack_chk_fail()은 __builtin_return_address(0)으로 호출 지점 주소를 포함한 패닉 메시지를 출력합니다. 또한 per-task 카나리 교체는 __switch_to()가 호출하는 아키텍처별 매크로에서 처리되며, 컨텍스트 스위치마다 현재 태스크(Task)의 카나리 값이 %gs:0x28 위치에 기록됩니다.
아키텍처별 카나리 구현 차이
| 아키텍처 | 카나리 저장 위치 | 접근 방식 | 비고 |
|---|---|---|---|
| x86_64 | fixed_percpu_data + 0x28 | %gs:0x28 (per-CPU 세그먼트) | GCC 하드코딩 오프셋, 변경 불가 |
| ARM64 | task_struct->stack_canary | sp_el0 → current 기반 로드 | -mstack-protector-guard=sysreg |
| RISC-V | task_struct->stack_canary | tp (thread pointer) + 오프셋 | -mstack-protector-guard=tls |
x86_64는 GCC가 %gs:0x28 오프셋을 하드코딩하므로, 커널은 반드시 해당 오프셋에 카나리를 배치해야 합니다(fixed_percpu_data 구조체의 stack_canary 필드). 반면 ARM64와 RISC-V는 컴파일러 옵션으로 카나리 위치를 지정할 수 있어 구현에 유연성이 있습니다.
사용자 공간 vs 커널 동작 차이
| 항목 | 사용자 공간 | 커널 |
|---|---|---|
__stack_chk_fail 동작 | __fortify_fail() → abort() → SIGABRT | panic() → 시스템 정지 |
| 영향 범위 | 해당 프로세스만 종료 | 전체 시스템 정지 |
| 카나리 소스 | glibc 초기화 시 /dev/urandom | get_random_bytes() / 하드웨어 RNG |
| 카나리 위치 | TLS (%fs:0x28) | per-CPU (%gs:0x28) |
스택 프로텍터의 한계
- 사후 탐지 한정: 오버플로 발생 시점이 아닌 함수 반환 시점에만 감지합니다. 함수 실행 중 이미 함수 포인터가 덮어씌워져 제어 흐름이 변경될 수 있습니다.
- 선택적 보호:
-fstack-protector-strong은 선택 기준에 해당하지 않는 함수를 보호하지 않습니다. 배열이나 주소 참조가 없는 함수는 카나리가 삽입되지 않습니다. - 비선형 덮어쓰기 무력: 배열 인덱스 기반으로 특정 오프셋만 정확히 덮어쓰는 공격(write-what-where)은 카나리를 건드리지 않고 리턴 주소를 변조할 수 있습니다.
- 카나리 유출 가능성: 포맷 스트링 취약점(
%p,%x), 부채널 공격(side-channel), 정보 유출 버그로 카나리 값이 노출될 수 있습니다. 유출된 카나리를 오버플로 데이터에 포함시키면 검증을 우회할 수 있습니다. - fork() 서버 모델 취약 (사용자 공간):
fork()기반 서버는 자식 프로세스가 동일 카나리를 공유합니다. 바이트 단위 무차별 대입으로 카나리를 추출할 수 있습니다 (커널에서는 해당 없음).
VMAP_STACK 커널 스택 가드 페이지
스택 프로텍터는 함수 복귀 시점에 카나리 훼손을 사후 감지합니다. 반면 VMAP_STACK은 커널 스택을 vmalloc 영역에 할당하여 양쪽 끝에 매핑되지 않은 가드 페이지(Guard Page)를 배치합니다. 스택 오버플로우(Stack Overflow) 발생 즉시 하드웨어 페이지 폴트(Page Fault)가 트리거되어, 인접 메모리가 훼손되기 전에 커널이 안전하게 중단됩니다.
감지 흐름 비교
CONFIG 옵션
| CONFIG 옵션 | 도입 버전 | 기능 | 기본값 |
|---|---|---|---|
CONFIG_VMAP_STACK | 4.9 (x86_64) | vmalloc 기반 커널 스택 + 양쪽 Guard page | x86_64 기본 활성 |
CONFIG_THREAD_SIZE_ORDER | — | 스택 크기 (기본 order-2 = 16KB) | 아키텍처별 기본값 |
STACK_END_MAGIC 폴백
VMAP_STACK이 비활성인 환경(일부 임베디드, 구형 아키텍처)에서는 스택 최하단에 매직 넘버(Magic Number)를 배치하여 오버플로우를 간접 감지합니다.
/* include/linux/magic.h */
#define STACK_END_MAGIC 0x57AC6E9D
/* include/linux/sched/task_stack.h */
static inline unsigned long *end_of_stack(const struct task_struct *task)
{
return task->stack; /* 스택 최하단 (낮은 주소) */
}
/* kernel/fork.c — 새 태스크 생성 시 매직 설정 */
void set_task_stack_end_magic(struct task_struct *tsk)
{
unsigned long *stackend = end_of_stack(tsk);
*stackend = STACK_END_MAGIC;
}
/* kernel/sched/core.c — schedule_debug()에서 매 스케줄 시 검사 */
if (unlikely(*end_of_stack(prev) != STACK_END_MAGIC))
panic("corrupted stack end detected inside scheduler\n");
schedule()이 호출될 때만 수행됩니다. 오버플로우 발생 시점과 감지 시점 사이에 이미 인접 메모리가 훼손될 수 있으며, 스케줄링 없이 크래시가 발생하면 아예 감지되지 않습니다. CONFIG_VMAP_STACK 활성화가 가능한 환경이라면 반드시 VMAP_STACK을 우선 사용하십시오.
스택 훼손 방어 체계 종합
앞서 개별적으로 설명한 Stack Protector, VMAP_STACK, Shadow Call Stack, STACKLEAK, INIT_STACK_ALL_ZERO는 각각 스택의 서로 다른 지점을 보호합니다. 이 메커니즘들이 결합되면 방어 심층(Defense-in-Depth) 체계를 형성하여, 어느 하나가 우회되더라도 다음 계층이 공격을 차단합니다.
스택 훼손 관련 CVE 사례
커널 스택 훼손은 실제 환경에서 반복적으로 발생하며, 여러 주요 CVE의 근본 원인입니다. 다음 사례들은 각기 다른 스택 훼손 유형과 이를 방지하는 방어 메커니즘을 보여줍니다.
| CVE | 유형 | 구성요소 | 영향 | 방어 메커니즘 |
|---|---|---|---|---|
CVE-2016-10153 | 스택 깊이 고갈 | ecryptfs | 8KB 스택 오버플로 → THREAD_SIZE 16KB 확대 계기 | VMAP_STACK |
CVE-2017-1000364 | Stack Clash | 사용자/커널 경계 | guard page 건너뛰기 → 권한 상승 | 확장된 guard gap (1MB) |
CVE-2019-10126 | 드라이버 스택 오버플로 | Marvell WiFi (mwifiex) | 원격 코드 실행 | Stack Protector |
CVE-2022-0435 | 원격 스택 오버플로 | TIPC 모듈 | 원격 코드 실행 (인증 불요) | FORTIFY_SOURCE |
사례 분석: CVE-2022-0435 (TIPC 원격 스택 오버플로)
TIPC(Transparent Inter-Process Communication) 프로토콜 모듈에서 도메인(Domain) 레코드 수를 검증하지 않아, 원격 공격자가 조작된 패킷으로 커널 스택을 오버플로시킬 수 있었습니다. 인증 없이 네트워크를 통해 트리거 가능하여 CVSS 7.8의 높은 위험도를 받았습니다.
/* 취약 코드 (net/tipc/mon.c) — 간략화 */
static void tipc_mon_rcv(struct net *net,
struct tipc_mon_domain *arrv_dom, ...)
{
struct tipc_mon_domain dom_bef; /* 스택에 할당된 버퍼 */
struct tipc_mon_domain *dom;
/* ❌ arrv_dom->member_cnt에 대한 상한 검사 없음!
* member_cnt가 MAX_MON_DOMAIN(64)을 초과하면
* memcpy가 스택 프레임 경계를 넘어 쓰기 수행 */
memcpy(&dom_bef, dom, dom_size(dom));
/* ↑ dom_size()는 member_cnt에 비례
* → 스택 오버플로 → 리턴 주소 변조 → 임의 코드 실행 */
}
/* 수정 코드 (패치) */
static void tipc_mon_rcv(...)
{
/* ✓ member_cnt 상한 검증 추가 */
if (arrv_dom->member_cnt > MAX_MON_DOMAIN) {
pr_warn_ratelimited("Too large domain record\n");
return;
}
/* ... */
}
memcpy()의 길이로 사용하면 스택 오버플로가 발생합니다. FORTIFY_SOURCE가 활성화되어 있었다면 memcpy()의 대상 크기 초과를 런타임에 감지하여 패닉으로 전환했을 것입니다. 스택 오버플로 전파 과정의 상세 설명은 Context Switching — 스택 오버플로 전파와 훼손 영역을 참조하십시오.
사례 분석: CVE-2016-10153 (ecryptfs 스택 깊이 고갈)
ecryptfs는 암호화 파일시스템으로, 하위 파일시스템(ext4, btrfs 등) 위에 계층적으로 동작합니다. ecryptfs → VFS → 하위 FS의 호출 체인이 깊어지면서, 특히 페이지 캐시 경로에서 8KB 스택을 초과하는 사례가 보고되었습니다.
# 당시 스택 트레이스 (간략화) — 깊은 호출 체인으로 스택 고갈:
ecryptfs_writepage
→ ecryptfs_encrypt_page
→ ecryptfs_write_lower_page_segment
→ vfs_write
→ ext4_file_write_iter
→ ext4_da_write_begin
→ __block_write_begin
→ ext4_get_block
→ ext4_map_blocks
→ ext4_ext_map_blocks ← 8KB 스택 거의 소진
→ ext4_find_extent
# → 14단계 이상의 중첩 호출, 각 프레임 200~500바이트
# → 총 ~7.5KB 사용으로 8KB 스택 경계 도달
THREAD_SIZE를 8KB에서 16KB로 확대하는 직접적인 계기가 되었습니다(v3.15). 동시에 CONFIG_VMAP_STACK(v4.9)이 도입되어, 깊이 고갈 시 가드 페이지가 즉시 트랩을 발생시키도록 개선되었습니다.
스택 방어 메커니즘 비교
아래 표는 모든 스택 관련 방어 메커니즘을 감지 시점, 보호 대상, 성능 영향, 아키텍처 지원 관점에서 비교합니다.
| 메커니즘 | CONFIG 옵션 | 감지 시점 | 보호 대상 | 오버헤드 | 아키텍처 |
|---|---|---|---|---|---|
| Stack Protector | STACKPROTECTOR_STRONG | 함수 반환 시 | 스택 버퍼 오버플로 | ~0.5% | 모든 아키텍처 |
| VMAP_STACK | VMAP_STACK | 즉시 (HW 트랩) | 스택 깊이 고갈 | 무시 가능 | x86_64, arm64 |
| Shadow Call Stack | SHADOW_CALL_STACK | 함수 반환 시 | 리턴 주소 변조 | ~1% | arm64 |
| STACKLEAK | GCC_PLUGIN_STACKLEAK | 함수 반환 시 | 정보 유출(Info Leak) | ~1% | GCC only |
| INIT_STACK_ALL_ZERO | INIT_STACK_ALL_ZERO | 컴파일 시 | 미초기화 변수 사용 | 1~3% | GCC 12+, Clang |
| STACK_END_MAGIC | SCHED_STACK_END_CHECK | schedule() 시 | 스택 오버플로 (폴백) | 무시 가능 | 모든 아키텍처 |
| KSTACK_OFFSET | RANDOMIZE_KSTACK_OFFSET | 런타임 (예방) | 스택 레이아웃 예측 | ~1% | x86_64, arm64 |
| KASAN Stack | KASAN_STACK | 즉시 (SW 계측) | 스택 변수 OOB | 높음 (디버그) | 모든 아키텍처 |
환경별 권장 조합
| 환경 | 필수 활성화 | 권장 추가 | 비고 |
|---|---|---|---|
| 프로덕션 서버 | STACKPROTECTOR_STRONG, VMAP_STACK, RANDOMIZE_KSTACK_OFFSET | INIT_STACK_ALL_ZERO | 성능과 보안의 균형 |
| 고보안 환경 | 위 전체 + STACKLEAK + INIT_STACK_ALL_ZERO | Shadow Call Stack (arm64) | 최대 방어 심층, ~5% 오버헤드 감수 |
| 임베디드/IoT | STACKPROTECTOR_STRONG, SCHED_STACK_END_CHECK | VMAP_STACK (지원 시) | VMAP_STACK 미지원 아키텍처 고려 |
| 개발/디버그 | 위 전체 + KASAN_STACK + DEBUG_STACK_USAGE | ftrace stack tracer | 성능 무관, 최대 탐지 우선 |
FORTIFY_SOURCE
FORTIFY_SOURCE는 memcpy(), strcpy(), memset() 등 메모리/문자열 조작 함수의 버퍼(Buffer) 크기를 컴파일 타임과 런타임에 검증하는 방어 기술입니다. 컴파일러가 대상 버퍼의 크기를 __builtin_object_size()로 추론할 수 있으면 컴파일 에러를 발생시키고, 런타임에만 크기를 알 수 있으면 래퍼 함수가 오버플로를 탐지합니다.
동작 메커니즘
커널 구현
/* include/linux/fortify-string.h — FORTIFY_SOURCE 래퍼 */
/* 컴파일러 내장 함수로 버퍼 크기 추론 */
#define __underlying_memcpy __builtin_memcpy
#define __fortify_size(p) __builtin_object_size(p, 0)
/* memcpy 래퍼: 컴파일 타임 + 런타임 이중 검증 */
__FORTIFY_INLINE void *memcpy(void *dest,
const void *src,
size_t count)
{
size_t dest_size = __fortify_size(dest);
size_t src_size = __fortify_size(src);
/* 컴파일 타임 검증: 크기가 상수이면 빌드 에러 */
if (__builtin_constant_p(count)) {
if (dest_size != (size_t)-1 && count > dest_size)
__write_overflow(); /* 빌드 에러 트리거 */
if (src_size != (size_t)-1 && count > src_size)
__read_overflow2();
}
/* 런타임 검증: 동적 크기일 때 */
if (dest_size != (size_t)-1 && count > dest_size)
fortify_panic(__func__);
return __underlying_memcpy(dest, src, count);
}
/* 보호 대상 함수 목록: */
/* memcpy, memmove, memset, strcpy, strncpy, strcat, strncat,
* strlen, strnlen, memcmp, kmemdup, ... */
탐지 사례
/* FORTIFY_SOURCE 탐지 예시 (개념 예시) */
struct my_data {
char name[16];
int flags;
};
static void copy_name(struct my_data *data, const char *input)
{
/* ❌ 컴파일 에러: sizeof(data→name) = 16인데 32바이트 복사 */
memcpy(data->name, input, 32);
/* ✓ 안전: 크기 범위 내 */
memcpy(data->name, input, min(strlen(input) + 1,
sizeof(data->name)));
}
CONFIG_FORTIFY_SOURCE 하나의 옵션만 제공하지만, 내부적으로 __builtin_object_size(p, 0)(전체 객체 크기)와 __builtin_object_size(p, 1)(서브-객체 크기)를 모두 활용합니다. v6.x 커널에서는 서브-객체 단위(구조체(Struct) 멤버)까지 검증이 확장되어, 구조체 내부 필드 간 오버플로도 감지합니다.
안전한 문자열/메모리 API
FORTIFY_SOURCE가 컴파일타임/런타임에 버퍼 오버플로를 탐지한다면, 안전한 API를 사용하는 것은 오버플로 자체를 원천적으로 방지하는 전략입니다. 리눅스 커널은 위험한 C 표준 함수를 대체하는 안전한 API를 제공하며, KSPP(Kernel Self Protection Project)를 중심으로 위험 함수 제거가 진행되고 있습니다.
문자열 함수 대체표
| 위험 API | 안전한 대체 API | 이유 |
|---|---|---|
strcpy() | strscpy() | 대상 버퍼 크기 제한, NUL 종단 보장 |
strncpy() | strscpy() | strncpy는 NUL 미종단 가능, 나머지 zero-fill 낭비 |
strncpy() + zero-fill 필요 | strscpy_pad() | NUL 종단 + 나머지 zero-fill |
strlcpy() | strscpy() | strlcpy는 소스 전체 strlen 수행 (DoS 위험) |
sprintf() | snprintf() / scnprintf() | 버퍼 오버플로 방지 |
sprintf() (동적) | kasprintf() | 필요한 크기를 자동 할당 |
sysfs에서 sprintf() | sysfs_emit() | PAGE_SIZE 초과 방지 |
simple_strtoul() | kstrtoul() | 에러 검출, 오버플로 검사 |
simple_strtol() | kstrtol() | 에러 검출, 오버플로 검사 |
문자열 API 사용 예제
/* strscpy — strcpy/strncpy 대체 */
char buf[16];
ssize_t ret = strscpy(buf, user_input, sizeof(buf));
if (ret < 0)
pr_warn("input truncated (original: %zu bytes)\n",
strlen(user_input));
/* kasprintf — 동적 크기 안전 포맷 */
char *path = kasprintf(GFP_KERNEL, "/dev/%s/%d", name, id);
if (!path)
return -ENOMEM;
/* ... 사용 ... */
kfree(path);
/* kstrtoul — simple_strtoul 대체 */
unsigned long val;
int ret = kstrtoul(buf, 10, &val);
if (ret)
return ret; /* -EINVAL 또는 -ERANGE */
/* scnprintf — snprintf의 실제 기록 바이트 수 반환 */
char msg[64];
int len = 0;
len += scnprintf(msg + len, sizeof(msg) - len, "cpu=%d ", cpu);
len += scnprintf(msg + len, sizeof(msg) - len, "irq=%u", irq_count);
- strscpy()반환값이 음수면 절단이 발생한 것이며, 항상 NUL 종단을 보장합니다.
strncpy()와 달리 불필요한 zero-fill을 수행하지 않아 성능도 우수합니다. - kasprintf()내부적으로
vsnprintf()로 필요한 크기를 계산한 후kmalloc()+vsnprintf()를 수행합니다. 호출자는 반드시kfree()로 해제해야 합니다. - kstrtoul()오버플로 시
-ERANGE를 반환하고, 유효하지 않은 입력 시-EINVAL을 반환합니다.simple_strtoul()과 달리 에러 경로가 명확합니다. - scnprintf()
snprintf()와 달리 실제 기록된 바이트 수(NUL 제외)를 반환하여 연쇄 호출에 안전합니다. 버퍼가 가득 차면 0을 반환하므로 오버플로 없이 누적 작성이 가능합니다.
메모리 보안 API
| 위험 패턴 | 안전한 API | 이유 |
|---|---|---|
kfree(ptr) (민감 데이터) | kfree_sensitive(ptr) | 해제 전 메모리를 zero-fill |
memset(p,0,n); kfree(p) | kfree_sensitive(ptr) | 컴파일러가 memset 최적화 제거 방지 |
kvfree(ptr) (민감 데이터) | kvfree_sensitive(ptr) | kvmalloc 할당에 대한 보안 해제 |
memset(buf, 0, n) (보안용) | memzero_explicit(buf, n) | 컴파일러 dead store 최적화 방지 |
/* kfree_sensitive — 민감 데이터 안전 해제 */
struct crypto_key *key = kmalloc(sizeof(*key), GFP_KERNEL);
/* ... 암호키 사용 ... */
kfree_sensitive(key); /* memzero_explicit() + kfree() */
/* memzero_explicit — 스택 버퍼의 민감 데이터 소거 */
char password[128];
/* ... 패스워드 처리 ... */
memzero_explicit(password, sizeof(password));
/* 컴파일러가 "어차피 함수 끝이니 불필요" 하고 제거하지 않음 */
/* check_object_size — 복사 크기 런타임 검증 */
void my_copy(void *dst, const void *src, size_t n)
{
check_object_size(dst, n, true); /* true = 쓰기 대상 */
check_object_size(src, n, false); /* false = 읽기 소스 */
memcpy(dst, src, n);
}
- kfree_sensitive()내부적으로
memzero_explicit(ptr, ksize(ptr))후kfree()를 호출합니다.ksize()는 실제 할당된 slab 크기를 반환하므로 요청 크기보다 큰 영역까지 완전히 소거됩니다. - memzero_explicit()
barrier()호출로 컴파일러의 dead store elimination을 방지합니다. 일반memset(p, 0, n)은 이후p를 사용하지 않으면 컴파일러가 제거할 수 있습니다. - check_object_size()
CONFIG_HARDENED_USERCOPY가 활성화되면copy_to_user()/copy_from_user()에서 자동으로 호출됩니다. slab 객체 경계를 넘는 복사를 탐지하여 커널 메모리 유출을 방지합니다.
strcpy(), strncpy(), strlcpy(), simple_strtoul() 등이 점진적으로 제거되고 있습니다. 새로 작성하는 커널 코드에서는 반드시 안전한 대체 API를 사용해야 하며, checkpatch.pl도 위험 함수 사용 시 경고를 출력합니다.
Lockdown LSM
Lockdown LSM은 커널 무결성을 보호하기 위해 커널 자체의 수정을 제한하는 보안 모듈입니다. Secure Boot 환경에서 부팅 체인의 신뢰를 런타임까지 연장하는 핵심 메커니즘으로, integrity와 confidentiality 두 단계의 보안 수준을 제공합니다.
Lockdown LSM과 Secure Boot의 관계에 대한 자세한 내용은 Secure Boot & 부팅 보안 — Lockdown 문서를 참조하세요.
Lockdown 모드
커널 구현
/* security/lockdown/lockdown.c */
enum lockdown_reason {
LOCKDOWN_NONE,
LOCKDOWN_MODULE_SIGNATURE,
LOCKDOWN_DEV_MEM,
LOCKDOWN_KEXEC,
LOCKDOWN_HIBERNATION,
LOCKDOWN_PCI_ACCESS,
LOCKDOWN_IOPORT,
LOCKDOWN_MSR,
LOCKDOWN_ACPI_TABLES,
LOCKDOWN_PCMCIA_CIS,
LOCKDOWN_TIOCSSERIAL,
LOCKDOWN_MODULE_PARAMETERS,
LOCKDOWN_MMIOTRACE,
LOCKDOWN_DEBUGFS,
LOCKDOWN_XMON_WR,
LOCKDOWN_BPF_WRITE_USER,
LOCKDOWN_DBG_WRITE_KERNEL, /* integrity 끝 */
LOCKDOWN_INTEGRITY_MAX,
LOCKDOWN_KCORE,
LOCKDOWN_KPROBES,
LOCKDOWN_BPF_READ_KERNEL,
LOCKDOWN_PERF,
LOCKDOWN_TRACEFS,
LOCKDOWN_XMON_RW,
LOCKDOWN_XFRM_SECRET,
LOCKDOWN_CONFIDENTIALITY_MAX,
};
/* LSM 훅: 요청된 작업이 현재 lockdown 수준에서 허용되는지 검사 */
static int lockdown_is_locked_down(enum lockdown_reason what)
{
if (what <= LOCKDOWN_NONE || what >= LOCKDOWN_CONFIDENTIALITY_MAX)
return -EPERM;
if (kernel_locked_down >= what)
return -EPERM; /* 차단 */
return 0; /* 허용 */
}
제한 항목 상세 테이블
| 제한 항목 | integrity | confidentiality | 영향받는 인터페이스 |
|---|---|---|---|
| 미서명 모듈 로드 | 차단 | 차단 | init_module(), finit_module() |
| /dev/mem 쓰기 | 차단 | 차단 | /dev/mem, /dev/kmem |
| /dev/mem 읽기 | 허용 | 차단 | /dev/mem |
| kexec_load (미서명) | 차단 | 차단 | kexec_load() |
| MSR 쓰기 | 차단 | 차단 | /dev/cpu/*/msr |
| ACPI 테이블 오버라이드 | 차단 | 차단 | acpi_table_upgrade() |
| /proc/kcore | 허용 | 차단 | /proc/kcore |
| kprobes | 허용 | 차단 | kprobe_register() |
| eBPF 커널 읽기 | 허용 | 차단 | bpf(BPF_PROG_LOAD) |
| perf 이벤트 | 허용 | 차단 | perf_event_open() |
| tracefs | 허용 | 차단 | /sys/kernel/tracing/ |
lockdown=integrity를 적용합니다. 이는 security/lockdown/lockdown.c의 lock_kernel_down()에서 EFI 보안 부팅 상태를 확인하여 설정됩니다. lockdown=confidentiality로 강화하려면 커널 커맨드라인에 명시해야 합니다.
Spectre/Meltdown 방어
CPU 투기적 실행(speculative execution) 취약점은 마이크로아키텍처 수준에서 정보를 유출합니다. Spectre(변형 1, 2), Meltdown(변형 3), 그리고 후속 변형(MDS, TAA, MMIO Stale Data, BHI, Retbleed, GDS, Inception, SRSO 등)에 대해 커널은 소프트웨어와 마이크로코드 기반 완화책을 제공합니다.
Spectre/Meltdown 취약점의 공격 관점 분석은 커널 보안 취약점 사례 — Spectre 문서를 참조하세요.
주요 취약점 및 완화책
| 취약점 | CVE | 공격 벡터 | 커널 완화책 | 성능 영향 |
|---|---|---|---|---|
| Meltdown (V3) | CVE-2017-5754 | 커널 메모리 읽기 | KPTI | 1~5% |
| Spectre V1 | CVE-2017-5753 | 경계 검사 우회 | 배열 인덱스 마스킹, LFENCE | ~1% |
| Spectre V2 | CVE-2017-5715 | 분기 예측(Branch Prediction)기 주입 | Retpoline, IBRS/IBPB, eIBRS | 2~8% |
| MDS | CVE-2018-12130 | 마이크로아키텍처 버퍼 | VERW, SMT 비활성화 | ~3% |
| TAA | CVE-2019-11135 | TSX 비동기 중단 | TSX 비활성화, VERW | TSX 워크로드 |
| MMIO Stale Data | CVE-2022-21123 | MMIO 레지스터 잔여 데이터 | VERW, 마이크로코드 | 최소 |
| Retbleed | CVE-2022-29900/01 | RET 명령 투기적 실행 | IBRS, untrained RET | 5~15% |
| GDS | CVE-2023-34083 | Gather Data Sampling | 마이크로코드, VERW | 최소 |
| BHI | CVE-2022-0001 | 분기 히스토리 주입 | BHI_DIS_S, 소프트웨어 시퀀스 | ~2% |
| SRSO | CVE-2023-20569 | Speculative RAS Overflow | Safe RET, IBPB | AMD 특화 |
방어 메커니즘 흐름
Retpoline 구현
Retpoline은 간접 분기(indirect branch)를 RET 명령으로 대체하여 분기 예측기(BTB)를 우회하는 소프트웨어 완화책입니다. 투기적 실행을 무한 루프(speculation trap)에 가두어 사이드 채널 유출을 방지합니다.
/* arch/x86/lib/retpoline.S — Retpoline 트램폴린 (개념 예시) */
/* 간접 호출: call *%rax → retpoline 대체 */
/* __x86_indirect_thunk_rax: */
/* call .Lspec_trap ← RSB(Return Stack Buffer)에 .Lspec_trap 주소 push */
/* .Lcapture: */
/* pause ← 투기적 실행이 여기서 무한 루프 */
/* lfence */
/* jmp .Lcapture */
/* .Lspec_trap: */
/* mov %rax, (%rsp) ← 실제 대상 주소로 RSB 엔트리 교체 */
/* ret ← 실제 대상으로 반환 (투기적 실행은 .Lcapture에 갇힘) */
/* IBRS/IBPB: 마이크로코드 기반 대안 */
/* IBRS (Indirect Branch Restricted Speculation): */
/* — MSR 0x48에 IBRS 비트 설정 → 간접 분기 투기를 제한 */
/* IBPB (Indirect Branch Prediction Barrier): */
/* — MSR 0x49에 IBPB 비트 설정 → 분기 예측 버퍼 플러시 */
/* eIBRS (Enhanced IBRS): 커널/사용자 전환 시 자동 적용 */
Spectre V1 방어: array_index_nospec()
/* include/linux/nospec.h — Spectre V1 경계 클램핑 */
/* 투기적 실행 시에도 배열 인덱스가 범위를 벗어나지 않도록 마스킹 */
#define array_index_nospec(index, size) \
({ \
typeof(index) _i = (index); \
typeof(size) _s = (size); \
unsigned long _mask = array_index_mask_nospec(_i, _s); \
((typeof(_i)) (_i & _mask)); \
})
/* 사용 예: syscall 테이블 접근 */
if (nr < NR_syscalls) {
nr = array_index_nospec(nr, NR_syscalls);
return sys_call_table[nr];
}
취약점 상태 확인
# 현재 시스템의 CPU 취약점 완화 상태 확인
for f in /sys/devices/system/cpu/vulnerabilities/*; do
echo "$(basename $f): $(cat $f)"
done
# 출력 예시:
# meltdown: Not affected (또는 Mitigation: PTI)
# spectre_v1: Mitigation: usercopy/swapgs barriers and __user pointer sanitization
# spectre_v2: Mitigation: Enhanced / Automatic IBRS; IBPB: conditional; RSB filling; ...
# mds: Not affected
# tsx_async_abort: Not affected
# mmio_stale_data: Not affected
# retbleed: Not affected
# spec_rstack_overflow: Not affected
# 완화책 비활성화 (벤치마크 용도, 프로덕션 금지!)
# 커널 커맨드라인: mitigations=off
mitigations=auto(기본값)는 CPU에 적합한 완화책을 자동 선택합니다. mitigations=auto,nosmt는 SMT(하이퍼스레딩)까지 비활성화하여 MDS/TAA 방어를 강화합니다. 벤치마크 결과 없이 mitigations=off를 프로덕션에 적용하지 마세요.
W^X 정책 (Write XOR Execute)
W^X 정책은 메모리 페이지가 쓰기 가능(W)과 실행 가능(X)을 동시에 가질 수 없도록 강제하는 보안 원칙입니다. 공격자가 커널 메모리에 쉘코드를 주입한 후 실행하는 것을 원천적으로 차단합니다.
| CONFIG 옵션 | 보호 대상 | 설명 |
|---|---|---|
CONFIG_STRICT_KERNEL_RWX | 커널 코드 영역 | 커널 텍스트 섹션을 읽기전용+실행, 데이터 섹션을 읽기쓰기+비실행으로 분리 |
CONFIG_STRICT_MODULE_RWX | 커널 모듈(Kernel Module) | 모듈의 코드/데이터 영역에 동일한 W^X 강제 |
CONFIG_DEBUG_WX | 디버깅 | 부팅 시 W+X 매핑 존재 여부를 스캔하여 경고 (디버그 빌드) |
/* arch/x86/mm/init.c — 커널 페이지 권한 분리 */
/* mark_rodata_ro(): init 이후 텍스트/rodata 영역을 읽기 전용으로 설정 */
void mark_rodata_ro(void)
{
/* .text → PAGE_KERNEL_ROX (읽기+실행, 쓰기 불가) */
/* .rodata → PAGE_KERNEL_RO (읽기 전용, 실행 불가) */
/* .data → PAGE_KERNEL (읽기+쓰기, 실행 불가) */
set_memory_ro((unsigned long)__start_rodata,
(unsigned long)__end_rodata);
set_memory_nx((unsigned long)__start_rodata,
(unsigned long)__end_rodata);
}
/* free_initmem(): init 섹션 메모리 해제 후 권한 회수 */
/* 부팅 후 사용하지 않는 __init 코드/데이터를 해제하여 공격 표면 축소 */
# W^X 위반 검사 (CONFIG_DEBUG_WX=y 필요)
dmesg | grep "W+X"
# x86/mm: Checked W+X mappings: passed, no W+X pages found.
# 커널 섹션별 권한 확인
cat /sys/kernel/debug/kernel_page_tables | head -30
# (debugfs 접근 가능 시)
HARDENED_USERCOPY
HARDENED_USERCOPY는 copy_from_user()/copy_to_user()에서 커널 메모리 객체의 경계를 검증하여, 잘못된 크기의 복사를 탐지하는 런타임 방어입니다. SLAB 객체 크기, 스택 범위, 텍스트/rodata 영역 검증을 수행합니다.
/* mm/usercopy.c — HARDENED_USERCOPY 핵심 로직 */
void __check_object_size(const void *ptr, unsigned long n,
bool to_user)
{
/* 1단계: 0 크기 → 무시 */
if (!n)
return;
/* 2단계: 커널 텍스트/rodata 영역 검증 */
if (overlaps(ptr, n, _stext, _etext))
usercopy_abort("kernel text", NULL, to_user, 0, n);
/* 3단계: 스택 범위 검증 */
if (check_stack_object(ptr, n) == BAD_STACK)
usercopy_abort("process stack", NULL, to_user, 0, n);
/* 4단계: SLAB 객체 경계 검증 */
if (check_heap_object(ptr, n, to_user))
return; /* 검증 통과 */
/* 5단계: vmalloc 영역 검증 */
if (is_vmalloc_addr(ptr) && check_vmalloc_object(ptr, n))
return;
}
/* SLAB 화이트리스트: usercopy 허용 오프셋/크기 */
/* kmem_cache_create_usercopy(name, size, align,
* flags, useroffset, usersize, ctor) */
/* useroffset~usersize 범위만 usercopy 허용 */
Usercopy 화이트리스트
커널 5.x부터 SLAB 캐시에 usercopy 화이트리스트 기능이 추가되었습니다. 구조체 내에서 사용자 공간으로 복사해도 안전한 영역만 명시적으로 지정합니다.
/* 화이트리스트 예시: pipe_buffer */
/* fs/pipe.c */
pipe_bufs = kmem_cache_create_usercopy(
"pipe_buffer",
sizeof(struct pipe_buffer),
0, /* align */
SLAB_ACCOUNT,
offsetof(struct pipe_buffer, data_offset),
sizeof((struct pipe_buffer){}.data_offset) +
sizeof((struct pipe_buffer){}.data_len),
NULL);
/* data_offset~data_len 범위만 copy_to_user 허용 */
/* 범위 밖(예: ops 포인터) 접근 시 BUG() */
CVE-2017-7895(nfsd)에서 발견된 버퍼 오버리드: NFSv2/v3 핸들러에서 copy_to_user()의 크기가 SLAB 객체 경계를 초과하여 커널 힙 메모리가 유출되었습니다. HARDENED_USERCOPY가 활성화된 커널에서는 이 복사가 BUG()로 즉시 차단됩니다.
SLAB/힙 하드닝
커널 힙(SLAB/SLUB) 할당자를 대상으로 한 공격을 완화하는 하드닝 옵션들입니다. Use-After-Free(UAF), 힙 스프레이, 프리리스트 포인터 오염 등의 공격을 어렵게 만듭니다.
/* 권장 SLAB 하드닝 설정 (.config) */
CONFIG_SLAB_FREELIST_RANDOM=y /* 프리리스트 순서 무작위화 */
CONFIG_SLAB_FREELIST_HARDENED=y /* 프리리스트 포인터 인코딩 */
CONFIG_RANDOM_KMALLOC_CACHES=y /* 다중 랜덤 캐시 (6.6+) */
/* === 메모리 초기화 === */
CONFIG_INIT_ON_ALLOC_DEFAULT_ON=y /* 힙 할당 시 0 초기화 */
CONFIG_INIT_ON_FREE_DEFAULT_ON=y /* 힙 해제 시 0 초기화 */
| CONFIG 옵션 | 방어 대상 | 동작 | 성능 영향 |
|---|---|---|---|
CONFIG_SLAB_FREELIST_RANDOM | 힙 스프레이 | SLUB freelist 순서를 랜덤화하여 할당 패턴 예측 불가 | 무시 가능 |
CONFIG_SLAB_FREELIST_HARDENED | freelist 포인터 오염 | freelist 포인터를 XOR 인코딩(주소 + random + pointer) | 무시 가능 |
CONFIG_RANDOM_KMALLOC_CACHES | cross-cache 공격 | 동일 크기 kmalloc에 대해 16개 랜덤 캐시 중 선택 | 1~2% |
CONFIG_INIT_ON_ALLOC_DEFAULT_ON | 정보 유출 | kmalloc/kzalloc 반환 시 0으로 초기화 | 1~3% |
CONFIG_INIT_ON_FREE_DEFAULT_ON | UAF 데이터 유출 | kfree 시 0으로 초기화 | 1~5% |
/* mm/slub.c — SLAB_FREELIST_HARDENED 핵심 메커니즘 */
/* freelist 포인터 인코딩: ptr XOR random XOR address */
static inline void *freelist_ptr(
const struct kmem_cache *s,
void *ptr, unsigned long ptr_addr)
{
/* 공격자가 freelist 포인터를 변조하면
* 디코딩 시 비정상 주소 → 즉시 크래시 */
return (void *)((unsigned long)ptr ^ s->random ^ ptr_addr);
}
CONFIG_KFENCE=y는 프로덕션 환경에서 약 1%의 오버헤드로 UAF와 OOB(Out-Of-Bounds) 접근을 샘플링 방식으로 감지합니다. KASAN과 달리 프로덕션 커널에서 상시 활성화할 수 있어, 하드닝 목적뿐 아니라 버그 조기 발견에도 유용합니다.
KFENCE/KASAN/KMSAN 메모리 안전성
커널은 메모리 안전성을 강화하기 위해 다양한 동적 검사 도구를 제공합니다. KASAN은 개발/테스트 환경에서 포괄적 탐지를, KFENCE는 프로덕션 환경에서 낮은 오버헤드로 샘플링 탐지를, KMSAN은 초기화되지 않은 메모리 사용을 탐지합니다.
# KFENCE 탐지 리포트 예시 (dmesg)
# ==================================================================
# BUG: KFENCE: out-of-bounds read in some_function+0x42/0x100
#
# Out-of-bounds read at 0xffff8880a1234560 (1 byte(s) past end of
# 64-byte region [0xffff8880a1234520, 0xffff8880a1234560))
#
# kfence-#42 [0xffff8880a1234520-0xffff8880a1234560, size=64,
# cache=kmalloc-64] allocated by task 1234:
# alloc_skb+0x40/0x80
# ...
# ==================================================================
# KFENCE 상태 확인
cat /sys/kernel/debug/kfence/stats
# enabled: 1
# sample_interval: 100
# total_bugs: 0
# total_allocs: 12345
페이지 할당자(Page Allocator) 하드닝
SLAB 하드닝 외에도 버디 시스템(페이지 할당자) 수준에서의 하드닝 옵션이 있습니다. 페이지 할당 패턴 예측을 어렵게 하고, 해제된 페이지의 데이터를 제거합니다.
| CONFIG 옵션 | 오버헤드 | 탐지 대상 | 환경 |
|---|---|---|---|
CONFIG_SHUFFLE_PAGE_ALLOCATOR | ~1% | 물리 주소(Physical Address) 예측 공격 | 프로덕션 |
CONFIG_INIT_ON_ALLOC_DEFAULT_ON | 1~3% | 정보 유출 | 프로덕션 |
CONFIG_INIT_ON_FREE_DEFAULT_ON | 1~5% | UAF 데이터 유출 | 프로덕션 (고보안) |
CONFIG_PAGE_POISONING | 3~5% | UAF (0xAA 패턴 검증) | QA/스테이징 |
CONFIG_DEBUG_PAGEALLOC | 10배+ | UAF (#PF 즉시 탐지) | 개발 전용 |
CONFIG_PAGE_TABLE_CHECK | ~1% | 이중 매핑 공격 | 프로덕션 (6.0+) |
GCC 보안 플러그인
GCC 컴파일러 플러그인을 통해 추가적인 보안 강화를 적용할 수 있습니다. 이 옵션들은 "선택(Nice)" 범주에 해당하지만, 최대 보안이 요구되는 환경에서 효과적입니다.
| CONFIG 옵션 | 기능 | 성능 영향 | 설명 |
|---|---|---|---|
CONFIG_GCC_PLUGIN_RANDSTRUCT | 구조체 레이아웃 랜덤화 | ~1% | 커널 구조체 멤버 순서를 빌드마다 랜덤화하여, 오프셋 기반 공격 차단 |
CONFIG_GCC_PLUGIN_LATENT_ENTROPY | 잠재 엔트로피 수집 | 무시 가능 | 컴파일 시 상수 연산에 랜덤 값 혼합, 커널 RNG 엔트로피 풀 보강 |
CONFIG_GCC_PLUGIN_STACKLEAK | 함수 반환 시 스택 클리어 | ~1% | 함수 반환 시 사용한 스택 프레임(Stack Frame)을 포이즌 값으로 덮어써 정보 유출 방지 |
/* GCC_PLUGIN_RANDSTRUCT 효과 (개념) */
/* 원본 구조체 */
struct cred {
atomic_t usage;
kuid_t uid, euid, suid, fsuid;
kgid_t gid, egid, sgid, fsgid;
/* ... */
};
/* RANDSTRUCT 적용 후 (빌드 A) */
/* struct cred {
* kgid_t fsgid; ← 순서 랜덤화
* kuid_t suid;
* atomic_t usage;
* kuid_t fsuid;
* ...
* }; */
/* 공격자가 offsetof(struct cred, uid)를 하드코딩하면
* 다른 빌드에서 오프셋이 달라져 공격 실패 */
/* GCC_PLUGIN_STACKLEAK 효과 */
/* 함수 에필로그에서 사용한 스택 영역을 STACKLEAK_POISON(0xBAAAAAAD)으로 덮음 */
/* → 이전 함수의 스택 데이터(커널 포인터 등)가 다음 함수에 노출되지 않음 */
CONFIG_RANDSTRUCT=y로 Clang 네이티브 지원이 제공됩니다 (커널 6.1+). STACKLEAK은 Clang에서도 CONFIG_GCC_PLUGIN_STACKLEAK을 통해 지원됩니다.
스택/힙 초기화 전략
초기화되지 않은 변수는 정보 유출(info leak)의 주요 원인입니다. 커널은 컴파일러와 런타임 양쪽에서 자동 초기화를 지원하여 이 공격 벡터를 제거합니다.
/* INIT_STACK_ALL_ZERO 효과 — 컴파일러가 삽입하는 코드 */
/* 원본 코드 */
int some_ioctl(struct file *filp, unsigned long arg)
{
struct my_info info; /* 미초기화 */
info.field1 = val1;
/* info.field2 미설정 → 이전 스택 데이터 잔류! */
copy_to_user((void __user *)arg, &info, sizeof(info));
}
/* INIT_STACK_ALL_ZERO 적용 후 (개념) */
int some_ioctl(struct file *filp, unsigned long arg)
{
struct my_info info = {}; /* 컴파일러가 자동 삽입 */
info.field1 = val1;
/* info.field2 = 0 (안전) */
copy_to_user((void __user *)arg, &info, sizeof(info));
}
ZERO_CALL_USED_REGS
함수 반환 직전에 호출 규약(Calling Convention)상 사용된 모든 레지스터를 0으로 클리어합니다. ROP/JOP 가젯이 레지스터에 남아있는 커널 포인터를 활용하는 것을 방지하고, 스택 이외의 레지스터 경로를 통한 정보 유출도 차단합니다.
/* CONFIG_ZERO_CALL_USED_REGS=y */
/* -fzero-call-used-regs=used-gpr (GCC 11+ / Clang 15+) */
/* 컴파일러가 함수 에필로그에 삽입하는 코드 (x86_64) */
/* 함수가 rax, rcx, rdx를 사용했다면: */
/* xor %ecx, %ecx ← rcx = 0 */
/* xor %edx, %edx ← rdx = 0 */
/* ret ← rax는 반환값이므로 유지 */
/* 효과: */
/* 1. ROP 가젯에서 레지스터 값 재사용 방지 */
/* 2. 레지스터에 남은 커널 포인터 유출 차단 */
/* 3. JIT 스프레이 공격에서 가젯 품질 저하 */
/*
* 오버헤드: ~1% (레지스터 xor만 추가)
* 보안 효과: 가젯 체인 구성 시 레지스터 초기화 단계 강제
*/
| 환경 | 스택 | 힙 | 레지스터 |
|---|---|---|---|
| 프로덕션 (표준) | INIT_STACK_ALL_ZERO | INIT_ON_ALLOC | - |
| 프로덕션 (고보안) | INIT_STACK_ALL_ZERO + STACKLEAK | INIT_ON_ALLOC + INIT_ON_FREE | ZERO_CALL_USED_REGS |
| 디버깅 | INIT_STACK_ALL_ZERO + STACKLEAK | INIT_ON_ALLOC + KMSAN | ZERO_CALL_USED_REGS |
하드닝 CONFIG 옵션 요약
| CONFIG 옵션 | 기능 | 오버헤드 |
|---|---|---|
CONFIG_RANDOMIZE_BASE | KASLR | 무시 가능 |
CONFIG_MITIGATION_PAGE_TABLE_ISOLATION | KPTI (Meltdown) | 1~5% |
CONFIG_MITIGATION_RETPOLINE | Spectre v2 완화 | ~2% |
CONFIG_STACKPROTECTOR_STRONG | 스택 canary | <1% |
CONFIG_VMAP_STACK | Guard page 스택 오버플로 감지 | 무시 가능 |
CONFIG_CFI_CLANG | 간접 호출 CFI | ~1% |
CONFIG_X86_KERNEL_IBT | FineIBT (CET) | <1% |
CONFIG_SHADOW_CALL_STACK | 리턴 주소 보호 (arm64) | <1% |
CONFIG_FORTIFY_SOURCE | 버퍼 오버플로 감지 | 무시 가능 |
CONFIG_HARDENED_USERCOPY | 사용자-커널 복사 검증 | <1% |
CONFIG_INIT_ON_ALLOC_DEFAULT_ON | 할당 시 0 초기화 | 1~3% |
CONFIG_INIT_ON_FREE_DEFAULT_ON | 해제 시 0 초기화 | 1~5% |
CONFIG_SLAB_FREELIST_RANDOM | Slab freelist 랜덤화 | 무시 가능 |
CONFIG_SLAB_FREELIST_HARDENED | Slab freelist 포인터 보호 | 무시 가능 |
CONFIG_STRICT_KERNEL_RWX | 커널 코드 W^X 강제 | 무시 가능 |
CONFIG_STRICT_MODULE_RWX | 모듈 코드 W^X 강제 | 무시 가능 |
KASLR/KPTI/CFI 통합 방어
KASLR(주소 배치 랜덤화), KPTI(커널 페이지 테이블 격리), CFI(Control-Flow Integrity)는 각각 독립된 공격 벡터를 차단하지만, 세 기술이 동시에 활성화될 때 공격 표면이 급격히 축소됩니다. 하나의 방어층이 우회되어도 나머지 기술이 익스플로잇 체인을 차단하는 종심 방어(defense in depth) 구조를 형성합니다.
공격 차단 범위 비교
| 방어 기술 | 차단 대상 | CONFIG 옵션 | 한계 |
|---|---|---|---|
| KASLR | 주소 예측 기반 ROP/JOP | CONFIG_RANDOMIZE_BASE | 정보 유출(info leak) 시 우회 가능 |
| KPTI | 사용자 공간에서 커널 메모리 읽기 (Meltdown) | CONFIG_PAGE_TABLE_ISOLATION | TLB flush 오버헤드, 커널 내부 공격엔 무효 |
| kCFI | 간접 호출 대상 변조 (JOP/COOP) | CONFIG_CFI_CLANG | 직접 호출/데이터 전용 공격엔 무효 |
| SMEP/SMAP | 커널이 사용자 코드 실행/데이터 접근 | CPU 기능 (CR4 비트) | 커널 공간(Kernel Space) 내 가젯엔 무효 |
상호 보완성
공격자가 ROP 체인을 구성하려면 (1) 커널 심볼 주소를 알아야 하고(KASLR이 차단), (2) 사용자 공간에서 커널 메모리를 읽으려면(KPTI가 차단), (3) 간접 호출 대상을 변조하려면(CFI가 차단) 모든 방어층을 동시에 우회해야 합니다.
kCFI 검증 흐름
/* Clang kCFI가 생성하는 간접 호출 검증 코드 (개념 예시) */
/* 컴파일러가 각 함수 앞에 타입 해시를 삽입 */
/* 함수 정의 앞에 .cfi_type_hash 삽입 */
/* __cfi_my_callback:
* .word 0xDEAD1234 ← 함수 시그니처의 타입 해시 */
void my_callback(struct sk_buff *skb) { ... }
/* 간접 호출 시 컴파일러가 삽입하는 검증 코드 */
static void dispatch(void (*fn)(struct sk_buff *), struct sk_buff *skb)
{
/* 컴파일러가 자동 삽입: fn[-1]의 해시 == 예상 해시 검증 */
/* 불일치 시 → __cfi_check_fail() → BUG() */
fn(skb); /* kCFI 검증 후 호출 */
}
CONFIG_RANDOMIZE_BASE=y + CONFIG_PAGE_TABLE_ISOLATION=y + CONFIG_CFI_CLANG=y + CONFIG_STRICT_KERNEL_RWX=y를 모두 활성화하세요. 개별 기술의 한계를 상호 보완하여 익스플로잇 체인 구성을 극도로 어렵게 만듭니다.
하드닝 성능 영향
| 옵션 | 성능 영향 | 보호 대상 | 권장 환경 |
|---|---|---|---|
FORTIFY_SOURCE | 1% 미만 | 버퍼 오버플로우 | 모든 환경 |
STACKPROTECTOR_STRONG | 1~2% | 스택 오버플로(Stack Overflow)우 | 모든 환경 |
INIT_STACK_ALL_ZERO | 1% 미만 | 정보 유출 | 모든 환경 |
HARDENED_USERCOPY | 1% 미만 | 커널↔유저 복사 오버런 | 모든 환경 |
CFI_CLANG | 1~3% | 제어 흐름 무결성 | 프로덕션 (Clang 빌드) |
KASAN | 2~3배 느림 | 메모리 오류 전체 | 테스트/개발 환경 전용 |
KPTI | 5~30% (시스콜 빈도 의존) | Meltdown 완화 | Intel CPU (필수) |
SLAB_FREELIST_HARDENED | 1% 미만 | 힙 익스플로잇 | 모든 환경 |
RANDOM_KMALLOC_CACHES | 1~2% | 힙 스프레이 | 6.6+ 프로덕션 |
성능 측정 방법: 하드닝 옵션 적용 전후로 fio(I/O), wrk(HTTP), sysbench(CPU/메모리) 벤치마크를 수행하세요. KPTI의 경우 시스템 콜 빈도가 높은 워크로드(DB, 웹서버)에서 영향이 크고, 계산 중심 워크로드(HPC)에서는 거의 무시 가능합니다.
커널 하드닝 체크리스트
프로덕션 환경에서 커널 보안을 강화하기 위한 체계적인 체크리스트입니다. CONFIG 옵션을 필수(Must), 권장(Should), 선택(Nice)으로 분류하고, 성능 영향을 함께 표기합니다.
CONFIG 옵션별 상세
| 분류 | CONFIG 옵션 | 기능 | 성능 영향 | 비고 |
|---|---|---|---|---|
| 필수 | CONFIG_RANDOMIZE_BASE | KASLR: 커널 주소 랜덤화 | 무시 가능 | x86_64 3.14+, arm64 4.6+ |
CONFIG_PAGE_TABLE_ISOLATION | KPTI: Meltdown 방어 | 1~5% (syscall 집중) | PCID 지원 CPU는 영향 최소 | |
CONFIG_STACKPROTECTOR_STRONG | 스택 카나리(Stack Canary) 검증 | ~0.5% | GCC 4.9+, Clang 지원 | |
CONFIG_FORTIFY_SOURCE | 버퍼 크기 검증 | ~0.5% | memcpy/strcpy 등 래핑 | |
CONFIG_STRICT_KERNEL_RWX | 커널 코드 W^X | 무시 가능 | 코드 영역 읽기전용 | |
CONFIG_VMAP_STACK | Guard page 스택 오버플로 감지 | 무시 가능 | x86_64 4.9+ 기본 활성 | |
| 권장 | CONFIG_INIT_ON_ALLOC_DEFAULT_ON | 할당 시 0 초기화 | 1~3% | 정보 유출 방지 |
CONFIG_SLAB_FREELIST_RANDOM | Slab 할당 순서 랜덤화 | 무시 가능 | heap spray 완화 | |
CONFIG_CFI_CLANG | kCFI 간접 호출 검증 | ~1% | Clang 16+ 필요 | |
CONFIG_SECURITY_LOCKDOWN_LSM | Lockdown 커널 보호 | 없음 | 디버깅 도구 제한됨 | |
CONFIG_RANDOMIZE_KSTACK_OFFSET | syscall 스택 오프셋 랜덤 | ~1% | 커널 5.13+ | |
| 선택 | CONFIG_INIT_ON_FREE_DEFAULT_ON | 해제 시 0 초기화 | 1~5% | UAF 정보 유출 방지 |
CONFIG_GCC_PLUGIN_RANDSTRUCT | 구조체 레이아웃 랜덤화 | ~1% | GCC 플러그인 필요 | |
CONFIG_GCC_PLUGIN_STACKLEAK | 함수 반환 시 스택 클리어 | ~1% | 정보 유출 방지 | |
CONFIG_INIT_STACK_ALL_ZERO | 스택 변수 0 초기화 | 1~3% | GCC 12+ / Clang | |
CONFIG_KFENCE | Kernel Electric Fence | ~1% | 프로덕션 버그 탐지 |
환경별 권장 프로파일
| 환경 | 필수 | 권장 | 선택 | mitigations | lockdown |
|---|---|---|---|---|---|
| 웹 서버 | 전체 | 전체 | INIT_ON_FREE | auto | integrity |
| 컨테이너(Container) 호스트 | 전체 | 전체 | KFENCE, STACKLEAK | auto | integrity |
| 금융/의료 | 전체 | 전체 | 전체 | auto,nosmt | confidentiality |
| HPC/성능 우선 | 전체 | 선별 | 비적용 | auto | none |
| IoT/임베디드 | 전체 | CFI, LOCKDOWN | 비적용 | auto | integrity |
| 개발/디버깅 | 전체 | KFENCE만 | STACKLEAK | auto | none |
CONFIG_KFENCE=y로 메모리 안전성 모니터링을 병행하세요. 하드닝 설정 변경 후에는 전체 워크로드 벤치마크(fio, wrk, sysbench 등)를 반드시 수행하세요.
런타임 sysctl 체크리스트
커널 하드닝은 빌드 시점(CONFIG) 옵션뿐 아니라 런타임 sysctl 설정으로도 강화해야 합니다. 다음은 프로덕션 환경에서 반드시 점검해야 할 sysctl 항목입니다.
| 분류 | 설정 | 확인 명령 | 권장값 |
|---|---|---|---|
| 커널 포인터 | kptr_restrict | sysctl kernel.kptr_restrict | 2 |
| dmesg 접근 | dmesg_restrict | sysctl kernel.dmesg_restrict | 1 |
| perf 접근 | perf_event_paranoid | sysctl kernel.perf_event_paranoid | 3 |
| BPF 접근 | unprivileged_bpf_disabled | sysctl kernel.unprivileged_bpf_disabled | 1 |
| userfaultfd | unprivileged_userfaultfd | sysctl vm.unprivileged_userfaultfd | 0 |
| User NS | max_user_namespaces | sysctl user.max_user_namespaces | 0 (불필요 시) |
| SysRq | sysrq | sysctl kernel.sysrq | 0 또는 176 |
| 코어 덤프(Core Dump) | core_pattern | sysctl kernel.core_pattern | 제한된 경로 |
| ASLR | randomize_va_space | sysctl kernel.randomize_va_space | 2 |
| ptrace | yama.ptrace_scope | sysctl kernel.yama.ptrace_scope | 1 이상 |
# 커널 하드닝 상태 종합 점검 스크립트
echo "=== 커널 하드닝 상태 점검 ==="
# sysctl 보안 설정 확인
for key in \
kernel.kptr_restrict \
kernel.dmesg_restrict \
kernel.perf_event_paranoid \
kernel.unprivileged_bpf_disabled \
kernel.yama.ptrace_scope \
kernel.randomize_va_space \
vm.unprivileged_userfaultfd; do
val=$(sysctl -n $key 2>/dev/null)
echo "$key = ${val:-NOT_FOUND}"
done
# CONFIG_ 옵션 확인
echo "=== 커널 빌드 보안 옵션 ==="
config=/boot/config-$(uname -r)
if [ -f "$config" ]; then
for opt in \
FORTIFY_SOURCE HARDENED_USERCOPY \
STACKPROTECTOR_STRONG CFI_CLANG \
INIT_STACK_ALL_ZERO SLAB_FREELIST_HARDENED \
RANDOMIZE_BASE PAGE_TABLE_ISOLATION \
SECURITY_LOCKDOWN_LSM KFENCE; do
result=$(grep "CONFIG_${opt}=" "$config" 2>/dev/null)
echo " ${opt}: ${result:-NOT_SET}"
done
fi
# 하드웨어 취약점 상태
echo "=== 하드웨어 취약점 완화 ==="
for f in /sys/devices/system/cpu/vulnerabilities/*; do
echo " $(basename $f): $(cat $f)"
done
실전 익스플로잇 체인과 하드닝 효과
실제 커널 익스플로잇은 단일 취약점이 아닌 여러 단계의 체인으로 구성됩니다. 각 하드닝 옵션이 체인의 어느 단계를 차단하는지 이해하면 방어 우선순위(Priority)를 정할 수 있습니다.
CVE-2022-0847(Dirty Pipe)은 메모리 손상이 아닌 논리 버그(플래그 미초기화)로 발생했습니다. KASLR, CFI, SMEP/SMAP, 힙 하드닝 등 대부분의 하드닝이 무효했습니다. 이는 하드닝이 만능이 아님을 보여주며, 코드 감사(audit), 퍼징(fuzzing), 정적 분석의 중요성을 강조합니다. INIT_ON_ALLOC이 활성화되었다면 플래그가 0으로 초기화되어 이 취약점이 차단되었을 가능성이 있습니다.
KSPP (Kernel Self Protection Project)
KSPP(Kernel Self Protection Project)는 커널 자체의 보안 강화를 목표로 하는 Linux Foundation 프로젝트입니다. 익스플로잇 완화 기술의 커널 메인라인 통합을 추진하며, 하드닝 옵션의 개발 로드맵을 제시합니다.
# KSPP 권장 옵션 자동 검증: kconfig-hardened-check 도구
pip install kconfig-hardened-check
# 현재 커널 설정 검사
kconfig-hardened-check -c /boot/config-$(uname -r)
# 출력 예시:
# CONFIG_RANDOMIZE_BASE | y | kspp | OK
# CONFIG_STACKPROTECTOR_STRONG | y | kspp | OK
# CONFIG_CFI_CLANG | y | kspp | FAIL: not set
# CONFIG_INIT_STACK_ALL_ZERO | y | kspp | FAIL: not set
# ...
# sysctl 설정도 함께 검사
kconfig-hardened-check -c /boot/config-$(uname -r) -l /proc/cmdline
자동 하드닝 검증 도구
수십 개의 CONFIG 옵션과 sysctl 설정을 수동으로 점검하는 것은 비효율적입니다. 자동화 도구를 활용하여 커널 하드닝 상태를 체계적으로 검증할 수 있습니다.
| 도구 | 검사 대상 | 특징 | 사용법 |
|---|---|---|---|
| kconfig-hardened-check | CONFIG, sysctl, 커널 커맨드라인 | KSPP/CLIP OS/GrapheneOS 권장 설정 기반 | pip install kconfig-hardened-check |
| lynis | 시스템 전체 보안 감사 | 커널 설정 + sysctl + 파일 퍼미션 + 서비스 | apt install lynis && lynis audit system |
| checksec | 바이너리/커널 보안 기능 | RELRO, PIE, NX, ASLR, Stack Canary | checksec --kernel |
| spectre-meltdown-checker | CPU 취약점 완화 상태 | Spectre V1/V2, Meltdown, MDS, TAA 등 | spectre-meltdown-checker.sh |
| Ansible hardening roles | sysctl + 서비스 + 설정 | CIS Benchmark, DISA STIG 준수 | ansible-galaxy install dev-sec.os-hardening |
# kconfig-hardened-check: 결과를 JSON으로 저장하여 CI 연동
kconfig-hardened-check -c /boot/config-$(uname -r) -m json > hardening-report.json
# spectre-meltdown-checker: CPU 취약점 종합 검사
curl -L https://meltdown.ovh -o spectre-meltdown-checker.sh
chmod +x spectre-meltdown-checker.sh
sudo ./spectre-meltdown-checker.sh
# checksec: 커널 보안 기능 상태
checksec --kernel
# 출력:
# GCC stack protector support: Enabled
# Strict user copy checks: Enabled
# Enforce read-only kernel data: Enabled
# Restrict /dev/mem access: Enabled
# Restrict /dev/kmem access: Enabled
# Ansible dev-sec.os-hardening 적용
ansible-playbook -i inventory hardening.yml
# sysctl.conf, modprobe.d, kernel 파라미터 일괄 강화
kconfig-hardened-check를 커널 빌드 파이프라인(Pipeline)에 통합하면, 새로운 커널 설정이 보안 기준을 충족하는지 자동 검증할 수 있습니다. JSON 출력을 파싱하여 FAIL 항목이 있으면 빌드를 실패 처리하는 게이트를 구성하세요.
배포판별 하드닝 비교
주요 리눅스 배포판은 커널 하드닝 기본 설정이 다릅니다. 배포판의 기본 하드닝 수준을 이해하고, 부족한 부분을 추가로 강화하는 전략이 필요합니다.
컨테이너 환경 커널 하드닝
컨테이너(Docker, Kubernetes)는 호스트 커널을 공유하므로, 컨테이너 탈출(container escape)은 곧 호스트 커널 익스플로잇을 의미합니다. 컨테이너 환경에서는 공격 표면을 축소하는 추가 하드닝이 필수입니다.
# 컨테이너 호스트 하드닝 sysctl 설정 (/etc/sysctl.d/99-container-hardening.conf)
# 비루트 BPF 차단 (컨테이너 내부에서 BPF 사용 방지)
kernel.unprivileged_bpf_disabled = 1
# userfaultfd 차단 (익스플로잇 레이스 윈도우 확대 방지)
vm.unprivileged_userfaultfd = 0
# 커널 포인터 완전 은닉
kernel.kptr_restrict = 2
kernel.dmesg_restrict = 1
# perf 이벤트 접근 차단 (사이드 채널 방지)
kernel.perf_event_paranoid = 3
# ptrace 제한 (디버거 연결 방지)
kernel.yama.ptrace_scope = 2
# User Namespace 비활성화 (필요 없는 경우)
# user.max_user_namespaces = 0
# 주의: Podman rootless 모드는 User NS가 필요
# SysRq 비활성화
kernel.sysrq = 0
gVisor/Kata Containers: 하드닝만으로 컨테이너 탈출을 완전히 방지할 수 없습니다. gVisor(사용자 공간 커널)는 호스트 커널 시스템 콜을 ~60개로 제한하고, Kata Containers는 각 컨테이너를 경량 VM에서 실행하여 커널 공격 표면을 근본적으로 축소합니다. 높은 보안이 요구되는 환경에서는 이러한 샌드박스(Sandbox) 런타임과 커널 하드닝을 병행하세요.
ARM64 아키텍처별 하드닝
ARM64는 x86_64와 다른 고유한 하드웨어 보안 기능을 제공합니다. 서버(Neoverse), 모바일(Cortex-A), IoT(Cortex-M) 환경에서 ARM64 커널 하드닝은 이러한 하드웨어 기능을 최대한 활용합니다.
PAC (Pointer Authentication Code) 상세
/* arch/arm64/include/asm/pointer_auth.h — PAC 커널 지원 */
/* PAC: 포인터에 암호학적 서명을 삽입하여 변조 탐지 */
/*
* 포인터의 상위 비트(TBI 영역)에 QARMA 암호 기반 MAC 삽입
* 5개 키: APIAKey, APIBKey, APDAKey, APDBKey, APGAKey
*
* 명령어:
* PACIA x30, sp — LR에 서명 (함수 진입)
* AUTIA x30, sp — LR 서명 검증 (함수 반환)
* RETAA — 인증 후 반환 (AUTIA + RET)
*
* 효과: ROP 공격 시 리턴 주소 변조 → PAC 검증 실패 → fault
* 커널에서는 per-task 키 + per-address-space 키 사용
*/
/* CONFIG_ARM64_PTR_AUTH_KERNEL=y */
/* 컴파일: -mbranch-protection=pac-ret+bti */
/* pac-ret: 리턴 주소 PAC 서명 (backward-edge) */
/* bti: BTI 명령 삽입 (forward-edge) */
ARM64 vs x86_64 보안 기능 비교: ARM64는 PAC(backward-edge)+BTI(forward-edge)+MTE(메모리 안전성)+GCS(하드웨어 shadow stack)를 하드웨어로 지원하여, x86_64의 CET(IBT+Shadow Stack)보다 포괄적인 보호를 제공합니다. 특히 MTE는 KASAN의 하드웨어 대체로, 프로덕션 환경에서 상시 활성화할 수 있는 유일한 메모리 안전성 도구입니다.
하드닝 우회 사례와 대응 진화
하드닝 기술은 공격자와의 군비 경쟁입니다. 새로운 방어가 도입될 때마다 우회 기법이 발견되고, 이에 대한 강화가 반복됩니다. 주요 우회 사례와 그에 따른 대응 진화를 이해하면 하드닝의 현재 위치와 한계를 파악할 수 있습니다.
| 세대 | 공격 기법 | 필요 조건 | 방어 기술 | 현재 상태 |
|---|---|---|---|---|
| 1세대 | ret2usr | 임의 코드 실행 | SMEP/SMAP | 완전 차단 |
| 2세대 | 커널 ROP | 가젯 주소 | KASLR + kCFI | 대부분 차단 |
| 3세대 | 정보유출→ROP | info leak | INIT_STACK/ALLOC, kptr_restrict | 경로 축소 |
| 4세대 | JOP/COOP | 함수 포인터 제어 | kCFI/FineIBT | 대부분 차단 |
| 5세대 | Data-only | 데이터 변조만 | MTE, RANDSTRUCT, 코드 감사 | 일부 탐지 |
| 6세대 | 논리 버그 | 설계 결함 | Rust, 형식적 검증, 퍼징 | 연구 중 |
현재 하드닝은 메모리 손상(memory corruption) 기반 공격에 매우 효과적이지만, 논리 버그(Dirty Pipe), Data-only 공격(modprobe_path 덮어쓰기), 사이드 채널(Spectre 변종)에는 근본적 한계가 있습니다. 장기적으로 Rust 커널 코드 확대, CHERI 능력 기반 포인터, 형식적 검증(formal verification)이 이러한 한계를 극복할 것으로 기대됩니다.
런타임 무결성 검증: 컴파일 타임 하드닝 외에 런타임에서 커널 코드/모듈 텍스트 해시와 프로세스(Process) 자격증명(cred)을 주기적으로 검증하는 도구로 LKRG (Linux Kernel Runtime Guard)가 있습니다. IDT/GDT/MSR 변조 탐지, SELinux 우회 탐지 등 하드닝 계층을 보완합니다.
관련 문서
커널 하드닝과 관련된 다른 주제를 더 깊이 이해하고 싶다면 다음 문서를 참고하세요.
외부 참고 자료
- Kernel Self Protection Project (KSPP) — 커널 자체 보호 프로젝트 추적 페이지입니다
- Kernel Self-Protection — Kernel Documentation — 커널 자체 보호 메커니즘 공식 문서입니다
- Hardware Vulnerabilities — Kernel Admin Guide — Spectre, Meltdown, MDS 등 하드웨어 취약점 공식 대응 가이드입니다
- Kernel Parameters — 보안 관련 부팅 파라미터(mitigations=, kpti=, randomize_kstack_offset= 등)를 포함합니다
- Clang CFI Documentation — kCFI의 기반인 Clang Control Flow Integrity 문서입니다
- GCC Instrumentation Options — Stack Protector, Shadow Call Stack 등 GCC 보안 계측 옵션입니다
- Kees Cook's Blog — KSPP 리더 Kees Cook의 블로그로 커널 하드닝 최신 동향을 다룹니다
- KSPP Issue Tracker — KSPP 보호 기능 구현 현황 및 추적 목록입니다
- Kernel sysctl — Admin Guide — randomize_va_space, dmesg_restrict, kptr_restrict 등 보안 sysctl 설정입니다
- grsecurity Blog — PaX/grsecurity 커널 하드닝 프로젝트의 기술 블로그입니다