
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)가 발생하는 백트레이스는 다음과 같습니다.```
mm_read_lock() mm_read_lock() expand_stack() find_vma_prev() expand_downwards() mas_walk() mas_store_prealloc() mas_state_walk() mas_wr_story_entry() mas_start() mas_wr_modify() mas_root() mas_wr_node_store() node = rcu_dereference_check() mas_replace() [ The node pointer is recorded ] mas_free() ma_free_rcu() call_rcu(&mt_free_rcu) [ The node is dead ] mm_read_unlock()
[ Wait for the next RCU grace period.. ] rcu_do_batch() mas_prev() mt_free_rcu() mas_prev_entry() kmem_cache_free() mas_prev_nentry() [ The node is freed ] mas_slot() mt_slot() rcu_dereference_check(node->..) [ UAF occurs here ] mm_read_unlock()
## 수정
저는 6월 15일에 이 취약점을 Linux 커널 보안 팀에 보고했습니다.
그 후, 이 버그를 해결하는 과정은 Linus Torvalds가 주도했습니다.
복잡성 때문에 합의를 얻은 패치 세트를 개발하는 데 거의 2주가 걸렸습니다.
6월 28일, Linux 커널 6.5의 머지 윈도우 기간 동안 해당 수정 사항이
Linus의 트리에 병합되었습니다. Linus는 패치 시리즈를 기술적 관점에서
설명하는 [포괄적인 머지 메시지][fix]를 제공했습니다.
[fix]: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=9471f1f2f50282b9e8f59198ec6bb738b4ccc009
이 패치들은 이후 안정 커널([6.1.37][6.1], [6.3.11][6.3], [6.4.1][6.4])로
백포트되어 7월 1일에 "Stack Rot" 버그를 효과적으로 해결했습니다.
[6.1]: https://lore.kernel.org/stable/2023070133-create-stainless-9a8c@gregkh/T/
[6.3]: https://lore.kernel.org/stable/2023070146-endearing-bounding-d21a@gregkh/T/
[6.4]: https://lore.kernel.org/stable/2023070140-eldercare-landlord-133c@gregkh/T/
## 익스플로잇
이 익스플로잇은 주로 Google kCTF 챌린지를 대상으로 하며, 특히
CONFIG_PREEMPT와 CONFIG_SLAB_MERGE_DEFAULT가 모두 설정되지 않은 경우를 다룹니다.
StackRot을 익스플로잇하려면 다음 기준을 충족하는 VMA 반복(iteration)을 찾는 것이
가장 중요한 작업입니다:
1. 반복의 타이밍을 제어할 수 있어야 합니다. 이 제어를 통해 VMA 반복 중에
RCU 유예 기간(grace period)이 종료되도록 보장할 수 있습니다.
2. 반복이 VMA 구조체에서 특정 정보를 검색하고 해당 정보를 사용자 공간으로
반환해야 합니다. 이 기능을 통해 maple 노드의 UAF 취약점을 악용하여
일부 커널 주소를 유출할 수 있습니다.
3. 반복이 VMA 구조체의 특정 함수 포인터를 호출해야 합니다. 이 특별한 기능은
maple 노드의 UAF를 악용하여 커널 모드 프로그램 카운터(PC)를 제어할 수
있게 해줍니다.
선택된 VMA 반복은 `/proc/[pid]/maps`의 내용을 생성하는 반복입니다.
다음 섹션에서는 이 반복이 위 기준을 어떻게 충족하는지 보여줍니다.
### 0단계: UAFBR에서 UAF로
VMA 반복 중에는 VMA 트리의 루트 노드에 대한 참조가 획득되고, 반복은 해당
슬롯들을 통해 진행됩니다. 따라서 VMA 반복 중에 별도의 CPU에서 다른 스레드의
스택 확장을 트리거하면 노드 교체가 동시에 시작될 수 있습니다. 이 시점에서
이전 노드에 접근하는 것은 RCU에 의한 use-after-free(UAFBR) 상황으로 간주됩니다.
그러나 실제 문제는 RCU 콜백에서 이전 노드가 실제로 해제될 때 발생합니다.
여기에는 두 가지 과제가 있습니다: (i) 이전 노드가 언제 해제되는지 확인하는 것과
(ii) 이전 노드가 해제되기 전에 VMA 반복이 완료되지 않도록 보장하는 것입니다.
첫 번째 질문은 비교적 간단합니다. 커널에서 `synchronize_rcu()` 함수를 사용하여
RCU 유예 기간이 종료될 때까지 대기할 수 있으며, 이를 통해 기존의 모든 RCU
콜백이 호출되었음을 보장할 수 있습니다. 사용자 공간에서는 궁극적으로
`synchronize_rcu()`를 호출하는 시스템 콜을 동일한 목적으로 사용할 수 있습니다.
따라서 그러한 시스템 콜이 종료되면 이전 노드가 해제되었음을 알 수 있습니다.
특히, `membarrier(MEMBARRIER_CMD_GLOBAL, 0, -1)`라는 시스템 콜은 오직
`synchronize_rcu()`만 호출합니다.```c
SYSCALL_DEFINE3(membarrier, int, cmd, unsigned int, flags, int, cpu_id)
{
// ...
switch (cmd) {
// ...
case MEMBARRIER_CMD_GLOBAL:
/* MEMBARRIER_CMD_GLOBAL is not compatible with nohz_full. */
if (tick_nohz_full_enabled())
return -EINVAL;
if (num_online_cpus() > 1)
synchronize_rcu();
return 0;
// ...
}
}
두 번째 질문은 추가적인 고려가 필요합니다. 몇 가지 잠재적인 해결책은 다음과 같습니다:
jiffies_till_first_fqs (기본값은 몇 jiffies)를 초과하면,
프로세서 간 인터럽트(IPI)가 대상 CPU로 전송되어
자발적 선점을 트리거합니다. VMA 순회의 경우, 자발적
선점으로 인해 RCU 유예 기간이 종료되고 maple 노드가 해제되어,
UAFBR을 실제 use-after-free(UAF) 시나리오로 효과적으로 변환합니다.한 가지 중요한 관찰은 VMA 순회 중에
/proc/[pid]/maps에 대해 파일 매핑된 메모리 영역의 전체 파일 경로를
생성한다는 것입니다. 디렉터리 이름은 일반적으로 최대
255자로 제한되지만, 디렉터리 깊이에는 제한이 없습니다. 즉,
매우 깊은 디렉터리 깊이를 가진 파일을 생성하고 이 파일에 대한
메모리 매핑을 설정하면, /proc/[pid]/maps에 접근할 때
VMA 순회 중 상당한 시간이 소요될 수 있습니다. 결과적으로, 이렇게
확장된 시간 덕분에 RCU 유예 기간을 종료하고
UAF 프리미티브를 획득할 가능성이 생깁니다.```c
static void
show_map_vma(struct seq_file *m, struct vm_area_struct *vma)
{
// ...
/*
* Print the dentry name for named mappings, and a
* special [heap] marker for the heap:
*/
if (file) {
seq_pad(m, ' ');
/*
* If user named this anon shared memory via
* prctl(PR_SET_VMA ..., use the provided name.
*/
if (anon_name)
seq_printf(m, "[anon_shmem:%s]", anon_name->name);
else
seq_file_path(m, file, "\n");
goto done;
}
// ...
}
이 단계는 다음 그림에 나와 있습니다:

### Step 1: slab UAF에서 page UAF로
이제 slab 내에서 UAF가 동작합니다. CONFIG_SLAB_MERGE_DEFAULT가
활성화되어 있고 maple 노드의 slab이 kmalloc-256과 병합되면,
이전 노드 내의 내용은 kmalloc-256에서 새 구조체를 할당하고
이를 사용자 공간 데이터로 채워 제어할 수 있습니다. 그러나
CONFIG_SLAB_MERGE_DEFAULT가 설정되지 않은 경우에는 대체 접근 방식이 필요합니다.
이 경우, 해제된 노드의 페이지를 페이지 할당자로 반환해야 하며,
새 페이지를 할당하고 그에 따라 채움으로써 이전 노드를 제어할 수
있습니다.
VMA 트리에는 노드가 하나만 포함된다는 점을 기억하십시오. 따라서
`fork()`/`clone()`을 활용하면 여러 VMA 트리와 동일한 수의 maple 노드가
생성됩니다. 하나의 slab이 M개의 maple 노드를 포함한다고 가정할 때, M개
노드 중 하나의 노드를 유지하고 다른 모든 노드는 `exit()`을 통해 해제하면,
나머지 노드들이 각각의 slab 내에서 유일한 노드가 됩니다. 처음에는 이
slab들이 CPU의 partial 목록에 상주합니다. partial 목록이
용량에 도달하면 slab들은 해당 NUMA 노드의 partial 목록으로 다시
플러시됩니다.
slab 내의 마지막 maple 노드가 해제되면 slab은 비어 있게 됩니다. 이
slab이 NUMA 노드의 partial 목록에 있고, 해당 NUMA 노드의 partial 목록이
이미 최대 용량이라면, 페이지는 즉시
페이지 할당자로 반환됩니다. 결과적으로 slab UAF는
page UAF 시나리오로 변환됩니다. 해제된 페이지 내의 내용은
`msgsnd()`를 통해 일부 데이터를 전송하여 조작할 수 있습니다. `msgsnd()`는
elastic 객체를 할당하고 제공된 사용자 데이터로 직접 채웁니다.```c
static void __slab_free(struct kmem_cache *s, struct slab *slab,
void *head, void *tail, int cnt,
unsigned long addr)
{
// ...
if (unlikely(!new.inuse && n->nr_partial >= s->min_partial))
goto slab_empty;
// ...
return;
slab_empty:
// ...
discard_slab(s, slab);
}
슬랩당 메이플 노드 수 M은 CPU 수에 따라 달라집니다. 익스플로잇 구현은 CPU가 2개인 상황을 고려하므로 M의 값을 16으로 가정하며, 이는 다음 그림에 나와 있습니다:

메이플 노드를 제어하게 되면 이후에 순회될 후속 VMA의 주소를 조작할 수
있게 됩니다. 대상 순회는 /proc/self/maps 생성을 목표로 하므로, VMA
구조체 안에 존재하는 시작 및 끝 주소와 같은 특정 VMA 정보가 사용자
공간으로 반환됩니다.
그러나 문제가 하나 발생합니다: 메이플 노드의 VMA 구조체 주소는 일부 주소가
이미 알려져 있어야만 적절히 설정할 수 있습니다. 다행히도 CVE-2023-0597이
정확히 이 용도로 사용됩니다. CVE-2023-0597에 따르면 cpu_entry_area의
주소는 무작위화되지 않습니다. 이 취약점은 Linux 6.2에서 패치되었지만, 작성
시점 기준으로 이전 안정 커널에는 백포트되지 않았습니다. 결과적으로 VMA
구조체의 주소를 마지막 IDT 엔트리의 주소로 덮어쓰면
asm_sysvec_spurious_apic_interrupt의 주소가 포함된 엔트리가 직접 누출되어
커널 코드와 커널 데이터의 베이스 주소가 드러납니다.

앞서 논의한 방법은 커널 데이터 섹션에서 더 많은 주소를 점진적으로
노출하기 위해 반복해서 사용할 수 있습니다. 예를 들어, 데이터 섹션의
init_task.tasks.prev 포인터는 가장 최근에 생성된 작업의 task_struct
구조체를 가리키며, 이 구조체는 의심할 여지없이 힙에 할당됩니다.

새로 생성된 모든 작업이 종료되면 해당 task_struct 구조체는 이후에
할당 해제됩니다. 이러한 작업의 수가 충분히 많으면 해당 페이지는 페이지
할당자에게 반환될 수 있습니다. 이를 통해 이 페이지들을 재할당하고 사용자
데이터로 채울 가능성이 생깁니다. 단, 해제된 페이지는 일반적으로 per-cpu
페이지(PCP) 목록에 속한다는 점을 명심하십시오. PCP 목록에 있는 페이지는
동일한 페이지 오더로만 재할당될 수 있습니다. 결과적으로 페이지
할당자에게 order-0 페이지만 요구하는 새 페이지를 사용자 공간에 매핑하는
것만으로는 목표를 달성할 수 없습니다.
그럼에도 불구하고 msgsnd 시스템 콜은 kmalloc을 통해 메모리 청크를 요청하고 이러한 청크에 사용자 정의 데이터를 채웁니다. kmalloc 캐시가 고갈되면 특정 오더로 페이지 할당자에게 페이지를 요청합니다. 메시지 크기를 정확히 조정하면 정확히 원하는 오더가 됩니다. 따라서 이전에 주소가 누출된 페이지가 재할당됩니다. 그 결과, 주소가 알려져 있고 사용자가 조작한 데이터가 담긴 페이지를 얻을 수 있게 됩니다.
이제 주소가 알려진 페이지에서 VMA 구조체를 위조하고
vma->vm_ops->name 함수 포인터를 제어할 수 있습니다. 다음 단계는
컨테이너를 탈출하고 루트 권한을 획득하기 위한 적절한 가젯을 찾는
것입니다.```c
static void
show_map_vma(struct seq_file *m, struct vm_area_struct *vma)
{
// ...
if (vma->vm_ops && vma->vm_ops->name) {
name = vma->vm_ops->name(vma);
if (name)
goto done;
}
// ...
}

가젯 구성은 다음과 같습니다:
1. 스택 피벗: `movq %rbx, %rsi; movq %rbp, %rdi; call
__x86_indirect_thunk_r13` -> `pushq %rsi; jmp 46(%rsi)` -> `popq %rsp; ret`
-> `popq %rsp; ret`, 여기서 %rdi, %rbx, %r13은 _처음에_ 사용자 제어 가능한
데이터를 가리킵니다.
2. 루트 권한 획득: `popq %rdi; ret` -> `prepare_kernel_cred` -> `popq
%rdi; ret` -> `movq %rax, (%rdi); ret`, 여기서 %rdi는 _이제_
스택 상단을 가리키며; `popq %rdi; ret` -> `commit_creds`, 사실상
`commit_creds(prepare_kernel_cred(&init_task))`를 실행합니다.
3. 컨테이너 탈출: `popq %rdi; ret` -> `find_task_by_vpid` -> `popq %rdi;
ret` -> `movq %rax, (%rdi); ret`, 여기서 %rdi는 _이제_ 스택 상단을 가리킵니다;
`popq %rdi; ret` -> `popq %rsi; ret` -> `switch_task_namespaces`,
사실상 `switch_task_namespaces(find_task_by_vpid(1),
&init_nsproxy)`를 수행합니다.
4. mm 잠금 해제: `popq %rax; ret` -> `movq %rbp, %rdi; call
__x86_indirect_thunk_rax`, 여기서 %rbp는 원래 seq_file을 가리킵니다;
`popq %rax; ret` -> `m_stop`, 사실상 `m_stop(seq_file, ..)`를 실행합니다.
5. 사용자 공간으로 복귀: `swapgs_restore_regs_and_return_to_usermode`를 사용하고,
`execve()`를 호출하여 셸을 얻습니다.
마지막으로, `nsenter --mount=/proc/1/ns/mnt`를 사용하여 마운트 네임스페이스를 복원하고
`cat /flag/flag`로 플래그를 얻습니다.
### 소스 코드
전체 익스플로잇 소스는 [여기](https://github.com/lrh2000/stackrot/blob/master/exp)에서 확인할 수 있습니다. 자세한 내용은
해당 README 파일을 참조하십시오.