Skip to content
KitploitKITPLOIT
도구블로그
제출
도구블로그
제출

해킹, 침투 테스트 및 사이버 보안 도구를 당신의 보안 무기고에!

Kitploit은 해킹, 사이버 보안 및 침투 테스트 도구 디렉토리입니다. 최신 프로젝트 업데이트를 발견하여 취약점을 찾고, 시스템을 분석하고, 테스트를 자동화하고, 보안을 강화하세요.

··피드·문의·개인정보·© 2026 Kitploit

도구 디렉토리

카테고리

모든 카테고리 보기
Loading categories
amazon-mustang-hack — Amazon Fire 7(Fire OS 7.3.3.1)에서 Mali kbase JIT use-after-free CVE-2022-38181을 통해 임시 root를 획득하고, modprobe_path 덮어쓰기 체인을 사용하는 커널 익스플로잇 연구. | Kitploit
도구/GitHubGitHub/artur9010/amazon-mustang-hack
Android SecurityPrivilege EscalationVulnerability AnalysisExploitationReverse EngineeringMobile SecurityPapers & ResearchPayload DevelopmentBinary Exploitation
GitHubartur9010/amazon-mustang-hack

amazon-mustang-hack

Amazon Fire 7(Fire OS 7.3.3.1)에서 Mali kbase JIT use-after-free CVE-2022-38181을 통해 임시 root를 획득하고, modprobe_path 덮어쓰기 체인을 사용하는 커널 익스플로잇 연구.

13시간 58분 전아직 검토되지 않음

인기

모두 보기 →

커뮤니티에서 가장 많이 사용되는 도구를 찾아보세요.

모든 도구 탐색

도구 컬렉션을 둘러보세요

모든 도구 보기 →
공유
저장소 보기웹사이트

AI 지원 프로젝트. 이 연구, 익스플로잇 개발 및 문서는 GLM-5.3과 DeepSeek V4.1 Flash 모델을 사용한 AI 지원으로 작성되었습니다.

amazon-mustang-hack

최종 펌웨어 — 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 전용), 남은 유일한 경로는 소프트웨어 커널 익스플로잇이다.

빠른 시작```

one-shot: build, run the exploit (retries across the probabilistic reclaim),

install a setuid-root su and verify it as an unprivileged user

nix-shell -p android-tools --run './run.sh' # add -p zig too if no zig

root@kitploit:~
성공 시:```
/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에서 다시 빌드할 수 있다.

아래 내용은 모델이 수행한 작업 로그일 뿐이며, 아래에 사람의 입력은 없음.

주요 타깃 (세션 5 이후): kbase CVE-2022-38181 — stage 2 입증됨

GhostLock(아래)은 보류 상태다: MTK의 BUG_ON rtmutex 변형 + 셸에서의 커널 주소 노출 부재 = 이 빌드에서는 구조적 막다른 길(세션 2-4). kbase JIT UAF는 재진단되었고(destroy-worker의 "무조건적 패닉"은 JIT_FREE 역참조였으며, 로그는 패닉 도중 adbd 종료로 유실됨) stage 2는 이제 오라클로 입증되었다 — SESSION 5 섹션 참조.

보류됨: GhostLock, CVE-2026-43499

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 오류 경로)
  • 트리거 표면 = 순수 futex 시스템 콜(WAIT_REQUEUE_PI/CMP_REQUEUE_PI), 디바이스 노드 없음, SELinux로 제한되는 것 없음 — kbase 경로의 치명적 장애물이 여기에는 존재하지 않음
  • 소비자: sched_setattr → __sched_setscheduler → sched/core.c:4706의 rt_mutex_adjust_pi(p) — 오래된 pi_blocked_on을 역참조 ✓

TODO (포팅 계획)

  1. 트리거 작성 (3-스레드 requeue-PI 데드락, 코어 0-3) — exp32/main.c의 포팅
  2. 스탬프 지오메트리: rt_waiter 프레임 오프셋 대 do_sys_select fd_set 영역 — 우리의 vmlinux 디스어셈블(do_sys_select stack_fds 대 futex_wait_requeue_pi 프레임), STAMP_NFDS/STAMP_WAITER_OFF를 튜너블로 노출
  3. arm32 48바이트 waiter용 fake-writer 인코딩 → "write V to ADDR" 슬롯
  4. 2개 슬롯 → modprobe_path, 발사, root 스크립트
  5. select-stamp가 도달할 수 없을 경우의 대안: setsockopt(MCAST_JOIN_SOURCE_GROUP) 스탬프

상태

  • Bootrom (amonet 하드웨어 방식) — 이 기기에서는 패치됨, 막다른 길
  • mtk-su (CVE-2020-0069) — 패치됨, Failed critical init step 3
  • 공격 표면 조사 — /dev/mali0 world-RW + SELinux gpu_device, kbase r26p0-01rel0
  • CVE-2022-38181이 정확한 빌드 소스에서 확인됨; stage-1 트리거 작동
  • CVE-2026-43499 (GhostLock) 검증되었으나 차단됨: MTK BUG_ON rtmutex 변형 + 셸에서의 커널 주소 노출 부재 (세션 2-4)
  • CVE-2022-38181 stage 2 입증됨 (세션 5): destroy-worker 패닉은 오진이었음; 스프레이된 영역으로의 UAF 리다이렉트, 오라클 검증됨
  • Stage 1: 트리거 + 스탬프 + 소비자 (크래시 = 체인 활성)
  • Stage 2 (kbase 경로): 스프레이된 영역으로의 UAF 리다이렉트 — 세션 5에서 입증됨
  • Stage 2b: raw-byte 슬롯 제어 (xattr 스탬프 churn) → unlink write
  • Stage 3: 임의 커널 함수 호출 → ROOT (세션 10) — nf LOCAL_OUT 훅 하이재킹, selroot 2-패킷 체인: 을 0으로 만들고, fake entry를 로 재작성. , SELinux Permissive.

주요 발견 사항

기기 / 펌웨어

  • 모델 KFMUWI, 디바이스 mustang, Fire OS 7.3.3.1 PS7331.4463N/0031575863040
  • 커널 4.9.117-g08fe75b-dirty, 빌드 Sat May 3 01:25:15 UTC 2025 (Linaro GCC 6.3-2017.05)
  • Amazon이 2025년 5월에 7.3.3.1을 조용히 재발행함 (새 incremental, 동일 버전 문자열)
  • 2020년 이후 리비전의 Bootrom: eMMC CMD를 GND로 단락시키면 preloader만 제공됨 (패치됨)
  • /dev/kb, /dev/dkb (Amazon 커널 백업 파티션) root:drmrpc 0660 — 잠김

CVE-2022-38181이 적용되는 이유

  • 드라이버: mali_kbase r26p0-01rel0 (Midgard, Mali-T720), NVD 영향 범위 r4p0–r31p0 내
  • Amazon의 2025년 5월 재빌드가 2018년 버그를 그대로 포함 — 백포트 없음
  • 정확한 취약 코드, 소스 검증됨:
    • 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 중에 발동)

익스플로잇 환경 (모두 라이브 config 덤프 + OTA vmlinux에서 검증됨)

  • armv7 32비트, non-LPAE → KASLR 없음 (커널은 고정 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
  • 1 GB RAM → 직접 reclaim (eviction에 필요)에 쉽게 도달 가능; ~1 GB 초과 압박은 커널 자체를 패닉시킴 (무관한 lowmem/OOM 버그) — 스프레이는 ≤ 900 MB로 유지, ~700 MB 사용

PoC 개발 중 마주친 UAPI 특이사항 (r26p0, _IOC_TYPE 0x80)

  • MEM_ALLOC union은 32바이트 (in에 extent 포함 4 × u64)
  • flags에는 BASE_MEM_PROT_GPU_RD|WR (비트 2|3)가 포함되어야 함, 레거시 R|W가 아님
  • 어떤 alloc보다도 tracking-page mmap이 먼저 필요: 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)
  • JIT: MEM_JIT_INIT (nr 14, v2 구조체), alloc/free는 JOB_SUBMIT을 통한 소프트 잡 (BASE_JD_REQ_SOFT_JIT_ALLOC=0x209, ...FREE=0x20a; =user ptr, =count)

Stage-1 차등 테스트 (버그가 발동한다는 증거)

패닉은 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

root@kitploit:~
## 참고 자료

- 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):

  • 9b44-9b54: retry head; pi_blocked_on==NULL → clean exit ret 0
  • 9abc-9adc: orig_waiter==NULL → skip pi_waiters checks (adjust_pi always passes NULL)
  • 9b10-9b28: prio check (prio==task->prio + MIN → exit 9b58)
  • 9b2c-9b38: trylock(lock+0) — ticket; fails → retry loop w/ counter bail (9a90-9aa8, limit @ *(0xc11189c8))
  • 9ba4-9bc0: deadlock checks
  • 9bcc-9bd8: THE BUG_ON (leftmost→W→W->lock==lock or die)
  • 9bdc-9c04: dequeue (leftover tree RB_CLEAR_NODE'd = EMPTY → safe skip), prio/deadline write
  • 9c04 bl: rt_mutex_enqueue → *link = waiter+0 at lock+4 ← THE WRITE
  • 9c3c+: owner==NULL → clean exit path

2. 커널 스택 주소 유출 (주소 비의존 요구사항을 무력화함)

/proc/self/task//stat 필드 28 (kstkesp)은 SHELL 컨텍스트에서 syscall-blocked 스레드의 실제 커널 SP를 반환한다 (검증됨: 0이 아닌 값 관측).

  • waiter가 read(blocking_pipe)에서 블록 → stat → kstkesp
  • 스택 베이스 = kstkesp & ~0x1fff (arm32 THREAD_SIZE=8192)
  • rt_waiter 절대 주소 = base + 고정 델타 (계산 가능: sp0 = base+0x2000-0x48 pt_regs; waiter = sp0-0x1d8)
  • 모든 자기 참조 스탬프 값이 계산 가능해짐!

완전한 자기 일관성 스탬프 (유출 이후):

  • L = waiter+0x24 (WINDOW 내부의 가짜 lock — 4개 워드 모두 제어 가능)
  • iov[0].base (waiter+0x1c lock) = L
  • iov[0].len (waiter+0x20 prio) = 1 (≠139)
  • W = waiter+0x1c; stamp *(W+0x1c) = *(waiter+0x38) = L (BUG_ON 통과)
  • lock+0 (waiter+0x24) = 0; lock+4 (waiter+0x28) = 0 (쓰기가 여기에 안착); lock+8 (waiter+0x2c) = W; lock+0xc (waiter+0x30) = 0 (owner NULL)

오라클 상태

  • walk 중 크래시 = walk 실행됨 (dead-lock probe: 결정적 크래시, 클린 코드)
  • 클린 walk + 쓰기 없음 = trylock-fail retry-bailout (kptr 앵커: 런타임 워드 0이 아님)
  • 이제 오염 수정 이후 모든 것이 결정적임.

다음 세션 TODO

  1. 유출 구현: waiter가 pipe에서 블록, main이 stat 읽고 base 계산
  2. 자기 일관성 window 스탬프, walk 발사 → 크래시 없는 완료 = 쓰기 증명
  3. 무기화: 쓰기는 항상 lock+4 (rb_link_node)에 안착 — lock이 window (완전히 제어되는 유일한 메모리) 안에 있어야 하므로, 타겟 선택 연구: window 내 called-slot 트릭을 찾거나, 2단계 구성.

SESSION 4 최종 상태 — THE WALL (정밀하게 규명됨)

전체 그림

walk는 결정적으로 발사된다 (dead-lock probe: 매번 크래시, 클린 코드). 쓰기가 안착할 수 없는 이유는 3중 커널 하드닝 우연의 일치 때문이다:

  1. MTK rtmutex BUG_ON 변종: lock+8 (leftmost)이 반드시 W를 가리켜야 하며 *(W+0x1c)==lock이어야 한다. 모든 0/쓰레기 앵커는 죽는다. 정적 자기 참조 패턴은 존재하지 않는다 (6571개 후보 스캔, 0 히트). 런타임 포인터는 미지.
  2. 셸로부터의 커널 주소 노출 없음:
    • arm32에서 kstkesp = USER SP (task_pt_regs->ARM_sp) — 커널 스택 아님. DEAD.
    • dmesg/pstore/pagetypeinfo/kallsyms/stack — 모두 거부됨.
    • 런타임에 kptr_restrict=1 (fops 앵커 워드도 런타임-비영 — kptr 앵커 실행은 trylock 성공이 아니라 trylock-fail retry-bailout으로 종료됨)
  3. rodata의 fops 테이블: trylock strex 중단 (session-3 sweep 크래시).

스탬프 window (waiter+0x1c..0x5b)는 유일하게 제어되고 내용이 알려진 메모리이지만, 그 ADDRESS가 우리가 필요로 하는 미지수다. 자기 참조 구성은 모두 커널 주소를 상수로 스탬프해야 한다 — 유출 없이는 순환적이다.

gitchw 비교 (그들의 ARM32 쓰기는 왜 작동했고 우리는 아직 안 되는가)

그들의 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 스타일이라 성공했다.

Session-4 검증된 사실

  • walk retry 루프에는 counter bailout이 있다 (limit @ *(0xc11189c8)); 런타임-비영 앵커 워드에서 trylock-fail → 클린 retry-bailout 종료 (kptr 앵커 실행)
  • No-requeue 경로 (9ce4, FULL walk)도 9d64에서 leftmost를 역참조 — 탈출구 없음
  • RB_CLEAR_NODE 자기 포인터가 window 내 leftover로 존재한다 (waiter+0과 +0xc가 자기 주소를 담고 있음) 그러나 알려진 주소를 스탬프하는 것을 피하는 방식으로 이를 사용하는 검사-비교는 없다
  • 실제 뮤텍스 후보 (chain mutex는 라이브 waiter를 가짐 = BUG_ON 통과할 것) — 그러나 &chain_mutex는 힙 주소이며, 유출 없이는 도달 불가

다음 세션 옵션 (순위)

  1. logcat 커널 포인터 사냥: Amazon HAL/데몬은 수다스럽다; 로깅된 커널 포인터 (오래된 것이라도)가 구성의 막힘을 푼다. 테스트 저렴.
  2. 이 빌드에서의 /proc/net %pK 동작: 일부 4.9 트리는 비특권 리더에게 /proc/net/tcp,udp,unix에서 해시되지 않은 포인터를 출력한다. 라이브 테스트.
  3. dangling pi_blocked_on에서의 스레드 종료 경로 (일회성, 다른 역참조).
  4. 축적된 4.9 지식으로 보류된 kbase JIT 버그 재검토.

SESSION 4 부록 — 유출 사냥: 소진됨 (확정)

셸 도메인에서 테스트되어 죽은 것:

  • /proc/net/{tcp,unix,packet,netlink,ptype}: %pK-해시되어 00000000 (kptr_restrict=1)
  • /proc/timer_list: 읽기 가능하지만 포인터 %pK-제로화 (심볼 보임, 주소 없음)
  • logcat: Amazon/wpa 수다에 커널 포인터 없음
  • kstkesp (stat f28): arm32에서 USER SP (task_pt_regs->ARM_sp)
  • MTK 노드 (/proc/ged, mtk_cmdq_debug, mtktz, ptp, chip, aed, driver/*): 모두 SELinux 거부
  • /proc/{iomem,vmstat,kmsg,keys,crypto,slabs...}: 거부
  • /sys/kernel/notes: 거부
  • CONFIG_VECTORS_BASE=0xffff0000 (high vectors — NULL+0x1c 폴트)
  • CONFIG_KUSER_HELPERS=y (kuser at 0xffff0000, not page 0)

결론: 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 버그는 이 정확한 소스에서 존재가 검증되었고, 작동하는 트리거가 있었으며, 그 블로커는 구조적이 아니라 기계적이다.

SESSION 5 — STAGE 2 증명됨 (옵션 b 실행)

재진단: "무조건적 destroy panic"은 존재한 적이 없다

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 흐름은 이 빌드에서 완전히 살아 있다.

완전한 프리미티브 인벤토리 (정확한 vmlinux 디스어셈블리)

kbase_jit_free(kctx, reg) @ 0xc058495c, 완전히 제어되는 가짜 reg:

  • reg->cpu_alloc NULL → backed size 0 → trim block 건너뜀 (0xc0584978)
  • bin 감소: 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 (가짜에 쓰기, 무해)
  • shrink_cpu_mapping은 new==old일 때 조기 종료 (nents=0 → return)
  • list_add(gpu_alloc->evict_node, &kctx->evict_list): evict_list head @ kctx+0x1427c; gpu_alloc+0x18/0x1c에 쓰기 (쓰기 가능해야 함)
  • WARN 경로 (0xc0584bd4)는 비치명적 (panic_on_warn 없음)이고 계속됨
  • UNLINK @ 0xc0584b08/b0c: r3=*(reg+0x3c) prev, r2=*(reg+0x38) next → *(next+4)=prev; *(prev+0)=next — 두 개의 임의 write-what-where, 그 다음 reg+0x38을 jit_pool_head @ kctx+0x148e8로 재링크

정적 가짜-gpu_alloc 체인 (오프라인 vmlinux 스캔, /tmp/opencode/scan_s.py)

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에서 미사용)
  • kbase_va_region 자체: kzalloc(72) → kmalloc-96! MEM_ALLOC(va=0x40, commit=0x10)은 region만 kmalloc-96에 넣음 (phy alloc → 384) → 결정적 reclaim 타입. 실제 region 희생자가 kbase_jit_free를 완전히 합법적 상태로 완료시킴 (빈 jit_node → self-unlink).
  • 압력 후 순차 스프레이: 항상 빗나감 — worker가 압력 중간에 슬롯을 partial slab으로 해제 (SLUB: 비활성 slab으로의 free ≠ cpu freelist); 압력 하에서 우리 alloc은 실패 → 순 볼륨 0 → 회전 없음
  • 고정 스프레이어 + caps: 여전히 빗나감 (512-cap이 eviction 전에 소진됨; worker는 어느 cpu에서든 실행될 수 있음)
  • 승자: commit_pages=0 스프레이 — 물리 페이지 없음 → MEM_ALLOC이 압력 폭풍 내내 성공 → ~6000 순 할당 → partial-list 회전 보장. 8 스레드 (2/cpu, cpus 0-3 하드코딩 — /proc/cpuinfo는 셸에 1 코어로 필터링됨, Cpus_allowed_list 사용) + cpu0에 고정된 압력 자식 + join 후 16-alloc 보존 배치.
  • query(jit_va) 함정: reclaim 후, 스프레이 region이 커스텀 존에서 해제된 VA를 재사용 → query=0은 모호함 (live-original vs 스프레이-커버-VA)

오라클 히트 — 기계 검증된 리다이렉트

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 리다이렉트.

Stage 3 계획 (raw-byte unlink)

Region-타입 reclaim은 합법적 dance 생존을 제공하지만 jit_node는 INIT된 self → unlink 프리미티브 없음. +0x38/+0x3c에 raw 바이트 필요:

  1. xattr 스탬핑: region-alloc 버스트 (순 볼륨 → slab 회전)와 xattr 폭풍 (모든 head 슬롯 스탬프, 바이트는 free 후 지속) 교대 → 조용한 window → 역참조
  2. 또는 고정된 sendmsg cmsg (optmem_max=10240 → ~106 × 96B 보유)
  3. 그 다음: W1 *(N+4)=P with P=유저랜드 셸코드 페이지 (PAN 없음!) — 후보: const fops는 .rodata (DEBUG_RODATA) → .data의 비-const fn ptr을 타겟, 또는 binfmt formats 리스트 head, 또는 sysctl proc_handler (테이블 쓰기 가능성 검증). 폴백: 바이트-체인 쓰기를 통한 modprobe_path (값은 쓰기 가능 주소여야 함 — 포인터 형태 타겟 사용)
  4. no-KASLR + 정확한 vmlinux: prepare_kernel_cred 0xc0149e3c, commit_creds 0xc014993c

SESSION 5B — STAGE 3: 무기 제작됨, reclaim 레이스 아직 미승

완료

  • Stage-3 무기 완성 & 준비됨 (poc/stage3.c):
    • 타겟: kern_table[pid_max].proc_handler @ 0xc1113f40 (쓰기 가능 .data, 문자열-포인터 스캔 + handler == proc_dointvec_minmax로 검증됨)
    • N = 셸코드 엔트리 0x11111112 (mmap 0x11111000; W1이 entry+4를 덮어씀, b +8로 건너뜀; W2가 P = handler 필드에 N을 씀)
    • arm32 ring0 셸코드 수동 인코딩: prepare_kernel_cred(0) + commit_creds + ret 0; 트리거 = read /proc/sys/kernel/pid_max (셸에서 읽기 가능); 자체 태스크 컨텍스트에서 실행 → creds가 우리에게 적용
    • benign 오라클 모드는 uts nodename (0xc110d561, unaligned ok)을 씀
  • 스프레이 프리미티브 선택:
    • NETLINK_USERSOCK sendmsg 핀 (msg_control kmalloc-96 복사, 블록 중 보유, 절대 파싱 안 됨): SELinux 거부 (socket create EACCES)
    • unix/UDP sendmsg: cmsg 파싱이 페이로드 바이트를 오염시킴 ✗
    • inotify 이벤트: 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과 동일한 의미)
  • 기계적 체인 종단 간 검증됨 (drain4, no-eviction 실행): 20K 드레인된 이벤트 + 13.5K 멀티-cpu 트레일링 rename + JIT_FREE + 오라클 + pause, 모두 클린. O_SYNC 로그 (/data/local/tmp/s3.log)가 panic에서 살아남음 — 정확한 크래시-지점 포렌식.

해제된 슬롯에 대한 reclaim 시도 (지금까지 모두 빗나감)

변형결과
폭풍 중 동시 rename 스프레이어rename 정체 (journal/GFP_NOFS) → 총 128 → 쓰레기 역참조
사전 드레인 12K 이벤트 + 압력 + 작은 트레일링

작동 가설: 폭풍-쓰레기 레이스 — destroy worker가 슬롯을 해제한 시점 (폭풍 중간)과 자식-킬/조용 사이에, 잔여 reclaim 활동이 희생자 slab의 유일한 빈 슬롯을 비-페이로드 바이트로 차지한다. Region 스프레이 (stage 2)는 폭풍 DURING 내내 지속적으로 할당하기 때문에 이긴다; rename은 그럴 수 없다.

겪은 함정

  • 스프레이 토글 버그: rename 소스가 페이로드 이름이어야 함 (임시 이름이었음 → 2 웨이브 후 ENOENT → 이벤트가 512개만 생성됨)
  • 기기 호스트명은 "localhost"/변동 — 오라클은 before/after 비교
  • 일시정지된 st3 + pkill → 기기 WEDGE (33K 이벤트로 teardown?!) — 일시정지된 프로세스는 재부팅으로만 킬; 세션의 두 번째 하드-웨지
  • /proc/cpuinfo는 셸에 1 cpu로 보임; Cpus_allowed_list 사용

다음 움직임 (순위)

  1. drain4 @ 500MB (eviction 임계값이 거기서 확인됨), 1ms 폴, 즉시 킬, 4-cpu 트레일링 × 3200 — 폭풍 window 축소
  2. xattr-스탬프 churn (setxattr alloc-copy가 SELinux 검사 전에 발생 — SELinux-증명됨)을 폭풍 + region 회전과 동시에, 스탬프로 종료
  3. region-reclaim 수용 (증명됨) + region-희생자 상태에서 2단계 프리미티브 찾기 (double jit_free 분석은 지금까지 부정적)

SESSION 5C — 블로커, 정밀하게 규명됨

이번 세션의 경험적 결과

  • drain4@500 (1ms 폴, 즉시 킬, +4.5K rename): 여전히 역참조에서 크래시
  • 킬과 트레일링 겹침 (drain5): 더 일찍 크래시 (킬-복구 폭풍 중 rename이 시스템-레벨 폴트를 침) — 겹침 포기
  • 격리 실험 (iso 모드): 동일 타이밍, commit-0 REGION + stage-2 pool-reuse 오라클로 트레일링 → 오라클 히트 → 타이밍/도달성은 괜찮음; 이벤트가 문제
  • mask-순환 이벤트 (inotify_merge를 무력화하기 위한 MOVED_TO/CREATE/DELETE 회전): 여전히 역참조에서 크래시
  • CONFIG_MEMCG=n → 이벤트와 region이 하나의 kmalloc-96을 공유 (memcg 이론 죽음); inotify_merge는 이름도 비교함 (merge 이론 죽음 — 우리의 토글링 이름은 절대 병합되지 않았음; 이벤트는 처음부터 큐잉되고 보유되었음)
  • /proc/slabinfo 없음; /proc/self/pagemap 읽기 가능하지만 PFN-제로화됨 (post-4.0 마스킹, CAP_SYS_ADMIN 없음)

실제 블로커 (두 부분, 둘 다 증명됨)

  1. Soft-job finish는 kbase job-scheduler WORKER 컨텍스트에서 실행됨 (jd_run_atom ← js dispatch, mali_kbase_jd.c:81-112/677), submit ioctl에 인라인이 아님 → current->mm이 커널 스레드의 것 → 우아한 "가짜의 포인터를 우리 자신의 유저스페이스 mmap으로 향하게" 설계 (PAN 없음!) 가 비결정적으로 FAULT. 그 외에는 완벽한 가짜로 5/5 크래시.
  2. 따라서 gpu_alloc은 생존 가능한 런타임 체인을 가진 KERNEL 메모리를 가리켜야 함: 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분 사이클).

Session-6 계획 (둘 다 완전히 명세됨)

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 마스킹됨) — 재검토하지 말 것.

SESSION 5D — physmap 가짜 제작됨; G-sweep 0/3; 교란 제거됨

이번 세션에서 확립됨 (모두 바이너리/기기 검증됨)

  • 캐시 동일성 확인됨: region = kmem_cache_alloc_trace( kmalloc_caches[7], GFP|0x8000, 0x48) [kbase_alloc_free_region disasm]; event = __kmalloc(89, GFP) → kmalloc-96. 이벤트와 region은 희생자의 캐시를 공유할 수 있음. (MEMCG off; 단일 캐시 세트.)
  • inotify 큐: ≥5000 이벤트 보유, 오버플로 없음, merge 붕괴 없음 (qmeas 1000 & 5000 실행) — 이벤트 스프레이가 지속됨
  • iso2 (physmap 스프레이 + region 트레일링 + 오라클): HIT — physmap 스프레이는 region reclaim을 깨지 않음; 기계 건전함
  • 이벤트 vs region reclaim: region 3/3 (iso, iso2, stage-2), 이벤트 0/10 그러나 3번의 pmap 실행은 각각 0.3-0.45 확률의 G-미스로 설명됨 (P(3 misses|events-work) ≈ 0.2-0.3 — 결정적이지 않음)
  • mix 모드 교란됨: 480MB 스프레이가 킬 후 rename을 질식시킴 (+0); 350MB OK (+4452); 스피닝 실패-rename 스레드도 region reclaim을 교란 (mix 크래시, iso2 클린)
  • query_commit은 EINVAL에서도 -1 반환 — 계측됨; 관측된 EINVAL = rbtree 조회 미스 = 진짜 해제됨 ✓ (거짓 양성 아님)

현재 pmap 설계 (stage3.c에 있음, 모드 pmap/mix/iso2)

  • G가 모든 스프레이된 페이지에 구워짐 (오프셋 0x2a4): nents@+0x2ac=0, evict self@+0x2bc/0x2c0, K=G+0x100@+0x2dc, D=G+0x200@+0x3a8; K+0x141c8/-0x1429c는 ~20 페이지 위에 안착 (sub-0 store 무해 / read는 0-또는-유효해야 함). PM_SPRAY_MB 350, G sweep 시도됨: c2a412a4, c2f4b2a4, c2a7d2a4 — 모두 역참조에서 크래시
  • physmap 범위 상식: RAM 1GB → physmap ~0xc0000000-0xc3fffffff; MTK carveout (GPU/M4U/secure)이 청크를 차지할 수 있음 — G 지뢰밭

Session-6 TODO (순위)

  1. 전체 커널 소스 추출 (2.2GB tarball at ~/Desktop/amazon-mustang/ — platform.tar): arch/arm + mm/ + drivers/of + MTK reserve mappings 확보 → carveout 맵 계산 → 검증된-RAM physmap 하위범위로 G 타겟팅; 또한 kmalloc 캐시 지오메트리 (ARCH_KMALLOC_MINALIGN!)와 0x8000 GFP 비트 검증
  2. 배치-정보 기반 추측으로 G sweep (다중 재부팅, 스프레이 크기 변화로 상관 해제)
  3. G sweep이 소진되면: 다중-G 페이로드 또는 런타임-그럴듯한 정적에서 S-후보 재고 (uts-인접 포인터 필드 실패: NULL-K)

SESSION 6 — zram 발견, 소스 추출, 이벤트 질문 여전히 미해결### 전체 커널 소스 추출 완료

/tmp/opencode/ksrc2/kernel/mediatek/mt8163/4.9/ (arch/arm 포함 mustang.dtsi, mm/, fs/eventpoll.c, fs/notify) — ksrc/platform.tar에서 추출. 발견 사항:

  • 0x8000 GFP 비트 = ___GFP_ZERO (단순 kzalloc; 캐시 분할 없음)
  • kmalloc-96은 진정한 96바이트 캐시 (kmalloc 캐시에 HWCACHE_ALIGN 없음)
  • mustang.dtsi: 메모리 노드 0x40000000/512MB — preloader에 의해 확장됨 (장치에서 MemTotal 977MB 표시); CONFIG_VMSPLIT_3G, HIGHMEM=y
  • zram0 활성 (SwapCached > 0) → "dirty anon = unevictable"은 틀렸음: physmap 스프레이는 압박 하에서 스왑 아웃됨 → G 별칭이 stale해짐 → kill 후 physmap_retouch() 추가 (trailing/deref 전에 모든 스프레이 페이지를 다시 폴트인)

이번 세션 실행 (모두 O_SYNC 로깅, ~6회 재부팅)

events에 대한 판정 (베이지안, 정직하게)

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 경계 편향, 할당자 배치).

세션-7 결정 트리

  1. G를 먼저 확정 (저렴, 익스플로잇 없음): ISO2 흐름을 일시적으로 계측 — region trailing + oracle — 하지만 PAYLOAD event를 physmap fake로 만들고, swept 범위의 어떤 G가 regions 경쟁 없이 nodename 변경을 일으키는지 확인 (순수 pmap, ~0xc1500000-0xc2a00000 lowmem 중심에 걸친 G sweep, 재부팅당 1 G, 4-5회 재부팅)
  2. G sweep이 소진되면 → events 사망 선언 → 셸에서 도달 가능한 대체 raw-byte kmalloc-96 할당자 탐색 (감사: seq_file, tty ldisc, fdtable, sk_filter (차단됨: +0x38의 code-field), netlink nlmsg (skb ✗), keys (거부됨)) — 또는 2단계 region 프리미티브 재검토 (지금까지 분석은 부정적)
  3. 나중에 root를 통한 UART/ramoops 언락 고려; 추적하지 않음

세션 7 — cmdline 충격적 발견; event 미스터리가 이제 정밀하게 한정됨

mustang_defconfig CONFIG_CMDLINE (배치에 대한 ground truth):

vmalloc=496M slub_max_order=0 slub_debug=O loglevel=8 initcall_debug

  1. vmalloc=496M → physmap = 0xc0008000..~0xc2080000만 (RAM의 낮은 520MB) — 모든 이전 G 추측 (0xc2a4xxxx+)은 VMALLOC 공간에 있었음. 세션 5D/6의 모든 "G-miss" 결론은 무효화됨; 크래시 해석은 유효하지만 sweep이 잘못된 맵을 겨냥했음.
  2. slub_max_order=0: 모든 slab 페이지 order-0
  3. KMALLOC_MIN_SIZE = ARCH_KMALLOC_MINALIGN = 64 (L1_CACHE_SHIFT 6) → 96/192에 대한 kmalloc_index() 특수 처리가 비활성화됨 → region kzalloc(0x48=72) → caches[7]; event __kmalloc(89) → caches[7] (disasm + include/linux/slab.h:287 검증됨) — 둘 다 병합된 128바이트 "kmalloc-128/96" 캐시에 있음. 캐시 동일성: 재확인됨.
  4. zram 활성 → anon physmap 스프레이를 kbm_spray로 교체: 160 × 2MB kbase MEM_ALLOC regions (GFP_KERNEL → ZONE_NORMAL → lowmem 전용, pinned → zram 면역), 패턴은 CPU mmap을 통해 작성 — 올바른 범위, ~65% lowmem 커버리지

실행 (O_SYNC 로깅)

  • kbm + G=0xc16412a4: deref에서 크래시 (+4831 renames)
  • kbm + G=0xc1c4b2a4 @200MB: eviction 없음 (query=16 — 압박 캘리브레이션이 pinned 스프레이에 따라 변함; 합법적 free, 생존)
  • kbm + G=0xc1c4b2a4 @400MB: eviction ✓, +4053 renames, deref에서 크래시
  • 유효 범위 G 기록: 0/2. events가 ~65% 커버리지로 작동한다면: P(2 misses) ≈ 12%. 질문은 여전히 열려 있지만 그 어느 때보다 좁아짐.

질문, 최종 형태

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 정렬 세부 사항이 ... 불명확.

세션-8 TODO

  1. 계측 수준 실험: 서로 다른 N/P 값을 가진 두 event payload 변형을 교대로 (두 이름 세트) — nodename이 변경되면 마지막 승자가 식별됨; kbm 스프레이로 [0xc1200000..0xc2000000]에서 G sweep, 3-4회 재부팅 예산
  2. 여전히 0/N이면: events 포기. 대안 순위: a. F_SETPIPE_SZ를 통한 pipe_buf 배열 (4096 → 1 buf? 아니오 — 16 bufs = kcalloc(16, 28)=448→512 ✗) — 사망 b. 다른 이름/데이터 운반 kmalloc-128 객체를 위해 fs/notify + fs 감사 (fanotify events? mq off; fanotify는 그룹 필요...) c. seq_file 버퍼 (kmalloc(PAGE_SIZE) ✗) d. sock 필터 (+0x38에서 code-field 충돌 ✗) e. region 타입 reclaim을 수용 + 두 번째 버그/기법 체인
  3. regions가 왜 이기는지 재검토 — 여러 jit id를 통한 계측: N개의 dangling 슬롯, 슬롯별 region-vs-event 경쟁, oracle이 어느 스프레이가 어느 슬롯을 차지했는지 감지 → 메커니즘의 통계적 지문

세션 8 — 장치에서 임의 쓰기 달성; dispatch 미스터리 남음

이정표```

[+] uname.nodename="X?lhost" (was "localhost") <<< ORACLE HIT

root@kitploit:~
**전체 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-실행-여부를 구별하기 위한 마커 실험이 추가되었다; 세션 종료 전에는 크래시 부팅만 있었음 — 아직 깨끗한 데이터 없음.)

적중률 / 부팅 상태 교훈 (피와 땀으로 얻은)

  • 작동하는 구성 (건드리지 말 것): 2 kids × 100MB 단계적 압박, 후행 100×50ms/+3200 renames, 260×2MB kbm 스프레이, G=0xc154b2a4, 사전 배수 ~5000 renames
  • 과압박 (20 kids / 10s 후행)은 회수를 깨뜨린다 — 되돌림
  • 부팅 안정화가 중요하다: boot_completed 직후 실행된 런은 시스템의 시작 할당과 경쟁한다 → 콜드 스트릭; 실행 전 부팅 후 60-90초 안정화
  • "W1이 발화하지 않았다" (nodename 검사)는 nf 페이로드에 대해 무의미하다 — W1은 우리 페이지에 쓴다; 대신 kbm_scan_for를 사용하라
  • free 시점 크래시 부팅 ≈ 가비지 슬롯 또는 G-미스 변환; 무해한 대조군 (pmap 200)이 환경 정상성 검사이다 (따뜻할 때 4/4 적중; 차가울 때 크래시-미스)
  • /data/local/tmp/.w* 디렉터리는 런 시작 시 정리된다 (누적된 디렉터리는 회수를 저하시킨다)

세션-10 TODO (결정 트리, 순서대로)

  1. 안정화 지연 부팅에서 nf 200을 실행하여 W1-CONFIRMED 줄이 나타날 때까지 반복한 뒤, MARKER 줄을 읽어라: a. 마커 존재, uid!=0 → 셸코드의 creds 실패 (prepare_kernel_cred/commit_creds 주소 확인; blx 인코딩) b. 마커 부재 → walk가 우리를 호출하지 않았다: ... 다음 진단으로 마커만 쓰고 1을 반환하는 hookfn (creds 없음)으로 검증 — 여전히 부재라면:
    • 트리거 직전에 CPU 매핑을 통해 우리의 실제 엔트리 바이트를 덤프하라 (그것들은 우리가 읽을 수 있다!)
    • [init_net+0x58c]가 실제로 참조되는지 확인하라: 대신 관찰 가능한 무언가를 손상시키도록 쓰기를 사용하라 (예: *cell = fn이 kfree 같은 커널 함수인 엔트리를 가리키게 하라 → 트리거 시 즉시 크래시 = 필드가 참조됨)
    • 0x58c 오프셋 재검증: 어쩌면 hooks_ipv4[NF_INET_LOCAL_OUT]이 다른 인덱스에 있을 수 있다 (NF_INET_POST_ROUTING=4?)
  2. walk가 우리를 호출한다면: creds 수정 → 루트 → 그런 다음 사용자의 계획: setenforce 0; cp /system/bin/sh /data/local/tmp/su; chown root; chmod 6755; ls -la 확인; 마커 파일 남기기
  3. 지속성 (루트 후): /dev/block/by-name/boot를 통한 부트 이미지 패치
    • dm-verity 비활성화, 또는 Magisk 스타일; /data에만 있는 su는 재부팅 후 셸 도메인 내 uid0이다 (SELinux 다시 enforcing) — setenforce 0은 런타임 전용
  4. 정리 노트: 일시정지된 st3 프로세스는 손상된 kctx 상태를 가진다 — 재부팅으로만 종료; nf 하이재킹은 모든 트래픽에 대해 LOCAL_OUT 훅을 깨뜨린다 — 루트 후 재부팅하여 복원

오늘 밤의 자산 추가

  • poc/stage3.c 모드: pin/root/drain{,2,3,4,5}/iso — 전체 무기 + 오라클 + 격리 하네스, O_SYNC 크래시 지점 포렌식
  • userland-fake 인프라 (ufake_prep) — 유지하되 컨텍스트 인라인 역참조 경로가 발견될 때만 사용 가능
  • tools/: kdis/scan_s/resolve/dumpb/findsysctl 오프라인 vmlinux 분석

세션 10 — 루트 달성 (2026-09-11)

nf-무기를 막고 있던 세 가지 버그, 모두 수정됨

  1. 잘못된 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.
  2. 이중 역참조 버그: 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를 모두 깨끗하게 처리).

SELinux 장벽과 2-패킷 우회

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)}.
  • 패킷 1: *(enforcing)=0 → SELinux Permissive.
  • kbm_cpu[reg]+off를 통해 엔트리를 {fn=commit_creds, priv=&init_cred}로 다시 쓴다.
  • 패킷 2: 발신자의 태스크에서 commit_creds(&init_cred) → permissive SELinux와 함께 uid 0 → 사용 가능한 루트, 모두 한 번의 회수로, 체인 불필요.

기기에서 검증됨 (2026-09-11)```

[+] 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

root@kitploit:~
- `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

SESSION 12 — LK RE: eng/unlocked 게이트는 실제이며, 서명되지 않은 플래그 저장소는 존재하지 않는다

목표: LK가 디바이스를 eng/unlocked로 취급하도록 만들거나, preloader/LK 버그를 찾아 verity/SELinux를 영구적으로 비활성화하는 것. 결과: 관련 LK 코드 경로를 end-to-end로 리버싱했으나, 사용 가능한 저장소로는 그 플립에 도달할 수 없다. 어떤 디바이스도 brick되지 않았으며, 유일한 boot1 실험은 원상태로 되돌렸다.

LK는 Thumb-2 PIC이며, 베이스 0xFF400000으로 재배치된다

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을 더하면 된다.

디코딩된 제어 흐름 (오프셋 -> 의미)

언락 코드는 Amazon-RSA로 서명됨 — 위조 불가

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에서 정적으로 악용 가능한 것 (경계/크기)은 발견되지 않았다. =>

verity/SELinux 플래그는 존재하는 저장소에서 오지 않는다

보안 플래그는 0x57c의 getter를 통해 읽힌다. 실증적 테스트:```

boot1 IDME item fos_flags data (offset 0x22B4, 8 bytes) set to "00000080"

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)

root@kitploit:~
`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의 보안 게이트에 의해서는 소비되지 않습니다.

결론 — 영구적인 eng/unlock이 차단되는 이유

  1. unlocked_kernel은 Amazon이 서명한 unlock_code(RSA/libtomcrypt, 내장 CA)를 요구합니다. 오프라인에서 위조할 수 없으며, 검증기 버그도 발견되지 않았습니다. 하드 차단.
  2. DM_VERITY_OFF / selinux=permissive 플래그는 LK env(para/ENV_v1)에서 소비되는데, 이 GPT에는 존재하지 않습니다. IDME fos_flags는 LK에 의해 경험적으로 무시됩니다(0x80이 지속되었으나 verity는 eio로 유지됨). 파티션 테이블을 수정하지 않는 한 하드 차단.
  3. fos_flags=0x80이 성공하더라도 androidboot.veritymode=disabled와 비-dm-0 root=만 설정될 뿐이며, unlock되지 않고 SELinux가 permissive로 전환되려면 여전히 동일한 부재 env의 dev_flags가 필요합니다.
  4. 따라서 Track A(부팅 시점 재익스플로잇)는 SESSION 11에서와 정확히 동일하게 차단된 상태로 남습니다: 유일한 unlock 경로가 동일한 LK 게이트이기 때문입니다.

남은 경로 (미래, 더 높은 위험; 시도되지 않음)

  • 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가 아니라는 것만 확인됨).
  • Preloader(boot0/EMMC_BOOT) 버그: 이번 세션에서는 리버싱되지 않았습니다. 원본 사본과 복구 경로가 마련되기 전까지 boot0 쓰기는 금지됩니다.
  • 검증기 연구: 엔지니어링 인증서 경로(0x316a0/0x31703)는 "engineering"으로 허용되는 기기 ID와 엔지니어링 키로 서명된 코드가 있어야만 도달할 수 있으며, 사용 가능한 개인 키가 없습니다.

산출물 / 재현성

  • 추가된 도구: tools/lk_xref.py — 베이스 독립적 LK 문자열 xref 리졸버.
  • 사용된 덤프: /tmp/opencode/mustang-dumps/lk.img, boot1.img(원본), boot0.img, mbr.img(GPT), pmt.img(모두 0).
  • boot1 실험 이미지(fos_flags=0x80)는 /tmp/opencode/s12/boot1_f80.img에 보관됨; 기기는 원본 boot1로 복원됨(/proc/idme/fos_flags -> 0 확인).

유용한 명령 (root 필요; ./run.sh로 재무장)```

re-arm runtime root (~1/3 per boot)

./run.sh --no-build

confirm LK's decisions without a UART

/data/metrics/su /system/bin/sh -p -c 'cat /proc/cmdline'

watch: root=/dev/dm-0 dm="system ... android-verity ..." (verity on)

androidboot.veritymode=eio ; androidboot.selinux=enforce ; prod=1

IDME read (Android copy; NOT what LK's gates use)

for f in fos_flags dev_flags usr_flags unlock_version serial; do cat /proc/idme/$f; echo; done

root@kitploit:~
## 세션 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

root@kitploit:~
`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 레지스터 맵이 확인되기 전까지는 사용해서는 안 됩니다).

상태 / 다음 단계

  • 미확인: mustang의 preloader가 실제로 0xD1/0xD4를 구현하는지 여부 (핸드셰이크 프로브가 이를 판단합니다). 만약 구현한다면, aftv2 eMMC 읽기/쓰기 경로를 그대로 포팅할 수 있을 가능성이 높습니다.
  • 그 다음: MT8163 MSDC 베이스를 찾고(커널 DT 또는 preloader), 파티션을 덤프한 뒤 (read_mmc), preloader에서 boot.img/lk를 패치하고 재부팅합니다.
  • 이는 SESSION 12의 모든 것보다 더 낮은 부트 단계이므로, amzn_verify_unlock이나 LK env 게이트에 의존하지 않습니다.
  • 프로토콜과 eMMC 레이아웃이 확인되기 전에는 SP Flash Tool / mtkclient 쓰기를 이에 대해 실행하지 마십시오.
도구 다운로드
  • 프록시 waiter는 waiter 스레드 자신의 스택에 존재함 (futex.c:1975가 this->rt_waiter를 전달, futex.c:2880의 futex_wait_requeue_pi에서 선언됨) → waiter가 arm32 select (nr 142) fd_sets를 통해 자신의 해제된 프레임을 스탬프함
  • 익스플로잇 환경: KASLR 없음 (고정 베이스 0xc0008000), PAN 없음, DEBUG_RT_MUTEXES 꺼짐 → 컴팩트한 48바이트 rt_mutex_waiter (tree_entry@0, pi_tree_entry@0xc, task@0x18, lock@0x1c, prio@0x20, deadline@0x28)
  • 최소 체인: 2개의 write-slot → 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.enforcing
    commit_creds(&init_cred)
    uid=0
  • [_] Stage 4: root 스크립트 (su, permissive, OTA off) + 지속성 — root 획득됨; 지속성 차단됨 (SESSION 11 참조): LK가 verity-off/SELinux-permissive를 eng/unlocked에 게이트함, 부팅 시 재익스플로잇에는 실행 가능한 실행자가 없음. 다음: LK/amzn_verify_unlock 리버싱.
  • Stage 5: 커스텀 OS 부팅 체인
  • jc
    nr_extres
  • JIT alloc 결과는 커널이 info->gpu_alloc_addr를 통해 기록함 (미리 할당해서 전달해야 하는 GPU VA)
  • evictable 객체압박결과
    없음900 MB생존
    없음1300 MB패닉 (시스템 lowmem 버그 — 무관)
    일반 영역 + DONT_NEED700 MB생존
    JIT 영역 + DONT_NEED700 MBreclaim 경로에서 패닉
    /proc/config.gz
  • ksrc/ — Amazon OSS 소스 (platform.tar + 추출된 midgard-r26p0 트리)
  • OTA: /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 400MBderef에서 크래시
    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=0
    cell 주소
    PM_PAGE+NF_CELL_OFF
    probe
  • Physmap 직접 매핑은 kernel_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의 코드가 아니어야 한다.
  • 리터럴 (파일 오프셋)값의미
    0x2700xFF4002F8str r4,[r6] 스크래치
    0x2740xFF40027C목적지 (base+0x27C)
    0x2780xFF54A440복사 끝 (BSS 포함)
    0x27C0xFF400484엔트리 포인트
    오프셋함수
    0xdf7cis_secure_or_prod() -> byte[ [[g]+0 ] + 0x163 ]; g = 전역 @0x52838. 이 유닛에서는 1.
    0x20b4verify_stored_unlock() = memset(buf,0,0x100); 0x57c를 통해 IDME/env unlock_code (0x100) 읽기; bl 0x222c; (verify==0) 반환.
    0x222c / 0x20f0amzn_verify_unlock(code,len) — libtomcrypt RSA/PKCS#1 검증 (아래 참조).
    0xda3eis_unlocked() = is_secure_or_prod() && verify_stored_unlock().
    0x29a28is_verity_disabled() = (fos_flags & 0x80) && !(is_secure_or_prod() && verify_stored_unlock()); 전역 @0x50c74에 캐시됨.
    0x29974SELinux 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=).
    0x27af8UART 게이트: fos_flags & 0x4 -> printk.disable_uart=0, 그렇지 않으면 =1.
    0x12fd4LK env 로더: 파티션 "para", 0x4000 바이트, 매직 ENV_v1, 체크섬 = 0x3ffc에 걸친 바이트 합계를 워드 @0x3ffc와 비교.
    0x1efd0이름으로 파티션 조회 ("para", "boot", ...에 사용됨).
    0x57c콜백 슬롯 @0x58218을 통한 getter 디스패처; 슬롯 @0x58200..0x5821c는 0x5a8-0x734의 테이블에서 등록됨.
    0x2a19cfastboot oem unlock: bl 0x222c(code,len); 성공 시 0x408을 통해 unlock_code (0x100) 기록.
    unlocked_kernel=false
    /proc/cmdline
    androidboot.unlocked_kernel
    androidboot.prod
    unlock_code
    문서화된 경로를 통한 eng/unlocked 플립은 암호학적으로 불가능하다.