
부트로더가 잠긴 ASUS Zenfone 9에서 CVE-2025-21479와 perf 기반 물리 주소 유출을 통해 임시 root(uid 0)를 획득합니다. GPLv3.
부트로더 잠금 해제가 불가능한 ASUS Zenfone 9(AI2202)에서 임시 루트(uid 0)를 획득하기 위한 연구 코드와 노트 — ASUS가 언락 도구를 단종시키고, 개발자 옵션에서 OEM 잠금 해제 토글을 제거했으며, 현재 펌웨어에서 fastboot oem unlock / flashing unlock을 강경하게 거부하기 때문이다.
이것은 부트로더 잠금 해제가 아니며 아무것도 플래시하지 않는다. 이것은 런타임 루트다: 호출 프로세스가 루트 자격 증명을 보유하는 것으로 끝나는 일련의 디바이스 측 프리미티브 체인이다. 재부팅하면 사라진다.
검증 환경: ASUS Zenfone 9 (AI2202), Android 14, 빌드 34.0304.2004.145, SPL 2024-07-05, 커널 5.10.205-android12-9-00029-g3f12df86bfdb-ab11799032, SM8475 / Adreno 730, 부트로더 잠김.
권한 / 범위. 여기의 모든 것은 운영자가 소유한 디바이스에 대해, 승인된 ADB 연결을 통해 실행된다. 어떤 벤더 서버도 공격하지 않고, 어떤 서명 키나 언락 토큰도 우회하지 않으며, 어떤 파티션에도 쓰지 않는다. 개발에 사용된 폰은 예비 기기로, 백업되어 있고 오프라인으로 유지된다. 소유하고 있으며 잃어도 감당할 수 있는 하드웨어에서 테스트하라.
| 단계 | 하는 일 | 위치 |
|---|---|---|
| 1 | CVE-2025-21479 (Adreno KGSL): SDS 패킷이 링버퍼 패킷으로 오분류되어, 유저랜드가 CP_SMMU_TABLE_UPDATE를 발행하고 GPU의 TTBR0를 공격자가 선택한 물리 주소로 지정할 수 있게 한다 | src/cheese.c |
| 2 | 물리 주소 유출: 이 빌드에서는 perf_event_paranoid = -1이므로, 우리가 소유한 페이지에 하드웨어 워치포인트를 걸면 PERF_SAMPLE_PHYS_ADDR — 즉 그 페이지의 물리 주소 — 가 반환된다. 업스트림이 의존하던 pagemap 유출을 대체한다(여기서는 PFN이 0으로 처리됨) | src/pa_leak.{c,h} |
| 3 | 임의 물리 읽기/쓰기: 가짜 페이지 테이블을 알려진 물리 주소(2단계에서 얻은)에 구성하므로, 스프레이 복권도, 무작위 탐색도 없다 | src/cheese_pa.c |
| 4 | 루트: 펌웨어 이미지에서 오프라인으로 커널 심볼을 해석하고, 디바이스에서 KASLR 슬라이드를 도출하고, __do_sys_capset을 commit_creds(&init_cred) 스텁으로 패치하고, capset()을 호출한다 | src/call_capset.c, scripts/ |
결과는 u:r:kernel:s0 SELinux 컨텍스트에서의 uid 0이다(init_cred의 SID를 상속받음), 즉 사실상 무제한이다. 이 단계에서는 SELinux가 Permissive여야 한다 — Enforcing에서는 자격 증명 교체가 SELinux 훅을 우회하기 때문에 프로세스가 대신 종료된다.
먼저 ZF9_SERIAL을 디바이스 시리얼로 설정하라(모든 스크립트가 이를 읽는다):
export ZF9_SERIAL=<your-device-serial>
# 0. 일회성: 디바이스 바이너리를 빌드하고 푸시한다 (Android NDK 필요)
# 사용된 정확한 clang 호출은 scripts/ 참조
adb push cheese_pa call_capset /data/local/tmp/
# 1. 전체 사이클: SELinux -> permissive, 패치, 루트로 명령 실행, 모든 것 복원
scripts/root-now.sh id
scripts/root-now.sh sh # 루트 셸
이 스크립트는 항상 원래의 커널 텍스트와 SELinux 상태를 복원하며(trap으로 보호됨), 읽기로 복원을 검증한다.
uid=0(root) gid=0(root) context=u:r:kernel:s0, capset(NULL,NULL) -> 0.gpu fault threshold exceeded 3 faults in 3000 msecs), 이후 명령은 EPERM으로 실패한다. 관측된 패치 성공률은 실행마다 13/13에서 2/13 dword 사이로 변한다.screenrecord 부하가 걸린 상태에서는 11-13/13으로 검증되었다. 스크립트는 모든 작업마다 자체 부하를 시작하지만(읽기도 경쟁한다), 이 도구들의 내부를 맨몸으로 실행하지는 않는다.capset 호출자가 쓰레기 코드를 실행할 수 있다). 스크립트는 진입 명령을 마지막에 쓰고, 모든 dword를 검증하며, 실패 시 복원한다 — 하지만 실행이 저하되면 재시도 전에 재부팅하라.지킬 가치가 있는 안전 규칙: 모든 쓰기 전에 읽기, 패치한 것은 항상 복원, 주입된 페이지 테이블 항목을 활성 상태로 남기지 않기, secure/TZ 물리 메모리를 건드리지 않기(치명적임), 저하된 상태에서 복구하려면 재부팅. ROADMAP.md에 각 항목의 근거와 함께 전체 함정 목록이 있다.
ROADMAP.md 지속적 인수인계: 검증된 상수, 절차, 함정, 열린 경로
STATUS.md 현재 상태 + 검증 결과
src/ pa_leak.{c,h} · cheese.c · cheese_pa.c (주력: PROBE/POKE/SELFTEST/ROOT 모드)
call_capset.c · host_kallsyms.c (오프라인 심볼 해석기)
scripts/ root-now.sh · patch-dwords.sh · demo-root.sh · verify-backup.sh
tools/ btf_offsets.py
펌웨어 이미지, OTA 패키지, 디바이스 로그는 의도적으로 커밋하지 않는다(.gitignore 참조).
GPLv3 — LICENSE 참조.