---
title: "리눅스 커널 15년 묵은 취약점 ‘GhostLock’, 권한 상승과 컨테이너 탈출까지 뚫렸다"
published: 2026-07-08T16:53:58.000Z
canonical: https://jeff.news/article/5021
---
# 리눅스 커널 15년 묵은 취약점 ‘GhostLock’, 권한 상승과 컨테이너 탈출까지 뚫렸다

GhostLock은 2011년 리눅스 2.6.39부터 들어간 커널 취약점으로, 패치 전 주요 배포판 대부분에 영향을 준다. 특별한 커널 설정이나 권한 없이 로컬 사용자가 커널 스택 use-after-free를 이용해 루트 권한 상승과 컨테이너 탈출까지 이어갈 수 있고, 연구팀은 구글 kernelCTF에서 안정도 97% 익스플로잇으로 9만2337달러 보상을 받았다.

## 15년 동안 살아남은 커널 버그

- GhostLock은 리눅스 커널의 stack-use-after-free 취약점이고, CVE-2026-43499로 등록됨
  - 2011년 리눅스 2.6.39에 들어갔고, 리눅스 7.1에서 수정됨
  - 연구팀 설명 기준으로 패치 전 주요 리눅스 배포판이 영향을 받음
  - 특별한 커널 설정이나 권한이 필요 없어서, 로컬 비권한 사용자가 공격 출발점에 설 수 있음

- 결과만 보면 꽤 세다. 권한 상승, 컨테이너 탈출, 제어 흐름 탈취까지 이어짐
  - 구글 kernelCTF에서 97% 안정도의 익스플로잇으로 인정받음
  - 보상액은 9만2337달러
  - 원격 kernelCTF 환경에서는 약 5초 만에 플래그를 얻었다고 함

> [!WARNING]
> 이건 “희귀 설정에서만 터지는 연구용 버그” 쪽이 아님. 패치 전 커널에서 일반 시스템콜만으로 출발할 수 있는 로컬 권한 상승 취약점이라, 멀티테넌트 서버나 컨테이너 호스트 입장에서는 꽤 직접적인 위험임.

## 공격 흐름을 한 줄로 줄이면

- 핵심은 해제된 커널 스택 포인터를 다시 잡고, 그 자리에 공격자가 원하는 가짜 구조체를 올리는 것임
  - 공격자는 dangling pointer가 rt_mutex_waiter처럼 해석되게 만듦
  - 그다음 커널의 rtmutex 처리 경로가 이 가짜 waiter를 정상 구조체처럼 따라가게 함
  - 커널은 크래시 없이 정상 복귀할 수 있어서, 단순한 서비스 거부가 아니라 안정적인 익스플로잇으로 발전함

- 익스플로잇 체인은 크게 7단계로 정리됨
  - prefetch로 커널 이미지 슬라이드와 physmap base를 leak함
  - GhostLock으로 waiter 태스크의 pi_blocked_on에 dangling rt_mutex_waiter를 남김
  - PR_SET_MM_MAP으로 같은 커널 스택 프레임을 다시 덮어 가짜 rt_mutex_waiter를 만듦
  - rtmutex rb-tree erase를 이용해 제한적 포인터 쓰기 primitive를 얻음
  - inet6_protos[IPPROTO_UDP]를 CPU entry area 쪽 포인터로 덮음
  - IPv6 UDP loopback 패킷으로 fake handler를 호출해 제어 흐름을 뺏음
  - DirtyMode로 core_pattern 권한 비트를 바꿔 사용자 공간에서 루트 권한 상승을 마무리함

```mermaid
sequenceDiagram
    participant 공격자
    participant 커널스택
    participant 락처리
    participant IPv6테이블
    participant 사용자공간
    공격자->>커널스택: 해제된 waiter 포인터 유지
    공격자->>커널스택: PR_SET_MM_MAP으로 가짜 waiter 배치
    락처리->>커널스택: rt_mutex_waiter로 해석
    락처리->>IPv6테이블: inet6_protos 포인터 덮기
    공격자->>IPv6테이블: loopback UDP로 handler 호출
    IPv6테이블->>사용자공간: DirtyMode 이후 루트 권한 상승
```

## 스택 재사용이 제일 맛있는 부분

- dangling object는 waiter 자신의 커널 스택에 있던 rt_mutex_waiter임
  - 공격자는 그 정확한 스택 프레임 위에 다시 제어 가능한 바이트를 올려야 함
  - 글에서는 prctl(PR_SET_MM, PR_SET_MM_MAP, ...) 경로를 사용함
  - prctl_set_mm_map() 안에서 user_auxv[AT_VECTOR_SIZE]라는 고정 크기 스택 버퍼가 만들어지고, 이 버퍼가 해제된 waiter와 비슷한 스택 깊이에 놓임

- 공격자는 auxv 내용을 조작해서 가짜 waiter 필드들을 맞춰 넣음
  - tree는 rb-tree erase가 원하는 child pointer를 root로 올리도록 구성함
  - task는 안전한 역참조를 위해 &init_task로 둠
  - lock은 쓰기 목표인 &inet6_protos[IPPROTO_UDP] - 8로 맞춤
  - wake_state는 0으로 설정함

- race window를 넓히는 디테일도 있다. memfd와 fallocate(PUNCH_HOLE)을 같이 씀
  - auxv를 페이지 경계에 걸치게 배치함
  - 형제 스레드가 trailing page에 구멍을 내면서 copy_from_user 시간을 늘림
  - 그 사이 다른 CPU에서 sched_setattr()를 호출해 PI chain walk를 태움
  - 연구팀은 단일 코어 CPU에서도 exploitable할 것으로 본다고 적음

## “임의 쓰기”가 아니라 제한적인 한 방

- GhostLock이 바로 완전한 arbitrary write를 주는 건 아님. 실제 primitive는 꽤 빡빡한 제한이 있음
  - rt_mutex_dequeue()의 rb-tree erase를 이용해 단일 child pointer를 root slot에 쓰는 구조임
  - 결과적으로 *(uint64_t *)target = W0_BASE 같은 제한적 포인터 쓰기에 가까움
  - target 앞 qword는 unlocked spinlock처럼 보여야 해서 low 4 bytes가 0이어야 함
  - target 뒤쪽 값들도 커널이 엉뚱한 waiter나 owner를 따라가지 않게 안전해야 함

- 그래서 연구팀은 inet6_protos[IPPROTO_UDP]를 골랐음
  - 쓰기 가능한 커널 데이터 영역에서 주변 레이아웃 조건이 잘 맞는 포인터 테이블을 찾은 것
  - 트리거도 편함. 비권한 사용자가 loopback IPv6 UDP 패킷을 보내면 됨
  - 덮은 뒤에는 커널이 CPU entry area에 있는 가짜 inet6_protocol을 진짜처럼 보고 handler를 호출함

> [!IMPORTANT]
> 익스플로잇의 포인트는 “완전한 임의 쓰기 없이도 충분하다”는 데 있음. 제한적인 포인터 쓰기 한 번을 inet6_protos와 CPU entry area에 연결해서, 결국 프로그램 카운터 제어까지 끌고 감.

## CPU entry area와 DirtyMode로 짧게 끝내기

- CPU entry area는 이 공격에서 작지만 고정 주소에 가까운 작업대 역할을 함
  - 제어 가능한 메모리를 알려진 direct-map 주소에 둘 수 있음
  - 크기는 약 120바이트라 넉넉하진 않음
  - 그래도 가짜 inet6_protocol, JOP/pivot slot, 최종 ROP stack을 같이 담는 식으로 사용함

- 구글 lts-6.12.80 타깃에서는 깔끔한 단일 stack pivot gadget을 못 찾아 한 단계를 더 거침
  - 먼저 CEA 주소를 rbp에 올림
  - 이후 mov rsp, rbp; pop rbp; ret 형태로 스택을 피벗함
  - ret2usr나 /proc/%P/fd/x 전체 overwrite 경로는 gadget이 10 qword쯤 필요해서 너무 길었다고 함

- 그래서 마지막은 DirtyMode를 선택함. 짧고 현실적임
  - coredump_sysctls[1].mode에 쓰기 가능한 비트를 세움
  - 그러면 /proc/sys/kernel/core_pattern이 world-writable이 됨
  - 비권한 프로세스가 core_pattern에 |/proc/%P/fd/666 %P를 쓰고 helper를 크래시시켜, 커널이 공격자 바이너리를 root로 실행하게 만듦

## 패치와 완화책

- 패치의 핵심은 waiter->task를 믿고 읽지 않게 바꾼 것임
  - 호출자가 owning task를 직접 넘기도록 수정함
  - self-blocking path에서는 current를, proxy lock rollback에서는 proxied task를 넘김
  - pi_blocked_on은 여전히 해당 waiter를 가리킬 때만 clear되도록 guard를 둠

- RANDOMIZE_KSTACK_OFFSET은 스택 재사용 단계를 흔들 수 있음
  - 이 공격은 해제된 waiter frame과 이후 user_auxv frame이 결정적으로 겹치는 데 의존함
  - RANDOMIZE_KSTACK_OFFSET이 켜지면 대략 1/32, 즉 5비트 stack-offset guess 문제가 됨
  - kernelCTF 제출 대상 두 개는 기본값으로 꺼져 있었고, mitigation target은 켜져 있어서 이 경로를 쓰지 않았다고 함

- STATIC_USERMODE_HELPER는 이 글의 DirtyMode 경로를 막는 데 도움이 됨
  - 다만 연구팀은 같은 아이디어가 다른 /proc/sys knob에도 일반화될 수 있다고 봄
  - 조건은 ctl_table::mode가 접근 권한을 막고, 해당 테이블이 예측 가능한 writable kernel data에 있어야 한다는 것

## 타임라인도 빠르게 보자

- 연구팀은 2026년 4월 18일 security@kernel.org에 버그와 초안 패치를 보냄
  - 4월 20일 다른 패치로 수정됨
  - 5월 4일 fix v1이 backport됨
  - 6월 30일 구글이 kernelCTF 제출을 인정함
  - 7월 7일 기술 글이 공개됨

---

## 기술 맥락

- 이 취약점에서 중요한 선택은 “해제된 스택 객체를 다시 덮을 수 있느냐”였어요. 커널 힙 UAF처럼 slab을 뿌리는 게 아니라, 같은 태스크의 커널 스택에서 비슷한 깊이의 로컬 버퍼를 찾아야 했거든요. 그래서 PR_SET_MM_MAP의 user_auxv 버퍼가 공격 체인의 핵심 발판이 됐어요.

- inet6_protos[IPPROTO_UDP]를 고른 이유도 단순히 함수 포인터가 있어서가 아니에요. GhostLock이 주는 쓰기는 조건이 빡센 한 번짜리 포인터 쓰기에 가깝기 때문에, 목표 주소 앞뒤 qword가 rtmutex 경로의 검사를 버틸 수 있어야 했어요. IPv6 UDP 테이블은 그 레이아웃과 트리거 편의성이 같이 맞아떨어진 케이스예요.

- CPU entry area를 쓴 건 공간보다 주소 예측 가능성이 더 중요했기 때문이에요. 약 120바이트밖에 안 되는 작은 창에 가짜 inet6_protocol, 피벗 슬롯, ROP 스택을 욱여넣은 건 kernelCTF에서 빠르고 안정적으로 플래그를 따기 위한 선택이었어요.

- DirtyMode는 긴 커널 ROP를 피하려는 마무리 전략이에요. 커널 안에서 모든 일을 끝내려면 gadget도 많이 필요하고 실패 지점도 늘어나요. 대신 sysctl mode 비트 하나를 바꿔 사용자 공간에서 core_pattern을 조작하면, 짧은 커널 체인으로 루트 권한 상승을 완성할 수 있어요.

## 핵심 포인트

- GhostLock은 CVE-2026-43499로 등록된 리눅스 커널 stack-use-after-free 취약점임
- 공격자는 일반 스레딩 시스템콜만으로 해제된 커널 스택 포인터를 잡고, 제한적 포인터 쓰기 primitive를 만든 뒤 제어 흐름 탈취까지 이어감
- 익스플로잇은 inet6_protos, CPU entry area, DirtyMode를 조합해 루트 권한 상승과 컨테이너 탈출을 달성함
- 리눅스 7.1에서 수정됐고, 영향받는 배포판은 최신 장기지원 커널로 올리는 게 핵심 대응임

## 인사이트

이 글이 무서운 건 ‘복잡한 커널 버그’라서가 아니라, 기본 설정에서 15년 동안 살아남은 취약점이 아주 현실적인 공격 체인으로 정리됐다는 점임. 컨테이너 격리를 커널 보안과 별개로 생각하면 안 된다는 얘기를 꽤 세게 해주는 사례다.
