# KernelSnitch[논문 리뷰]

[Paper](https://www.ndss-symposium.org/ndss-paper/kernelsnitch-side-channel-attacks-on-kernel-data-structures/)

---

## 1\. Intro

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

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

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

---

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

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

### 2\. 1 Futex(fast userspace mutex)란?

* 유저 공간의 빠른 동기화 매커니즘
    
* 대부분의 경우 커널 호출 없이 사용자 공간에서 처리.
    

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

```c
// kernel/futex/futex.h
struct futex_hash_bucket {
	atomic_t waiters;
	spinlock_t lock;
	struct plist_head chain;
} ____cacheline_aligned_in_smp;
```

```c
// 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 왜 취약한가?

```C
// 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**

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

**Case2: 5 elements**

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

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

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

### 2.3 사이클 측정하기

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

```c
// 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 해시 함수의 실제 위치와 동작

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

```c
// 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 비둘기집 원리로 증명되는 해시 충돌

```C
// 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개(시스템에 따라)
    

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

`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를 트리거하거나 임의 메모리 읽기/쓰기를 수행하거나 궁극적으로는 권한 상승을 달성할 수 있다.
