Skip to main content

Command Palette

Search for a command to run...

KernelSnitch[논문 리뷰]

Updated
7 min read

Paper


1. Intro

이 글은 NDSS 2025에서 발표된 KernelSnitch 논문을 소개이다. 이 연구는 커널의 평범한 데이터 구조체들이 가진 본질적인 특성이 어떻게 심각한 보안 취약점이 되는지를 보여준다.

핵심은 이러하다: "데이터 구조체의 크기에 따른 접근 시간 차이를 이용해 커널의 비밀 정보를 유출할 수 있다"

여기서는 커널 힙 포인터 유출에 집중해서 설명한다. 이 공격이 성공하면 KASLR을 우회하고 더 심각한 커널 익스플로잇의 발판이 될 수 있다.


2. 공격 대상 자료구조와 취약성 분석

Hash Table - Futex를 중심으로 먼저 가장 중요한 공격 대상인 futex 해시 테이블을 살펴보자.

2. 1 Futex(fast userspace mutex)란?

  • 유저 공간의 빠른 동기화 매커니즘

  • 대부분의 경우 커널 호출 없이 사용자 공간에서 처리.

아래와 같이 커널에 구현되어있다.

// kernel/futex/futex.h
struct futex_hash_bucket {
    atomic_t waiters;
    spinlock_t lock;
    struct plist_head chain;
} ____cacheline_aligned_in_smp;
// kernel/futex/core.c
static struct {
    struct futex_hash_bucket *queues;
    unsigned long            hashmask;
} __futex_data __read_mostly __aligned(2*sizeof(long));
#define futex_queues   (__futex_data.queues)
#define futex_hashmask (__futex_data.hashmask)

2.2 왜 취약한가?

// kernel/futex/core.c
// line 443~452
struct futex_q *futex_top_waiter(struct futex_hash_bucket *hb, union futex_key *key)
{
    struct futex_q *this;
    // 리스트의 모든 요소를 순회하는 매크로
    plist_for_each_entry(this, &hb->chain, list) {
        if (futex_match(&this->key, key))
            return this;
    }
    return NULL;
}

futex의 futex_top_waiter함수를 보면 리스트의 요소 개수에 따라 실행 명령어의 수가 달라질 수 있다는 것을 알 수 있다.

Case1: Empty Bucket

1. plist_for_each_entry 시작
2. 리스트가 비어있음 확인 (head->next == head)
3. 즉시 루프 종료
4. NULL 반환
실행 명령어: ~10개

Case2: 5 elements

1. plist_for_each_entry 시작
2. 첫 번째 요소 접근
   - this 포인터 로드
   - futex_match 호출 (key 비교)
   - 일치하지 않음 → 계속
3. 두 번째 요소 접근
   - 동일한 과정 반복
4. ... 5번째까지 반복
5. NULL 반환
실행 명령어: 10 + (8 × 5) = 50개

와 같다. 결과적으로 요소의 개수에 따라 실행 시간에 차이가 발생할 것임을 알 수 있다.

이 실행 시간 차이가 공격의 시작점이 된다.

2.3 사이클 측정하기

futex_wake의 코드를 살펴보면 다음과 같다.

// kernel/futex/waitwake.c
int futex_wake(u32 __user *uaddr, unsigned int flags, int nr_wake, u32 bitset) {
...
    plist_for_each_entry_safe(this, next, &hb->chain, list) {
        if (futex_match (&this->key, &key)) {
            if (this->pi_state || this->rt_waiter) {
                ret = -EINVAL;
                break;
            }

            /* Check if one of the bits is set in both bitsets */
            if (!(this->bitset & bitset))
                continue;

            this->wake(&wake_q, this);
            if (++ret >= nr_wake)
                break;
        }
    }

futex_wake는 전체 리스트를 순회하고, 특별한 권한 없이도 호출 가능하므로 좋은 실행 시간 측정 도구가 될 수 있다.

2.4 공격하기(커널 주소 유출)

공격의 목표는 mm_struct의 커널 주소이다. mm_struct는 프로세스의 메모리 관리 정보를 담고 있고, 이 주소를 알면 KASLR을 우회할 수 있다.

공격은 두 단계로 이루어지는데,

  1. 해시 충돌 감지 (같은 버킷에 들어가는 주소들 수집)

  2. 역해싱으로 커널 주소 추정 (수집한 정보로 mm_struct 주소 계산) 의 과정으로 진행된다.

2.4.1 Futex 해시 함수의 실제 위치와 동작

실제 커널 코드에서 해시 함수는 다음과 같다.

// kernel/futex/core.c
struct futex_hash_bucket *futex_hash(union futex_key *key)
{
    // key 전체를 해시값으로 변환하고
    u32 hash = jhash2((u32 *)key, offsetof(typeof(*key), both.offset) / 4,
              key->both.offset);
    // 해시값을 버킷 번호로 변환한다.
    return &futex_queues[hash & futex_hashmask];
}

여기서 공격의 첫 단계로 해시 충돌을 일으킨다.

2.4.2 비둘기집 원리로 증명되는 해시 충돌

// kernel/futex/core.c
// line 55. 56
#define futex_queues   (__futex_data.queues)
#define futex_hashmask (__futex_data.hashmask)

가능한 futex_queues는 CPU 개수에 종속적으로 결정된다.

따라서 비둘기집의 원리를 적용하면

  • 가능한 사용자 주소: $2^{48}$ 개

  • 버킷 수: 256 ~ 4096개(시스템에 따라)

가능한 사용자 주소 수 > 버킷 수에 따라 같은 버킷을 사용하는 서로 다른 사용자 주소의 존재성이 증명된다.

futex 해시 테이블은 uaddr(유저 주소)와 mm_struct을 이용하여 해시값을 생성하므로, 서로 다른 uaddr에 대해 같은 mm_struct를 사용했을 때 해시 충돌이 발생하는 조합을 찾는다.

여기서 해시 충돌의 발생 조합을 찾는 과정에서 논문이 제시하는 KernelSnitch가 사용된다. 이를 통해 직접 커널 내부를 보지 않고도 해시 충돌 조합을 구할 수 있다.


3. KernelSnitch의 원리

KernelSnitch는 캐시 사이드 채널을 이용해 커널 데이터 구조의 메모리 접근 여부를 사용자 공간에서 추론하는 도구다. 즉 커널이 특정 주소를 읽거나 썼는지를 CPU 캐시 상태를 통해 간접적으로 관찰할 수 있게 해준다.

기본 아이디어: Flush+Reload로,

공격자는 다음과 같은 과정을 통해 커널의 접근 여부를 관찰할 수 있다.

  1. 특정 구조체 필드가 위치한 주소를 clflush 명령어로 캐시에서 제거한다.

  2. 해당 구조체를 접근하도록 커널을 유도한다. (예: futex 호출)

  3. 다시 사용자 공간에서 해당 주소를 읽고 접근 시간을 측정한다.

    • 캐시 접근이면 빠르게 로드됨 → 커널이 접근한 것

    • 메모리 접근이면 느리게 로드됨 → 커널이 접근하지 않은 것

이와 같은 방법으로 커널 내부 구조체에 대한 접근 여부를 사용자 공간에서 추론할 수 있다.

구체적으로는 다음과 같다.

① 해시 충돌 탐지 시 Occupancy 측정 (Section: B. Kernel Heap Pointer Leak)

해당 공격에서 시간 측정은 다음과 같은 "간접적인 occupancy(버킷의 리스트 길이)" 측정 도구로 사용된다.

인용:

A low occupancy level, such as for the user identifier uaddrY, indicates a different hash bucket.
Conversely, a higher occupancy level, e.g., uaddrZ, means that the values futex_hash(uaddrA, mmA)
and futex_hash(uaddrZ, mmA) match.

의미:

  • 공격자가 uaddrA로 채운 버킷과 uaddrZ를 조합하여 충돌 여부를 확인한다.

  • 이때, 커널이 리스트를 몇 개 순회하는지를 futex_wake() 같은 인터페이스로 호출하고,
    호출 시간이 오래 걸리는지로 충돌 여부를 추론한다.

  • 즉, "같은 버킷이라면 요소가 많아지고 시간이 길어진다" 는 점을 이용하는 것이다.

요약:

  • 더 오래 걸리면 → 같은 해시 버킷 → 충돌 발생

  • 짧게 끝나면 → 다른 버킷 → 충돌 아님

② Flush+Reload로 구조체 필드 접근 여부 측정 (Section: KernelSnitch architecture)

공격자는 구조체의 특정 필드를 대상으로 다음과 같은 흐름을 수행한다:

인용:

We clflush a field of a kernel structure, invoke the syscall that may access it,
and then reload the same memory to measure access time.

의미:

  • clflush → 커널 구조체 필드를 CPU 캐시에서 제거한다.

  • 시스템콜 유도 (futex_wake() 등) → 커널이 해당 필드를 접근하면 다시 캐시에 올라간다.

  • 사용자 공간에서 해당 주소를 다시 읽고 접근 시간 측정:

    • 캐시에 올라가 있으면 → 빠르게 읽힘 → 커널이 접근했다는 뜻

    • 메모리 접근이면 → 느리게 읽힘 → 접근하지 않은 것

요약:

  • 접근 시간 짧음 → 캐시에 있음 → 커널이 접근함

  • 접근 시간 김 → 메모리 접근 → 커널이 접근하지 않음


4. 역계산

다시 본론으로 돌아와서 해시 충돌에 대한 충분한 정보를 얻었으므로 역계산을 통해 mm_struct를 알아낼 차례다.

해시 함수는 단방향 함수이므로 역계산을 하는 것은 어려워보이나, 위 과정으로부터 해시 함수의 알고리즘(jhash2)를 알고 있고 많은 사용자주소-버킷번호의 매핑을 수집했고, 해시 함수의 입력이 사용자 주소와 mm_struct 주소의 조합이라는 것도 알고 있다.

먼저 수집된 충돌 데이터를 정리한다. 예를 들어 주소 0x1000이 버킷 42로, 주소 0x3000도 버킷 42로 매핑되었다면, 이 두 주소는 같은 mm_struct와 조합될 때 같은 해시값을 만든다는 의미이다. 이런 식으로 10만 개의 충돌 데이터를 버킷별로 그룹화하면, 각 버킷에는 수백 개의 주소가 모인다.

다음으로 mm_struct가 위치할 수 있는 주소 공간을 이해해야 한다. x86_64 리눅스에서 커널 힙은 Direct Physical Mapping(DPM) 영역을 통해 접근되는데, 이 영역은 0xffff888000000000부터 0xffffc87fffffffff까지이다.

하지만 mm_struct는 아무 주소에나 할당되지 않는다. 슬랩 할당자의 규칙에 따라 8페이지(32KB) 단위로 정렬된 슬랩에 할당되며, 각 슬랩에는 23개의 mm_struct 객체가 들어간다.

이러한 제약 조건들을 활용하면 검색 공간을 2^46에서 2^35.5로 대폭 줄일 수 있다.

이제 실제 역계산을 수행한다. 가능한 모든 슬랩 베이스 주소를 순회하면서, 각 슬랩 내의 23개 위치에 대해 mm_struct 후보 주소를 생성한다. 각 후보 주소에 대해, 우리가 수집한 모든 충돌 패턴이 설명되는지 검증한다. 예를 들어 버킷 42에 속한 모든 주소들이 이 mm_struct 후보와 조합될 때 정말로 버킷 42로 해싱되는지 확인하는 것이다.

검증 과정은 매우 단순하다. 후보 mm_struct 주소와 사용자 주소로 futex_key를 재현하고, jhash2 함수로 해시값을 계산한 뒤, futex_hashmask와 AND 연산하여 버킷 번호를 구한다. 이 버킷 번호가 우리가 관찰한 버킷 번호와 일치하는지 확인한다. 모든 충돌 패턴이 일치하는 mm_struct 주소를 찾으면, 그것이 바로 우리가 찾던 커널의 정보다.

Cross-Cache Reuse로 공격 확장

위 과정을 통해 mm_struct의 주소를 얻었다. 그럼 이제 Cross-Cache Reuse를 통해 더 위험한 공격을 수행할 수 있다. 이는 커널의 메모리 관리 메커니즘을 악용하는 기법이다.

리눅스 커널은 효율적인 메모리 관리를 위해 슬랩 할당자(SLUB)을 사용하는데 슬랩 할당자는 같은 크기와 타입의 객체들을 미리 할당된 메모리 슬랩에서 관리한다. 예를 들면 mm_struct 전용 캐시같은 것이다. Cross-Cache Reuse의 핵심은 한 캐시에서 해제된 메모리를 다른 캐시에서 재할당받는 것이다.

이를 수행하려면, 먼저 유출된 mm_struct에서 슬랩 정보를 추출한다. mm_struct를 32KB(8 페이지)로 정렬하면 슬랩 베이스 주소를 얻을 수 있다. 이 슬랩에는 23개의 mm_struct 객체가 들어있는데, 우리의 목표는 이 슬랩 전체를 해제하는 것이다.

슬랩을 해제하는 방법은 여러 가지가 있는데 가장 직접적인 방법으로 해당 슬랩의 모든 mm_struct를 사용하는 프로세스들을 종료시키는 것이 있다.

슬랩이 해제되면 이제 같은 크기의 객체로 재할당받아야 한다. 그런데 여기서 중요한 것은 mm_struct와 같은 할당자 캐시를 사용하는 객체를 선택하는 것이다. msg_msg가 같은 할당자 캐시를 사용하기 때문에, 이를 할당한다.

이제 msg_msg의 주소도 알아냈으며, 이를 제어할 수 있다. msg_msg를 통해 use-after-free를 트리거하거나 임의 메모리 읽기/쓰기를 수행하거나 궁극적으로는 권한 상승을 달성할 수 있다.

131 views

More from this blog

락프리 데이터 구조와 알고리즘

여기서는 락프리 데이터 구조를 설명한다. 락프리(lock-free) 란 배타락을 이용하지 않고 처리를 수행하는 데이터 구조 및 그에 대한 조작 알고리즘을 총칭한다. 왜 락프리인가? 전통적인 동시성 제어 방법인 뮤텍스나 세마포어는 여러 문제점을 가지고 있다: 성능 저하: 락 경합(lock contention)으로 인한 대기 시간 데드락: 여러 스레드가 서로의 락을 기다리는 상황 우선순위 역전: 낮은 우선순위 스레드가 높은 우선순위 스레드를 ...

Jul 27, 20257 min read126

소프트웨어 트랜잭셔널 메모리

소프트웨어 트랜잭셔널 메모리 동시성 프로그래밍에서 공유 자원에 대한 안전한 접근은 항상 중요한 과제다. 전통적으로 뮤텍스 락과 같은 비관적 락(Negative Lock) 방식을 사용해왔다. 이 방식은 크리티컬 섹션에 진입하기 전에 반드시 락을 획득해야 하며, 락을 얻지 못하면 코드 실행 자체가 블록된다. 하지만 이와는 다른 접근 방식이 있다. 바로 낙관적 락(Optimistic Lock) 방식인데, 이는 "일단 실행하고 나중에 검증하자"는 철학...

Jul 20, 202517 min read263

공평한 배타 제어

공평한 배타 제어 여기서는 공평한 배타 제어에 대해 설명한다. 먼저 컨텐션(contention) 이라는 개념을 이해할 필요가 있다. 컨텐션이란 여러 스레드가 동시에 같은 락을 획득하려고 경쟁하는 상황을 말한다. 컨텐션이 높을수록 스레드들이 락을 기다리는 시간이 길어지고 성능이 저하된다. 이러한 컨텐션 상황은 시스템 아키텍처에 따라 더욱 복잡해질 수 있다. 특히 비균일 메모리 접근(Non-Uniform Memory Access, NUMA) 와 같...

Jul 13, 20259 min read21

멀티태스크와 액터 모델

멀티태스크 협조적/비협조적 멀티태스크 선점: 프로세스와의 협조 없이 수행하는 컨택스트 스위칭이라고는 하나, 결국 뺏어오는 게 가능하냐의 문제다. 협조적 멀티태스크(비선점형, cooperative): 각각의 프로세스가 자발적으로 컨택스트 스위칭을 수행하는 멀티태스크 방식. 장점: 멀티태스크 매커니즘을 구현하기 쉽다. 단점: 프로세스가 자발적으로 컨텍스트 스위칭을 해야하는데, 만약 버그가 발생하여 프로세스가 무한 루프에 빠지거나 정지하게 되면 그 ...

Jul 6, 20252 min read25
M

MaxLog

35 posts