콘텐츠로 이동

STRUCTLEAK / INIT_STACK_ALL_ZERO: 커널 구조체 및 스택 미초기화 정보 누출 방어

1. 개요 및 배경

C 언어로 작성된 리눅스 커널에서 함수의 로컬 변수(스택 메모리)는 함수 진입 시 자동으로 초기화되지 않음. 개발자가 명시적으로 memset()이나 {0} 초기화 구문을 작성하지 않으면, 스택 프레임에는 이전 함수 호출이 남겨둔 잔여 데이터(커널 포인터, 스택 카나리, 암호화 키 등)가 고스란히 남아 있게 됨.

특히 커널 내부 구조체(Structure)는 다음과 같은 두 가지 심각한 보안 사각지대를 가짐: 1. 정렬 패딩 홀(Alignment Padding Hole)의 암묵적 미초기화: - 64비트 아키텍처(x86_64, aarch64)에서는 CPU 메모리 버스 정렬 규격(자연 정렬, Natural Alignment)에 따라 4바이트 정수 뒤에 8바이트 포인터가 올 경우, 컴파일러가 그 사이에 4바이트 패딩 공간을 자동으로 삽입함. - 개발자가 구조체의 모든 가시적 멤버(s.id = 1; s.ptr = p;)에 값을 대입하더라도, 컴파일러가 생성한 패딩 홀에는 값이 대입되지 않아 이전 스택의 오염된 메모리가 그대로 유지됨. - 이후 copy_to_user() 등을 통해 해당 구조체를 유저 공간으로 복사할 때, 패딩 홀에 숨겨진 커널 스택 주소나 카나리가 여과 없이 누출되어 KASLR 무력화 및 공격 페이로드 구성에 악용됨 (CVE-2013-2141 등). 2. 참조 전달(By-Reference) 시 컴파일러 경고 우회: - 스택에 선언된 구조체의 포인터를 서브루틴(helper_init(&info))에 전달하면, 컴파일러는 해당 함수가 구조체를 초기화할 것이라고 가정하여 미초기화 변수 경고(-Wmaybe-uninitialized)를 묵살함. - 서브루틴이 일부 필드만 초기화하고 반환하면, 나머지 필드는 스택 잔여 값을 유지한 채 유저 공간으로 전송됨 (CVE-2017-1000410 등).

리눅스 커널은 이러한 문제를 해결하기 위해 과거 PaX/Grsecurity에서 개발된 GCC 플러그인인 CONFIG_GCC_PLUGIN_STRUCTLEAK을 도입하였으며, 현대 커널(6.x LTS) 및 최신 툴체인(GCC 12+, Clang 12+)에서는 이를 네이티브 컴파일러 플래그로 표준화한 CONFIG_INIT_STACK_ALL_ZERO=y (-ftrivial-auto-var-init=zero)를 주력 방어선으로 채택함.


2. 실세계 비유: 이면지 양식과 기밀 누출

STRUCTLEAK 및 스택 자동 0 초기화의 방어 원리는 사무실의 이면지 재활용과 파쇄 규정에 비유할 수 있음:

[ 취약한 방식 (Baseline: CONFIG_INIT_STACK_NONE) ]
  이전 기밀 회의록(스택 잔여 데이터) 이면지 뒷면에 공문서 양식 인쇄
  ┌────────────────────────────────────────────────────────┐
  │ [헤더: 정상 작성] │ [여백(패딩): 기밀 암호 누출!] │ [본문: 정상]│
  └────────────────────────────────────────────────────────┘
                           │ (외부 민원인에게 그대로 전달)
                           ▼
              외부인이 빛에 비춰 기밀 탈취 성공!

[ 하드닝 방식 (Hardened: CONFIG_INIT_STACK_ALL_ZERO) ]
  공문서 작성 전 반드시 새 백지(0x00 소거)를 꺼내어 인쇄
  ┌────────────────────────────────────────────────────────┐
  │ [헤더: 정상 작성] │ [여백(패딩): 0x00 0x00 깨끗함] │ [본문: 정상]│
  └────────────────────────────────────────────────────────┘
                           │ (외부 민원인에게 전달)
                           ▼
                 안전: 여백에 기밀 잔존 전무!
  1. 전통적 커널 (이면지 공문서):
  2. 함수가 호출될 때 기존 스택 메모리(이면지)를 지우지 않고 그대로 로컬 변수로 할당함.
  3. 빈칸(멤버 변수)에만 새 내용을 적어 외부 민원인(유저스페이스)에게 교부함. 문서의 여백(패딩 홀)이나 채우지 않은 빈 항목에는 이전 기밀 회의록(커널 주소/카나리)이 그대로 남아 있어 외부인이 손쉽게 기밀을 역추적함.
  4. STRUCTLEAK / INIT_STACK_ALL_ZERO (새 백지 전용 정책):
  5. 함수가 시작되자마자(Prologue) 스택 프레임 전체를 하얀 백지(0x00)로 완전히 밀어버린 뒤 문서를 작성함.
  6. 개발자가 미처 채우지 못한 여백(패딩 홀)이나 서브루틴이 빠뜨린 항목이 있더라도, 그 자리는 항상 순수한 0x00이므로 외부로의 기밀 누출이 원천 차단됨.

3. 핵심 아키텍처 및 동작 원리

3.1 C 구조체 정렬 패딩(Alignment Padding Hole) 메커니즘

64비트 아키텍처에서 컴파일러는 메모리 접근 효율을 위해 필드의 시작 주소를 해당 타입의 크기 배수로 정렬함:

struct demo_padding_leak {
    uint32_t header;       /* 4 바이트 (오프셋 0..3) */
    /* [패딩 홀] 4 바이트 (오프셋 4..7) - 컴파일러가 8바이트 정렬을 위해 자동 삽입 */
    uint64_t timestamp;    /* 8 바이트 (오프셋 8..15) */
    char msg[16];          /* 16 바이트 (오프셋 16..31) */
};

위 구조체에서 개발자가 다음과 같이 모든 멤버를 성실하게 초기화하더라도:

struct demo_padding_leak s;
s.header = 0x44454d4f;
s.timestamp = get_timestamp();
strncpy(s.msg, "STATUS_OK", 16);
copy_to_user(user_buf, &s, sizeof(s));
C 언어 표준상 개별 멤버 대입 연산은 구조체 내부의 패딩 바이트를 초기화하지 않음. 따라서 오프셋 4..7의 4바이트 패딩 홀에는 직전 함수가 스택에 남긴 데이터가 고스란히 유저 공간으로 유출됨.

3.2 컴파일러 레벨 자동 0 초기화 (CONFIG_INIT_STACK_ALL_ZERO)

컴파일러 플래그 -ftrivial-auto-var-init=zero가 활성화되면, 컴파일러는 함수의 시작부(Prologue)에 로컬 스택 할당 영역 전체를 0으로 채우는 인스트럭션을 자동 삽입함.

ARM64 구현 어셈블리:

ARM64에서는 64비트 0 레지스터 xzr을 활용하여 128비트(16바이트) 단위로 스택을 초고속 소거함:

// 함수 프롤로그 (ARM64)
sub  sp, sp, #32             // 32바이트 스택 프레임 확보
stp  xzr, xzr, [sp]          // [sp..sp+15]를 0으로 소거
stp  xzr, xzr, [sp, #16]     // [sp+16..sp+31]를 0으로 소거
// 이후 개발자의 개별 멤버 대입 코드 실행

x86_64 구현 어셈블리:

x86_64에서는 SSE/AVX 128비트 레지스터(%xmm0)를 0으로 리셋 후 벡터 저장하거나 rep stosq를 실행함:

// 함수 프롤로그 (x86_64)
subq  $32, %rsp              // 32바이트 스택 확보
xorps %xmm0, %xmm0           // %xmm0 레지스터를 0으로 초기화
movaps %xmm0, (%rsp)         // [%rsp..%rsp+15]에 0 기록
movaps %xmm0, 16(%rsp)       // [%rsp+16..%rsp+31]에 0 기록

이로써 패딩 홀을 포함한 모든 스택 바이트가 사전에 0x00으로 정화된 후 멤버 대입이 이루어지므로, 패딩 누출이 원천적으로 불가능해짐.

3.3 STRUCTLEAK 플러그인과의 역사적 진화

구분 레거시 CONFIG_GCC_PLUGIN_STRUCTLEAK 현대 CONFIG_INIT_STACK_ALL_ZERO
기반 기술 GCC 전용 외부 플러그인 (PaX/Grsecurity 포팅) 컴파일러 네이티브 옵션 (-ftrivial-auto-var-init=zero)
초기화 범위 _USER: __user 구조체만
_BYREF: 참조 전달 구조체
_BYREF_ALL: 참조 전달 모든 변수
스택 상의 모든 로컬 변수, 배열, 구조체 및 정렬 패딩 전체
지원 컴파일러 GCC 전용 (플러그인 헤더 필요) GCC 12+ 및 Clang 12+ 네이티브 지원
커널 채택 상태 구버전 커널에서 사용 (6.x부터 네이티브로 이관) 최신 프로덕션 배포판 (Android, ChromeOS, Cloud Linux) 표준

4. 인터랙티브 아키텍처 다이어그램

스택 정렬 패딩 홀의 정보 누출과 컴파일러 자동 0 초기화 방어 흐름을 직관적으로 시뮬레이션한 대화형 다이어그램임:


5. 실습 및 공격/방어 시연

5.1 취약점 실습 드라이버 (vuln_structleak.c)

/proc/vuln_structleak (모드 0666) 노드를 노출하여 두 가지 핵심 누출 시나리오를 제공함: - 모드 1 (echo 1 > /proc/vuln_structleak): - struct demo_padding_leak (32바이트)를 스택에 할당. - 사전에 poison_kernel_stack()으로 스택 프레임에 0x53544b5f4c45414b ("STK_LEAK") 패턴을 주입. - 가시적 멤버(header, timestamp, msg)는 모두 할당하되 4바이트 패딩 홀은 방치. - copy_to_user()로 유저 공간에 복사. - 모드 2 (echo 2 > /proc/vuln_structleak): - struct demo_byref_leak (48바이트)를 스택에 할당 후 서브루틴에 포인터 전달. - 서브루틴은 command와 status만 할당하고 민감 비밀 필드와 카나리 영역은 방치.

5.2 비특권 사용자 익스플로잇 PoC (exploit.c)

일반 사용자 lab (UID 1000) 권한으로 구동되어 구조체 바이트 덤프를 분석함:

# PoC 실행 (게스트 내부)
/bin/exploit_structleak
- 베이스라인 (Vulnerable): - 패딩 홀(Offset 4..7)에서 0x53544b5f 독성 스택 마커가 검출됨 ([VULNERABILITY DETECTED]). - By-Reference 미초기화 필드에서 잔여 커널 스택 데이터가 검출됨. - 익스플로잇 종료 코드: 42 (취약점 확인). - 하드닝 (Protected): - 패딩 홀 및 미초기화 필드가 모두 완벽하게 0x00000000으로 채워짐 ([PROTECTION ACTIVE]). - 익스플로잇 종료 코드: 0 (보안 무결성 통과).


6. 듀얼 아키텍처 검증 매트릭스

아키텍처 커널 설정 패딩 홀(Offset 4..7) By-Ref 미초기화 필드 판정 결과
ARM64 CONFIG_INIT_STACK_ALL_ZERO=y 🛡️ 0x00000000 (완전 소거) 🛡️ 0x00000000 (완전 소거) ✅ PASS (보안 통과)
ARM64 CONFIG_INIT_STACK_NONE=y ❌ 0x53544b5f (스택 누출) ❌ 오염 데이터 유출 ⚠️ FAIL (취약점 노출)
x86_64 CONFIG_INIT_STACK_ALL_ZERO=y 🛡️ 0x00000000 (완전 소거) 🛡️ 0x00000000 (완전 소거) ✅ PASS (보안 통과)
x86_64 CONFIG_INIT_STACK_NONE=y ❌ 0x53544b5f (스택 누출) ❌ 오염 데이터 유출 ⚠️ FAIL (취약점 노출)

7. 프로덕션 보안 가이드라인 및 트레이드오프

  1. 성능 오버헤드:
  2. CONFIG_INIT_STACK_ALL_ZERO는 모든 함수 호출 시 스택 0 초기화 인스트럭션을 동반하므로 마이크로 벤치마크 기준 약 0.5% ~ 1.5%의 CPU 오버헤드가 발생함.
  3. 그러나 SIMD 벡터 레지스터(stp xzr / movaps)를 활용한 초고속 소거와 현대 CPU의 Store Buffer 최적화 덕분에, 실제 워크로드(웹서버, 데이터베이스, 모바일)에서의 체감 성능 영향은 무시할 수 있는 수준임.
  4. 패턴 초기화(INIT_STACK_ALL_PATTERN) 대비 우수성:
  5. 디버깅 목적의 0xAA / 0xFF 패턴 초기화는 포인터 역참조 시 패닉을 빠르게 유발하는 장점이 있으나, 프로덕션에서는 잘못된 포인터 역참조로 인한 DoS 공격을 유발할 수 있음.
  6. 반면 0 초기화(INIT_STACK_ALL_ZERO)는 문자열 널 종료(\0), 널 포인터(NULL), 인덱스 0, 길이 0 등 안전한 기본값을 제공하므로 프로덕션 보안 완화책으로 가장 안전함.
  7. 권장 설정:
  8. 모든 보안 강화 커널 빌드에서 CONFIG_INIT_STACK_ALL_ZERO=y를 필수 기본값으로 적용 권장함.