
Amazon Fire 7(Fire OS 7.3.3.1)에서 Mali kbase JIT use-after-free CVE-2022-38181을 통해 임시 root를 획득하고, modprobe_path 덮어쓰기 체인을 사용하는 커널 익스플로잇 연구.
AI 지원 프로젝트. 이 연구, 익스플로잇 개발 및 문서는 GLM-5.3과 DeepSeek V4.1 Flash 모델을 사용한 AI 지원으로 작성되었습니다.
최종 펌웨어 — Fire OS 7.3.3.1, PS7331.4463N, kernel 4.9.117 (built 2025-05-03, SPL 2024-08-01) — 에서 Amazon Fire 7 9th gen (mustang, MT8163, Mali-T720) 을 위한 루트 익스플로잇 연구.
목표: LineageOS. 이 기기에서는 부트로더 경로가 막혀 있으므로 (패치된 bootrom — CMD 단락을 통한 preloader 전용), 남은 유일한 경로는 소프트웨어 커널 익스플로잇이다.
nix-shell -p android-tools --run './run.sh' # add -p zig too if no zig
성공 시:```
/data/metrics/su id # run a command as root
/data/metrics/su # interactive root shell
reclaim은 대략 3번의 부팅 중 1번 성공하며, 실패하면 태블릿이 패닉/재부팅된다;
run.sh는 재부팅을 기다렸다가 재시도할 뿐이다. SELinux는 익스플로잇의 일부로
Permissive로 강제되므로 root는 런타임 전용이다 — 재부팅하면 순정 상태로 돌아가고
run.sh를 다시 실행하면 된다.
미리 빌드된 st3와 su(armv7 정적)가 커밋되어 있으므로 실행에 툴체인은 필요 없다.
zig가 있으면 ./run.sh --build로 poc/*.c에서 다시 빌드할 수 있다.
GhostLock(아래)은 보류 상태다: MTK의 BUG_ON rtmutex 변형 + 셸에서의 커널 주소 노출 부재 = 이 빌드에서는 구조적 막다른 길(세션 2-4). kbase JIT UAF는 재진단되었고(destroy-worker의 "무조건적 패닉"은 JIT_FREE 역참조였으며, 로그는 패닉 도중 adbd 종료로 유실됨) stage 2는 이제 오라클로 입증되었다 — SESSION 5 섹션 참조.
rtmutex remove_waiter() futex-PI 스택 UAF (NebuSec 공개 2026-07, 수정 3bfdc63936dd
2026-04 반영). 취약 범위 2.6.39–7.1 → 우리의 4.9.117 (2025년 5월)이 영향받음.
우리의 정확한 빌드에서 검증됨:
CONFIG_FUTEX=y, rtmutex가 컴파일되어 포함됨, 버그가 그대로 존재:
rtmutex.c:1108-1111이 current->pi_lock/current->pi_blocked_on을 사용(원래는
waiter->task여야 함); 버그가 있는 호출 지점 rtmutex.c:1723 (rt_mutex_start_proxy_lock 오류 경로)WAIT_REQUEUE_PI/CMP_REQUEUE_PI), 디바이스 노드 없음,
SELinux로 제한되는 것 없음 — kbase 경로의 치명적 장애물이 여기에는 존재하지 않음sched_setattr → __sched_setscheduler → sched/core.c:4706의
rt_mutex_adjust_pi(p) — 오래된 pi_blocked_on을 역참조 ✓exp32/main.c의 포팅rt_waiter 프레임 오프셋 대 do_sys_select fd_set 영역 — 우리의
vmlinux 디스어셈블(do_sys_select stack_fds 대 futex_wait_requeue_pi 프레임), STAMP_NFDS/STAMP_WAITER_OFF를
튜너블로 노출Failed critical init step 3/dev/mali0 world-RW + SELinux gpu_device, kbase r26p0-01rel0selroot 2-패킷 체인: 을 0으로 만들고,
fake entry를 로 재작성. , SELinux Permissive.mustang, Fire OS 7.3.3.1 PS7331.4463N/00315758630404.9.117-g08fe75b-dirty, 빌드 Sat May 3 01:25:15 UTC 2025 (Linaro GCC 6.3-2017.05)/dev/kb, /dev/dkb (Amazon 커널 백업 파티션) root:drmrpc 0660 — 잠김mali_kbase r26p0-01rel0 (Midgard, Mali-T720), NVD 영향 범위 r4p0–r31p0 내mali_kbase_mem.c:2721 kbase_jit_destroy_worker가 영역을 해제하지만, kctx->jit_alloc[id]를 절대 지우지 않음mali_kbase_softjobs.c:1270 kbase_jit_free_finish가 오래된 jit_alloc[ids[j]]를 역참조mali_kbase_mem.c:3138 kbase_jit_backing_lost → destroy 경로 (reclaim 중에 발동)0xc0008000 VA / 0x40080000 PA)ARM_SW_DOMAIN_PAN 없음 → ret2usr 가능; CONFIG_PANIC_ON_OOPS=y (실패한 시도 = 재부팅)SLAB_FREELIST_RANDOM/HARDENED 없음, CONFIG_USER_NS/USERFAULTFD/NF_TABLES 없음CONFIG_MODULES=y, STATIC_USERMODEHELPER 없음 → modprobe_path 덮어쓰기 = root_IOC_TYPE 0x80)MEM_ALLOC union은 32바이트 (in에 extent 포함 4 × u64)BASE_MEM_PROT_GPU_RD|WR (비트 2|3)가 포함되어야 함, 레거시 R|W가 아님mmap(fd, offset=3<<12, PROT_NONE)JOB_SUBMIT stride는 sizeof(base_jd_atom_v2) = 48과 같아야 함 (base_jd_prio/base_jd_dep_type은 u8 typedef)MEM_JIT_INIT (nr 14, v2 구조체), alloc/free는 JOB_SUBMIT을 통한 소프트 잡
(BASE_JD_REQ_SOFT_JIT_ALLOC=0x209, ...FREE=0x20a; =user ptr, =count)패닉은 eviction 자체 중에 발생함 (evictable_reclaim_scan_objects → backing_lost →
destroy worker) — dangling 참조(jit_alloc[], evict 리스트)는 우리가 JIT_FREE를 제출하기
훨씬 전에 순회됨. Stage 2는 경쟁에서 이겨야 함: 압박이 여전히 진행 중일 때 우리 자신의
MEM_ALLOC 스프레이로 해제된 kbase_va_region을 재할당해야 함.
poc/stage2.c — stage-2 익스플로잇 (모드: step/uaf/spstep/spfree/spray/keys)
— spray 700 = 전체 오라클 실행; 생존하고 일시정지함 (정리하려면 kill)poc/mustang_jit_uaf.c — stage-1 PoC (모드: jit N / control N / pressure N)poc/build.sh — zig 크로스 빌드 (정적 musl armv7)kernel/vmlinux — 정확한 OTA 빌드에서 복구한 심볼 (vmlinux-to-elf)kernel/config-* — 실행 중인 기기에서 덤프한 nix-shell -p zig --run 'zig cc -target arm-linux-musleabihf -static -O2 -o juaf poc/mustang_jit_uaf.c' adb push juaf /data/local/tmp/juaf && adb shell chmod 755 /data/local/tmp/juaf adb shell /data/local/tmp/juaf jit 700 # full trigger (~reboots device) adb shell /data/local/tmp/juaf control 700 # no-JIT control adb shell /data/local/tmp/juaf pressure 700 # raw memory-pressure control
## 참고 자료
- GHSL-2022-054 권고: https://securitylab.github.com/advisories/GHSL-2022-054_Arm_Mali/
- Mo의 Pixel 6 익스플로잇 분석 글: https://github.blog/2023-01-23-pwning-the-all-google-phone-with-a-non-google-bug/
- Fire HD 10 (trona) 선례, 동일 버그 계열: ericpardee.github.io/fire-hd-ownership
- Amazon OSS 포털: amazon.com gp/help/customer/display.html nodeId=200203720
- XDA 언락 스레드 (이 hw rev에서는 무효): xdaforums.com/t/fire-7-2019-mustang-unbrick-downgrade-unlock-root.3944365/
## 세션 3 부록 (심층 syscall-stamp 조사)
측정된 copy-source 깊이 (절대값 vs syscall-entry sp0; waiter 범위 -0x1d8..-0x1a8):
- sendto sockaddr @ -0xc4 | process_vm iov @ -0x104 | recvmsg iov @ -0x11c
- sendmsg iov @ -0x12c | recvmmsg iov @ -0x154 | select fds @ -0x174
- pselect6 fds @ -0x19c | **sendmmsg iov @ -0x1bc (최적 — waiter+0x00까지 0x1c 부족)**
- poll entries @ -0x3e0 (완전히 아래; 잘못된 방향)
이번 세션에서 배제됨:
- io_submit 체인 너무 얕음 (~-0x130) | semtimedop: CONFIG_SYSVIPC=n (스텁)
- configfs 마운트됨 그러나 등록된 서브시스템 ZERO (mkdir 대상 없음)
- /sys/kernel/debug, /config: shell에 대해 SELinux 거부
- /proc/sys/kernel: getdents 작동 (29개 항목 나열), pid_max만 OPEN 가능;
kptr_restrict/hotplug/hostname/domainname 읽기 모두 거부
- 쓰기 값은 항상 waiter+0 (커널 스택 주소, 실행 가능, +0x1c에 셸코드):
rb_link_node *link = node, insert_color가 parent-color를 씀 — 트리 필드
스탬핑 없이는 제어된 값 변형 불가능 (갭 -0x1d8..-0x1bc)
- 이중 역참조 디스패치 필드는 *(waiter+0)=1, *(waiter+4/+8)=0을 읽음 — BLX 1/0
(nf_hooks, net_families, inet[6]_protos, seq_file->op 모두 무효)
- timer_list.function@+0xc와 work_struct.func@+0xc는 *(waiter+0xc) =
pi_tree self-ptr = 실행 가능한 waiter+0xc를 읽을 것 — 그러나 waiter+0을
timer/work로 큐잉하는 경로 없음 (링크 손상 / 간접 큐 소스 없음)
- 트리 루트 비영 (sysctl 핸들러 슬롯) = .text를 rb-tree로 통한 결정론적
포인터 추적; 제로 워드에서 종료 — 오프라인 시뮬레이션 가능하나, 유용한
쓰기 가능 슬롯에 착지하는 것은 비현실적
세션 4를 위한 남은 단서:
1. ioctl 심층 경로: dev_ioctl ifreq 복사 (40B 사용자 데이터) — SyS_ioctl→sock_ioctl→dev_ioctl 체인 깊이를 -0x1d8과 비교 측정
2. sendmmsg보다 0x1c 더 깊은 다른 복사 (아직 발견 없음)
3. stamp-surface 탐색 실패 시: walk-chained 구성 재고 또는 아직 열거되지 않은 쓰기 가능-제로-호출 슬롯 클래스 탐색
## 세션 4 — 두 가지 돌파구
### 1. MTK의 rtmutex_common.h가 모든 수수께끼의 핵심
MTK는 업스트림의 NULL-안전 rt_mutex_top_waiter를 다음으로 교체했다:```c
w = rb_entry(lock->waiters_leftmost, struct rt_mutex_waiter, tree_entry);
BUG_ON(w->lock != lock); // compiled to: ldr sb,[lock+8]; ldr r3,[sb+0x1c]; cmp; bne→udf#0x12
NULL 검사 없음 + BUG_ON. 모든 0인 앵커는 *(NULL+0x1c)에서 죽고, 쓰레기 앵커는 udf에서 죽는다. THE WALK REQUIRES: lock->waiters_leftmost (lock+8)가 가짜 waiter W (쓰기 가능)를 가리켜야 하며 W->lock (+0x1c) == lock이어야 한다.
Walk 흐름 완전 매핑됨 (rt_mutex_adjust_prio_chain @ 0xc0189a58):
/proc/self/task//stat 필드 28 (kstkesp)은 SHELL 컨텍스트에서 syscall-blocked 스레드의 실제 커널 SP를 반환한다 (검증됨: 0이 아닌 값 관측).
walk는 결정적으로 발사된다 (dead-lock probe: 매번 크래시, 클린 코드). 쓰기가 안착할 수 없는 이유는 3중 커널 하드닝 우연의 일치 때문이다:
스탬프 window (waiter+0x1c..0x5b)는 유일하게 제어되고 내용이 알려진 메모리이지만, 그 ADDRESS가 우리가 필요로 하는 미지수다. 자기 참조 구성은 모두 커널 주소를 상수로 스탬프해야 한다 — 유출 없이는 순환적이다.
그들의 5.4 커널은 UPSTREAM rtmutex_top_waiter (NULL-safe: if (!leftmost) return NULL)를 가진다 — 빈 트리 앵커가 살아남고, 그들의 쓰기는 null_fops
(그들의 커널에서는 쓰기 가능)에 안착했다. 그들조차 dispatch에서 막혀 있다
("ioctl reboot"). Mustang의 4.9.117 MTK 트리는 BUG_ON 변종을 가진다 —
Fire OS 8 태블릿 (GhostLock-5.10)은 그들의 5.10 커널이 upstream 스타일이라
성공했다.
셸 도메인에서 테스트되어 죽은 것:
결론: mustang에서의 GhostLock은 이 커널이 셸 도메인에 노출하지 않는 커널 주소 유출을 요구한다. 자기 참조 가짜 lock은 그것 없이는 구성될 수 없다.
(a) 부팅 결정적 갈기: 재부팅 → 크래시 오라클로 스택 주소 보정 (~20-30회 재부팅), 재현성 검증. 장기전 — 늦은 부팅 스레드 스택 할당이 안정적일 가능성 낮음. (b) 축적된 자산으로 kbase CVE-2022-38181로 PIVOT: 정확한 빌드 vmlinux + 전체 소스 + 툴체인 + O_SYNC 추적 규율 + 깊은 4.9 지식. 원래 블로커 (JIT eviction 중 destroy-worker panic)는 스프레이 타이밍 문제이며, 이제 더 잘 이해됨. (c) 정직한 ~45%에서 중단: 트리거 증명됨, walk는 명령어까지 매핑됨, 쓰기는 MTK BUG_ON + no-leak에 의해 차단됨.
권장: (b) — kbase 버그는 이 정확한 소스에서 존재가 검증되었고, 작동하는 트리거가 있었으며, 그 블로커는 구조적이 아니라 기계적이다.
step 모드 (alloc id=1 → DONT_NEED → 700MB 압력 → MEM_QUERY, free 없음)
생존: query=-1 (destroy worker에 의해 region 해제됨, rbtree-clean).
worker 경로는 압력 하 JIT_FREE 합법 흐름과 바이트 단위로 동일하다.
Session-1의 크래시는 항상 JIT_FREE dangling 역참조였다; 그 로그 라인은
panic이 flush 중간에 adbd를 죽여서 유실되었다. uaf 모드로 두 번 더
검증됨 (bare free → panic, 동일한 로그 컷오프). GHSL-2022-054 흐름은
이 빌드에서 완전히 살아 있다.
kbase_jit_free(kctx, reg) @ 0xc058495c, 완전히 제어되는 가짜 reg:
reg->cpu_alloc NULL → backed size 0 → trim block 건너뜀 (0xc0584978)kctx+0x147dd (byte) + kctx+0x147de+bin_id (byte)mark_reclaim(reg->gpu_alloc) @ 0xc059b158: 체인
K=*(gpu_alloc+0x38) → *(K+0x1429c)==0이 mm-atomics 건너뜀 →
atomic_sub nents@K+0x141c8, D=*(K+4) → atomic_sub nents@D+0x538.
nents=0이면 모든 쓰기는 no-op 저장 (같은 값의 strex).reg->flags |= 0x100000 (가짜에 쓰기, 무해)list_add(gpu_alloc->evict_node, &kctx->evict_list): evict_list head
@ kctx+0x1427c; gpu_alloc+0x18/0x1c에 쓰기 (쓰기 가능해야 함)r3=*(reg+0x3c) prev, r2=*(reg+0x38) next
→ *(next+4)=prev; *(prev+0)=next — 두 개의 임의 write-what-where,
그 다음 reg+0x38을 jit_pool_head @ kctx+0x148e8로 재링크9개 후보; S=0xc118b7ec (xfrm 데이터, 이 기기에서 휴면):
*(S+8)=0 (nents), *(S+0x18)=S+0x18 (빈 evict_node → WARN 없음),
K=*(S+0x38)=0xc118b820 → *(K+0x1429c)=0, K/D+0x141c8/+0x538 모두
쓰기 가능 데이터에 있음. 오라클 타겟 준비됨: init_uts_ns.name.nodename=0xc110d561
("(none)", uname으로 읽기 가능), scratch P=0xc118bd58 (xfrm 0).
S=0xc111cba4 (tracepoint 인접) 회피. CONFIG_DEBUG_RODATA=y → 모든 쓰기
타겟은 .data/.bss에 있어야 함 (bss 0xc11d9000-0xc12d9000).
add_key (CONFIG_KEYS=y): 셸에 대해 SELinux 거부. Dead.setxattr 값 버퍼: kvmalloc(96)+copy_from_user가 SELinux 검사 전에
발생 → 호출이 실패해도 alloc dance는 SELinux-증명됨;
일시적 (syscall 종료 시 해제), 바이트는 +4..95에 지속 (freelist ptr이
+0..3 = rblink를 덮어씀, kbase_jit_free에서 미사용)spray 700 실행 2026-09-11: 압력 중 5895개 region 스프레이됨, dangling
id=1에 대한 JIT_FREE가 재확보된 region에서 완료됨, 그 다음
JIT_ALLOC(0x40, bin 0)이 jit_pool_head를 걸어 스프레이된 region
#4251의 VA (0x142701000) 를 반환 — dangling 포인터가 소비한 바로 그
region. 이후 프로세스 킬: kctx teardown 클린, 크래시 없음. Stage 2 완료:
제어된 객체 타입 + 내용으로 결정적 UAF 리다이렉트.
Region-타입 reclaim은 합법적 dance 생존을 제공하지만 jit_node는 INIT된 self → unlink 프리미티브 없음. +0x38/+0x3c에 raw 바이트 필요:
*(N+4)=P with P=유저랜드 셸코드 페이지 (PAN 없음!) —
후보: const fops는 .rodata (DEBUG_RODATA) → .data의 비-const
fn ptr을 타겟, 또는 binfmt formats 리스트 head, 또는 sysctl proc_handler
(테이블 쓰기 가능성 검증). 폴백: 바이트-체인 쓰기를 통한 modprobe_path
(값은 쓰기 가능 주소여야 함 — 포인터 형태 타겟 사용)poc/stage3.c):
kern_table[pid_max].proc_handler @ 0xc1113f40 (쓰기 가능
.data, 문자열-포인터 스캔 + handler == proc_dointvec_minmax로 검증됨)b +8로 건너뜀; W2가 P = handler 필드에 N을 씀)read /proc/sys/kernel/pid_max
(셸에서 읽기 가능); 자체 태스크 컨텍스트에서 실행 → creds가 우리에게 적용inotify_handle_event가 name_len+0x1d를 kmalloc,
name 바이트 (완전 제어, NUL/slash-free 제약)가 event+0x1c에;
name_len=60 → kmalloc-96; 큐잉됨 → 보유됨; 4 인스턴스 → rename당
4 이벤트; 셸에서 SELinux-OK. 가짜 재설계 NUL-free: cpu_alloc이
S를 가리킴 (nents@S+8 = 0 → NULL과 동일한 의미)| 변형 | 결과 |
|---|---|
| 폭풍 중 동시 rename 스프레이어 | rename 정체 (journal/GFP_NOFS) → 총 128 → 쓰레기 역참조 |
| 사전 드레인 12K 이벤트 + 압력 + 작은 트레일링 |
작동 가설: 폭풍-쓰레기 레이스 — destroy worker가 슬롯을 해제한 시점 (폭풍 중간)과 자식-킬/조용 사이에, 잔여 reclaim 활동이 희생자 slab의 유일한 빈 슬롯을 비-페이로드 바이트로 차지한다. Region 스프레이 (stage 2)는 폭풍 DURING 내내 지속적으로 할당하기 때문에 이긴다; rename은 그럴 수 없다.
iso 모드): 동일 타이밍, commit-0 REGION + stage-2
pool-reuse 오라클로 트레일링 → 오라클 히트
→ 타이밍/도달성은 괜찮음; 이벤트가 문제current->mm이 커널 스레드의 것 → 우아한
"가짜의 포인터를 우리 자신의 유저스페이스 mmap으로 향하게" 설계 (PAN 없음!)
가 비결정적으로 FAULT. 그 외에는 완벽한 가짜로 5/5 크래시.K=*(S+0x38) 읽기 가능, *(K+0x1429c)==0 런타임에
(mm-atomics 건너뜀), K+0x141c8 쓰기 가능, D=*(K+4) → D+0x538
쓰기 가능, nents *(S+8)은 0이 바람직. 9개의 오프라인 S-후보는
FILE 바이트에 대해 검증됨 — 런타임 드리프트 (xfrm/tracepoint init)가
이들을 미검증 상태로 만듦. 잘못된 체인 = 크래시 = 재부팅 (~3분 사이클).A. Physmap-스프레이 가짜 (ret2dir, 고전적 arm32 no-PAN): ~450MB의 유저 페이지를 스프레이, 각각 하나의 추측 주소 G에 맞춰 구운 가짜 패턴 포함 (G&0xfff = 0x141 for NUL-free name bytes; S=G; K=G-0x141b4 so K+4 lands in-page; K+0x141c8/+0x1429c → G+0x10/+0xd4 in-page; D=G+0x300; stray sub-0 stores hit random mapped RAM - harmless with nents=0). 스프레이는 eviction 압력 역할도 겸함 (dirty anon = unevictable → ~100-200MB 추가만 필요). 확률 ≈ 45% (페이지 히트) × ~50% (외부 K+0x1429c 워드가 0... 위 레이아웃대로 K+0x1429c를 in-page로 유지하면 확률 = 페이지-히트만). 빗나감 = 크래시 = 재부팅, 재시도. B. 9개 정적 S-후보 무차별 대입 (xfrm 0xc118b7ec 먼저, tracepoint-인접 0xc111cba4 두 번째...): 후보당 1회 재부팅, benign-오라클 페이로드 먼저, 히트 시 무기. C. pagemap 기반 정확한 G (죽음: PFN 마스킹됨) — 재검토하지 말 것.
/tmp/opencode/ksrc2/kernel/mediatek/mt8163/4.9/ (arch/arm 포함
mustang.dtsi, mm/, fs/eventpoll.c, fs/notify) — ksrc/platform.tar에서 추출.
발견 사항:
physmap_retouch() 추가 (trailing/deref 전에
모든 스프레이 페이지를 다시 폴트인)Regions reclaim: 3/3. Events: 0/~12 시도 — 28K 독점 할당을 head start와 구성상 올바른 fake와 함께 포함. events가 p_hit(G)≈0.35로 reclaim한다면, 다섯 번의 pmap 미스 ≈ 11.6% — 가능하지만 이제는 낮음 (~10-15%). events가 구조적으로 이 슬롯을 차지할 수 없거나 (이유 불명 — 같은 캐시, 같은 컨텍스트, 같은 타이밍) 우리의 G 추측이 체계적으로 빗나가고 있음 (highmem 경계 편향, 할당자 배치).
vmalloc=496M slub_max_order=0 slub_debug=O loglevel=8 initcall_debug
Regions는 victim 슬롯을 3/3 차지; events는 0/13. 같은 캐시 (소스 + disasm 수준에서 입증), 같은 프로세스 컨텍스트, 같은 pinned cpus, 같은 post-kill 타이밍, 독점 head start를 가진 수천 개의 할당. 메커니즘 불명. 남은 용의자: 할당률/빈도와 partial-list 회전의 상관관계 (region ioctls는 ~1ms 간격 vs event renames는 ~100µs 간격 — 반대 방향?), 또는 slub_max_order=0 하의 SLUB freelist 정렬 세부 사항이 ... 불명확.
[+] uname.nodename="X?lhost" (was "localhost") <<< ORACLE HIT
**전체 raw-byte 체인이 라이브 디바이스에서 동작한다**: 이벤트가
해제된 영역 슬롯을 회수 → kbase_jit_free가 우리의 fake를 역참조(S=kbm 스프레이의 physmap 페이지, G=0xc154b2a4) → unlink가 우리의 두 번의 쓰기를 실행한다.
benign payload(nodename 쓰기)로 반복 검증됨.
### 세션에서 발견한 연쇄
1. kbm 페이지는 GFP_HIGHUSER → HIGHMEM이었고, physmap에서 보이지 않음
(mali_kbase_mem_pool.c:164!) — zonelist spill로 수정: 260 × 2MB
스프레이 > highmem-free → 잉여분이 ZONE_NORMAL(physmap)에 안착
2. 첫 무기 시도는 크래시: W1 타깃 N+4가 USER 페이지였음 —
unlink는 kworker ctx(no mm)에서 실행 → fault. 수정: ring-0
shellcode를 physmap 패턴의 page+0x600에 심음(arm32 non-LPAE에서 direct map RWX) — 커널 상주 코드, ret2usr 불필요
3. ctl_table 오프셋 버그: proc_handler는 entry+0x14에 있고 +0x18이 아님(원래 스캔은 맞았고, 내 define이 틀렸음) — extra1에 쓰고 있었음
4. 과압 회귀 발견 및 되돌림: kid 예산 20×100MB +
trailing 10s가 reclaim을 깨뜨림; 동작하는 설정은 2 kids/200MB +
5s/+3200 trailing(되돌린 후 benign 1/1 적중)
5. **diag2: W2 → &pid_max 전역(0xc1114d7c) → 읽기가 우리 값을 반환
(-1055861411 = 0xc110d55d as int32) — 쓰기 + 되읽기 입증됨**
### 남은 미스터리(실험 하나만 남음)
올바른 핸들러 주소(0xc1113f3c)로 diag1: W1 발화(nodename
변경), W2는 실행됐어야 함(다음 명령) — 그런데도 pid_max 읽기는
여전히 깨끗한 값을 반환 → 우리가 쓰는 핸들러 필드가 inode가
디스패치하는 그 필드가 아님. diag3(대기 중; hit boot 필요): W2 →
entry->data FIELD(0xc1113f2c)가 nodename을 가리키게 — 만약 읽기가
nodename 바이트를 int로 보여주면 우리 entry는 살아 있고 핸들러
오프셋만 어쩐지 틀린 것; 영향이 없으면 inode는 shadow table
복사본을 사용하는 것이므로 라이브 쪽을 찾아야 함.
### 적중률 현실
부팅당 동전 던지기(~25-40%), 군집화됨; 안전한 미스/크래시 부팅이
연속으로 여러 번 나오는 것은 정상. 대략 3-4번 부팅 중 1번이 적중. 동작하는
설정을 정확히 유지할 것(2 kids, 5s trailing, 260-region kbm, G=0xc154b2a4).
### Session-9 TODO
1. hit boot에서 diag3 완료("W1 FIRED"가 나올 때까지 롤)
2. entry가 살아 있으면: 핸들러 오프셋을 경험적으로 재확인(proc_dostring의 주소를 핸들러로 쓰기... N은 유용한 값과 같아야 함 — unlink를 써서 대신 entry->data를 쓰고 피벗: 예를 들어 data=selinux_enforcing 인접...)
3. shadow table이면: 라이브 쪽을 찾기 — kallsyms에는 데이터 심볼이 없음;
후보: /proc/sys 동작 스캔, 또는 .data에서 헤더 리스트 패턴으로
두 번째 ctl_table 영역 찾기(handler=proc_dointvec_minmax이고
maxlen=4인 0x20-stride 엔트리 — 전부 열거하고 각각 diag-write)
4. 디스패치를 완전히 피하는 대안 타깃 클래스: shell에서 도달 가능한
경로에서 호출되는 .data 함수 포인터(audit 필요)
5. 쓰기 프리미티브 자체는 완료 — 이제 신뢰할 수 있는 커널 주소
타깃이면 root에 충분
## SESSION 9 — nf-WEAPON: reclaim+unlink+G-hit 무기 내부에서 입증됨; 남은 것은 hook walk뿐
### 새로운 트리거 설계(sysctl-handler 경로를 완전히 대체)
사용자의 아이디어를 커널 메모리로 옮김: SUID 파일 없음(시스템은
dm-verity RO; 프리미티브는 커널 RAM에 씀). 대신: **fake netfilter
hook**. 이 커널에는 NEW nf_hook_entries API의 Android-common
백포트가 있지만 LINKED LIST로 구현됨(nf_hook_slow + helper 0xc09897d4의 disasm으로 검증):
- `__ip_local_out(net, sk, skb)`가 entries CELL을
**[net+0x58c]**에서 로드해 state+0x1c에 저장하고 nf_hook_slow 호출
- walk: `entry = *cell`; while(entry){ if (state->[4] <= entry->[0x20])
call entry->[0xc](entry->[0x14], skb, state); entry = entry->[0]; }
— 즉 **fn@entry+0x0c, priv@entry+0x14, priority@entry+0x20,
next@entry+0x00**; state+4 = INT_MIN 임계값(항상 통과)
- init_net = **0xc1104548** (CONFIG_NET_NS=n → sock_net()이
상수를 인라인; 6678 movw/movt 참조, 히스토그램 챔피언; nf_hook_slow 자체의 리터럴로 교차 확인됨). TARGET: **[init_net+0x58c] = 0xc1104ad4**
- LOCAL_OUT hook은 SENDER의 프로세스 컨텍스트에서 실행 → 우리 hookfn의
commit_creds(prepare_kernel_cred(0))가 패킷을 보낸 프로세스를 root로 만듦. 트리거 = sendto(127.0.0.1:9) UDP.
### nf 모드 레이아웃(poc/stage3.c, 모드 `nf 200`)
- kbm 패턴 페이지(page-relative, 단일 진실 공급원 — 예전
무기에는 이제 수정된 세 가지 버그가 있었음: proc_handler@+0x14이지 +0x18이 아님; W1
타깃은 KERNEL mem이어야 함(kworker ctx, no mm); 심은 코드가
page+0x600인데 entry G+0x600=page+0x8a4 불일치):
- +0x2ac/+0x2bc/+0x2c0/+0x2dc/+0x3a8: fake phy-alloc 체인(변경 없음)
- +0x600: nf_code hookfn(marker 저장 + prepare_kernel_cred +
commit_creds + return NF_ACCEPT(1))
- +0x700: fake entry {next=0, fn=PM_PAGE+0x600, priv=0, prio=0x100}
- +0x740: cell → PM_PAGE+0x700
- payload: N = PM_PAGE+0x740 (0xc154b740), P = 0xc1104ad4
→ W1: *(cell+4)=P (우리 페이지에 안착), W2: *(init_net+0x58c)=cell
### 자기 진단 instrumentation(kbm_scan_for)
kbm CPU 매핑은 유지됨; free 후에 스프레이된 모든 페이지를 알려진
워드로 스캔:
- page+0x744에서 scan(HOOKS_PTR_ADDR) → reclaim + unlink를 입증하고
G 추측을 뒷받침하는 phys 페이지가 어느 것인지 밝힘
- page+0x7f0에서 scan(0x600d600d) → hookfn이 실행되었음을 입증
(nf_code가 두 번째 동작으로 이 marker를 씀)
### 중요한 실행(2026-09-11, 늦은 session 9)```
[+] W1 CONFIRMED: region 135 page+0x185000 <- 0xc1104ad4 (reclaim + G hit!)
[+] nf trigger done, uid=2000 euid=2000
전체 무기 체인이 라이브 부팅에서 발사되었다: 이벤트가 슬롯을 회수했고, 페이크가 실행되었으며, unlink가 수행되었고, G-추측 페이지(0xc154b000)는 진짜로 우리 것이었다(영역 135 페이지 389). W2 = *(0xc1104ad4)=cell은 인접 명령어이다 — 그것은 실행되었음에 틀림없다. 그런데도 UDP sendto는 우리를 루트로 만들지 못했다 → 실패는 훅 경로 내부에 있다: walk 시맨틱, [state+0x1c] 배관, 우선순위 비교, 또는 엔트리 필드.
(hookfn-실행-여부를 구별하기 위한 마커 실험이 추가되었다; 세션 종료 전에는 크래시 부팅만 있었음 — 아직 깨끗한 데이터 없음.)
pmap 200)이 환경 정상성 검사이다 (따뜻할 때 4/4 적중; 차가울 때 크래시-미스)nf 200을 실행하여 W1-CONFIRMED 줄이 나타날 때까지 반복한 뒤, MARKER 줄을 읽어라:
a. 마커 존재, uid!=0 → 셸코드의 creds 실패 (prepare_kernel_cred/commit_creds 주소 확인; blx 인코딩)
b. 마커 부재 → walk가 우리를 호출하지 않았다: ... 다음 진단으로 마커만 쓰고 1을 반환하는 hookfn (creds 없음)으로 검증 — 여전히 부재라면:
ls -la 확인; 마커 파일 남기기poc/stage3.c 모드: pin/root/drain{,2,3,4,5}/iso — 전체 무기 + 오라클 + 격리 하네스, O_SYNC 크래시 지점 포렌식init_net: 0xc1104548은 __stack_chk_guard이다 (movw/movt 히스토그램이 스택 카나리 로드로 오염되었다 — 2025년에 전체 nf 계획을 그것 위에 세웠다). 실제 init_net = 0xc1185040 (확인됨: ip_send_skb(net,...)가 이 리터럴로 호출됨; ~994개 참조 모두 net 스택에 있음). IPv4 LOCAL_OUT cell = init_net+0x58c = 0xc11855cc.nf_iterate는 [init_net+0x58c]를 nf_hook_ops 포인터 자체로 취급한다 — 그 값으로부터 fn@+0xc, priv@+0x14, prio@+0x20을 직접 읽는다. 세션-9 페이크 엔트리는 PM_PAGE+0x700에 있었고 cell이 그것을 가리켰다 (cell의 미사용 next/fn 필드 → → 크래시). 페이크 엔트리는 반드시 (0xc154b740)에 있어야 한다. 이것을 수정하자 훅 호출이 입증되었다 ( 모드 = SAFE_FN이 200개의 sendto를 모두 깨끗하게 처리).commit_creds(prepare_kernel_cred(0)) / override_creds(&init_cred)는 uid 0을 주지만 kernel SELinux SID에 들어가며, 이 Fire OS 정책은 그것이 /data나 /sys/fs/selinux/enforce에 쓰는 것을 허용하지 않는다 (확인됨: EACCES). 실제 init SID (7)도 거부된다 (가짜 struct cred 테스트). enforcing_setup은 __init이다 (해제됨 → 크래시). mark_reclaim의 atomic_sub는 nents=1이 필요하며 이는 shrink_cpu_mapping의 조기 종료를 깨뜨린다.
승리: 익스플로잇 프로세스는 kbm CPU 매핑을 유지하므로, 페이크 nf 엔트리는 패킷 사이에 제자리에서 다시 쓸 수 있다:
selroot 모드: entry = {fn=0xc01d503c (mov r3,#0;str r3,[r0];bx lr), priv=0xc1213ea8 (selinux_state.enforcing)}.*(enforcing)=0 → SELinux Permissive.kbm_cpu[reg]+off를 통해 엔트리를 {fn=commit_creds, priv=&init_cred}로 다시 쓴다.commit_creds(&init_cred) → permissive SELinux와 함께 uid 0 → 사용 가능한 루트, 모두 한 번의 회수로, 체인 불필요.[+] W1 CONFIRMED: region 67 page+0xad000 <- 0xc11855cc (reclaim + G hit!) [*] sendto 0 -> -1 errno=1 uid=0 euid=0 [+] nf trigger done, uid=0 euid=0 <<< HOOK RAN - commit_creds OK [+] ROOT: uid=0 euid=0 [+] setenforce write=1 [+] su copied bytes=236220
- `getenforce` → **Permissive**; 일시정지된 st3는 `Uid: 0 0 0 0`,
`CapEff: 3fffffffff`.
- `/data`는 **nosuid**로 마운트되어 setuid `su`가 동작할 수 없다. 작은
`rootshell`(UDP 전송 → 자기 자신에 `commit_creds` → `execl sh`)이
대화형 root 셸을 제공한다: `uid=0(root) context=u:r:kernel:s0`.
- Root는 `/dev/block/by-name/*`를 읽고 쓸 수 있다 (`dd if=boot ...` OK).
### Stage-3 코드 상태 (`poc/stage3.c`)
- 모드: `nf <mb> [probe|uprobe|oc|chain|fc <sid>|notrig|selroot]`
- `selroot`가 동작하는 무기다. 주요 정적 값: `init_net=0xc1185040`,
`HOOKS_PTR_ADDR=0xc11855cc`, `ENFORCING_ADDR=0xc1213ea8`,
`ZERO_GADGET=0xc01d503c`, `commit_creds=0xc014993c`,
`init_cred=0xc1114f54`.
- 익스플로잇 후에는 직접 syscall을 사용한다 (`system()` 없음); teardown 크래시를
피하기 위해 kctx를 살려둔다 (`pause()`).
### 남은 작업 (Stage 4/5)
- 재부팅 후에도 유지 (verity / boot 이미지 / recovery), 셀 하이재킹 + permissive SELinux는
런타임 전용이며 익스플로잇 재실행에는 ~1/3 reclaim 동전 던지기가 필요하기 때문이다.
- `su`에는 non-nosuid 홈(`/system`) 또는 재트리거하는 런처가 필요하다.
## SESSION 11 — 지속성 정찰 (Track B + Track A) 및 RE 핸드오프
목표는 지속적 root였다. 두 트랙이 범위로 잡혔다:
- **Track B**: `/system`을 패치할 수 있도록 verified boot (dm-verity / SELinux)를 비활성화.
- **Track A**: 부팅 시 익스플로잇 재실행.
둘 다 동일한 블로커로 귀결된다: **LK가 디바이스를 `eng`/`unlocked`로 취급하게 만들기.**
### Verified-boot 사실 (정확한 빌드)
- 부트로더 잠김, AVB `green`, `ro.boot.unlocked_kernel=false`, `ro.boot.secure_cpu=1`,
`rpmb_state=1`. Bootrom 패치됨 (BROM 없음); preloader는 CMD short로만.
- `/system`은 **lk가 만든 kernel cmdline에서 Android dm-verity에 의해** 마운트된다:
`root=/dev/dm-0 dm="system none ro,0 1 android-verity PARTUUID=b6404ef3-… "`,
`veritykeyid=id:f3530e18f64d11fc25eb2dd762979f078de990bf`, `androidboot.veritymode=eio`,
`skip_initramfs` (system-as-root). `dm-0` = `system`이라는 이름의 verity 디바이스; `dm-1` = `/vendor`.
- LK: Amazon **UFBL**, `ro.boot.lk_version=0x0006`, 빌드 `0db73c9-20231025_030009`;
preloader `pl_version=0x000a`, 빌드 `80c6fcb-20230523_065640`. `/dev/block/by-name/lk` = mmcblk0p5 (1 MB).
- 전체 GPT (16개 파티션, `persist`/`seccfg`/`nvram`/`protect`/`para` 없음):
`proinfo` p0, `PMT` p1, `kb` p2, `dkb` p3, `lk` p4, `tee1` p5, `tee2` p6, `metadata` p7,
`MISC` p8, `reserved` p9, `boot` p10, `recovery` p11, `system` p12, `vendor` p13,
`cache` p14, `userdata` p15. eMMC boot0 (1 MB) = preloader (`EMMC_BOOT` 매직);
boot1 (4 MB) = IDME 저장소.
### LK (UFBL) 정적 분석 결과
`lk.img` 헤더: `88 16 88 58 | 00052974 | "LK"`; 0x200에 ARM 벡터 테이블, 나머지는 Thumb-2,
위치 독립적/재배치됨 (리터럴 풀이 `ldr+add pc`를 사용하므로 순진한 베이스 상대 디스어셈은 실패).
관련 문자열 (파일 오프셋): `amzn_image_verify`, `amzn_verify_unlock`, `amzn_verify_code_internal`,
`unlock_code`, `unlock code error`, `unlock failed`, `$Common Kernel Signing Engineering CA0`,
`seccfg`, `para`, `ENV_v1`, `LK_ENV`, `Kfos_flags`/`Kdev_flags`/`Kusr_flags`/`Kunlock_code`/`Kunlock_version`,
`FOS_FLAGS_{NONE,ADB_ON,ADB_ROOT,CONSOLE_ON,RAMDUMP_ON,VERBOSITY_ON,ADB_AUTH_DISABLE,FORCE_DM_VERITY,DM_VERITY_OFF,BOOT_DEXOPT}`,
`[DM-VERITY] verify for system(root) is enabled`, `[DM-VERITY] verify off by fos_flags`,
`[DM-VERITY] disabled by fos_flags on eng devices or unlocked device`,
`[SELINUX] set to permissive mode by dev_flags`, `androidboot.prod=1|0`, `androidboot.unlocked_kernel=%s`.
**결론: LK는 `fos_flags`/`dev_flags`의 보안 효과를 eng/unlocked에 게이트한다.**
### IDME 저장소 (eMMC **boot1**) — 쓰기 가능, 지속적, LK와 Android가 읽음
- 0x0에 매직 `beefdeed` + `"2.1\0"` + count(0x19=25); 항목은 0x10부터.
- 항목 형식: `char name[16]; u32 size; u32 type(=1); u32 magic(=0x124); u8 data[size] (pad4)`.
- 항목 오프셋 (원본): `board_id@0x10 serial@0x3c mac_addr@0x68 mac_sec@0x94 bt_mac_addr@0xd0
bt_mfg@0xfc product_name@0x198 productid@0x1d4 productid2@0x210 region@0x24c bootmode@0x26c
postmode@0x28c bootcount@0x2ac manufacturing@0x2d0 unlock_code@0x4ec sensorcal@0x908 alscal@0x9c4
KB@0xa00 DKB@0x1e1c device_type_id@0x2238 dev_flags@0x2274 fos_flags@0x2298 usr_flags@0x22bc
wifi_mfg@0x22e0 unlock_version@0x26fc`. 값은 ASCII (플래그는 **hex 문자열**).
- 런타임 읽기: `/proc/idme/<name>` (읽기 전용). 마지막 부팅 값이 캐시됨; boot1에 쓰면
다음 부팅에 적용된다. 쓰기 경로는 `/sys/block/mmcblk0boot1/force_ro`를 해제해야 한다 (root).
- **LK가 boot1을 읽음이 확인됨**: `serial`을 바꾸면 다음 부팅에서 `ro.boot.serialno`가 바뀌었다.
그러나 LK는 **serial을 16바이트로 자르고** `fos_flags=0x80`, `dev_flags=0xff`,
all-ones 등을 무시했다 — verity/selinux/`prod`는 변하지 않았다. 따라서 serial을 통한 cmdline 주입은 실패한다.
### IDME 플래그의 Android 측 소비자
- `/init.fosflags.sh` (서비스 `fosflags`, `u:r:fosflags:s0`): `FOS_FLAGS_ADB_ON=0x1`,
`CONSOLE_ON=0x4`, `RAMDUMP_ON=0x8`, `VERBOSITY_ON=0x10`, `ADB_AUTH_DISABLE=0x20`,
`BOOT_DEXOPT=0x100`. 확인됨: 플래그 설정이 적용됨 (`sys.usb=adb`, `noadbauth=1`).
- **adbd** (스트립되지 않은 ARM ET_EXEC; `.text` VA 0x8160 / 파일 0x160; fileoff = VA-0x8000):
- `amzn_is_root_allowed` @0x2d5b8 = `amzn_is_dev_unlocked() && (fos_flags & 0x2)`
- `amzn_is_adb_auth_disable_allowed` @0x2d5e8 = `fos_flags & 0x20` (게이트 없음)
- `amzn_is_dev_unlocked` @0x2d5fc = `/proc/cmdline`에 `androidboot.prod=0` **또는**
`androidboot.unlocked_kernel=true` 포함
- `fos_read_debug_flags` @0x2d724는 `/proc/idme/<name>`을 읽고 **hex**를 파싱
- `restart_root_service` @0xcb74 / `restart_unroot_service` @0xcc64
- 문자열: `amzn_fos: ADB: Auto-root succeeded`, `… eng_device=%d`, `… unlocked_kernel=%d`,
`adbd cannot run as root in production builds`, `ro.debuggable`
- `adb root` → "cannot run as root in production builds" (`ro.debuggable=0`) — 따라서
auto-root 게이트가 충족되어도 AOSP prod 검사가 명령 경로를 게이트한다.
### Session-10 root가 지속되지 않는 이유
- SELinux permissive + 셀 하이재킹 + root는 런타임 전용이다.
- `/data/metrics`는 **vpartition**이다: `/system/bin/vpartition.sh`가 매 부팅마다 `/data/vp/metrics.img`
(ext4, non-nosuid/noexec)를 `/data/metrics`에 마운트한다; 거기에 쓴 `su`는 재부팅 후
**살아남지 않는다**. (setuid `su`가 uid 0을 주지만 **caps가 전혀 없는** 이유이기도 하다.)
### Track A (부팅 시 재익스플로잇) — 차단됨
- 제어 가능한 코드를 실행하는 init `.rc` 트리거가 없다 (imports 모두 검증됨; `persist.*` 트리거는
고정 서비스만 `start`; `/system`/`/vendor`의 스크립트).
- Root 서비스는 `/data` 설정을 읽지만 그로부터 exec하지 않는다 (`perfmonitord`, `amazonfiled`,
`vpartition.sh`, `kisd`, …).
- 유일한 부팅 실행자 = **앱**, 그러나 익스플로잇의 일시정지된 풋프린트는 **VmRSS 534 MB**
(`kbm` 스프레이) → lmkd가 죽인다; 게다가 reclaim 실패는 패닉(`PANIC_ON_OOPS`) → 부트루프.
- **adbd auto-root**가 존재하지만 LK가 만든 cmdline (`prod=0`/`unlocked_kernel=true`)에 게이트된다.
### 결론 / 다음 목표 (선택: Track B RE)
모든 것이 LK가 `eng`/`unlocked`를 보고하게 만드는 데 달려 있다. 사정권 내:
`androidboot.prod=1|0`과 `androidboot.unlocked_kernel=false`는 LK가 설정한다. LK를 리버싱하여 찾을 것:
1. `fos_flags`/`dev_flags`/`usr_flags` (`K*` 항목)를 읽는 위치와 정확한 게이트;
2. `prod`/`unlocked` 결정 (IDME 항목? buildvariant? `amzn_verify_unlock` 결과?);
3. `amzn_verify_unlock` (libtomcrypt RSA 검증)의 우회 또는 약한 unlock_code/version 경로;
4. `seccfg`/`para`/`ENV_v1`(LK_ENV) 저장소 (덤프된 어떤 파티션에도 없음 — 아마 tee 보호);
5. preloader (`boot0`, `EMMC_BOOT`)의 버그.
이 중 하나라도 eng/unlocked를 (boot1 또는 raw 파티션 쓰기를 통해 지속적으로) 설정할 수 있게 하면,
`FOS_FLAGS_DM_VERITY_OFF`가 system(root) verity를 비활성화하고 `/system`을 지속적으로 패치할 수 있다.
### 산출물 (이 세션에서)
`/tmp/opencode/mustang-dumps/` (호스트 재부팅 시 지워질 수 있음): `lk.img`, `boot1.img` (원본),
`boot.img`, `MISC.img`, `metadata*.img`, `pmt.img`, `mbr.img`, `kb.img`, `dkb.img`, `reserved.img`,
`cache.img`, `boot0.img`, `boot1.img`, `adbd.bin`, `perfmonitord.bin`, `amazonfiled.bin`.
헬퍼: `tools/findinitnet.py`, `findgadget*.py`, `findstores.py`, `adbd_sym.py` (/tmp에 있음);
repo에는 `run.sh`, `poc/stage3.c` (`selroot`), `poc/su.c`, `rootcmd.sh`가 있다.
### 유용한 명령어```
# IDME read
/data/metrics/su sh -p -c 'for f in fos_flags dev_flags usr_flags serial region device_type_id unlock_version; do echo -n "$f="; cat /proc/idme/$f; echo; done'
# write boot1 (root; su lives only until reboot -> re-run run.sh first)
/data/metrics/su sh -p -c 'echo 0 > /sys/block/mmcblk0boot1/force_ro; dd if=/data/local/tmp/boot1.img of=/dev/block/mmcblk0boot1 bs=4096 count=4; sync; echo 1 > /sys/block/mmcblk0boot1/force_ro'
# dump a partition to host
adb exec-out '/data/metrics/su dd if=/dev/block/by-name/lk bs=4096 2>/dev/null' > lk.img
목표: LK가 디바이스를 eng/unlocked로 취급하도록 만들거나, preloader/LK 버그를 찾아
verity/SELinux를 영구적으로 비활성화하는 것. 결과: 관련 LK 코드 경로를
end-to-end로 리버싱했으나, 사용 가능한 저장소로는 그 플립에 도달할 수 없다.
어떤 디바이스도 brick되지 않았으며, 유일한 boot1 실험은 원상태로 되돌렸다.
lk.img는 작은 ARM 스텁으로 시작한다 (파일 0x200). 0x224의 재배치자는
0x200에서 리터럴 목적지로 복사하고 리터럴 엔트리로 분기한다:
따라서 오프셋 >= 0x200에 대해 런타임 주소 = 0xFF400000 + 파일 오프셋이다.
스텁 이후의 모든 것은 Thumb-2이며 위치 독립적이다. 문자열은
ldr rT,[pc,#imm] (T1 오프셋 = imm8*4; ldr.w 오프셋 = imm12) 뒤에
add rT, pc를 붙여 구성되며, 대상은 (add+4) + *pool이다. ARM 스텁과
리터럴 풀을 견디는 견고한 스캐너가 tools/lk_xref.py로 추가되었다 (16비트 및
32비트 형식을 처리하며, 매 2바이트를 스캔한다). 아래의 모든 오프셋은 파일 오프셋이며,
런타임 주소를 얻으려면 0xFF400000을 더하면 된다.
0x20b4는 unlock_code IDME 항목 (0x400 바이트, 이 유닛에서는 전부 0)을 읽고
amzn_verify_unlock (0x222c -> 0x20f0)을 실행한다. 이 함수는
libtomcrypt (수십 개의 /features/libtomcrypt/src/pk/asn1/der/... 경로와
RSA 검증)를 구동하며, 이미지에는 인증서 자료가 내장되어 있다:
0x317d9+의 Sunnyvale / Amazon Lab126 / "$Common Kernel Signing Engineering CA0",
그리고 진단 메시지
Image FAILED AUTHENTICATION on PRODUCTION device (0x3166e),
Authentication failed on engineering device with production certificate (0x316a0),
Image FAILED AUTHENTICATION on ENGINEERING device (0x31703),
Image AUTHENTICATED with PRODUCTION certificate (0x31736).
빈 코드 / 길이 / 버전 지름길은 없다: verify(zeros) != 0, 따라서
(에서 확인됨).
또는 를 플립하려면 유효한 Amazon 서명
(개인 키 사용 불가) 또는 검증기의 코드 실행 버그가 필요하다. 0x20b4/0x222c/0x20f0에서
정적으로 악용 가능한 것 (경계/크기)은 발견되지 않았다. =>
보안 플래그는 0x57c의 getter를 통해 읽힌다. 실증적 테스트:```
dd if=/dev/block/mmcblk0boot1 ... ; reboot /proc/idme/fos_flags -> 00000080 (persisted, Android sees it) ro.boot.veritymode -> eio (unchanged!) root=/dev/dm-0 dm="system none ro,0 1 android-verity PARTUUID=..." (unchanged) androidboot.prod=1 / secure_cpu=1 / buildvariant=user (unchanged)
`fos_flags=0x80`은 `FOS_FLAGS_DM_VERITY_OFF`입니다. 디코딩된 게이트는 getter가 그것을 반환했다면 verity를 껐을 **것입니다**. 하지만 반환하지 않았습니다. 이 게이트는 죽은 코드가 아니라 살아 있습니다. 일회성 캐시 센티넬이 이미지에서 `-1`(`*(u32*)0x50c74 == 0xffffffff`)이므로, 함수는 실제로 `check_flag("fos_flags",0x80)` 경로를 실행했고 0을 받았습니다. 따라서 getter는 (적어도 verity 보호 시점에는) boot1 IDME 항목을 읽고 있지 **않습니다**.
다른 후보 저장소는 **LK env**로, 문자 그대로 `"para"`라는 이름의 파티션에서 로드됩니다(loader 0x12fd4, magic `ENV_v1`, checksum @0x3ffc). LK 자체의 파티션 테이블(0x4fcc0..0x50340)에는 preloader/proinfo/nvram/protect1/protect2/persist/seccfg/secro/**para**/logo/custom/expdb/tee1/tee2/metadata/system/cache/userdata가 나열되어 있지만, 태블릿의 실제 GPT에는 **단 16개 항목**만 있으며, 모두 타입이 `af3dc60f838472478e793d69d8477de4`입니다:```
#0 proinfo 0x400 #1 PMT 0x1c00 #2 kb 0x4000 #3 dkb 0x4800
#4 lk 0x5000 #5 tee1 0x5800 #6 tee2 0x8000 #7 metadata 0xa800
#8 MISC 0x1e400 #9 reserved 0x1e800 #10 boot 0x22800 #11 recovery 0x2a800
#12 system 0x34800 #13 vendor 0x644000 #14 cache 0x6b4800 #15 userdata 0x7ae800
이 제품에는 para, seccfg, nvram, protect, persist 파티션이 존재하지 않습니다 (PMT/pmt.img 덤프는 모두 0). 따라서 LK env는 비어 있고, Kfos_flags/Kdev_flags 키는 결코 존재하지 않으며, 모든 fos_flags/dev_flags 검사는 IDME 항목의 내용과 무관하게 0으로 귀결됩니다. boot1 IDME 항목은 Android(/init.fosflags.sh, adbd, /proc/idme/*)에 의해 소비되지만 LK의 보안 게이트에 의해서는 소비되지 않습니다.
unlocked_kernel은 Amazon이 서명한 unlock_code(RSA/libtomcrypt, 내장 CA)를 요구합니다. 오프라인에서 위조할 수 없으며, 검증기 버그도 발견되지 않았습니다. 하드 차단.DM_VERITY_OFF / selinux=permissive 플래그는 LK env(para/ENV_v1)에서 소비되는데, 이 GPT에는 존재하지 않습니다. IDME fos_flags는 LK에 의해 경험적으로 무시됩니다(0x80이 지속되었으나 verity는 eio로 유지됨). 파티션 테이블을 수정하지 않는 한 하드 차단.fos_flags=0x80이 성공하더라도 androidboot.veritymode=disabled와 비-dm-0 root=만 설정될 뿐이며, unlock되지 않고 SELinux가 permissive로 전환되려면 여전히 동일한 부재 env의 dev_flags가 필요합니다.para/ENV_v1 저장소 합성: userdata 뒤의 여유 공간(userdata는 LBA 0x3a3dfde에서 끝남; 디스크 = 30535680 섹터)에 para라는 이름의 GPT 항목을 추가하고(프라이머리 + 백업 GPT 모두 갱신해야 함), 그런 다음 fos_flags=0x80과 dev_flags=0x40을 가진 env를 작성합니다(체크섬은 +0x3ffc = 0x3ffc에 걸친 바이트 합). 이것이 verity-off로 가는 유일하게 남은 경로입니다. 위험: 프라이머리/백업 GPT를 손상시키면 벽돌이 될 수 있으며, verity 게이트가 실제로 para를 읽는다는 것은 입증되지 않았습니다(단지 boot1 IDME가 아니라는 것만 확인됨).boot0/EMMC_BOOT) 버그: 이번 세션에서는 리버싱되지 않았습니다. 원본 사본과 복구 경로가 마련되기 전까지 boot0 쓰기는 금지됩니다.tools/lk_xref.py — 베이스 독립적 LK 문자열 xref 리졸버./tmp/opencode/mustang-dumps/lk.img, boot1.img(원본), boot0.img, mbr.img(GPT), pmt.img(모두 0)./tmp/opencode/s12/boot1_f80.img에 보관됨; 기기는 원본 boot1로 복원됨(/proc/idme/fos_flags -> 0 확인)../run.sh로 재무장)```./run.sh --no-build
/data/metrics/su /system/bin/sh -p -c 'cat /proc/cmdline'
for f in fos_flags dev_flags usr_flags unlock_version serial; do cat /proc/idme/$f; echo; done
## 세션 13 — 프리로더는 결국 접근 가능하다 (`1949:20ff` = MTK 프리로더, HID 전송)
전원이 꺼진 상태에서 USB에 연결하면, 태블릿은 **`1949:20ff`**
(`Lab126`)로 열거된다 — Android도 *아니고* `0e8d:0003` 부트롬도 *아니다*. 디스크립터:```
bInterfaceClass 3 (HID), iConfiguration "HID", iInterface "HID Interface"
HID report descriptor = 05 01 09 00 a1 01 c0 (empty collection!)
EP 0x81 IN interrupt 4 bytes, bInterval 4
EP 0x01 OUT interrupt 4 bytes, bInterval 4
iSerial = GCC0X90805310009 (the IDME serial)
식별. 0x20FF는 mtkclient의 config/usb_ids.py에서 **"MTK Preloader"**로 나열되어 있습니다 (MediaTek VID 0x0e8d 아래: 0xe8d:{0x0003 Brom, 0x2000/0x2001/0x20ff/0x3000 Preloader}). Amazon은 preloader PID를 유지하고 VID를 0x1949로 변경했으며, 더미 report descriptor와 함께 HID 엔드포인트 쌍으로 표시합니다. 따라서 이것은 MediaTek preloader / USBDL 모드로, LK 아래 단계이며 — 여기서는 CMD 단락이 아닌 전원 끄기 + 연결로 도달합니다.
descriptor 문자열 "HID"/"HID Interface"는 lk.img, boot0.img, boot.img 또는 다른 덤프에 존재하지 않습니다. 즉, 이 모드는 우리가 덤프하지 않은 구성 요소(bootrom/TEE)에 의해 생성되거나 런타임에 조립됩니다.
중요한 이유. aftv2-tools에서 사용하는 Amazon preloader는 이 정확한 바이트 스트림을 통해 내장된 Download-Agent-less 명령을 노출합니다:```
handshake : host A0 0A 50 05 -> dev 5F F5 AF FA
0xD1 read32 (addr, n_words) : echo cmd/addr/n, 00 00, nu32, 00 00
0xD4 write32(addr, words[]) : echo cmd/addr/n, 00 00, nu32, 00 00
`aftv2-tools/read_mmc.py`는 `read32`/`write32`를 사용하여 MSDC 컨트롤러(MT8173에서는 베이스 `0x11230000`; MT8163의 경우 확인 필요)를 조작하고, DA 없이 **raw eMMC 블록**을 읽고 쓰므로 AVB/verity가 방해하지 않습니다. mustang의 preloader가 0xD1/0xD4를 허용한다면, 이는 RSA 언락 코드와 부재하는 LK 환경과 무관하게 영구 언락(`boot` / `lk` 패치)으로 가는 직접적인 경로입니다.
### 추가된 도구 (루트 필요; 먼저 USB 노드에 chmod 적용)```
lsusb -d 1949:20ff # note Bus/Dev, e.g. Bus 001 Device 003
sudo chmod 666 /dev/bus/usb/001/003
# 1) does it answer the MTK handshake? (no DA, no flash access)
nix-shell -p python3Packages.pyusb --run \
'python3 tools/mtk_preloader_hid.py handshake'
# 2) read-only arbitrary memory read
nix-shell -p python3Packages.pyusb --run \
'python3 tools/mtk_preloader_hid.py read32 0x00100000 4'
tools/probe_preloader.py는 최소한의 핸드셰이크 전용 프로브이며,
tools/mtk_preloader_hid.py는 전체 트랜스포트입니다(handshake/read32/
write32; write32는 보호되어 있으며 eMMC 레지스터 맵이 확인되기 전까지는
사용해서는 안 됩니다).
read_mmc), preloader에서 boot.img/lk를 패치하고 재부팅합니다.amzn_verify_unlock이나 LK env 게이트에 의존하지 않습니다.futex.c:1975가 this->rt_waiter를 전달,
futex.c:2880의 futex_wait_requeue_pi에서 선언됨) → waiter가 arm32 select (nr 142) fd_sets를
통해 자신의 해제된 프레임을 스탬프함DEBUG_RT_MUTEXES 꺼짐
→ 컴팩트한 48바이트 rt_mutex_waiter (tree_entry@0, pi_tree_entry@0xc, task@0x18,
lock@0x1c, prio@0x20, deadline@0x28)modprobe_path @ 0xc111488c (문자열은
vmlinux에서 자체 위치 확인; KALLSYMS_ALL이 꺼져 있어 데이터 심볼에는 이 트릭이 필요) → unknown-binfmt exec → root
스크립트 (setenforce 0, OTA 비활성화, su)refs/의 참조 자료: NebuSec/CyberMeowfia (원본), GhostLock-5.10 (Fire OS 8 포팅,
src/exp32/의 완전한 32비트 ARM 트리거), ghostlock-...-4.19-k40 (Qualcomm 4.19 Android 포팅)selinux_state.enforcingcommit_creds(&init_cred)uid=0jcnr_extresinfo->gpu_alloc_addr를 통해 기록함 (미리 할당해서 전달해야 하는 GPU VA)| evictable 객체 | 압박 | 결과 |
|---|
| 없음 | 900 MB | 생존 |
| 없음 | 1300 MB | 패닉 (시스템 lowmem 버그 — 무관) |
| 일반 영역 + DONT_NEED | 700 MB | 생존 |
| JIT 영역 + DONT_NEED | 700 MB | reclaim 경로에서 패닉 |
/proc/config.gzksrc/ — Amazon OSS 소스 (platform.tar + 추출된 midgard-r26p0 트리)/tmp/opencode/mustang_ota.bin (sha256 6068515a…가 fireos-archive와 일치)
및 2.2 GB 커널 소스 타르볼은 ~/Desktop/amazon-mustang/에 보관ksrc/kernel/mediatek/mt8163/4.9/drivers/misc/mediatek/gpu/gpu_mali/mali_midgard/midgard-r26p0/| 역참조에서 크래시 |
| + eviction 시 자식 킬 (10ms 폴) | 역참조에서 크래시 |
| + cpu0-고정 라이프사이클 (drain3) | 역참조에서 크래시 |
| 단계적 압력 (drain4 v1) | 자식이 종료 시 메모리 해제 → eviction 없음 (합법 경로 검증됨) |
| run | 결과 |
|---|
| pmap G=c2a412a4 400MB | deref에서 크래시 |
| pmap G=c2f4b2a4 480MB | 크래시; +0 post-kill renames (480MB가 fs를 질식시킴) |
| iso2 (spray + regions + oracle) | REGION HIT — 스프레이가 reclaim을 깨지 않음 |
| mix v1 (동시 events+regions) | 크래시; 교란됨 (스피닝 event 스레드) |
| pmap G=c2a7d2a4 350MB + retouch | 크래시; +4452 renames OK |
| mix2 (순차: 2초 events 후 regions) | 크래시; +7126 renames (28K event allocs), 2715 regions |
fn=0PM_PAGE+NF_CELL_OFFprobekernel_x_end 위에서 XN이다: arch/arm/mm/mmu.c의 map_lowmem()은 커널 텍스트 아래의 lowram을 MT_MEMORY_RWX로 매핑하지만, kernel_x_end 위의 모든 것은 MT_MEMORY_RW → PMD_SECT_XN (509행). 0xc154b600에 구워진 셸코드는 프리페치 중단을 일으킨다. 페이로드는 실제 커널 함수 포인터여야 하며, physmap의 코드가 아니어야 한다.| 리터럴 (파일 오프셋) | 값 | 의미 |
|---|
| 0x270 | 0xFF4002F8 | str r4,[r6] 스크래치 |
| 0x274 | 0xFF40027C | 목적지 (base+0x27C) |
| 0x278 | 0xFF54A440 | 복사 끝 (BSS 포함) |
| 0x27C | 0xFF400484 | 엔트리 포인트 |
| 오프셋 | 함수 |
|---|
0xdf7c | is_secure_or_prod() -> byte[ [[g]+0 ] + 0x163 ]; g = 전역 @0x52838. 이 유닛에서는 1. |
0x20b4 | verify_stored_unlock() = memset(buf,0,0x100); 0x57c를 통해 IDME/env unlock_code (0x100) 읽기; bl 0x222c; (verify==0) 반환. |
0x222c / 0x20f0 | amzn_verify_unlock(code,len) — libtomcrypt RSA/PKCS#1 검증 (아래 참조). |
0xda3e | is_unlocked() = is_secure_or_prod() && verify_stored_unlock(). |
0x29a28 | is_verity_disabled() = (fos_flags & 0x80) && !(is_secure_or_prod() && verify_stored_unlock()); 전역 @0x50c74에 캐시됨. |
0x29974 | SELinux cmdline 빌더: dev_flags & 0x20 -> androidboot.selinux=enforce, dev_flags & 0x40 -> ...=permissive (각각 byte[+0x162]로 게이트됨). |
0x118xx/0x11bxx | 커널 cmdline 빌더 (unlocked_kernel, prod=1/0, verifiedbootstate, rpmb_state, secure_cpu, 버전, root=). |
0x27af8 | UART 게이트: fos_flags & 0x4 -> printk.disable_uart=0, 그렇지 않으면 =1. |
0x12fd4 | LK env 로더: 파티션 "para", 0x4000 바이트, 매직 ENV_v1, 체크섬 = 0x3ffc에 걸친 바이트 합계를 워드 @0x3ffc와 비교. |
0x1efd0 | 이름으로 파티션 조회 ("para", "boot", ...에 사용됨). |
0x57c | 콜백 슬롯 @0x58218을 통한 getter 디스패처; 슬롯 @0x58200..0x5821c는 0x5a8-0x734의 테이블에서 등록됨. |
0x2a19c | fastboot oem unlock: bl 0x222c(code,len); 성공 시 0x408을 통해 unlock_code (0x100) 기록. |
unlocked_kernel=false/proc/cmdlineandroidboot.unlocked_kernelandroidboot.produnlock_code