
Android 16과 MediaTek MT6993 SoC를 탑재한 Xiaomi 17T Pro(warhol)의 CVE-2026-43499를 대상으로 하는 로컬 권한 상승 익스플로잇.
CVE-2026-43499 (IonStack) 로컬 권한 상승 — Xiaomi 17T Pro (warhol) — MediaTek MT6993, Android 16용으로 포팅됨.
GKI 6.12 / android16 전용. waiter 오버레이,
pselect스택 레이아웃 및 모든 struct 오프셋은 해당 브랜치에 고정되어 있으므로 여기의 어떤 것도 6.6, 6.1 또는 5.10으로 이전되지 않습니다 — 해당 커널에는 전용으로 빌드된 베이스가 필요합니다.generate_target.py는 기기에서 fault를 일으킬 헤더를 생성하는 대신 다른 배너를 거부합니다.상태: 동작 확인. 클린 부팅 후 2026-07-26에 기기에서 두 preloader 변형 모두로 검증됨 —
uid=0(root) context=u:r:kernel:s0, SELinux permissive,su설치됨. 루트는 영구적이지 않습니다: 부팅할 때마다LD_PRELOAD라인을 다시 실행하세요. pselect 레이스는 100%가 아닙니다: 실패한 실행은 폰을 패닉시킬 수 있으며, 재부팅 후 재시도하는 것은 정상입니다. 전체 기록은WARHOL_PORT.md를 참조하세요.이 기기는 kernel-MTE 수정이 필요합니다. 스톡 업스트림 popsicle은 이 기기를 루팅할 수 없습니다 —
kernel page retry N/12에서 영원히 회전합니다. Kernel MTE를 참조하세요.
빌드는 PRELOADER=retail(기본값)과 PRELOADER=eng 두 가지 preloader 변형으로 제공됩니다.
둘은 서로 다른 대상 디렉터리를 선택하므로 어느 쪽도 다른 쪽의 주소를 덮어쓸 수 없습니다 —
Preloader variant를 참조하세요.
아래 팩트는 리테일 전체 OTA 패키지에서 읽어낸 것입니다 — 메타데이터, A/B payload 매니페스트,
그리고 payload.bin에서 추출한 boot/vendor_boot/dtbo 이미지.
여기서는 SoC보다 커널이 더 중요합니다: warhol은 업스트림 popsicle과 동일한 GKI 브랜치
(android16-5, 4K 페이지)에 있으며, 패치레벨 하나 차이입니다 — 6.12.38 vs popsicle의
검증된 6.12.23. struct 오프셋은 여전히 빌드마다 재생성해야 합니다; exploit의 형태만 그대로
이어집니다. 다른 OS3.0.x 빌드는 자체 target.h가 필요합니다.
exploit 체인은 SoC가 아닌 GKI 브랜치에 버전이 고정되어 있습니다. rt_mutex waiter
레이아웃, pselect 스택 오버레이 및 pipe_buffer struct 오프셋은 모두 커널을 따르므로,
6.12/android16 베이스가 이전 브랜치의 동일 벤더 베이스보다 낫습니다.
x-spy/CVE-2026-43499-popsicle는 6.12/android16 GKI 라인(Xiaomi 17 / Pro / Ultra, 커널
6.12.23-android16-5)에서 검증된 유일한 공개 구현체이므로, source/는 여기서 가져와
업스트림과 최대한 가깝게 유지합니다 — 두 줄만 제외하고 (이 기기가 전혀 작동하려면 반드시
필요한 Kernel MTE 참조).
MediaTek 델타의 대부분은 빌드 타임 대상 생성에 국한되며, 두 부분으로 나뉩니다:
xbl_config 파티션에서 p0_phys_offset과 p0_kernel_phys_load를
읽지만, warhol에는 이 파티션이 없습니다. 대신 generate_target.py --dtb는
vendor_boot FDT의 /memory 노드에서 DRAM 베이스를 읽고, arm64_memblock_init이 하는
것과 동일한 방식으로 내림 반올림합니다. 커널의 물리 로드 주소는 --preloader로 preloader의
mb_kernel 예약에서 읽습니다; 이 기기의 델타는 0으로 나오며, lk는 다른 위치에 배치된
커널 부팅을 거부합니다.boot.img 안에 커널을 bare arm64 Image가 아닌 LZ4-legacy 압축 형태로
제공하므로, 생성기는 분석 전에 이를 압축 해제합니다.WARHOL_PORT.md §2에 전체 유도 과정이 있습니다.
MediaTek lk는 커널을 컴파일 타임 상수로 로드하지 않습니다 — mb_kernel이라는 DRAM 예약을
조회하고 정확히 그 위치에 도달했는지 확인합니다. 이 예약은 preloader가 만듭니다. 그래서
폰에서 실행 중인 preloader 빌드가 여기서 빌드 입력이 되는 것입니다: boot.img 외부에서
P0_KERNEL_PHYS_LOAD를 움직일 수 있는 유일한 요소이며, 그 값이 틀리면 exploit 실패가
아니라 잘못된 linear-map 별칭과 죽은 폰을 의미합니다.
따라서 두 가지 변형이 있으며, 별도의 대상 디렉터리로 유지됩니다:
make preload # PRELOADER=retail -> out/preload-<device>.so
make PRELOADER=eng preload # -> out/preload-<device>-eng.so
make both # both of the above, and print their sha256
두 아티팩트는 서로 다른 이름으로 나란히 유지됩니다. 이 펌웨어에서는 바이트 단위로 동일하게 나옵니다 — 이는 아래 분석의 결과이지 우회 수단이 아니므로, 여전히 두 개로 나누어 빌드하고 이름을 지정합니다.
다음 명령으로 두 preloader의 메모리 레이아웃 테이블을 직접 읽어보세요:
python3 tools/preloader_memlayout.py <preloader.bin> --diff <preloader_eng.bin>
이 펌웨어에서는 두 테이블이 동일합니다. 둘 다 mb_kernel.start = 0x80000000이므로,
eng target.h는 retail과 바이트 단위로 동일하게 나오고 두 빌드는 동일한 preload.so를
생성합니다.
실제로 exploit에 도달하는 것은 차이값 P0_KERNEL_PHYS_LOAD − P0_PHYS_OFFSET이며, 그 두
항은 동일하게 확실한 근거에 기반하지 않습니다:
P0_KERNEL_PHYS_LOAD — 위의 mb_kernel.start처럼 eng 이미지에서 측정된 값.P0_PHYS_OFFSET — 측정이 아닌 논증으로 결정됨. 이는 memstart_addr이기 때문인데,
커널은 DRAM 초기화 후 preloader가 보고하는 내용을 lk가 런타임에 출력하는 /memory 노드에서
가져오며, 생성기가 읽는 정적 FDT가 아닙니다. 논증은 다음과 같았습니다: 두 빌드는 전체 DRAM
경로를 공유하며 — 동일한 dramc/emi/mblock/memory_layout 소스, eng 전용 문자열 중
메모리 레이아웃 코드 없음 — 그리고 eng preloader의 추가 예약 하나(3 MiB 동적
security_fe_rsv, mapping=1)는 고정 주소 항목을 옮기지도 DRAM 하단에 구멍을 내지도
못합니다. 기기 실행으로 결판이 났습니다: linear-map 별칭이 eng에서 정확히
맞아떨어졌으므로 이 항은 올바릅니다.전체 유도 과정은
targets/warhol-OS3.0.304.0.WPSJPXM-eng/NOTES.md에
있습니다.
기기 실행에서 얻은 참고 사항:
ro.boot.verifiedbootstate=green과
flash.locked=1이 변경되지 않은 채 정상적으로 Android로 부팅되었습니다. 이는 직접
시도하기 전까지 미해결 질문이었습니다. efuse에 각인된 루트 키 해시로 결정되며, 이 빌드는
retail과 동일한 키로 서명되었습니다.%s META DIS는
retail 전용 문자열인 반면, eng는 retail에는 없는 전체 META 구현(Enable fastmeta.,
FAST META GPIO: %d, META_COM PORT: %d, read_meta_proinfo, …)을 포함합니다.
여기서는 Android로 바로 부팅되었지만, eng가 플래시된 폰이 다른 곳으로 떨어진다면 그것이
유력한 원인이며 어떤 target.h 값도 연루되지 않습니다.mb_kernel을 옮기면 두 대상은 갈라집니다 — 별도 디렉터리로 유지하는 것이
이를 조용히 지나치지 않게 막는 방법입니다.이 커널은 KASAN_HW_TAGS로 실행됩니다 — /proc/cmdline에
kasan.stack_ring_size=524288이 포함됨 — 따라서 slab 포인터는 비트 59:56에 할당 태그를
지닙니다. 업스트림 popsicle은 태그 없는 포인터를 가정하므로 이 기기를 전혀 루팅할 수
없습니다: [-] kernel page retry N/12 mode=1에서 무한히 반복되며, 로그에는 이유가
나타나지 않습니다.
source/의 두 줄이 이를 수정하며, 둘 다 warhol-mte-fix.patch에 있습니다:
검사만 태그를 제거합니다. 포인터 자체는 태그된 상태로 유지됩니다. 태그가 가리키는 메모리에 대해 올바른 태그이기 때문입니다 — 커널이 이를 역참조할 때 정확히 필요한 것입니다.
이 용도로 arm64.memtag.bootctl을 읽지 마십시오: 이 속성은 사용자 공간 MTE 제어이며
커널이 자체 할당에 태그를 지정하는지 여부를 알려주지 않습니다.
이 저장소는 포팅입니다. exploit 체인의 공로는 업스트림에 있습니다.
source/ exploit core (upstream popsicle + the kernel-MTE fix; see warhol-mte-fix.patch)
src/ main.c slide.c fops.c pipe.c util.c preload.c su_daemon.c + kernelsnitch/
targets/ per-build generated target.h, one dir per fingerprint x preloader variant
warhol-OS3.0.304.0.WPSJPXM/ PRELOADER=retail
warhol-OS3.0.304.0.WPSJPXM-eng/ PRELOADER=eng
tools/
payload_dump.py extract partitions from an A/B payload.bin (verifies size+sha256)
kernel_banner.py read the Linux banner out of a boot.img
bootinfo.py dump boot / vendor_boot header fields
generate_target.py target.h generator; upstream's, MediaTek-only, plus kernel decompression
preloader_memlayout.py dump/diff a MediaTek preloader's static DRAM reservation table
lz4legacy.py LZ4 legacy-frame decompressor (kernel images)
out/ build artifacts (gitignored)
target.h가 존재하기 전에는 아무것도 빌드되지 않습니다 — Makefile은 잘못된 커널용
바이너리를 생성하는 대신 분명하게 실패합니다.
# 1. extract boot.img straight out of the OTA zip (payload.bin is stored uncompressed,
# so --base seeks into the zip; no need to unpack 7.6 GB first)
python3 tools/payload_dump.py <ota.zip> --base 5081 -p boot,vendor_boot,dtbo -o <dir>
# 2. confirm the kernel banner — every offset downstream is pinned to it
python3 tools/kernel_banner.py <dir>/boot.img
python3 tools/bootinfo.py <dir>/boot.img
# 3. generate the target header. --preloader reads the kernel's physical load address
# out of the preloader's mb_kernel reservation; pass the preloader the phone runs.
python3 tools/generate_target.py \
--boot <dir>/boot.img --dtb <dir>/vendor_boot.img --preloader <preloader.bin> \
-o targets/warhol-OS3.0.304.0.WPSJPXM/target.h
# 4. build
make preload # DEVICE=warhol-OS3.0.304.0.WPSJPXM, PRELOADER=retail by default
손에 preloader 이미지가 없다면 --kernel-phys-delta 0이 이전 형태이며 이 펌웨어에서는
동일한 헤더를 생성합니다 — 그러나 이는 판독이 아닌 단언이므로 --preloader를 권장합니다.
둘 다 전달하면 생성기가 교차 검증하여 불일치 시 실패합니다.
--base 5081은 이 특정 OTA의 payload.bin 로컬 헤더 오프셋입니다; 다른 패키지에서는
META-INF/com/android/metadata의 ota-property-files에서 읽으세요.
Android NDK(NDK_ROOT / ANDROID_NDK_HOME)와 llvm-objdump가 필요합니다.
# <device> is the target name: <fingerprint> for PRELOADER=retail, <fingerprint>-eng for eng
adb push out/preload-<device>.so /data/local/tmp/preload.so
adb shell chmod 0644 /data/local/tmp/preload.so
adb shell "LD_PRELOAD=/data/local/tmp/preload.so /system/bin/true"
adb shell "/data/local/tmp/su -c id"
cred와 SELinux 상태는 메모리에서 패치되므로 부팅할 때마다 이 작업을 반복해야 합니다.
부트로더 잠금 해제는 필요하지 않습니다.
소유한 하드웨어에 대한 연구용입니다. 실행하면 기기가 부팅 루프에 빠지거나 벽돌이 될 수 있으며 보증이 무효화됩니다. 어떠한 종류의 보증도 제공되지 않습니다.
업스트림 프로젝트는 각 저자의 라이선스를 따릅니다; 이 포팅은 그 조건을 계승합니다.
| 항목 | 값 | 출처 |
|---|
| 코드네임 | warhol | pre-device=warhol |
| 빌드 핑거프린트 | Xiaomi/warhol_global/warhol:16/BP2A.250605.031.A3/OS3.0.304.0.WPSJPXM:user/release-keys | post-build |
| Incremental | OS3.0.304.0.WPSJPXM (JP 글로벌) | post-build-incremental |
| Android | 16, SDK 36 | post-sdk-level=36 |
| 보안 패치 | 2026-05-01 | post-security-patch-level |
| OTA 유형 | A/B (payload.bin, CrAU, 39개 파티션) | ota-type=AB |
| SoC | MediaTek MT6993 (Dimensity 9500) | vendor_boot DTB의 mediatek,mt6993-* 호환 목록; cmdline bootopt=64S3,32N2,64N2 |
| 커널 | 6.12.38-android16-5-g1d46253471dd-ab15048002-4k, clang 19.0.1 | 추출된 boot.img에서 읽은 배너 |
| boot 이미지 | header v4, 커널 18,898,125 B, LZ4-legacy 압축, ramdisk 없음 | tools/bootinfo.py |
PRELOADER= | 폰의 Preloader | 대상 디렉터리 | 상태 |
|---|
retail (기본값) | 출고된 preloader_<device>.bin — fastboot ROM의 preloader_raw.img와 바이트 단위로 동일 | targets/warhol-OS3.0.304.0.WPSJPXM/ | ✅ 2026-07-26 기기에서 검증됨 |
eng | 엔지니어링 빌드, preloader_<device>_eng.bin | targets/warhol-OS3.0.304.0.WPSJPXM-eng/ | ✅ 2026-07-26 기기에서 검증됨 |
| 파일 | 변경 | 이유 |
|---|
src/util.c | kernelsnitch_setup(..., mte_enabled=1) | 0이면 mm_struct 검색이 태그 없는 후보만 시도하여 태그된 포인터에 도달할 수 없음 — 나쁜 운이 아니라 구조적 누락 |
src/util.c | is_kernel_ptr() / is_direct_ptr() 범위 검사 전 태그 제거 | 커널 메모리에서 읽은 태그된 포인터는 그렇지 않으면 linear-map에 없는 것으로 거부됨 (direct-entry-fatal reason=bad-task-or-cpu) |
src/kernelsnitch/kernelsnitch.h | 태그 스윕 < 15 → < 16 | 태그 0xf 는 태그 없음/모든 포인터 일치이므로, 이제 스윕은 MTE 없는 커널도 포함 — mte_enabled=1은 어느 경우든 안전 |
| 저장소 | 여기서의 역할 |
|---|
| https://github.com/MobiusM/CVE-2026-43499 | 원본 CVE-2026-43499 PoC / 크래시 트리거 |
| https://github.com/x-spy/CVE-2026-43499-popsicle | 이 포팅의 베이스. Xiaomi 17 Pro Max (popsicle), 커널 6.12.23-android16-5; source/와 tools/generate_target.py는 여기서 가져옴 |
| https://github.com/Kananosa/CVE-2026-43499-For-Xiaomi-17T-chagall | 가장 가까운 자매 기기(Xiaomi 17T, chagall, 역시 MediaTek). popsicle 기반, 사전 빌드된 preload.so 포함 — 포함된 컴파일 상수가 물리 로드 오프셋을 확인해 줌 |
| https://github.com/MiCode/Xiaomi_Kernel_OpenSource | Xiaomi 커널 소스, struct 레이아웃 교차 확인용 |