
Android GKI 6.12 기기에서 CVE-2026-43499를 악용: 결정적 임의 커널 읽기/쓰기, KASLR 우회, LD_PRELOAD 페이로드를 통한 전체 루트 권한 획득.
Android GKI 6.12 라인의 Linux 커널 익스플로잇(Samsung 및 Pixel 디바이스)으로,
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
su 데몬과 월페이퍼를 root 획득 후 아티팩트로 설치합니다.
상태: 활발한 개발 진행 중. 대상별 스택 깊이 오프셋(
PSELECT_*워드 시프트)과 슬라이드 동작은 디바이스마다 다르며 아직 기기에서 검증 중입니다. m1q (ZF1) 타깃이 현재 부팅(bring-up) 테스트의 초점입니다.
FUTEX_CMP_REQUEUE_PI가 waiter를 requeue하고 requeue의 교착 상태 감지 체인
탐색이 -EDEADLK를 반환하면, __rt_mutex_start_proxy_lock()은
remove_waiter()(rtmutex.c:1535)를 통해 롤백합니다. 롤백은 wait 트리에서
waiter를 올바르게 dequeue하지만 requeue 호출자의 pi_blocked_on을
클리어할 뿐 waiter->task->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의 저장된
return PC(stack_trace_save_tsk)를 링 버퍼에서 읽어 컴파일 시 포함된
worker_thread 오프셋과 비교합니다.SLIDE_LOGGERS_0_1을 boot_id sysctl 데이터에
심고, 누출된 값으로 stext를 재구성합니다.mm_struct 크기의 객체를 스프레이하고 futex-hash
충돌을 이용해 힙에서 mm_struct를 찾아, 가짜 객체 스프레이 베이스로
사용되는 커널 힙 페이지 주소를 누출합니다..data ashmem file_operations의
write_iter 슬롯(+0x30)에 configfs_bin_write_iter를 씁니다. 그 후
ashmem을 열면 configfs 바이너리 fs 경로를 통한 임의 커널 읽기/쓰기가
가능해집니다.선택적 STAGE3=1 단계로, 브리지 자식을 포크하여 mm->pgd를 스테이징된
가짜 페이지 테이블(3단계: PGD→L1→L2, RX 루프 리프 + RW 스택 리프)로 교체하고,
자식이 2MB 물리 스캔 윈도우를 통해 자신의 cred를 패치하는 레지스터 전용
어셈블리 블롭을 실행하게 합니다 — configfs/파이프 프리미티브 없이 전체 RAM
물리 R/W를 수행합니다. 현재 m1q 타깃에만 연결되어 있으며 아직 기기에서
실행되지 않았습니다.
src/targets/<코드네임>-<빌드>/에 41개 타깃이 있으며, 각각 최소한
target.h(커널 심볼 오프셋, KIMAGE_TEXT_BASE, 직접 맵 베이스, 구조체
오프셋)가 필요합니다. Pixel 타깃(comet, tokay, tegu, caiman, komodo,
frankel, mustang, rango, stallion, blazer)은 공유 소스를 오버라이드하고,
Samsung 타깃(m1q-*)은 디바이스별 로직을 추가합니다.
make list-projects # 전체 목록
커널 구조체 레이아웃은 디바이스마다 다릅니다 — 오프셋은 각 타깃의 실제 커널 바이너리 / kallsyms에 대해 검증해야 합니다.
출력: build/<PROJECT>/bin/preload.so (LD_PRELOAD로 로드되는 공유 라이브러리).
# 기본 프로젝트
CC=clang make
# 특정 디바이스 타깃 (기본값: blazer-CP2A.260605.012)
CC=clang make PROJECT=m1q-BP4A.251205.006
# CC=clang 없이: NDK가 발견되면 사용 ($NDK_ROOT / $ANDROID_NDK_HOME /
# $ANDROID_NDK_ROOT), 그렇지 않으면 호스트 clang + Android sysroot 사용
make PROJECT=m1q-BP4A.251205.006
# 빌드 설정 표시
make info
# 정리
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 아래에 설치됩니다.
exploit/
Makefile
assets/ # wallpaper.webp (내장된 root 획득 후 아티팩트)
src/
main.c # run_exploit() 엔트리 포인트 (공유)
preload.c # LD_PRELOAD 생성자, su/월페이퍼 설치
util.c # 주소 변환, 페이지 준비, configfs 헬퍼
pipe.c # 파이프 기반 물리 읽기/쓰기
root.c # cred 패치 / task 탐색
slide.c # KASLR 슬라이드 (tracefs + boot_id 경로)
fops.c # pselect 변조 + fops 조작
faketables.c # 3단계 가짜 페이지 테이블 빌더 (3-레벨)
stage3.c # pgd-스왑 브리지 자식 (구성 시에만 작동)
stage3_loop.S, stage3_poll.S # 브리지 블롭 + 진입 트램펄린
su_daemon.c # root 데몬 (별도 PIE로 컴파일)
su_blob.S, wallpaper_blob.S # 내장 바이너리
offset.h # -DTARGET_CONFIG_H로 대상별 target.h를 가져옴
kernelsnitch/ # mm_struct futex-hash 누출 라이브러리
targets/<코드네임>-<빌드>/ # 디바이스별 target.h + 선택적 오버라이드
poc/poc.c # 독립형 POC (익스플로잇과 분리)
타깃은 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_LOGGERS_0_1을 사용하며, SLIDE_NFULNL_LOGGER가
아닙니다; 해당 워드는 무작위화되지 않은 선형 맵 별칭이므로 boot_id 경로는
슬라이드와 무관합니다 (slide=0에서만 사용하세요 — 먼저 tracefs 누출을 실행).이것은 Android 디바이스용 익스플로잇 코드이며, 보안 연구, 승인된 취약점 테스트, 그리고 본인이 소유하거나 명시적 테스트 허가를 받은 하드웨어에 대한 방어 작업을 위한 것입니다. 본인이 소유하지 않은 디바이스에서의 오용은 불법입니다. 저자는 벽돌된 디바이스, 벽돌된 커널, 또는 보증 상실에 대해 책임을 지지 않습니다.
preload.c가 내장된 su 데몬(/apex/com.android.virt/bin에 tmpfs 마운트,
adbd 네임스페이스 및 로컬 변형 포함)을 설치하고 월페이퍼를 교체합니다.| 변수 | 기본값 | 용도 |
|---|
PSELECT_ROUTE_SHIFT | 컴파일 시 | 메인 경로용 A/B fd_set 워드 시프트 (m1q) |
SLIDE_SHIFT | 컴파일 시 | 슬라이드 경로용 A/B fd_set 워드 시프트 |
PSELECT_ROUTE_DELAY_USEC | 50000 | sched_setattr 전 consumer 지연 (> 0이어야 함) |
SLIDE_CONSUME_USEC, SLIDE_ENTER_DELAY_USEC, SLIDE_CONSUMER_CORE | — | 슬라이드 경로 타이밍/핀 고정 |
SKIP_SLIDE | 0 | 직접 맵 폴백 사용 (slide = 0) |
SLIDE_ONLY | 0 | KASLR 슬라이드만 실행 후 종료 |
SLIDE_P0_OFFSET, SLIDE_BOOTID_OFF | — | 슬라이드 p0-별칭 / boot_id 오프셋 오버라이드 |
KSNITCH_COLLISIONS | 4 | KernelSnitch 충돌 횟수 |
STAGE3 | 0 | pgd-스왑 브리지 자식 단계 활성화 (m1q) |
STAGE3_DRYRUN | 0 | 테이블 스테이징/검증 후 스왑 전 중단 |
PAGE_RECLAIM_SENDS, PSELECT_SIMPLE_LAYOUT | — | 스프레이/fd-set 레이아웃 조정 |
TMP_PAGE_* / TMP_UNAME_* | — | tmp_page 실험 경로 (m1q) |