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

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

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

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

도구 디렉토리

카테고리

모든 카테고리 보기
Loading categories
ghostlock-pfem10 — GhostLock (CVE-2026-43499) for OPPO Find X5 Pro (PFEM10) — OPlus watchdog 및 heap-spray detector 리버스 엔지니어링 | Kitploit
도구/GitHubGitHub/imeiplus/ghostlock-pfem10
Android SecurityPrivilege EscalationMemory ForensicsVulnerability AnalysisExploitationReverse EngineeringMobile SecurityPayload DevelopmentBinary Exploitation
GitHubimeiplus/ghostlock-pfem10

ghostlock-pfem10

GhostLock (CVE-2026-43499) for OPPO Find X5 Pro (PFEM10) — OPlus watchdog 및 heap-spray detector 리버스 엔지니어링

2일 전아직 검토되지 않음

인기

모두 보기 →

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

모든 도구 탐색

도구 컬렉션을 둘러보세요

모든 도구 보기 →
공유
저장소 보기

GhostLock — OPPO Find X5 Pro (PFEM10)

English · 中文

build

ColorOS 16 기반 OPPO Find X5 Pro용 GhostLock (CVE-2026-43499) 포팅. uid=0 자식 프로세스와 로드된 kernelsu.ko에 도달하며, 루트 프로세스는 가로채진다.

취약점

CVE-2026-43499 — futex PI use-after-free. remove_waiter()는 current가 requeuer일 때 current->pi_blocked_on을 지우며, 이는 rt_mutex_start_proxy_lock()의 -EDEADLK 롤백 경로에서 발생한다.

remove_waiter @ 0xffffffc0081ed254 — 패치 이전 형태.

디바이스

상태

"루트 프로세스 생존"에 관하여: evidence/kill.log의 실행들은 uid=0에 도달하고 kernelsu.ko를 로드하며, 실제로 이를 폴링한 실행에서 KernelSU 관리자 프로세스는 120초 동안 생존했고 kernelsu는 여전히 /proc/modules에서 Live 상태였다. 이후 실행에서 동일한 체인은 모듈이 여전히 Live인 상태에서 Android 프레임워크 서비스에 도달할 수 없게 만들었다(Can't find service: package/power/input/phone/wifi). 어떤 [ROOTCHECK-*] 커널 라인도, 어떤 $$sys_call_number@@ 페이로드도 결코 캡처된 적이 없으므로, 이후 실행의 상태 원인은 귀속되지 않는다. evidence/notes.md §2.3, §2.4 및 §7 참조.

오프셋

task_struct

thread_info

필드오프셋
flags

cred

익스플로잇 흐름```

LT perf leak target task_struct → file W7 stage 1 task+0x778 = V (real_cred → private sprayed page; V observed) W7 stage 2 task+0x780 = V (cred) ★ V12_W7_VALUE=V — THE SAME VALUE, not a new page W7 stage 3 V+8 = 0 (LOCAL repair of the page that was installed, ZERO shape) LT child fexecve(memfd of loader) — no execve of a /data path loader ksud late-load → kernelsu ... Live

root@kitploit:~
**2단계에는 1단계의 값을 명시적으로 전달해야 한다.** 1단계와 2단계는 각자
자체적인 spray를 가진 두 개의 독립적인 프로세스이므로, "cred 페이지를 두
슬롯 모두에 기록하라"는 것은 함정이다: 순진하게 읽으면 `(pageA, pageB)`를
생성하고, `commit_creds`는 **포인터**를 비교하기 때문에 두 쓰기가 모두
성공하더라도 그 쌍은 서로 다르다. 이것은 가상의 상황이 아니다 — 정확히
실행 3과 9에서 일어난 일이다:```
run 9   0x778 shot  write value = 0xffffff88679bade0
        0x780 shot  write value = 0xffffff8785d6ade0     <- a different page
run 3   0x778 shot  write value = 0xffffff8787b5ade0
        0x780 shot  write value = 0xffffff881bad2de0     <- a different page

run_bootA.sh는 따라서 V12_W7_VALUE=<stage 1's observed value>로 stage 2를 실행하며, 그 값을 복구할 수 없으면 아예 실행을 거부한다. HOLD는 stage 2보다 오래 살아 있어야 한다. 그렇지 않으면 stage 1의 페이지가 해제되고 재할당되어 "같은 값"이 dangling pointer가 된다. the same-value rule을 참조하라.

부팅당 한 페이지가 복구된다. Stage 3는 V+8을 0으로 만든다. 두 개의 서로 다른 페이지가 있을 경우, 둘 다 0으로 만들면 gid/suid 스탬프(아래)가 지워지고 divergence가 agreement처럼 보이게 되므로, runner는 실제로 설치된 페이지만 복구하고 두 값이 다르면 중단한다.

cred 페이지는 payload.c에 의해 구성된다: 8개의 id 필드 모두 0, 5개의 capability set 모두 full, 그리고 user / user_ns / group_info는 root_user / init_user_ns / init_groups를 가리킨다. Stage 3가 존재하는 이유는 write의 side effect가 설치하는 cred의 cred+8(gid/suid)을 항상 덮어쓰기 때문이다.

init_cred에 관하여 — 명시적 이분법

여기 두 섹션이 서로 모순되곤 했다("절대 전역 init_cred가 아니다" vs "CONTROL=1은 cell 2를 재현한다", 그리고 cell 2는 바로 init_cred이다). 두 진술 모두 서로 다른 역할에 대해 참이다:

  • 타깃으로서 금지됨. init_cred 포인터를 쓰면 side effect가 init_cred+8을 전역적으로 손상시킨다 — init_cred는 모든 커널 스레드가 공유하며, Uid: 0 0 4294967176 0이 바로 그 손상이다. 코드는 V12_ALLOW_INIT_CRED=1이 의도적으로 설정되지 않는 한 이 경로를 거부한다.
  • 유일하게 입증된 일관된 쌍으로서 유지됨. ksud에 도달한 09-14 체인은 두 슬롯 모두에 하나의 고정 주소(0xffffff802a7e0be0)를 썼으므로, 구성상 real_cred == cred이다 — 그래서 execve까지 살아남았다. CONTROL=1이 이를 재현한다. 이는 control이지, 기반으로 삼을 구성이 아니다.

perf leak: PERF_TYPE_SOFTWARE / PERF_COUNT_SW_CPU_CLOCK, PERF_SAMPLE_REGS_INTR, exclude_user=1. [0xffffff8400000000, 0xffffff90000000)를 허용, 투표 ≥ 15%.

write primitive와 그 side effect

UAF는 rb_erase_cached Case 1-left를 통해 구동된다. 이는 하나가 아니라 두 개의 store를 제공한다:``` *(write_target) = write_value // the store you aim *(write_value + 0x08) = write_target // unavoidable side effect

root@kitploit:~
`write_value`는 비트 0이 클리어된 상태에서 8바이트 정렬되어야 합니다 — 이는 `0`이거나 유효한 커널 주소입니다. **이것이 `g_boot_state`를 이 프리미티브로 설정할 수 없는 이유입니다**: `1`이 되어야 하는 바이트는 정렬 요구사항에 의해 하위 비트가 `0`으로 강제되며, `write_value`는 부작용이 발생하는 주소와 동일한 값입니다.

### 부작용은 `write_value`가 가리키는 대상에 기록됩니다

`write_value`는 *저장되는 값*이자 *부작용이 기록되는 주소*(`+8` 위치)입니다. 이를 전역 커널 객체를 가리키게 하면 해당 객체가 손상됩니다.

**W7은 정확히 이렇게 했습니다** — `write_value`를 `init_cred` 별칭을 향하게 하여 — 그리고 이는 읽기에서 확인할 수 있습니다. `out/t5_w7_778.txt`에서:```
shape shift=0 wps=5: in[0]=0xffffff802a7e0be0 (write_value) in[2]=0xffffff8800cdd178 (write_target)
W7[W7] write_target= 0xffffff8800cdd178
Uid:	0	0	4294967176	0

write_value는 init_cred 별칭이었고 write_target은 child_task+0x778이었습니다. init_cred+8은 gid/suid이므로, 부작용으로 0xffffff8800cdd178이 거기에 저장되었습니다: init_cred.gid = 0x00cdd178 그리고 init_cred.suid = 0xffffff88 = 4294967176 — 정확히 위 Uid: 줄의 4번째 awk 필드입니다. init_cred+8을 0으로 만들자 복구되었습니다(out/t5_repair.txt: Uid: 0 0 4294967176 0 → Uid: 0 0 0 0). 이것이 "W7 stage 3"의 전부였습니다.

이 경로는 이제 코드에서 거부됩니다. V12_W7_INIT_CRED=1은 V12_ALLOW_INIT_CRED=1도 함께 설정되지 않으면 설명과 함께 중단되며, W2/W6/LTC 경로는 private cred 페이지가 없을 때 더 이상 init_cred로 폴백하지 않고 대신 중단합니다. 기본값이자 유일하게 온전한 경로는 스프레이된 cred 페이지입니다.

부작용 자체는 피할 수 없습니다: write_value는 반드시 cred 포인터여야 하므로, cred+8은 항상 쓰기 대상으로 덮어써집니다. 선택할 수 있는 것은 그 위치뿐이며 — 그리고 복구는 이제 전역 객체에 대한 쓰기가 아니라 cred_page+8(stage 3)의 로컬 0 쓰기입니다.

잘못된 groups= 출력은 이 증상이 아니라 별개의 증상입니다. 이는 gid와 egid가 깨끗하게 읽힌 실행에서 관찰되었으므로, init_cred+8 부작용에서 비롯될 수 없으며, 가짜 cred 자체의 group_info 필드를 가리킵니다. evidence/notes.md §10.6을 참조하세요.

★ 단일 필드 착륙은 태스크를 분기 상태로 남기며 — 그것은 강성 BUG_ON입니다

이 프리미티브는 패스당 정확히 하나의 주소에 저장합니다. task+0x778 (real_cred)과 task+0x780 (cred)은 서로 다른 두 주소이므로, 0x778만 또는 0x780만 착륙한 쓰기는 태스크를 cred != real_cred 상태로 남깁니다 — 분기 상태입니다.

이 이미지에서 그 상태는 경고가 아니라 강성 패닉입니다. commit_creds는 BUG_ON(task->cred != task->real_cred)로 시작합니다:``` commit_creds @0xffffffc008186784 0x1867a4 ldr x19, [x20, #0x778] ; old = task->real_cred 0x1867a8 ldr x8, [x20, #0x780] ; task->cred 0x1867ac cmp x8, x19 0x1867b0 b.ne #0xffffffc008186b68 0x186b68 brk #0x800 ; == BUG()

root@kitploit:~
and the kernel is built with **`CONFIG_PANIC_ON_OOPS=y`** (`CONFIG_PANIC_ON_OOPS_VALUE=1`).
`__put_cred @0xffffffc008185530` carries the same family of assertions
(`usage != 0` → BUG; `cred == current->cred` / `current->real_cred` → BUG).

So the divergence is *latent* — it does nothing while the victim just spins —
until **any** `commit_creds` happens on that task: `setresuid` / `setresgid` /
`setuid` / `setgid` / `capset`, or **`execve` via `install_exec_creds`**.

> **⛔ 철회됨 (2026-09-18 늦게): 이것은 재부팅 메커니즘이 아니다.**
>
> 이 섹션의 이전 개정판에서는 이 divergence를 "재부팅의 유력한 메커니즘 후보"라고
> 불렀고 "shape split을 설명한다"고 했다. 그렇지 않으며, 그 이유는 이제 논증이
> 아니라 측정으로 확인된다:
>
> * `commit_creds`는 **`current`**에서 task를 가져온다 — `0x1867a0 mrs x20, sp_el0`.
>   시그니처는 `commit_creds(struct cred *new)`이며, task 인자가 없다.
>   따라서 divergence는 이를 **보유한** task가 스스로 `commit_creds`를 호출할 때만
>   문제가 된다.
> * 재부팅이 발생한 실행은 모두 `V12_NO_EXEC=1`이었다 (`run3_0445.log:18`,
>   `run9_0606.log:20`, `run10_0616.log:24`에 그대로 명시됨). 따라서 victim은
>   `execve`를 전혀 발행하지 않았고 `commit_creds`에 도달하지도 않았다.
> * Run 10에는 poke가 전혀 없었다 (`grep -c poke` = 0).
> * `exit_creds`는 `put_cred` 전에 **두** 포인터를 모두 null로 만든다
>   (`0x185cb8 str xzr,[x19,#0x778]`; `0x185d24 str xzr,[x19,#0x780]`). 따라서
>   victim의 `_exit(0)`은 divergence에 걸리는 대신 이를 **지워버린다**.
>
> ⇒ 그 실행들에서 divergence는 **무해(inert)**했다. `BUG_ON`은 실재하지만,
> 터지지 않은 지뢰일 뿐이다. "Shape A는 절대 재부팅하지 않는다"는 다시 상관관계로
> 돌아간다. 이 지뢰가 실제로 제약하는 것은 **laundering**이다. 왜냐하면
> `setresgid`/`setresuid`가 스스로 `commit_creds`를 호출하기 때문이다.
>
> **체인을 실제로 구분하는 양은 포인터 동등성이며, 이는 두 shot이 하나의 값을
> 써야 함을 의미한다** — 다음 하위 섹션을 참조.

### ★★★ 두 shot은 *동일한* 값을 써야 한다 — 단지 둘 다 성공하는 것으로는 부족하다

`BUG_ON`은 **포인터**를 비교한다. 둘 다 `uid 0`을 담고 있는 두 페이지는 여전히
서로 다른 객체다. 캡처가 이 구분을 구체화한다:

| chain | 0x778 shot | 0x780 shot | pointers |
|---|---|---|---|
| old (`t5loop.sh MODE=CRED`) | `in[0]=0xffffff802a7e0be0` | `in[0]=0xffffff802a7e0be0` | **동일** → ksud 로드됨, manager 120초 생존 |
| new (`run_bootA.sh`) | `0xffffff88679bade0` (run 9) | `0xffffff8785d6ade0` | **상이** → 둘 다 성공해도 divergent |

`tools/t5loop.sh`는 **모든** 오프셋에 **하나의** `$ENVV`를 적용하므로,
`MODE=CRED`는 두 shot을 *구성상* 동일하게 만들었다. `run_bootA.sh`는 step 5와
step 6을 각각 빈 `$extra`로 발사했으므로, 각각 **자기 자신의** 페이지를
spray했다.

⇒ 요구사항은 **"두 shot이 동일한 값을 쓴다"**이다. `run_bootA.sh`는 이제 이를
강제한다 (`SAME_VALUE=1`, 기본값): step 6은 step 5에서 관측된 `write_value`를
그대로 재사용하며, 그 값을 복구할 수 없으면 **아예 발사를 거부한다** — 발사하면
divergent 쌍을 만들게 되기 때문이다.

⚠ `HOLD`는 두 번째 shot보다 오래 살아남아야 한다. 첫 번째 shot의 PIN 자식이
먼저 죽으면 페이지가 해제되고 재할당되어 "동일한 값"이 dangling pointer가 된다.
기본 `HOLD=20`은 **너무 짧다**; `HOLD=600`을 사용하라. 이제 `SAME_VALUE=1`일
때는 이것이 *기본값*이다 — 기존의 무조건적인 20초는 기본 구성 자체가 함정이었다
— 그리고 `SAME_VALUE=1`과 함께 명시적으로 짧은 `HOLD`를 지정하면 조용히
dangling pointer를 만드는 대신 이제 크게 경고한다.

⚠ `CONTROL=1`은 예전에 **step 5만** 변경했으므로, `(init_cred, fresh page)` —
divergent 쌍 — 을 만들어냈으며, 이 파일은 그것이 cell 2를 재현한다고 주장했다.
수정됨: 이제 두 shot 모두 `init_cred`로 설정한다. (Cell 2의 비용은 그대로다:
부작용이 `init_cred+8`을 전역적으로 손상시키며, 이것이 `Uid: 0 0 4294967176 0`이다.)

**credential을 laundering하려는 모든 것에 대한 결과**
(`setresgid` + `setresuid`로 spray된 페이지를 실제 `struct cred`로 교체):

- 메커니즘은 실재하며 검증되었다 — `commit_creds`는 `x21`을 **`task+0x778`과
  `task+0x780` 모두**에 쓴다 (`0x186998` / `0x1869a0`). 따라서 한 번의 호출로
  split이 영구적으로 복구된다; `prepare_creds @0xffffffc008186070`은
  `kmem_cache_alloc(cred_jar)` + `memcpy(new, task->cred, 0xA8)` +
  `security_prepare_creds(...)`이며, 147/149는 둘 다 guard의 예외 목록에 있다.
- **그러나 그 전제조건은 "0x778 shot을 건너뛴다"의 반대다.** launder 자체가
  `commit_creds`를 호출하므로, **두** 포인터가 이미 동일한 값을 보유할 때만
  발행되어야 한다.
- `V12_LAUNDER=1`은 **두** 가지에 의해 게이트되며, 첫 번째는 관측이 아니다:
  1. **`V12_W7_SAME_VALUE=1`** — 두 shot에 동일한 값이 주어졌다는 *출처
     (provenance)* 사실. read primitive가 없으면 포인터 동일성은 관측
     불가능하므로, 더 나은 userspace 검사로 대체할 수 없다; 선언해야 한다.
  2. `consistent=1` — `0x780` 뷰 (`getuid()`)가 `0x778` 뷰
     (`/proc/self/status` `Uid:`)와 일치함. **그 자체로는 필요조건일 뿐
     충분조건이 아니다**: 둘 다 `uid 0`을 담은 두 개의 서로 다른 페이지는
     포인터가 다르면서도 동일하게 읽힌다 — 이는 정확히 runner가 예전에
     만들어내던 경우다. (1)이 주어지면 충분조건이 된다: 일치 + 동일한 값 ⇒
     둘 다 같은 페이지에 성공적으로 기록됨.
  둘 중 하나라도 검사에 실패하면 ⇒ 거부하고, 네 가지 경우의 표가 evidence에
  들어간다. LT report 라인은 두 뷰 (`uid=` / `real_uid=` / `consistent=`)와
  `same_value_declared=`를 출력하여 상태를 추론하지 않고 읽도록 한다.

### ★ Instrument 1 — 부작용은 target을 겨냥한 STAMP다

`*(write_value + 8) = write_target`이며, `cred+8` / `cred+0xc`는 `gid` / `suid`이므로,
하나의 8바이트 store가 둘 모두에 걸쳐 기록된다:```
cred.gid  = low32(write_target)
cred.suid = hi32(write_target)

That is a measurement, not a model. out/t5_w7_778.txt has write_target = 0xffffff8800cdd178 and Uid: 0 0 4294967176 0, where 4294967176 = 0xffffff88 = hi32(write_target); notes.md §11 records the other half, init_cred.gid = 0x00cdd178 = low32(write_target).

두 가지 용도:

  1. task+0x778의 착지 오라클이다. /proc/<pid>/status는 real_cred = task+0x778 — 방금 설치된 바로 그 cred — 를 읽으므로, 스탬프는 사용자 공간에서 직접 읽을 수 있다. 스테이지 3 이전에 읽어라: 복구는 cred+8을 0으로 만들고 그것을 지운다 (notes.md §11의 t5_repair.txt는 성공적인 복구 전에 4294967176을, 후에 0을 읽는다).
  2. 세탁 게이트가 서로 다른 두 페이지를 잡을 수 있는 두 번째, 독립적인 이유다. uid 절반만으로는 불가능하다: uid가 0인 어떤 페이지든 0을 읽으므로, 서로 다른 두 페이지가 모두 "일관됨"을 보고한다. 그러나 low32(T+0x778)과 low32(T+0x780)은 정확히 8만큼 다르므로, 두 페이지가 있으면 getgid() (cred에서)와 (에서)가 불일치한다 — 그리고 는 uid뿐 아니라 gid도 비교한다.

⇒ V12_W7_SAME_VALUE는 두 번째 게이트이지, 유일한 게이트가 아니다. 여전히 중요하다: 스탬프는 두 부작용이 모두 발동했을 때만 구별하므로, 동일값 규칙이 그 잔여 구멍을 막는다. 그리고 게이트가 무엇인지 주목하라 — 방지기가 아니라 탐지기다. 그것은 거부할 수만 있을 뿐이며, 부팅의 나머지 동안 태스크를 분기된 상태로 남겨둔다. 동일값 규칙이 그 쌍을 올바르게 만드는 것이며, 이는 옛 체인이 가지고 있던 것이고 execve에 도달하기 위해 필요한 것이다.

★ 도구 2 — probe_state는 착지 기준이 아니다

이 프로젝트에서 세 번이나 틀렸다: W1은 전역에 착지하고 R을 보고했다; 런 12의 D는 cred가 아니라 전역을 겨냥했다; 그리고 런 11의 R은 착지인 것처럼 테이블에 기록되었다 (run11_w778r1_miss.txt와 런 7의 w7_w7781.txt는 줄 단위로 동형이다 — 둘 다 probe_state = R, probe_done = 0). 대상별 오라클을 사용하라:

run_bootA.sh는 이제 스테이지를 1에 스탬프를 사용한다 — 이것이 task+0x778에 대한 ROUNDS>1 재시도를 의미 있게 만든다, 실패한 라운드가 추론되는 대신 읽을 수 있기 때문이다 — 그리고 스테이지 1이 착지하지 않으면 스테이지 2를 발동하지 않는다.

⛔ "awk 필드"라고 말하라, "3번째 필드"라고 절대 말하지 마라. uid_line은 레이블도 출력하므로 (Uid: 0 0 4294967176 0), awk의 $1은 "Uid:"이고 네 개의 id 값은 $2..$5이다: $2=uid $3=euid $4=suid $5=fsuid. 스탬프는 cred+8, 즉 gid (low32)와 suid (hi32)에 위치한다 — 따라서 Gid: $2와 Uid: ****이다. 그것을 "3번째 필드"라고 부르는 것(을 세는 것이며, §11이 그렇게 표현한다)은 코드가 을 읽도록 유도하는데, 이는 가짜 cred에서 = 이고 결코 과 같을 수 없다. 이 오프바이원이 여기에 존재했다: 는 착지한 샷에 대해 "스탬프 없음"을 반환했고, 그래서 스테이지 2가 결코 발동하지 않았고 세탁 게이트가 영원히 거부했다 — , 왜냐하면 "스탬프 없음"은 진짜 미스의 정상적인 결과이기도 하기 때문이다.

알려진 양성 샘플에 대해 결코 테스트되지 않은 기준은 기준이 아니라 추측이다 — 그리고 이 부류의 실패 (이 오프바이원, probe_state, dmesg -w, 빈 klog.host, 공백 리드백)는 항상 *"아무 일도 일어나지 않았다"*로 나타나는데, 이는 또한 정당한 실험적 결과이기도 하다. 그래서 이 검사는 이제 두 번 방어된다:

  • **stamp_selftest()**는 프리플라이트에서 실행되고 실패 시 exit 9하며, 게이트가 사용하는 동일한 추출 함수를 out/t5_w7_778.txt의 측정값 (write_target = 0xffffff8800cdd178 → Uid $4 = 4294967176, Gid $2 = 13488504)과 음성 및 읽을 수 없는 샘플에 대해 구동한다. 검사를 재구현하는 자체 테스트는 아무것도 증명하지 못하므로, 필드 추출은 uid_suid_field / gid_gid_field로 분리되었다.
  • tools/test_stamp_criterion.sh — run_bootA.sh에서 실제 함수를 추출하는 독립형 회귀 테스트로서의 같은 것이다.

stamp_ok()는 세 가지 상태를 반환한다, 왜냐하면 "읽을 수 없음"은 "스탬프 없음"이 아니기 때문이다 (그 혼동이 런 13을 "변화 없음"처럼 보이게 만들었다): 0 = 존재, 1 = 읽을 수 있고 스탬프 없음, 2 = 읽을 수 없음. 그리고 probe_state = D인 동안 1을 반환하면, 러너는 "착지하지 않음" 메시지 대신 ⛔ ORACLE INCONSISTENT — "가서 기준을 확인하라" — 를 출력하는데, 이는 운영자를 완전히 다른 곳 (새 부팅, 또는 적중률 사냥)으로 보낸다.

전체 유도: evidence/2026-09-18-divergence-is-latent.md 그리고 evidence/2026-09-18-cred-launder-verification.md (후자의 §2.3은 제자리에서 철회된다). 쓰기 형태 중첩 자체 검사는 오프라인으로 닫힌다: 형태 단어들은 커널 스택의 fd_set 그리드에 있는 반면 부작용은 스프레이된 페이지 내부에 착지하므로, 둘은 어느 형태에서도 중첩될 수 없다.

탐지 경로

세 개의 독립적인 리포터. 어느 것도 다른 것의 폴백이 아니며, 경로 1만이 호출 태스크를 죽일 수 있다.

경로 2는 자격 증명 변경이 아니라 execve에서 발동하며, 경로 1과는 별개의 코드 경로다. 그것은 kevent_send_to_user를 통해 보고하므로, 다음에 일어나는 일은 커널이 아니라 사용자 공간 데몬의 결정이다.

검사는 exec되는 이미지의 경로에 대한 것이므로, 로더의 페이로드를 memfd로 로드하는 것만으로는 충분하지 않다: 로더 자체가 /data/local/tmp에서 exec되면, 그 첫 execve가 이미 보고한다. memfd에 대한 d_path()는 /memfd:…이므로, 로더 자체가 memfd를 통해 exec되어야 한다 — 바로 이 이유로 V12_EXEC_MEMFD가 이제 기본으로 켜져 있다. 옛 동작은 RUN 4에서 볼 수 있다:``` LT child exec /data/local/tmp/glx12 (4 args) <- execve of a /data path while uid=0 LT child memfd loaded 5014624 bytes (fd=5) <- memfd only protected the second image

root@kitploit:~
경로 2에 대한 Grep 가능한 마커:```
[ROOTCHECK-EXEC-INFO]:common %s result %s      with  "execve_report" / "execve_block"
%d,path@@%s                                    kevent payload fragment

path 2와 path 3은 kevent를 통해서만 보고하므로, "커널 로그에 [ROOTCHECK-*]가 없다"는 것은 둘 중 어느 것도 발동하지 않았다는 것을 배제하지 못한다. 그 추론에는 우리가 아직 찾지 못한 userspace 수신기가 필요하다.

Watchdog — oplus_security_guard.ko

sys_enter 캐시:```asm ldrsw x8, [x1, #0x118] ; regs->syscallno mrs x9, sp_el0 ; current ldr x10, [x9, #0x780] ; cred str x8, [x9, #0xdf8] ldr w8, [x10, #4] → str x8, [x9, #0xe00] ; uid ldr w8, [x10, #0x14] → str x8, [x9, #0xe08] ; euid ldr w8, [x10, #8] → str x8, [x9, #0xe10] ; gid ldr w8, [x10, #0x18] → str x8, [x9, #0xe18] ; egid

root@kitploit:~
`sys_exit` 확인:```asm
ldr   x0,  [x8, #0xe00]         ; cached uid
cbz   x0, #0x48c                ; cached uid == 0 → return
adrp  x9, #0 ; ldrb w9, [x9]    ; g_boot_state
tbnz  w9, #0, #0x48c            ; is_unlocked → return
ldr   x9,  [x8, #0x780]         ; cred
ldr   w3,  [x8, #0xdf8]         ; cached syscallno
cmp   x0, w10 ; b.hi #0x468     ; uid descending → kill path
                                ; euid / gid / egid, same shape
ldr   x9,  [x8, #8]             ; addr_limit
cmp   x9,  #0x8000000001
b.lo  #0x48c                    ; addr_limit != KERNEL_DS → return
sub   w9,  w3, #0x8f            ; syscallno - 143
cmp   w9,  #0x47
b.hi  #0x4a0                    ; outside 143..214 → kill
ldrsw x12, [x10, x9, lsl #2]    ; jmp table @ .rodata+0
br    x11
0x48c: ret
0x4a0: bl oplus_root_check_succ ; printk + kevent_send_to_user
       bl oplus_root_killed     ; printk + do_exit(SIGKILL)

제어 흐름 참고 (익스플로잇 순서에 중요). 네 개의 내림차순 엣지 비교는 0x468으로 곧바로 분기하며, 이는 syscall 번호 디스패치이다 — 이들은 addr_limit 게이트를 통과하지 않는다. 0x454–0x464는 어떤 id도 내려가지 않았을 때만 도달된다. 따라서 디스패치는 어떤 id가 내려갔거나 addr_limit == KERNEL_DS일 때 진입된다; 이는 addr_limit에 의해 게이팅되지 않는다.

결과:

  • 킬러는 자격 증명이 변경된 그 syscall 도중에 발동한다 — 즉 sys_enter가 아직 이전 uid를 캐시하고 있던 syscall이다. syscall이 진입할 때 태스크가 이미 uid=0이라면 (0x400 cbz), 훅은 반환하고 그 이후로는 계속 눈이 먼 상태가 된다.
  • 따라서 자격 증명 변경은 모듈을 건드리지 않고도 생존 가능하다: 피해자가 사용자 공간에서 스핀하는 동안 다른 태스크가 쓰기를 수행하도록 하거나, 변경을 12개의 면제 syscall 중 하나로 라우팅하라. delivery/外部建议评审_2026-09-18.md를 참조하라.

g_boot_state — 1바이트 .data..ro_after_init, 모듈 init 시 verified_bootstate로부터 strstr을 통해 설정된다. is_unlocked() = LDRB + RET.

모듈 VA 쓰기는 폴트를 발생시킨다 (CONFIG_STRICT_MODULE_RWX=y) — physmap 별칭 0xffffff80…을 사용하라.

리포트 페이로드: $$sys_call_number@@%d, $$set_id_flag@@%d, $$addr_limit@@%lx, $$enforce@@%d.

면제 syscall — .rodata+0, 인덱스 143–214

나머지 60개 항목 → 리포트 + 킬.

자격 증명이 변경되는 동안 스레드를 sendmsg (211)에서 블록하지 마라. 이는 테이블에 없으므로, 해당 스레드는 리포트되고 킬된다. 그 목적으로 블록된 상태로 있어도 안전한 유일한 syscall은 위의 열두 개이다: setregid, setgid, setreuid, setuid, setresuid, setresgid, connect, getsockname, setsockopt, shutdown, readahead, brk.

정정 (2026-09-18). 이 테이블의 이전 개정판은 모든 항목을 실제 arm64 syscall 번호보다 하나 낮게 표기했다 (146을 setresuid라고 불렀으나, 이는 setuid이다 — setresuid는 147이다). 번호는 항상 올바랐으며, 이름만 잘못되었다. 이름은 이제 이 기기의 커널 이미지에서 sys_call_table @ 0xffffffc00a13d8c0로부터 확인된다. 특히 sendmsg (211), munmap (215), getsockopt (209), getpeername (205)은 면제가 아니다 — 자격 증명이 변경되는 동안 이들 중 어느 것에서든 스레드를 블록하는 것은 통과가 아니라 킬이다. tools/gen_exempt_table.py로 재생성하라.

힙 스프레이 탐지기 — oplus_secure_harden.ko

root@kitploit:~
`entry_handler_setxattr` → `oplus_heapspray_check(7)`, `entry_handler_cpuinfo` → `(6)`, 둘 다 무조건적입니다.

`oplus_heapspray_check` — 카운터 `heapspary_ip4` / `ip6` / `xttr` / `cpuinfo`, `ktime_get_real_ts64` 델타, 7개의 보고 지점(`snprintf` + `printk` + `kevent_send_to_user`), `verified_bootstate`에 따라 게이트됩니다.

### 회피

| 프리미티브 | |
|---|---|
| `setsockopt` optname 46 `MCAST_JOIN_SOURCE_GROUP` | 필터링되지 않음 |
| `setsockopt` level `SOL_IPV6` (41) | 필터가 `level`을 읽는 경우 필터링되지 않음 |
| `setxattr` | 항상 카운트됨 |
| `/proc/cpuinfo` | 항상 카운트됨 |
| `socket()` / `socketpair()` | 후킹되지 않음 |
| `sendmsg`, `pipe`, `memfd`, `add_key`, `io_uring`, mmap | 후킹되지 않음 |

## Config```
CONFIG_CFI_CLANG=y
CONFIG_PTR_AUTH=y
CONFIG_SHADOW_CALL_STACK=y
CONFIG_STRICT_MODULE_RWX=y
CONFIG_STATIC_USERMODEHELPER=y
CONFIG_STATIC_USERMODEHELPER_PATH=""
CONFIG_SET_FS=y
CONFIG_RANDOMIZE_BASE=y
CONFIG_RANDOMIZE_MODULE_REGION_FULL=n
CONFIG_UNMAP_KERNEL_AT_EL0=y
CONFIG_ARM64_VA_BITS=39
CONFIG_ARM64_SW_TTBR0_PAN=y
CONFIG_SLAB_FREELIST_RANDOM=y
CONFIG_SLAB_FREELIST_HARDENED=y
CONFIG_INIT_ON_ALLOC_DEFAULT_ON=y
CONFIG_RANDOM_KMALLOC_CACHES=n
CONFIG_USER_NS=n
CONFIG_NF_TABLES=n
CONFIG_SYSVIPC=n
CONFIG_ANDROID_BINDER_IPC=y
CONFIG_KASAN=y

perf_event_paranoid = -1

빌드

NDK r28c. -O1 / API 26 / -D__ARM=1은 고정되어 있습니다 — 이들은 reclaim 스택 프레임 구조(delta=0 캘리브레이션)를 유지합니다. 이 중 하나라도 변경하면 기기에서 재캘리브레이션이 필요합니다.```bash export ANDROID_NDK_HOME=/path/to/android-ndk-r28c make # → exploit_guard ./build.sh # same, with NDK auto-detection

root@kitploit:~
수동:```bash
"$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android26-clang" \
  -D__ARM=1 -O1 -Wall -Wextra -pthread -Isrc/core -Isrc/devices/pfem10 \
  -o exploit_guard src/core/exploit.c

CI는 모든 푸시마다 빌드됩니다 (.github/workflows/build.yml, Ubuntu + NDK r28c, 아티팩트 exploit_guard).

설정```bash

adb push exploit_guard /data/local/tmp/e adb shell chmod 755 /data/local/tmp/e adb shell /data/local/tmp/e

root@kitploit:~
## 파일```
src/core/                 exploit.c  payload.c  payload.h  fdset_map.h
src/lib/                  KernelSnitch — kernelsnitch.h  futex_hash.h  timeutils.h  utils.h
src/devices/pfem10/       pfem10_target.h
model/                    model.c — host-side rtmutex chain-walk model
tools/                    kdis.py  kdis_ko.py  kdis_ko_reloc.py  gen_guard_disasm.py
                          gen_exempt_table.py  mod_layout.py  sct_dump.py
                          find_task_off.py  slide_resolve.py
                          test_stamp_criterion.sh   regression test for the 0x778
                                                    landing criterion (run it after
                                                    touching uid_line/gid_line)
artifacts/                guard_post_handler.s   kill chain, relocations resolved
                          guard_relocs.txt       raw .text relocation dump
                          guard_disasm.txt       guard + heap-spray detector
                          guard_exempt_table.txt 72-slot jump table, real names
evidence/                 kill.log  notes.md — device captures and their limits
Makefile  build.sh        exploit build (-O1, API 26, NDK r28c)
run.sh                    device-side run orchestration (retry across reboots)
.github/workflows/        build.yml — cloud build + artifact

Evidence

evidence/kill.log — 네 번의 root 실행에 대한 adb shell 트랜스크립트 원문: 전체 타임라인, 대상 태스크의 uid가 0이 되는 순간, kernelsu.ko 로드, 그리고 그 이후의 상태. 먼저 헤더 블록을 읽어라: 이 파일이 포함하지 않는 것과 그 이유를 나열하고 있다.

evidence/notes.md — 커널 측. 모듈 주소와 어떤 SELinux 상태에서 어떤 /proc 채널이 작동하는지; strstr 키를 포함한 전체 g_boot_state 도출; 수정된 exempt 테이블; 누락된 커널 절반을 생성할 캡처 레시피; 그리고 아직 열려 있는 항목들의 목록.

evidence/2026-09-18-bootA/ — 현재 설계의 첫 기기 실행, 13회 부팅. 재부팅은 질서 정연하며(bootreason=reboot) 패닉 라인이 한 번도 캡처된 적이 없다 — 하지만 그것을 열세 번이 아니라 한 번의 샘플로 읽어라. 재부팅한 네 번의 실행 중 두 번은 빈 klog.host를 저장했고, 한 번은 다음 부팅의 로그를 저장했으며, 오직 한 번의 윈도우만이 자기 자신의 재부팅을 그럴듯하게 포괄할 수 있다. 마찬가지로, 한 실행의 probe_state와 victim readback은 둘 다 비어 있으며(기기는 이미 사라졌다), 따라서 그 쓰기가 성공했는지에 대한 정보를 담고 있지 않다 — 빈 필드는 "변화 없음"이 아니다. 이 디렉터리는 또한 알아둘 가치가 있는 방법론적 오류를 문서화한다: dmesg -w는 이 기기에서 no-op이다(toybox가 한 번 덤프하고 종료한다), 그래서 이전 실행의 커널 로그에는 캡처 이전 이력만 담겨 있었다 — "[ROOTCHECK-*] 없음"은 아무것도 증명하지 못했다. evidence/notes.md §6에 수정된 poll-and-stream-to-host 레시피가 있다.

evidence/2026-09-18-cred-launder-verification.md — 이 이미지 자체의 디스어셈블리(일반적인 5.10 소스가 아님)에 대한 credential-laundering 제안의 검증: commit_creds 이중 저장, prepare_creds 할당 및 측정된 sizeof(struct cred) = 0xA8, 위에서 언급한 BUG_ON(cred != real_cred) 분기 위험, 닫힌 write-shape 중첩 자체 점검, 그리고 13회 부팅에 대한 증거 커버리지 감사. §2.3은 제자리에서 철회된다 — 분기는 잠재적일 뿐, 재부팅 메커니즘이 아니다.

evidence/2026-09-18-divergence-is-latent.md — 2차 검증. commit_creds는 current에서 태스크를 가져오고(0x1867a0 mrs x20, sp_el0), exit_creds는 put_cred 전에 두 포인터를 모두 null로 만들며, 원시 캡처는 이전 체인이 두 슬롯에 하나의 동일한 값(0xffffff802a7e0be0)을 쓴 반면 새 체인은 서로 다른 두 페이지를 썼음을 보여준다. 따라서 기준은 포인터 동등성이다 — "두 발 모두 같은 값을 쓴다"이지, "두 발 모두 성공한다"가 아니다.

postreboot_forensics.sh — 폴러에 의존하지 않는 재부팅 포렌식. 기준은 단일 조건이다: CONFIG_PSTORE_CONSOLE=y는 panic()이 kmsg_dump(KMSG_DUMP_PANIC) 시점에 콘솔 꼬리를 ramoops에 쓰도록 만든다 — 어떤 리셋보다도 먼저 — 따라서 그 후 박스가 재부팅하든 멈추든 무관하다. /sys/fs/pstore/를 가져오고, kernel BUG / __put_cred / cred.c를 grep하며, 부팅 이유 문자열을 출력한다(이력 항목에는 reboot,shell / bootloader / reboot,edl 접미사가 붙어 있었으므로, 이유는 epoch가 구분하지 못하는 행위자를 구분한다).

⚠ "깨끗한 bootreason=reboot"을 "패닉 없음"으로 읽지 마라. QCOM에서 SoC 워치독 어서트는 PMIC PON 블록을 통해 리셋되므로, panic → panic_timeout=-1 → hang → watchdog → PMIC reset → clean bootreason 은 우리가 보유한 증거로는 하드웨어 리셋과 구분할 수 없는 자기 일관적 체인이다. 이 저장소 자체의 total_17_dump_0_pmic_17은 17번의 비정상 재부팅 모두를 pmic으로 귀속시키는데, 이는 정확히 워치독의 정상적인 형태이지 "커널이 아님"의 증거가 아니다. bootreason은 여기서 아무것도 좁혀주지 않는다; ramoops가 유일한 기준이다.

두 가지 전제 조건이 충족되지 않으면 스크립트의 판정은 무효다(铁律 8 — 무신호 결론은 채널이 먼저 도달 가능함이 입증되어야 한다):

  • 제3의 상태가 필요하다. /sys/fs/pstore/*는 root 전용이므로, Enforcing 하에서는 adb pull과 cat 모두 실패한다 — 그리고 "읽을 수 없음"은 "읽었는데 비어 있었음"과 동일한 출력을 낸다. 2-상태 스크립트는 한 번도 열지 않은 채널로부터 "pstore가 비어 있음 ⇒ 패닉 반증됨"을 출력한다. 따라서 스크립트는 **CHANNEL UNREACHABLE**을 내보내고(ls 실패, 또는 알려진 모든 항목이 존재하지 않는 것이 아니라 읽기에 실패), getenforce를 함께 보고한다.
  • 널 테스트를 먼저. 깨끗한 adb reboot 후 즉시 가져오기. 알려진 정상 재부팅이 읽을 수 있는 것을 아무것도 산출하지 못하면, 채널은 입증되지 않은 것이며 이후의 모든 "빈 pstore"는 증거가 아니다. 순서가 중요하다: 기기는 부팅 직후 레코드를 이동시키고 unlink하므로, 순서는 reboot → Permissive 획득(W1) → 즉시 스크립트 실행이다.

run_bootA.sh — 그 한 번의 부팅에 대한 오케스트레이션으로, 중요한 순서대로(0x778 → 동일한 값으로 0x780 → 실제로 설치된 cred의 로컬 복구 → 확인 → 그제서야 poke). ADB=/SER=/BIN_LOCAL= 재정의 가능; SAME_VALUE=1(기본값)은 동일 값 규칙을 강제하고, LAUNDER=1은 게이트된 launder를 활성화하며, HOLD=600은 동일 값 시퀀스에 필요하다.

재시도는 단계별로 분리된다(R5/R6), 두 단계가 상반된 위험 프로파일을 가지기 때문이다:

R5는 기본값이 ROUNDS, R6는 1이다.

권장 launder 실행 — 기본 sprayed-page 경로이며, CONTROL=1이 아니다:```bash LAUNDER=1 R5=3 R6=1 HOLD=600 CHAINWAIT=6000 NODRAIN=1 WATCH=180 ./run_bootA.sh

root@kitploit:~
`CONTROL=1`도 일관된 쌍을 만들 수 있지만, `init_cred` 포인터를 기록하면 그 부작용이 `init_cred+8`을 **전역적으로** 손상시킨다 — 그리고 "프레임워크가 죽는다"는 것은 감시 대상 중 하나이므로, 장치 전체에 걸친 결함을 측정의 배경으로 끌고 들어가면 이 실행이 존재하는 바로 그 판독값을 흐리게 만든다. 스프레이된 페이지 경로는 "두 발 모두 명중해야 한다"는 비용만 치르며, 이것이 바로 `R5=3`이 필요한 이유다. `CONTROL=1`은 유일하게 *입증된* 일관된 쌍이자 대조군으로 남으며, 권장 구성으로는 남지 않는다.

**어떤 페이지가 설치되었는지는 무조건 실행되는 줄에서 읽힌다.** `run_w7`은 쓰기 값을 두 줄에 출력하는데, 그중 한 줄만 무조건 실행된다:```
L1793  W7[..] write value = private cred page 0x..   — spray path only
L1802  W7[..] write_value = 0x..                     — after the if/else, ALL paths

러너는 첫 번째 문구와 일치하도록 되어 있었기 때문에, CONTROL=1에서 추출 결과가 비어 있었고, if [ -n "$CRED" ]가 복구를 건너뛰었으며, 동일 값 결합의 자체 [ -n "$CRED" ] 항이 이를 0으로 유지했습니다 — 세탁 게이트는 영원히 거부했을 것입니다. 두 가지 모두에서 조용한 no-op이 발생한 것은 두 개의 출력 지점 중 하나만 인식한 정규식 때문이었습니다. 이제 그것과 write_target 추출(스탬프 기준의 입력)은 모두 wv_from/wt_from을 거치며, 이들은 기준 자체와 함께 회귀 테스트에서 검증됩니다.

두 스트림 모두 측정 대상보다 먼저 시작합니다. uid.stream은 스테이지 1부터 실행되고, cred.stream은 poke에서 시작하며, watch 이후가 아닙니다 — poke는 자식을 NO_EXEC 보고 루프로 풀어주는데, 이는 240 × 0.5초 = 120초이고 그 다음 _exit(0)입니다 (exploit.c: "LT child NO-EXEC mode done (120s)"), 따라서 t+~135초에 있던 예전 배치는 자식이 이미 사라진 후에 샘플링을 시작했으며, 이는 정확히 이 계측기가 존재하는 창이었습니다. 또한 러너는 stamp_selftest()가 실패하면 진행을 거부하고, 스탬프와 probe_state가 불일치할 때 "did not land" 대신 ORACLE INCONSISTENT를 출력합니다.

artifacts/guard_post_handler.s — 재배치가 채워진 킬 체인입니다. 이전 목록의 adrp x9, #0은 .data..ro_after_init이고, bl #0x4ac는 oplus_root_check_succ입니다. 자신의 기기에서 벤더 모듈을 가져온 후 tools/gen_guard_disasm.py로 재생성하세요.

tools/kdis_ko.py — RELA는 sh_info로 매칭됩니다. 이 빌드에서는 .text 재배치가 .rela.text.<func>에 있으므로, 이름 기반 조회는 아무것도 반환하지 않습니다.

Related

Project
JoinChang/ghostlock-oneplus참조 구현; 5.10 컴팩트 웨이터
NebuSec CyberMeowfia원본 GhostLock 연구

License

GPL-3.0 — LICENSE 참조.

도구 다운로드
디바이스OPPO Find X5 Pro (PFEM10)
SoCSM8450 / Adreno 730
OSColorOS 16.0.3.520 (CN01)
커널5.10.236-android12-9-o-gaf2075ad2c06
부트로더잠김, green
VA_BITS39 — KIMAGE_TEXT_BASE = 0xffffffc008000000
단계
Compact waiter 트리거 (CMP_REQUEUE_PI → EDEADLK)동작함
task_struct 유출 (perf)동작함
PI 쓰기 (8바이트; 값 = 0 또는 유효한 커널 주소)동작함
task+0x778 또는 task+0x780 단독 → Uid=root동작함 — 하지만 단일 필드 착지는 태스크를 분기 상태로 남기며, 이는 잠재적인 hard BUG_ON이다. 분기 위험 참조
두 필드에 하나의 값으로 쓰기 (일관된 쌍)❌ 스프레이된 페이지로는 결코 생성되지 않음. 전역 init_cred 별칭(09-14, CONTROL=1)으로만 관찰됨. 러너는 이제 이를 강제함(SAME_VALUE=1); 디바이스에서 실행되지 않음
자격 증명 세탁 (setresgid + setresuid)V12_LAUNDER=1 뒤에 구현됨; 디바이스에서 실행되지 않음
kernelsu.ko 로드됨동작함
루트 프로세스 생존⚠ 확립되지 않음 — 아래 참조
재부팅 메커니즘❌ 확립되지 않음. 하나의 후보(분기)는 이제 배제됨; 아래 참조
착지 기준으로서의 probe_state❌ 잘못됨 — 사용하지 말 것. 세 가지 반례; 아래 표 참조
pstore/ramoops 패닉 채널⚠ 계측기는 존재함; 채널은 결코 검증되지 않음 (아직 null 테스트 없음)
"피해자가 순수 사용자 공간에서 스핀함"⚠ 아직 판독값 없음 — uid.stream이 이제 utime/stime/nvcsw를 기록하므로 확인 가능
pi 측 단일 패스 이중 쓰기⚠ 확립되지 않음; pi.pc/pi.left는 fdset_map.h에서 하드코딩된 0
경로 A (UMH / modprobe_path)STATIC_USERMODEHELPER_PATH=""
필드오프셋
real_cred / cred0x778 / 0x780
캐시된 syscallno0xdf8
캐시된 uid / euid / gid / egid0xe00 / 0xe08 / 0xe10 / 0xe18
0x0
addr_limit0x8
ttbr00x10
preempt_count0x18
필드오프셋필드오프셋
uid0x4cap_inheritable0x28
gid0x8cap_permitted0x30
suid0xccap_effective0x38
sgid0x10cap_bset0x40
euid0x14cap_ambient0x48
egid0x18
fsuid / fsgid0x1c / 0x20
status_gid
real_cred
lt_cred_ids_agree()
대상착지 오라클
task+0x778Uid: 4번째 awk 필드 = hi32(write_target) 그리고 Gid: 2번째 awk 필드 = low32(write_target) — 위의 스탬프; 스테이지 3 이전에 읽기
task+0x780피해자 자신의 getuid()
전역 selinux_enforcinggetenforce
probe_state❌ 기준이 아니다. 기껏해야 체인에 대한 힌트일 뿐이며, 쓰기가 착지했다는 증거가 결코 아니다
$4
값
notes.md
$3
euid
0
hi32(write_target)
stamp_ok()
어디에도 오류 없이
#훅트리거동작
1oplus_root_check_post_handler, sys_exit tracepoint일부 id가 하강했거나, addr_limit == KERNEL_DSoplus_root_killed → printk + do_exit(SIGKILL); 그리고 oplus_root_check_succ → kevent_send_to_user
2oplus_exe_block_ret_handler, sys_exit이지만 execve (221)에만 해당d_path(mm->exe_file)가 /data, /data/local/tmp, /data/nativetest, /data/nativetest64로 시작oplus_RWO_root_check → printk + kevent_send_to_user (do_exit 없음)
3oplus_secure_harden kretprobessetsockopt optname ∈ {41,42,48}, setxattr, /proc/cpuinfo, SELinux 정책 재로드oplus_heapspray_check → kevent_send_to_user
143 setregid144 setgid145 setreuid146 setuid
147 setresuid149 setresgid203 connect204 getsockname
208 setsockopt210 shutdown213 readahead214 brk
kretprobe후킹 대상필터
socket_kretprobeip_setsockoptregs[1] ∈ {41, 42, 48}
socket_ip6_kretprobedo_ipv6_setsockoptregs[1] ∈ {41, 42}
cpuinfo_kretprobecpuinfo_open—
setxattr_kretprobesetxattr—
sepolicy_reload_kretprobespolicy_reload—
ldr w8, [x1, #8] ; regs[1]
cmp w8, #0x29 ; 41 IP_MSFILTER
b.eq #0xd58
cmp w8, #0x30 ; 48 MCAST_MSFILTER
b.eq #0xd60
cmp w8, #0x2a ; 42 MCAST_JOIN_GROUP
b.ne #0xd68 ; else → return, no call
bl oplus_heapspray_check
단계재시도 안전성
R55단계, task+0x778안전 — 실패하면 아무것도 설치되지 않고, 스탬프 기준이 실패한 라운드를 읽을 수 있게 만들어 주므로, 또 한 발은 그저 또 한 번의 시도일 뿐이다. R5=3은 발당 적중률을 ~p에서 ~1−(1−p)³로 끌어올린다.
R66단계, task+0x780안전하지 않고, 필요하지도 않다 — 5단계가 성공한 후에만 발동하므로, 재시도는 이미 분기된 태스크를 향해 쏘는 것이다: 두 번째의 서로 다른 페이지를 적중시킬 또 한 번의 기회일 뿐, 상승 효과는 없다. 하나가 적중하면 쌍이 완성되기 때문이다. 1로 유지하라.