
CVE-2026-98260(posix-cpu-timers exec UAF) 익스플로잇 연구: S9280 Android 14 GKI 6.1 — UAF 트리거 + 제로 쓰기 프리미티브 + SELinux Permissive. 인수인계 문서는 HANDOFF.md에 있음
exec: Cleanup POSIX timers right after de_thread() (작성자 Hyunwoo Kim, 2026-09-11 업스트림,
Fixes: 55e8c8eb2c7b = 5.7부터).
비 leader 스레드 E에 CLOCK_THREAD_CPUTIME_ID 타이머가 걸려 있음; execve() 안에서
de_thread()가 tid를 교환한 후, 타이머가 PID로 task를 역조회하는 데 실패하고 노드는 여전히 E의
posix_cputimers에 남아 있음. 만약 exec_mmap()의 mmap_read_lock_killable()이 -EINTR을
반환하면 (exec 실패이고 정리 코드는 더 뒤에 있음), do_exit() → exit_itimers()가 k_itimer를
해제하지만 노드는 제거되지 않음; 부모 프로세스가 wait4()로 회수할 때 cleanup_timerqueue() →
timerqueue_del()이 이미 해제된 객체에 대해 동작 → UAF.
| 브랜치 | posixtimer_exec | 상태 |
|---|---|---|
| linux-6.1.y | 없음 | 미수정 |
| linux-5.15.y | 없음 | 미수정 |
| linux-6.6.y | 있음 | 수정됨 (6.6.158+) |
| linux-6.12.y | 있음 | 수정됨 (6.12.112+) |
→ 본 프로젝트 대상 (5.15 / 6.1 라인, 그리고 6.6.158 미만의 6.6)은 전부 영향 범위에 있음.
fragment_vmas()는 memfd + MAP_FIXED로 주소 공간을
--vmas (기본 50000)개의 병합 불가 VMA로 분할함 (각각의 vm_pgoff가 모두 0이고,
인접 주소의 pgoff가 연속되지 않음 → vma_merge 실패). locker 스레드가 반복적으로 mprotect로
전체 구간을 덮어, 수만 개의 VMA를 순회하게 하여 mmap 쓰기 락이 밀리초 단위로 유지됨.tgkill(tgid, 이전 tid, 0)이 ESRCH를 반환하는 것으로
"de_thread()가 tid 교환을 완료함"을 판단하고, 이때 비로소 SIGKILL을 보냄.exec_mmap()에서 반드시 블록됨 — mmap_read_lock_killable()
(읽기 락이 쓰기 락에 막힘)에서, 그리고 SIGKILL은 그 뒤 마이크로초 단위로 도달 → 해당 호출이 -EINTR을
반환 → exec 실패. 윈도우 (ms) ≫ 반응 시간 (µs), 따라서 운에 의존하지 않음.⚠️ 구분에 주의: 트리거는 결정적임; UAF에서 임의 쓰기/cred 변경까지는
posix_timers_cache의 slab 재사용 편성 (order, objects/slab, CPU partial,
memcg 귀속)에 달려 있으며, 이는 측정 가능하고 반복 가능한 힙 엔지니어링이지 경쟁 조건이 아님.
export ANDROID_NDK_HOME=/home/nt/项目/samsung_root_research/06_工具链/android-ndk-r29
make # 产物 trigger(aarch64 / Android 21+ / 仅依赖 libc、libdl)
adb push trigger /data/local/tmp/
adb shell "chmod 755 /data/local/tmp/trigger"
adb shell "/data/local/tmp/trigger --check" # 先看环境
adb shell "/data/local/tmp/trigger; echo exit=\$?"
성공 표시(셋 중 하나만 충족하면 됨):
[*] 开始触发가 출력된 후 기기 재부팅/블랙스크린(panic_on_oops=1일 때 가장 직접적)exit=137/139), 그리고 이후에도 시스템이 살아 있음cleanup_timerqueue / timerqueue_del /
rb_erase / rb_next의 호출 스택이 나타남실패(미발동) 시 프로그램이 스스로 --attempts회 재시도하고 매 회차 wait4 결과를 출력하며,
마지막에 파라미터 조정을 안내함: --vmas 120000 --lockers 4 --cpu 0.
panic_on_oops가 0일 수 있음: 이때는 작업이 종료되고 커널이 손상된 rbtree를 가진 채 계속 실행되므로,
2~3회 연속 실행하여 후속 panic이 나타나는지 관찰할 것을 권장.테스트 환경: S9280ZHU6EZIF, 커널 6.1.162-android14-11-34343818-abS9280ZHU6EZIF
(빌드 2026-09-20), 루팅되지 않음, shell 도메인(uid 2000), SELinux Enforcing.
| 회차 | 관측 | 의미 |
|---|---|---|
| 매 회차 | 부모 프로세스 tgkill(child_tgid, etid, 0)가 ESRCH 반환 | de_thread()가 tid 교환을 완료함 |
| 매 회차 | 이어서 kill(child_tgid, SIGKILL), 자식 프로세스 rc=-9 | exec가 중단됨(성공한 exec의 rc=0이 아님) |
--no-killer 대조 | exec가 armed에서 종료까지 소요 277 ms(killer 있을 때 287 ms) | mmap 쓰기 락이 실제로 exec_mmap()의 mmap_read_lock_killable()을 막았음 |
5/5 회차 모두 "ESRCH → SIGKILL → exec 실패"에 적중, exec 성공은 단 한 번도 없었음.
이는 de_thread 이후 exec를 중단하는 윈도우가 제어 가능하고 안정적임을 증명함; 윈도우는 locker의
mprotect(6만 VMA, 단일 200–700 ms)에서 오며, 부모 프로세스의 반응은 µs 수준.
de_thread()는 zap_other_threads()로 같은 프로세스의
다른 스레드를 모두 죽이므로, 자식 프로세스에 작성된 killer는 de_thread 시점에 스스로 먼저 죽어
ESRCH를 영원히 볼 수 없음(exec가 매번 성공하는 것으로 나타남).SIGKILL 직후 spray는 무효——그때 자식 프로세스가 아직 죽지 않았고
k_itimer가 아직 RCU로 해제되지 않았음. 올바른 순서는
waitid(WNOWAIT)로 좀비 대기 → RCU 콜백 해제 대기 → 페이지 선점 → 마지막에 wait4로 회수 경로 발동.현재 "해제 후 노드가 여전히 큐에 있음" 단계는 안정적으로 달성했으나, wait4 시점의 rb_erase에
oops가 없음 —— 희생 slab 페이지를 아직 우리 spray가 선점하지 못했음을 의미함(posix_timers_cache의
partial slab이 여전히 캐시되어 페이지가 buddy로 돌아가지 않음). 이는 exploit의 회수 오케스트레이션이며
레이스 문제가 아니므로, vivo 노트의 방식대로: 먼저 k_itimer 자체로 slab을 가득 채우고/비워
전체 페이지 반환을 촉진한 뒤, order-0 캐리어(AIO ring 등)로 split된 반쪽 페이지를 정확히 받아내야 함.
다음 단계 제안: 이 기기를 기준으로 삼아 docs/08-reclaim-choreography.md(vivo 노트)의
사고방식대로 표적 회수를 수행하거나, KASAN이 포함된 자체 빌드 커널로 UAF가 실제로 발생하는지 검증할 것.
공개 체인(Honor uid0.c)은 tracefs의 kmem 이벤트에 의존하여 두 가지 핵심 작업을 수행함:
| 용도 | Honor 611/6.12에서 사용하는 것 | 본 기기(Samsung 6.1.162) |
|---|---|---|
피해자 k_itimer 주소 V 찾기(payload의 페이지 내 오프셋 계산) | kmem:kmem_cache_alloc | 이벤트는 존재하지만 events/kmem/*/enable이 0644 root:root라서 shell이 enable할 수 없음(events/sched/*/enable은 쓰기 가능, kmem은 불가) |
| order-1 slab 페이지가 buddy로 반환되었는지 판정(정확한 페이지 탈취 시점) | kmem:mm_page_free | 위와 동일, 존재하지만 enable 불가 |
| 원시 trace 버퍼 읽기 | per_cpu/cpuN/trace_pipe_raw | 읽기 가능(-r--r----- root readtracefs)✓ |
| KASLR slide | 각 trace 이벤트의 static site | 가능: sched 이벤트는 enable 가능, 프로젝트에 이미 동일한 기법 있음 ✓ |
결론: 공개 체인은 그대로 이식할 수 없다. 부족한 것은 커널 능력(이벤트는 모두 있음)이 아니라 shell에 kmem 이벤트의 enable 권한이 없다는 점이다.
V가 필요 없음: payload를 페이지 내 위치 무관하게 만든다——8KB 페이지 안의 가능한 모든
object 슬롯(k*0x108 + 0x78 + 0x10)마다 target을 한 번씩 기록하면, 피해자 객체가
slab의 어느 슬롯에 떨어지든 rb_erase가 target을 때린다. 이는 「전 슬롯 커버리지」로
kmem_cache_alloc의 주소 발견을 대체하는 것과 같다.mm_page_free의 정밀 동기화를 제거한 뒤에는 RCU 유예 기간 후
대량으로 sk_buff 데이터 페이지(AF_UNIX SO_SNDBUF)를 spray하여 방금 반환된 order-1 페이지를
노려야 하므로 성공률이 떨어지며, precheck + 재시도 루프가 필요하다.SLIDE_TRACEFS_* 메커니즘 있음).cred 주소를 어디서 얻을 것인가. 공개 체인은 동일한 kmem 누출로 cred를 얻지만,
본 기기에서는 불가능하다. 선택지:
selinux_state.enforcing, slide는 알려짐)를
공격하지만, 그것은 SELinux를 끌 수 있을 뿐 uid 0을 얻지는 못한다.결론: 완전한 체인 = 수일 규모의 이식(먼저 위치 무관 페이지 탈취로 UAF를 가시화한 뒤, cred 누출을 해결), 한 번의 라운드로 통과할 수 있는 일이 아니다.
기기: SM-S9280 /
6.1.162-android14-11-34343818-abS9280ZHU6EZIF(One UI 9.0 / Android 17) 경로: UAF → 제로 쓰기 원시 연산 → cred 제로 쓰기 → uid 0. 세 단계로 추진하며, 현재 두 번째 단계에서 막혀 있음.
| 단계 | 목표 | 상태 |
|---|---|---|
| 1. 트리거 | posix-cpu-timers UAF를 결정적으로 발생시킴 | ✅ 실기 확인 |
| 2. UAF 가시화/제어 | 해제된 객체를 우리가 제어(캐리어 페이지 탈취 or 동일 slab 재사용) | ✅ 페이지 탈취 실기 성립(P10: victim 페이지가 buddy로 반환되어 우리 측이 받아냄); ⚠ 타이밍 수정 필요: 캐리어 안착이 reap보다 반드시 선행해야 함 |
| 3. 권한 상승 | 제로 쓰기로 cred 변경 → uid 0 | 🔶 체인은 구성됨, 실기 미성공 |
de_thread() 후 exec 실패 → exit_itimers()가 k_itimer를 해제하지만 rbtree 노드는 제거되지 않음 →
부모 프로세스가 reap할 때 timerqueue_del()이 이미 해제된 객체를 조작. 실기 panic 스택이 곧 증거:
pc : rb_next+0x24/0x54 lr : timerqueue_del+0x38/0x74
Call trace: rb_next / posix_cpu_timers_exit+0x78 / release_task+0xec /
wait_consider_task+0x65c / do_wait / kernel_wait4 / __arm64_sys_wait4
두 가지 핵심 구현 포인트(모두 코드에 이미 반영됨):
clone(CLONE_VM|SIGCHLD) 독립 프로세스여야 하며, pthread를 사용할 수 없다 ——
zap_other_threads()는 같은 thread group만 죽이므로, pthread locker는 de_thread() 시작 부분에서 죽고,
쓰기 락 해제 → exec 성공 → UAF 없음.pthread_create와 locker 스택의 mmap은 lockers가 쓰기 락을 획득하기 전에 완료되어야 한다.