
CVE-2023-3269: Linux 커널 권한 상승 취약점
Linux 커널 6.1부터 6.4까지 스택 확장 처리에서 발견된 결함으로, 일명 "Stack Rot"이라고 합니다. 가상 메모리 영역을 관리하는 메이플 트리(maple tree)가 MM 쓰기 잠금(MM write lock)을 제대로 획득하지 않은 채 노드 교체를 수행할 수 있어 use-after-free 문제가 발생합니다. 권한이 없는 로컬 사용자가 이 결함을 이용하여 커널을 손상시키고 권한을 상승시킬 수 있습니다.
StackRot는 메모리 관리 하위 시스템에서 발견된 Linux 커널 취약점이므로 거의 모든 커널 구성에 영향을 미치며, 트리거하는 데 최소한의 권한만 필요합니다. 그러나 메이플 노드가 RCU 콜백을 사용하여 해제되므로 실제 메모리 할당 해제가 RCU 유예 기간(grace period) 이후까지 지연된다는 점에 유의해야 합니다. 결과적으로 이 취약점을 악용하는 것은 어려운 것으로 간주됩니다.
현재까지 제가 아는 한, RCU에 의한 use-after-free(UAFBR) 버그를 대상으로 하는 공개적으로 이용 가능한 익스플로잇은 없습니다. 이번이 CONFIG_PREEMPT 또는 CONFIG_SLAB_MERGE_DEFAULT 설정이 없는 환경에서도 UAFBR 버그가 악용 가능하다는 것이 입증된 첫 번째 사례입니다. 특히 이 익스플로잇은 Google kCTF VRP (bzImage_upstream_6.1.25, config)에서 제공하는 환경에서 성공적으로 시연되었습니다.
StackRot 취약점은 VMA 트리 구조가 레드-블랙 트리에서 메이플 트리로 변경된 Linux 커널 버전 6.1부터 존재해 왔습니다.
mmap() 시스템 콜을 사용하여 메모리 매핑을 설정할 때마다 커널은 해당 가상 메모리 영역(VMA)을 나타내는 vm_area_struct라는 구조체를 생성합니다. 이 구조체는 매핑과 관련된 플래그, 속성 및 기타 관련 세부 정보를 포함한 다양한 정보를 저장합니다.```c
struct vm_area_struct {
long unsigned int vm_start; /* 0 8 /
long unsigned int vm_end; / 8 8 /
struct mm_struct * vm_mm; / 16 8 /
pgprot_t vm_page_prot; / 24 8 /
long unsigned int vm_flags; / 32 8 /
union {
struct {
struct rb_node rb attribute((aligned(8))); / 40 24 /
/ --- cacheline 1 boundary (64 bytes) --- /
long unsigned int rb_subtree_last; / 64 8 /
} attribute((aligned(8))) shared attribute((aligned(8))); / 40 32 /
struct anon_vma_name * anon_name; / 40 8 /
} attribute((aligned(8))); / 40 32 /
/ --- cacheline 1 boundary (64 bytes) was 8 bytes ago --- /
struct list_head anon_vma_chain; / 72 16 /
struct anon_vma * anon_vma; / 88 8 /
const struct vm_operations_struct * vm_ops; / 96 8 /
long unsigned int vm_pgoff; / 104 8 /
struct file * vm_file; / 112 8 /
void * vm_private_data; / 120 8 /
/ --- cacheline 2 boundary (128 bytes) --- /
atomic_long_t swap_readahead_info; / 128 8 /
struct vm_userfaultfd_ctx vm_userfaultfd_ctx; / 136 0 */
/* size: 136, cachelines: 3, members: 14 */
/* forced alignments: 1 */
/* last cacheline: 8 bytes */
} attribute((aligned(8)));
그 후, 커널은 페이지 폴트나 기타 메모리 관련 시스템 호출을 처리할 때 주소만으로 VMA를 빠르게 조회해야 합니다. 이전에는 VMA가 레드-블랙 트리(red-black trees)로 관리되었습니다. 그러나 Linux 커널 버전 6.1부터는 메이플 트리(maple trees)로의 전환이 이루어졌습니다. [메이플 트리][mt]는 겹치지 않는 범위를 저장하도록 최적화된 RCU-안전 B-트리 데이터 구조입니다. 그럼에도 불구하고, 이들의 복잡한 특성은 코드베이스에 복잡성을 더하고 StackRot 취약점을 유발합니다.
[mt]: https://docs.kernel.org/6.4/core-api/maple_tree.html
핵심적으로, 메이플 트리는 메이플 노드들로 구성됩니다. 트리의 구조는 복잡할 수 있지만, 이러한 복잡성은 StackRot 버그와 아무 관련이 없다는 점에 유의해야 합니다. 따라서 이 글 전체에서는 메이플 트리가 단 하나의 노드, 즉 루트 노드로만 구성되어 있다고 가정합니다.
이 루트 노드는 최대 16개의 구간을 포함할 수 있습니다. 이 구간들은 각각 갭(gap)을 나타내거나 VMA를 가리킬 수 있습니다. 갭도 구간으로 간주되므로 모든 구간은 순차적으로 연결되며, 그 결과 노드 구조 내에는 피벗(pivot)이라고도 알려진 15개의 끝점만 필요합니다. 맨 왼쪽 끝점과 맨 오른쪽 끝점은 부모 노드에서 가져올 수 있으므로 생략된다는 점에 유의하세요.```c
struct maple_range_64 {
struct maple_pnode * parent; /* 0 8 */
long unsigned int pivot[15]; /* 8 120 */
/* --- cacheline 2 boundary (128 bytes) --- */
union {
void * slot[16]; /* 128 128 */
struct {
void * pad[15]; /* 128 120 */
/* --- cacheline 3 boundary (192 bytes) was 56 bytes ago --- */
struct maple_metadata meta; /* 248 2 */
}; /* 128 128 */
}; /* 128 128 */
/* size: 256, cachelines: 4, members: 3 */
};
위에 표시된 maple_range_64 구조는 메이플 노드를 나타냅니다. 피벗 외에도 슬롯은 노드가 리프 노드로 기능할 때 VMA 구조를 참조하거나, 노드가 내부 노드로 기능할 때 다른 메이플 노드를 참조하는 데 사용됩니다. 간격이 갭에 해당하는 경우 슬롯은 단순히 NULL 값을 포함합니다. 피벗 포인트와 슬롯의 배치는 아래 그림과 같이 시각화할 수 있습니다:```
Slots -> | 0 | 1 | 2 | ... | 12 | 13 | 14 | 15 |
┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬ ┬
│ │ │ │ │ │ │ │ └─ Implied maximum
│ │ │ │ │ │ │ └─ Pivot 14
│ │ │ │ │ │ └─ Pivot 13
│ │ │ │ │ └─ Pivot 12
│ │ │ │ └─ Pivot 11
│ │ │ └─ Pivot 2
│ │ └─ Pivot 1
│ └─ Pivot 0
└─ Implied minimum
동시 수정(concurrent modification)과 관련하여, maple tree는 특정 제약을 부과한다. 즉, 쓰기 작업자는 배타적 잠금 (*Rule W*)을 보유해야 한다. VMA 트리의 경우, 배타적 잠금은 MM 쓰기 잠금에 해당한다. 읽기 작업자에 대해서는 두 가지 옵션이 있다. 첫 번째 옵션은 MM 읽기 잠금 (*Rule A1*)을 보유하는 것으로, 이 경우 MM 읽기-쓰기 잠금에 의해 쓰기 작업자가 차단된다. 두 번째 옵션은 RCU 임계 구역 (*Rule A2*)에 진입하는 것이다. 이렇게 하면 쓰기 작업자가 차단되지 않으며, maple tree가 RCU에 안전하므로 읽기 작업자는 작업을 계속할 수 있다. 대부분의 기존 VMA 접근 방식은 첫 번째 옵션(즉, Rule A1)을 선택하지만, Rule A2는 잠금 없는 페이지 폴트(lockless page faults)와 같은 소수의 성능에 민감한 시나리오에서 사용된다.
그러나 특별한 주의가 필요한 추가적인 측면이 있으며, 이는 스택 확장(stack expansion)에 관한 것이다. 스택은 MAP_GROWSDOWN 플래그로 매핑된 메모리 영역을 나타내며, 이는 영역 아래의 주소에 접근할 때 자동으로 확장됨을 의미한다. 이러한 경우 해당 VMA의 시작 주소와 maple tree 내의 관련 구간도 함께 조정된다. 주목할 점은 이러한 조정이 MM 쓰기 잠금을 보유하지 않은 채 이루어진다는 것이다.```c
static inline
void do_user_addr_fault(struct pt_regs *regs,
unsigned long error_code,
unsigned long address)
{
// ...
if (unlikely(!mmap_read_trylock(mm))) {
// ...
}
// ...
if (unlikely(expand_stack(vma, address))) {
// ...
}
// ...
}
일반적으로 스택 VMA와 인접한 VMA 사이에는 간격이 존재하는데, 커널이 스택 가드를 강제하기 때문입니다. 이 시나리오에서 스택을 확장할 때는 메이플 노드의 피벗 값만 업데이트하면 되며, 이 과정은 원자적으로 수행될 수 있습니다. 그러나 인접한 VMA도 MAP_GROWSDOWN 플래그를 가지고 있다면 스택 가드가 적용되지 않습니다.```c int expand_downwards(struct vm_area_struct *vma, unsigned long address) { // ...
if (prev) {
if (!(prev->vm_flags & VM_GROWSDOWN) &&
vma_is_accessible(prev) &&
(address - prev->vm_end < stack_guard_gap))
return -ENOMEM;
}
// ...
}
그 결과, 스택 확장은 갭을 제거할 수 있습니다. 이러한 상황에서는 maple node 내부의
갭 구간을 제거해야 합니다. maple tree는 RCU-safe하므로 노드를 제자리에서
덮어쓰는 것은 불가능합니다. 대신 새 노드가 생성되어 노드 교체가 이루어지며,
이전 노드는 이후 RCU 콜백을 통해 파괴됩니다.```c
static inline void mas_wr_modify(struct ma_wr_state *wr_mas)
{
// ...
if ((wr_mas->offset_end - mas->offset <= 1) &&
mas_wr_slot_store(wr_mas)) // <-- in-place update
return;
else if (mas_wr_node_store(wr_mas)) // <-- node replacement
return;
// ...
}
RCU 콜백은 기존의 모든 RCU 임계 구역이 종료된 후에만 호출됩니다. 그러나 VMA에 접근할 때 문제가 발생합니다. MM 읽기 잠금만 보유하고 RCU 임계 구역에 진입하지 않기 때문입니다(Rule A1에 따라). 결과적으로 이론적으로 콜백은 언제든지 호출될 수 있어 이전 maple 노드가 해제될 수 있습니다. 하지만 이전 노드에 대한 포인터는 이미 획득되었을 수 있으므로, 이후 해당 노드에 접근하려 할 때 use-after-free 버그가 발생할 수 있습니다.
use-after-free(UAF)가 발생하는 백트레이스는 다음과 같습니다.```