
Android GKI 6.12 커널 익스플로잇으로, CVE-2026-43499를 대상으로 하며 rt_mutex 롤백 버그와 pselect 스택 덮어쓰기를 연쇄하여 Samsung 및 Pixel 기기에서 root 권한을 획득합니다.
Android GKI 6.12 라인(Samsung 및 Pixel 기기)을 대상으로 하는 Linux 커널 익스플로잇으로, CVE-2026-43499를 겨냥합니다: rt_mutex_start_proxy_lock() 내의 버그가 있는 remove_waiter() 롤백이 waiter::task 대신 current를 사용하여, waiter의 pi_blocked_on이 (나중에 pop되는) 커널 스택 rt_mutex_waiter를 가리킨 채로 남게 됩니다. 여기에 pselect() fd_set 스택 덮어쓰기와 consumer의 sched_setattr 기반 가짜 rb_tree 순회를 결합하면 결정론적인 커널 쓰기 프리미티브와 완전한 root를 얻을 수 있습니다.
이 익스플로잇은 LD_PRELOAD 공유 라이브러리(preload.so)로 실행되며, root 획득 후 아티팩트로 root su 데몬과 월페이퍼를 설치합니다.
상태: 활발히 개발 중. m1q (ZF1) 타겟이 bring-up의 초점입니다. pipei tmp_page-uname 부트스트랩이 현재 활성 경로입니다: 순회는 온디바이스에서 깨끗함이 입증되었고(build #33: 자식별 시드된 rt_mutex 영역), 전체 168개 후보 스윕이 진행 중입니다. configfs CFI 경로는 m1q에서 막다른 길이며(Rust ashmem — 주입 가능한 fops 슬롯 없음), C-ashmem 타겟을 위한 경로로 남아 있습니다. 타겟별 오프셋은 기기마다 다르므로 신뢰하기 전에 실제 커널 바이너리와 대조하여 검증하십시오.
FUTEX_CMP_REQUEUE_PI가 waiter를 requeue하고 requeue의 교착 감지 체인 순회가 -EDEADLK를 반환하면, __rt_mutex_start_proxy_lock()은 remove_waiter()를 통해 롤백합니다(rtmutex.c:1535). 롤백은 waiter를 대기 트리에서 올바르게 디큐하지만, waiter->task->pi_blocked_on 대신 requeue 호출자의 pi_blocked_on을 클리어합니다. WAITER의 pi_blocked_on은 스택 rt_mutex_waiter를 가리킨 채 dangling 상태로 남고, 이는 futex가 타임아웃되면 pop됩니다.
타임아웃 후 waiter의 커널 스택 영역이 재사용됩니다: core_sys_select()가 세 개의 fd_set을 해당 스택 버퍼로 복사합니다(ZF1의 nfds < 344 스택 경로). 조작된 fd_set 워드 배열이 가짜 rt_mutex_waiter / 가짜 task / 가짜 rt_mutex(포인터가 제어되는 rb_tree 루트 포함)를 재구성합니다. 그런 다음 consumer 스레드가 sched_setattr_tid(waiter) → rt_mutex_adjust_pi() → rb_erase_cached를 호출하여 제어된 값의 임의 주소 쓰기를 만들어냅니다.
전체 프리미티브는 PI 체인 사이클을 필수로 요구합니다: owner가 f_pi_target을 보유하면서 동시에 (waiter가 보유한) f_pi_chain에서 블록되므로, 체인 순회가 owner → chain → waiter → target → owner에 도달하여 -EDEADLK로 실패합니다. 사이클을 제거하면 requeue가 성공을 반환하고 커널 효과는 전무합니다.
sched_blocked_reason (m1q 주 경로, 온디바이스 입증): 블록된
kworker의 저장된 반환 PC(stack_trace_save_tsk)를 링 버퍼에서 읽어
컴파일 내장된 worker_thread 오프셋과 비교합니다. 부트_id 경로 워드가
사용되기 전에 실행되므로 데이터-앨리어스 쓰기 타겟(data_addr() = p0
앨리어스 + slide_p0_offset)이 0이 아닌 slide에서도 올바르게 유지됩니다.SLIDE_LOGGERS_0_1을 boot_id sysctl 데이터에 심고,
유출된 값이 stext를 재구성합니다. m1q에서는 보류됨: W1 타겟
(SLIDE_RANDOM_BOOT_ID_DATA_OFF, 낮은 물리 RAM)은 페이지 의존적 쓰기
가능성을 가지며 — slide가 부팅마다 타겟을 페이지 경계 너머로 이동시키므로
약 6/7 실행이 폴트합니다. SLIDE_FORCE_BOOTID(메커니즘 테스트)로 명시적
실행할 때만 tree_pc/tree_left가 스프레이된 페이지로 리다이렉트됩니다.mm_struct 크기의 객체를 스프레이하고 futex-hash 충돌을
사용하여 힙에서 mm_struct를 찾아내어, 가짜 객체 스프레이 베이스로 사용되는
커널 힙 페이지 주소를 유출합니다. 정리 시 waiter는 (join하지 않고) 분리되어
#26 이전 빌드를 크래시시킨 커널 스택 OOM을 피합니다.pselect fd_set 워드가 waiter의 커널 스택에 가짜 rt_mutex_waiter를 재구성하고, consumer 스레드의 sched_setattr_tid(waiter)가 rt_mutex_adjust_pi()를 통해 이를 순회합니다. ZF1 워드 맵(디스어셈 검증): PSELECT_WAITER_WORD_SHIFT = 0, word 12 = task (@+0x50), word 13 = lock (@+0x58), word 14 = wake_state (@+0x60 = 3). 시드된 가짜 rt_mutex를 사용하면 순회가 깨끗하게 완료됩니다: [7]의 rb_erase W1 *(tree_left)=tree_pc / W2 *(tree_pc&~3+8)=tree_left가 유일한 커널 쓰기이며, [11] (setprio / dequeue_pi)과 [9] wake는 모두 건너뜁니다(prio 100의 로컬 가짜 waiter가 스택 노드를 top-waiter 밖에 유지). build #19 이후 모든 순회 진입 완료가 온디바이스에서 생존합니다; 그 이전의 결정론적 크래시는 순회 본체 결함이 아니라 8바이트 페이로드 배치 오류(SKB_DATA_DELTA)였습니다.
선택적 STAGE3=1 단계로, 브리지 자식을 fork하고 그 mm->pgd를 준비된 가짜 페이지 테이블(3레벨: PGD→L1→L2, RX 루프 리프 + RW 스택 리프)로 교체한 뒤, 자식이 레지스터 전용 어셈블리 블롭을 실행하여 2MB 물리 스캔 윈도우를 통해 자신의 cred를 패치하도록 합니다 — configfs/pipe 프리미티브 없이 전 RAM 물리 R/W. 현재 m1q 타겟에만 연결되어 있으며 온디바이스에서 실행된 적은 없습니다.
src/targets/<codename>-<build>/에 41개 타겟이 있으며, 각각 최소한 target.h(커널 심볼 오프셋, KIMAGE_TEXT_BASE, direct-map 베이스, 구조체 오프셋)를 필요로 합니다. Pixel 타겟(comet, tokay, tegu, caiman, komodo, frankel, mustang, rango, stallion, blazer)은 공유 소스를 오버라이드하고, Samsung 타겟(m1q-*)은 기기별 로직을 추가합니다.
make list-projects # full list
커널 구조체 레이아웃은 기기마다 다르므로 — 오프셋은 각 타겟의 실제 커널 바이너리 / kallsyms와 대조하여 검증해야 합니다.
출력: build/<PROJECT>/bin/preload.so (LD_PRELOAD로 로드되는 공유 라이브러리).
# Default project
CC=clang make
# Specific device target (default: blazer-CP2A.260605.012)
CC=clang make PROJECT=m1q-BP4A.251205.006
# Without CC=clang: uses NDK if found ($NDK_ROOT / $ANDROID_NDK_HOME /
# $ANDROID_NDK_ROOT), otherwise host clang + Android sysroot
make PROJECT=m1q-BP4A.251205.006
# Show build configuration
make info
# Clean
make clean
빌드는 PIE su_daemon 바이너리(src/su_daemon.c, build/embed/su_daemon_aarch64_pie로 빌드됨)와 assets/wallpaper.webp를 src/su_blob.S / src/wallpaper_blob.S를 통해 preload.so에 내장합니다.
빌드 출력을 /data/local/tmp로 푸시한 다음, LD_PRELOAD 하에서 익스플로잇을 실행합니다. 라이브 bring-up 타겟에서는 tee 없이 실행하십시오 — tee는 자체 stdio를 버퍼링하여 커널 패닉 시 로그의 꼬리를 잃습니다(익스플로잇 stdout은 언버퍼링이므로 파일로 바로 리다이렉트하면 모든 줄이 보존됩니다):
adb push build/m1q-BP4A.251205.006/bin/preload.so /data/local/tmp/preload.so
adb push build/embed/su_daemon_aarch64_pie /data/local/tmp/su_daemon_aarch64_pie
adb shell "chmod 755 /data/local/tmp/preload.so /data/local/tmp/su_daemon_aarch64_pie"
adb shell "LD_PRELOAD=/data/local/tmp/preload.so \
/data/local/tmp/su_daemon_aarch64_pie \
> /data/local/tmp/output.log 2>&1"
성공하면 데몬이 /data/local/tmp/temp_su.sock에서 리스닝하고 su가 /apex/com.android.virt/bin 아래에 설치됩니다.
tools/run_pipei.sh는 바이너리를 푸시하고, 온디바이스에서 sha256 검증한 뒤(오래된 preload.so는 조용히 판독 불가능한 판정을 낳습니다 — ladder가 한때 로그 형식이 다른 build < #22를 실행했습니다), 전체 168개 후보 스윕을 3x56으로 청크하여 패닉 시 최대 한 청크(~10분)만 잃도록 실행합니다. 익스플로잇 stdout은 adb를 통해 직접 스트리밍됩니다(온디바이스 리다이렉트 없음 — 기기 측 tee는 패닉 시 꼬리를 잃습니다); 각 청크의 호스트 측 사본은 /tmp/m1q_pipei_chunk{1..3}_stream.log에 보관됩니다:
sh tools/run_pipei.sh
PASS = 어느 청크든 tmp_page uname changed + after uname='CatOS를 보이는 경우. PIPEI_CHILD_REGIONS는 최소한 청크 크기로 설정되어야 하며, 그렇지 않으면 자식별 rt_mutex 영역이 시드되지 않은 채로 남아 스윕이 후보 0에서 패닉합니다. 전체 커버리지는 또한 TMP_UNAME_PIPEI_SPLIT_ORDER_PAGES=1을 요구합니다 — 이것 없이는 해제된 8페이지 mm 블록 중 한 페이지만 스윕됩니다. run_pipei_slim.sh는 단일 후보 진단 변형이고, run_m1q_ladder.sh는 순회/격리 배터리(B16-2 오라클, slide 메커니즘, 스윕 단계)입니다.
exploit/
Makefile
assets/ # wallpaper.webp (embedded post-root artifact)
src/
main.c # run_exploit() entry point (shared)
preload.c # LD_PRELOAD constructor, su/wallpaper install
util.c # addr translation, page prep, configfs helpers
pipe.c # pipe-based physical read/write
root.c # cred patching / task walk
slide.c # KASLR slide (tracefs + boot_id routes)
fops.c # pselect corruption + fops manipulation
faketables.c # stage-3 fake page-table builders (3-level)
stage3.c # pgd-swap bridge child (no-op unless configured)
stage3_loop.S, stage3_poll.S # bridge blob + enter trampoline
su_daemon.c # root daemon (compiled as separate PIE)
su_blob.S, wallpaper_blob.S # embedded binaries
offset.h # pulls per-target target.h via -DTARGET_CONFIG_H
kernelsnitch/ # mm_struct futex-hash leak library
targets/<codename>-<build>/ # per-device target.h + optional overrides
poc/poc.c # standalone POC (separate from the exploit)
타겟은 Makefile pick_src 메커니즘을 통해 공유 소스를 오버라이드합니다: src/targets/<PROJECT>/<file>.c가 존재하면 공유 src/<file>.c 대신 사용되고, 그렇지 않으면 공유 버전이 사용됩니다.
-EDEADLK(errno 35)로 실패하는 것에 의존합니다. 사이클이 없으면 커널 효과
없이 success=1로 나타납니다.PSELECT_WAITER_WORD_SHIFT는 0이어야 합니다 (ZF1 스택 깊이를
ELF에서 검증). 다른 타겟은 여전히 기본값 1이므로 — 신뢰하기 전에 기기별로
검증하십시오.p0_data_alias()는 DIRECT_MAP_BASE(선형 맵)를 사용하며, 이는 ZF1의
P0_PAGE_OFFSET과 다릅니다. 역참조되는 커널 객체의 데이터 앨리어스는 선형
맵을 사용해야 합니다.pr_error()는 치명적입니다(exit(-1)), 경고/정보 매크로와 달리.SLIDE_NFULNL_LOGGER가 아니라 SLIDE_LOGGERS_0_1을
사용합니다; 이 워드는 무작위화되지 않은 선형 맵 앨리어스이므로 boot_id
경로는 slide 독립적입니다(slide=0에서만 사용 — tracefs 유출을 먼저
실행하십시오).SKB_DATA_DELTA는 -0xe78입니다 (온디바이스 측정, build #19) — 실제
skb frag head는 디스어셈 유도값 -0xe80보다 8바이트 작습니다; 그 8바이트
오류가 build #6-18의 모든 결정론적 pselect 크래시를 일으켰습니다
(순회 진입 → 가비지 역참조). GKI 6.12 공유 값; 타겟별 util.c 사본에는
환경 A/B 노브가 있습니다. 내장 상수 위에
를 중첩하지 마십시오.이것은 Android 기기용 익스플로잇 코드이며, 보안 연구, 승인된 취약점 테스트, 그리고 본인이 소유하거나 명시적 테스트 허가를 받은 하드웨어에서의 방어 작업을 위한 것입니다. 소유하지 않은 기기에서의 오용은 불법입니다. 저자는 벽돌이 된 기기, 벽돌이 된 커널, 또는 무효화된 보증에 대해 책임지지 않습니다.
write_iter 슬롯이 없고 ASHMEM_SET_NAME 힙 객체는 레이아웃이
configfs_bin_write_iter의 private_data와 정렬되지 않는 KVec입니다. m1q는
대신 kmalloc 쓰기로 전환합니다: 유출된 mm order-3 블록을 pipe_inode_info
객체로 재점유하고 pselect W1 쓰기(*(tree_left) = tree_pc)를 사용하여
후보의 tmp_page 슬롯(+0x90)을 UTS 네임스페이스 페이지(init_uts_ns)로
덮어씁니다. fanout 쓰기가 sysname 오프셋에 마커 이름을 심고 uname()이
"CatOS"를 보고합니다 — 정적 객체 의존성이 없는 검증 가능한 커널 쓰기입니다.
C-ashmem 타겟은 configfs 경로를 유지합니다.preload.c가 내장된 su 데몬(/apex/com.android.virt/bin에 tmpfs 마운트,
plus adbd-namespace 및 로컬 변형)을 설치하고 월페이퍼를 교체합니다.| Variable | Default | Purpose |
|---|
PSELECT_ROUTE_SHIFT | compile-time | 주 경로(m1q)의 A/B fd_set 워드 시프트 |
SLIDE_SHIFT | compile-time | slide 경로의 A/B fd_set 워드 시프트 |
PSELECT_ROUTE_DELAY_USEC | 50000 | sched_setattr 전 consumer 지연 (0보다 커야 함) |
SLIDE_CONSUME_USEC, SLIDE_ENTER_DELAY_USEC, SLIDE_CONSUMER_CORE | — | Slide 경로 타이밍/피닝 |
SKIP_SLIDE | 0 | direct-map 폴백 사용 (slide = 0) |
SLIDE_ONLY | 0 | KASLR slide만 실행하고 종료 |
SLIDE_P0_OFFSET, SLIDE_BOOTID_OFF | — | slide p0-앨리어스 / boot_id 오프셋 오버라이드 |
SLIDE_FORCE_BOOTID | 0 | tracefs slide 후 boot_id pselect 경로 강제 (메커니즘 테스트) |
KSNITCH_COLLISIONS | 4 | KernelSnitch 충돌 횟수 |
STAGE3 | 0 | pgd-swap 브리지 자식 단계 활성화 (m1q) |
STAGE3_DRYRUN | 0 | 테이블 준비/검증 후 swap 전에 중단 |
PAGE_RECLAIM_SENDS, PSELECT_SIMPLE_LAYOUT | — | 스프레이/fd-set 레이아웃 조정 |
TMP_PAGE_UNAME | 0 | m1q pipei tmp_page-uname 부트스트랩 실행 |
TMP_UNAME_PIPEI_SWEEP, TMP_UNAME_PIPEI_ALIGN_ORDER, TMP_UNAME_PIPEI_SPLIT_ORDER_PAGES | — | 스윕 모드; split_order는 해제된 mm 블록의 8페이지 전부를 스윕 |
TMP_UNAME_PIPEI_SLOT, TMP_UNAME_PIPEI_SLOT_CANDIDATES | — | 스윕 범위 (청크 시작 / 개수) |
PIPEI_CHILD_REGIONS | 0 | 자식별 시드된 rt_mutex 영역; 스윕 청크 이상이어야 하며, 그렇지 않으면 시드되지 않은 영역이 후보 0에서 패닉 |
PIPEI_RECLAIM_COUNT, PIPEI_DRAIN_COUNT | — | pipe 쌍 재점유/드레인; 재점유 실패 시 증가 |
PIPEI_SPONGE_MB | 3072 | 재점유 전반에 걸쳐 유지되는 order-0 플러드 스폰지 (build #29) |
PSELECT_W0_PRIO_OVERRIDE | 130 | Top-waiter prio; 시드된 영역에서는 무효 (레거시 stale-tree 가드, A/B 전용) |
PSELECT_SKB_DELTA_OVERRIDE | 0 | A/B 페이로드 페이지 내 오프셋 (기기가 -0xe78과 다를 경우에만) |
TMP_UNAME_NAME, TMP_UNAME_HOLD_SEC | "CatOS", 5 | 성공 후 마커 이름 / 유지 시간 |
SKB_DATA_DELTAPSELECT_SKB_DELTA_OVERRIDEASHMEM_SET_NAME KVec 엔벨로프가
configfs_bin_write_iter와 정렬되지 않습니다. pipei tmp_page 부트스트랩을
사용하십시오. m1q target.h의 PSELECT_WRITE_RIGHT_SPRAYED /
PSELECT_PI_TOP_TASK_SELF 가드가 순회의 [11] 쓰기를 RO 정적 fops 밖으로
리다이렉트합니다.payload+0x6000 + idx*0x40)으로 리다이렉트합니다.
공유 fake_lock 스윕은 후보 ~9-12에서 패닉했고(오래된 커널 스택 노드 재활용),
build #32의 빈 영역은 후보 0에서 패닉했습니다(0x41로 채워진 fake_task에서
[9] owner==NULL wake). PIPEI_CHILD_REGIONS를 설정하지 않으면 영역이
시드되지 않은 채로 남습니다.