frames,jar,passes: verifier-driven hardening from real jars
Proven by paranoid runs over lorep (17 classes, fully green), LoVisual (4081 classes) and oc2r (1278 classes) with per-class java -Xverify:all, each fix first reproduced as a red test: - merge: Top absorbs instead of Top-wins (javac declares Top for slots unwritten on some inflow; precise types reject those edges). Regression: loop_head_keeps_top_for_loop_local. - emit: collapse [Long|Double, Top] pairs on operand stacks (javac writes one entry; the pair is rejected). Regression: wide_stack_join_emits_single_entries. - xfer ArrayLoad: push the wide pair, fixing lcmp underflow. Regression: array_load_wide_pushes_pair. - xfer <init>: convert only the receiver's own allocation site instead of every Uninitialized (nested construction clobbered the outer object). Regression: nested_construction_keeps_outer. - xfer ANewArray: array operands gain one dimension ([B] -> [[B) instead of an illegal L-wrap ([L[B;). Covered by the same test. - jar load: duplicate class names (multi-release module-info twins) keep the first occurrence instead of crashing save. Test: duplicate_class_names_keep_first. - strings/constants: skip methods past MAX_METHOD_INSNS (8000) so giants cannot overflow the 64KB writer ceiling. Tests: oversized_methods_are_skipped in both passes. - merge errors now name both types being merged. Also: em-dash removed repo-wide (724 spots, rule added to todo.md), allow-lints only in tests (scratch example deleted), code comments migrated to English in every file this change touches.
This commit is contained in:
parent
2172206e83
commit
139aea9a8f
96 changed files with 1053 additions and 831 deletions
|
|
@ -1,6 +1,6 @@
|
|||
# Документация noxis
|
||||
|
||||
Обфускатор JVM/Java-байткода на Rust. Этот файл — индекс.
|
||||
Обфускатор JVM/Java-байткода на Rust. Этот файл - индекс.
|
||||
|
||||
## Что где
|
||||
|
||||
|
|
@ -10,34 +10,34 @@
|
|||
| [`techniques.md`](techniques.md) | как работает каждая техника + плюсы/минусы/цена |
|
||||
| [`stack.md`](stack.md) | почему свой IR, какой backend, `StackMapTable`, PRNG, генерация рантайма |
|
||||
| [`learnings/lessons.md`](learnings/lessons.md) | уроки из разбора EvoVis: что защита сделала плохо и чего мы не делаем |
|
||||
| [`learnings/reference.md`](learnings/reference.md) | разбор референсного обфускатора (Java/ASM) — что берём, что нет |
|
||||
| [`learnings/reference.md`](learnings/reference.md) | разбор референсного обфускатора (Java/ASM) - что берём, что нет |
|
||||
| [`learnings/priorities.md`](learnings/priorities.md) | что улучшим: стратегия и приоритеты |
|
||||
| [`learnings/ai-resistance.md`](learnings/ai-resistance.md) | новые техники обфускации и устойчивость к LLM-анализу |
|
||||
|
||||
План выполнения — [`../todo.md`](../todo.md). Исходный материал разбора —
|
||||
План выполнения - [`../todo.md`](../todo.md). Исходный материал разбора -
|
||||
[`../info/`](../info/README.md).
|
||||
|
||||
## Структурные правила проекта
|
||||
|
||||
Правила — **только про Rust-код**: `crates/*/src`, `crates/*/tests`,
|
||||
Правила - **только про Rust-код**: `crates/*/src`, `crates/*/tests`,
|
||||
`crates/*/benches`. Документация (`docs/**`, корневые `.md`) и `info/` вне
|
||||
гейта: писать подробно там, где это документация, а не ограничивать объём
|
||||
искусственно. Не учитываются также `Cargo.lock`, `.gitignore`, корень крейта
|
||||
(в нём всегда лежат `Cargo.toml` + `build.rs`).
|
||||
|
||||
1. **Не больше 250 строк кода** в одном `.rs` файле. Пустые строки и
|
||||
полно-строчные комментарии (`//`, `///`, `//!`) не считаются —
|
||||
полно-строчные комментарии (`//`, `///`, `//!`) не считаются -
|
||||
измеряется объём кода, а не документации.
|
||||
2. **Не больше 4 `.rs`-файлов** в одной директории. Подпапки не ограничены:
|
||||
`src/` может содержать `lib.rs` и три модуля-директории, каждая из которых
|
||||
в свою очередь содержит не больше четырёх файлов.
|
||||
|
||||
`tests/` и `benches/` не исключены намеренно — иначе правило
|
||||
`tests/` и `benches/` не исключены намеренно - иначе правило
|
||||
«250 строк» обходится выносом кода в тесты.
|
||||
|
||||
Правило 2 — это не косметика: файл из тысячи строк означает, что в нём
|
||||
Правило 2 - это не косметика: файл из тысячи строк означает, что в нём
|
||||
смешаны разные причины к изменению (нарушение KISS/DRY/SOLID), а команда из
|
||||
восьми файлов в одной папке — что модуль перестал быть модулем.
|
||||
восьми файлов в одной папке - что модуль перестал быть модулем.
|
||||
|
||||
### Как проверяется
|
||||
|
||||
|
|
@ -51,7 +51,7 @@ fn main() {
|
|||
|
||||
`noxis-structure-check` обходит деревья `src/`, `tests/` и `benches/` **этого
|
||||
крейта** и **падает со стопкой ошибок**, если лимит нарушен. Поэтому
|
||||
`cargo build` не соберёт нарушение — специально запускать проверку не нужно.
|
||||
`cargo build` не соберёт нарушение - специально запускать проверку не нужно.
|
||||
Деревья регистрируются через `cargo:rerun-if-changed`, чтобы гейт не
|
||||
срабатывал на правки вне проверяемых путей.
|
||||
|
||||
|
|
@ -64,9 +64,9 @@ fn main() {
|
|||
### Что делать при нарушении
|
||||
|
||||
- файл разрослся → разрезать по одной ответственности на файл:
|
||||
`mod.rs` = публичный контракт модуля, соседние файлы — внутренности;
|
||||
`mod.rs` = публичный контракт модуля, соседние файлы - внутренности;
|
||||
- папка переполнилась → завести подпапку и вынести подфункцию целиком,
|
||||
а не «чтобы влезло».
|
||||
|
||||
Каждый модуль получает `mod.rs` с `//!`-документацией — это точка входа,
|
||||
Каждый модуль получает `mod.rs` с `//!`-документацией - это точка входа,
|
||||
читающаяся вместо пяти файлов.
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@
|
|||
|
||||
## Собственный IR
|
||||
|
||||
Пассы не работают с API стороннего парсера — они работают с моделью:
|
||||
Пассы не работают с API стороннего парсера - они работают с моделью:
|
||||
|
||||
```text
|
||||
Program все классы + ресурсы архива
|
||||
|
|
@ -63,15 +63,15 @@ Program все классы + ресурсы архива
|
|||
└─ Resource путь + байты внутри архива
|
||||
```
|
||||
|
||||
Ключевое решение — **`Insn::Mark(Label)` вместо байтовых смещений**:
|
||||
Ключевое решение - **`Insn::Mark(Label)` вместо байтовых смещений**:
|
||||
|
||||
- любой пасс, меняющий порядок или длину инструкций, не пересчитывает
|
||||
jump-офсеты;
|
||||
- пересчёт делается ровно один раз — в writer'е;
|
||||
- пересчёт делается ровно один раз - в writer'е;
|
||||
- в IR нет состояния, в котором «смещение уехало, а метка осталась».
|
||||
|
||||
Атрибуты, которые мы не понимаем, хранятся как `Attribute::Raw { name, data }`
|
||||
и возвращаются на место без изменений — round-trip без потерь.
|
||||
и возвращаются на место без изменений - round-trip без потерь.
|
||||
|
||||
## Контракт пасса
|
||||
|
||||
|
|
@ -83,18 +83,18 @@ pub trait Pass: Send + Sync {
|
|||
}
|
||||
```
|
||||
|
||||
- `Context` — всё состояние прогона: конфиг, мастер-сид, маппинги, заметки.
|
||||
Из него берётся RNG: `ctx.rng("rename/classes")` — доменное разделение,
|
||||
- `Context` - всё состояние прогона: конфиг, мастер-сид, маппинги, заметки.
|
||||
Из него берётся RNG: `ctx.rng("rename/classes")` - доменное разделение,
|
||||
чтобы случайности разных пассов не были статистически коррелированы.
|
||||
- `PassReport` — что трансформировано, что сознательно пропущено и почему.
|
||||
- `Report` — собирает отчёты всех пассов + счётчики, сериализуется в JSON.
|
||||
- `PassReport` - что трансформировано, что сознательно пропущено и почему.
|
||||
- `Report` - собирает отчёты всех пассов + счётчики, сериализуется в JSON.
|
||||
|
||||
Детерминированность — свойство, а не пожелание: одинаковый seed на одинаковой
|
||||
Детерминированность - свойство, а не пожелание: одинаковый seed на одинаковой
|
||||
версии даёт байт-в-байт одинаковый выход. Это проверяется тестами.
|
||||
|
||||
## Конвейер
|
||||
|
||||
Порядок пассов — не деталь:
|
||||
Порядок пассов - не деталь:
|
||||
|
||||
```text
|
||||
Rename → Resources → Strings → Constants → Flow → Noise → DebugInfo
|
||||
|
|
@ -102,18 +102,18 @@ Rename → Resources → Strings → Constants → Flow → Noise → DebugInfo
|
|||
|
||||
почему именно так:
|
||||
|
||||
1. **Rename первым** — все остальные техники порождают новые ссылки
|
||||
1. **Rename первым** - все остальные техники порождают новые ссылки
|
||||
(`LDC`-шум, вызовы декодера, junk-классы), и они уже должны быть в
|
||||
обфусцированных именах.
|
||||
2. **Resources сразу после Rename** — патч `fabric.mod.json`, `*.mixins.json`,
|
||||
2. **Resources сразу после Rename** - патч `fabric.mod.json`, `*.mixins.json`,
|
||||
refmap и access widener должен видеть *итоговые* имена классов.
|
||||
3. **Strings и Constants** — работают по телам методов, ещё не искажённым
|
||||
3. **Strings и Constants** - работают по телам методов, ещё не искажённым
|
||||
control-flow'ем.
|
||||
4. **Flow** — искажает CFG. Если поставить его раньше, Strings/Constants
|
||||
4. **Flow** - искажает CFG. Если поставить его раньше, Strings/Constants
|
||||
перестанут находить свои шаблоны в инструкциях.
|
||||
5. **Noise** — junk-классы и аннотации добавляются в самом конце, когда
|
||||
5. **Noise** - junk-классы и аннотации добавляются в самом конце, когда
|
||||
целевой граф уже финален.
|
||||
6. **DebugInfo последним** — strip/spoof отладочной информации после всех
|
||||
6. **DebugInfo последним** - strip/spoof отладочной информации после всех
|
||||
трансформаций, чтобы не породить свежие `LineNumberTable`.
|
||||
|
||||
## Статус
|
||||
|
|
@ -126,4 +126,4 @@ Rename → Resources → Strings → Constants → Flow → Noise → DebugInfo
|
|||
| **готово** | пасс `flow` (4 схемы + ротация + мешанина, приёмка настоящим `java`) |
|
||||
| **план** | фикстуры и regression-тесты |
|
||||
|
||||
Детали выбора — [`stack.md`](stack.md), план — [`../todo.md`](../todo.md).
|
||||
Детали выбора - [`stack.md`](stack.md), план - [`../todo.md`](../todo.md).
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
|
||||
## 1. Elastic Security Labs: агент с IDA Pro против Tigress (апрель 2026)
|
||||
|
||||
[«The Cost of Understanding»](https://www.elastic.co/security-labs/llm-reversing-vs-llm-obfuscation) —
|
||||
[«The Cost of Understanding»](https://www.elastic.co/security-labs/llm-reversing-vs-llm-obfuscation) -
|
||||
Claude Opus 4.6 с доступом к IDA Pro (через MCP) реверсил crackme,
|
||||
обфусцированный [Tigress](https://tigress.wtf/), затем команда сама собрала
|
||||
("vibecode"-ила) три защиты, нацеленные именно на модель. Это ближе всего
|
||||
|
|
@ -26,32 +26,32 @@ Claude Opus 4.6 с доступом к IDA Pro (через MCP) реверсил
|
|||
indirect/regs и switch/stack).
|
||||
|
||||
- Opus решил ~40% из 20 валидных задач. Успех стоил в среднем **$2.39**,
|
||||
провал — **$4.83**. Ни одна задача уровня "многослойные комбинации" не
|
||||
провал - **$4.83**. Ни одна задача уровня "многослойные комбинации" не
|
||||
была решена.
|
||||
- **`JIT` (XTEA + рантайм-расшифровка) оказался самой сложной одиночной
|
||||
трансформацией** — модель понимала структуру движка, но не смогла
|
||||
трансформацией** - модель понимала структуру движка, но не смогла
|
||||
восстановить ключевые константы. Это ровно наш подход в `Strings`
|
||||
(ключ выводится в рантайме, не лежит в вызове), только применённый ко
|
||||
**всему коду метода**, а не к строкам.
|
||||
- На самых жёстких комбинациях цена и время выросли в ~4.5× и ~4.3×, но
|
||||
задачи, которые Opus уже умел решать, он решил всё равно. **Flatten + MBA
|
||||
било по модели сильнее, чем Virtualize + MBA** — авторы объясняют это тем,
|
||||
било по модели сильнее, чем Virtualize + MBA** - авторы объясняют это тем,
|
||||
что флаттенинг сильнее раздувает размер функции.
|
||||
|
||||
### Три кастомные защиты, нацеленные на агента
|
||||
|
||||
| защита | идея | результат |
|
||||
|---|---|---|
|
||||
| Matryoshka Wall | crackme внутри 100 000 вложенных слоёв ChaCha20 (+ подделка под Salsa20) | v1 — агент сдался не расшифровав ($1.50); v2 (каждый ключ зависит от предыдущего слоя) — исчерпал бюджет $10 за 56 минут, 0 успехов |
|
||||
| Double Fond | патч open-source крипто-библиотеки: скрытый загрузчик через изменённую lookup-таблицу, шифрованные указатели, RWX-сегмент | v7 — 1/5 у "осведомлённого" агента (12 мин, $5.20), 0/5 у "слепого" |
|
||||
| Dispatch Maze | шифр раздроблен на 20 фрагментов по 3000 функций, большинство — ловушки | v1 решена за 12 минут ($2.56); v2 (разнообразные шаблоны кода + ложные переходы) — провал агента через 46 минут и $8.83 |
|
||||
| Matryoshka Wall | crackme внутри 100 000 вложенных слоёв ChaCha20 (+ подделка под Salsa20) | v1 - агент сдался не расшифровав ($1.50); v2 (каждый ключ зависит от предыдущего слоя) - исчерпал бюджет $10 за 56 минут, 0 успехов |
|
||||
| Double Fond | патч open-source крипто-библиотеки: скрытый загрузчик через изменённую lookup-таблицу, шифрованные указатели, RWX-сегмент | v7 - 1/5 у "осведомлённого" агента (12 мин, $5.20), 0/5 у "слепого" |
|
||||
| Dispatch Maze | шифр раздроблен на 20 фрагментов по 3000 функций, большинство - ловушки | v1 решена за 12 минут ($2.56); v2 (разнообразные шаблоны кода + ложные переходы) - провал агента через 46 минут и $8.83 |
|
||||
|
||||
### Что из этого реально работает, по механизму
|
||||
|
||||
1. **Контекстное окно.** Длинные тупиковые пути (decoy-функции, ложные
|
||||
переходы) забивают контекст и портят рассуждение модели.
|
||||
2. **Бюджет шагов/денег.** Каждый впустую потраченный цикл стоит агенту
|
||||
реальных токенов; если он упирается в лимит и сдаётся — это победа
|
||||
реальных токенов; если он упирается в лимит и сдаётся - это победа
|
||||
защиты, даже если код теоретически вскрываем.
|
||||
3. **Shortcut bias.** Вводящая в заблуждение структура (ловушки в
|
||||
Dispatch Maze) заставляет модель остановиться раньше времени.
|
||||
|
|
@ -59,14 +59,14 @@ indirect/regs и switch/stack).
|
|||
|
||||
**Ключевое экономическое наблюдение авторов:** нативное исполнение занимает
|
||||
~55 мс, а статический провал агента стоит **$1.50–$9.67**. Это и есть
|
||||
реальный рычаг — не "нечитаемость навсегда", а разрыв между ценой атаки и
|
||||
реальный рычаг - не "нечитаемость навсегда", а разрыв между ценой атаки и
|
||||
ценой сборки для нас. Авторы прямо предупреждают: преимущество схлопнется,
|
||||
когда у агентов появятся инструменты динамического исполнения (сейчас
|
||||
тестировался только статический анализ).
|
||||
|
||||
## 2. Acoda: атака на safety-alignment и токенизацию (ICSE 2026)
|
||||
|
||||
[Acoda](https://arxiv.org/pdf/2606.11755) — другая постановка задачи: не
|
||||
[Acoda](https://arxiv.org/pdf/2606.11755) - другая постановка задачи: не
|
||||
агент с дизассемблером, а LLM, которой **в чат вставили исходный код** и
|
||||
попросили его проанализировать/деобфусцировать. Генетический алгоритм
|
||||
подбирает комбинацию из 8 трансформаций, до 70% ASR (attack success rate)
|
||||
|
|
@ -81,7 +81,7 @@ indirect/regs и switch/stack).
|
|||
| до анализа | Vulnerability Code Injection | фрагмент с SQL-инъекцией/`pickle.loads`/command injection в заведомо мёртвой ветке (`while (сложное_но_ложное_условие)`) |
|
||||
| во время анализа | Misleading Summarization | модель сама просится описать код, затем это описание (намеренно неверное) вклеивается рядом с кодом |
|
||||
| во время анализа | String Obfuscation | `base64` + словарь, расшифровка даёт слова типа "virus"/"weapon" |
|
||||
| во время анализа | Try-Except Wrapping | настоящая логика лежит в `except`-ветке, в `try` — правдоподобный, но бесполезный код |
|
||||
| во время анализа | Try-Except Wrapping | настоящая логика лежит в `except`-ветке, в `try` - правдоподобный, но бесполезный код |
|
||||
| после анализа | EOS Token Insertion | в текст вставлены литералы токенов-разделителей (`<|EOT|>`), из-за чего генерация обрывается раньше |
|
||||
|
||||
### Честная оценка: что из этого применимо к нам
|
||||
|
|
@ -93,24 +93,24 @@ indirect/regs и switch/stack).
|
|||
- `DebugInfo` уже вычищает всё, что могло бы стать комментарием или
|
||||
осмысленным описанием (`LineNumberTable`, `Signature`, исходные имена);
|
||||
в скомпилированном `.class` комментариев не бывает в принципе.
|
||||
- Цель Acoda — заставить модель **отказаться анализировать свой же код**,
|
||||
- Цель Acoda - заставить модель **отказаться анализировать свой же код**,
|
||||
вставленный в чат пользователем. У нас код анализирует **чужой** агент
|
||||
чужого jar — нам не нужно, чтобы он отказался, если он не наш союзник;
|
||||
и вызывать намеренно подозрительные паттерны в легитимном продукте —
|
||||
чужого jar - нам не нужно, чтобы он отказался, если он не наш союзник;
|
||||
и вызывать намеренно подозрительные паттерны в легитимном продукте -
|
||||
прямой риск получить ложное срабатывание антивируса/EDR на свой же jar.
|
||||
Это противоречит `README.md` → «Ответственное использование».
|
||||
- **EOS Token Insertion** не переносится: это свойство текстового
|
||||
стриминга токенов модели, а не байткода.
|
||||
|
||||
Единственное, что частично переносится: **String Obfuscation** и
|
||||
**Variable Renaming** — ровно то, что `Strings` и `Rename` уже делают,
|
||||
просто дешевле (у Acoda — обратимый `base64`, у нас — ротация шифров с
|
||||
**Variable Renaming** - ровно то, что `Strings` и `Rename` уже делают,
|
||||
просто дешевле (у Acoda - обратимый `base64`, у нас - ротация шифров с
|
||||
выводом ключа в рантайме, см. [`../stack.md`](../stack.md) §4-5). Здесь мы
|
||||
уже на шаг впереди статьи, а не позади.
|
||||
|
||||
**Vulnerability Code Injection** как идея (мёртвый код, похожий на уязвимый
|
||||
паттерн) пересекается с нашим `Noise`, но внедрять **реально похожие на
|
||||
эксплойт сигнатуры** в выдаваемый клиентам jar — плохая идея для легального
|
||||
эксплойт сигнатуры** в выдаваемый клиентам jar - плохая идея для легального
|
||||
продукта защиты: это именно то, от чего предостерегает
|
||||
[`info/evo-weaknesses.md`](../../info/evo-weaknesses.md) с другой стороны
|
||||
(не подставлять защищённый продукт под чужие red-листы).
|
||||
|
|
@ -118,24 +118,24 @@ indirect/regs и switch/stack).
|
|||
## 3. Что добавить в noxis практически
|
||||
|
||||
Экономический рычаг из Elastic (цена агента > цены нашей сборки) ложится на
|
||||
архитектуру без переделок, конкретные пункты — в [`../../todo.md`](../../todo.md)
|
||||
архитектуру без переделок, конкретные пункты - в [`../../todo.md`](../../todo.md)
|
||||
§7:
|
||||
|
||||
1. **Вложенные слои шифрования строк** (аналог Matryoshka Wall) — расширить
|
||||
1. **Вложенные слои шифрования строк** (аналог Matryoshka Wall) - расширить
|
||||
`StringsConfig` полем "глубина", decoder разворачивает N слоёв вместо
|
||||
одного. Цена агента растёт с глубиной почти линейно, цена сборки — нет.
|
||||
2. **Больше decoy-точек ветвления ключа** (аналог Dispatch Maze) — наш
|
||||
одного. Цена агента растёт с глубиной почти линейно, цена сборки - нет.
|
||||
2. **Больше decoy-точек ветвления ключа** (аналог Dispatch Maze) - наш
|
||||
декодер уже собирает мастер-секрет из `k1^k2` (`stack.md` §4); можно
|
||||
поднять число частей и раскидать их сборку по случайным junk-методам
|
||||
из `Noise`, а не держать в одном месте.
|
||||
3. **MBA вместо простых тождеств в `Constants`** — то же решение, что ловит
|
||||
3. **MBA вместо простых тождеств в `Constants`** - то же решение, что ловит
|
||||
и символьный исполнитель, и SMT-решатель, и, по данным Elastic,
|
||||
"Flatten + MBA" бьёт сильнее, чем просто Flatten.
|
||||
4. **Не делать:** имитацию вредоносных паттернов, prompt injection в
|
||||
строках, EOS-токены. Либо не переносится на байткод, либо создаёт риск
|
||||
ложного срабатывания защитных систем на легитимном продукте.
|
||||
|
||||
Критерий "сработало/не сработало" для всего этого — как и для остальных
|
||||
Критерий "сработало/не сработало" для всего этого - как и для остальных
|
||||
техник: прогон конкретного агента на конкретной сборке, а не вера в
|
||||
методичку. Эффект устаревает с каждой новой версией модели.
|
||||
|
||||
|
|
@ -151,17 +151,17 @@ indirect/regs и switch/stack).
|
|||
- **Для нас это ломает собственное допущение** из
|
||||
[`../stack.md`](../stack.md) §3: `noxis-verify` верифицирует метод через
|
||||
`java -Xverify:all`, потому что это *обычный* байткод с валидным
|
||||
`StackMapTable`. Виртуализированный метод — уже не обычный байткод,
|
||||
`StackMapTable`. Виртуализированный метод - уже не обычный байткод,
|
||||
верификатор проверит только обвязку интерпретатора, не тело. Big-ticket
|
||||
item, не для ближайшего спринта.
|
||||
- **MBA** укладывается без архитектурных изменений — см. выше, §3.
|
||||
- **MBA** укладывается без архитектурных изменений - см. выше, §3.
|
||||
|
||||
## Источники
|
||||
|
||||
- [Elastic Security Labs — The Cost of Understanding: LLM-Driven Reverse Engineering vs Iterative LLM Obfuscation](https://www.elastic.co/security-labs/llm-reversing-vs-llm-obfuscation)
|
||||
- [Elastic Security Labs - The Cost of Understanding: LLM-Driven Reverse Engineering vs Iterative LLM Obfuscation](https://www.elastic.co/security-labs/llm-reversing-vs-llm-obfuscation)
|
||||
- [Acoda: Adversarial Code Obfuscation for Defending against LLM-based Analysis (arXiv 2606.11755, ICSE-Companion '26)](https://arxiv.org/pdf/2606.11755)
|
||||
- [A Systematic Study on Static Control Flow Obfuscation Techniques in Java (arXiv 1809.11037)](https://arxiv.org/pdf/1809.11037)
|
||||
- [Promon Shield: Virtual Machine obfuscation SDK protection](https://promon.io/security-news/virtual-machine-obfuscation-sdk-protection)
|
||||
- [CoTDeceptor: Adversarial Code Obfuscation Against CoT-Enhanced LLM Code Agents (arXiv 2512.21250)](https://arxiv.org/pdf/2512.21250)
|
||||
- [Deconstructing Obfuscation: a four-dimensional framework for evaluating LLM deobfuscation (arXiv 2505.19887)](https://arxiv.org/pdf/2505.19887)
|
||||
- [Tigress — диверсифицирующий обфускатор для C, использованный в бенчмарке Elastic](https://tigress.wtf/)
|
||||
- [Tigress - диверсифицирующий обфускатор для C, использованный в бенчмарке Elastic](https://tigress.wtf/)
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
# Уроки: что защита EvoVis сделала плохо
|
||||
|
||||
Источник: `info/evo-weaknesses.md` — полный разбор за один проход, без
|
||||
рантайм-хаков. Здесь — что из этого следует для **нашей** архитектуры.
|
||||
Источник: `info/evo-weaknesses.md` - полный разбор за один проход, без
|
||||
рантайм-хаков. Здесь - что из этого следует для **нашей** архитектуры.
|
||||
|
||||
Правило, которое стоит держать головой: **защита ломается не в рантайме,
|
||||
а на этапе, когда атакующий смотрит на статику.** Всё, что видно в `javap -p`
|
||||
или в содержимом jar, — это утечка.
|
||||
или в содержимом jar, - это утечка.
|
||||
|
||||
## Сводная таблица: провал → наше решение
|
||||
|
||||
|
|
@ -13,21 +13,21 @@
|
|||
|---|---|---|---|---|
|
||||
| 1 | ключ лежит рядом с шифротекстом | вывод мастер-секрета в декодере, ключ из `master‖salt` | Strings | **готово** |
|
||||
| 2 | переименованы типы, но не члены | rename полей/методов + семейства virtual | Rename | **готово** |
|
||||
| 3 | отладочная информация нетронута | strip + ложный `SourceFile`/номера строк | DebugInfo | **готово** (`SourceFile`; номера строк — план) |
|
||||
| 3 | отладочная информация нетронута | strip + ложный `SourceFile`/номера строк | DebugInfo | **готово** (`SourceFile`; номера строк - план) |
|
||||
| 4 | шаблонный flattening | 4 схемы CFG + мешанина внутри метода | Flow | **готово** (4 схемы, приёмка `java`) |
|
||||
| 5 | opaque-предикаты = `A+B` | ротация тождеств + предикаты от состояния | Constants | **готово** (int/long) |
|
||||
| 6 | junk-классы вне графа reachability | `weave_reachable` — обязательная вплетённость | Noise | **готово** (без аннотаций) |
|
||||
| 6 | junk-классы вне графа reachability | `weave_reachable` - обязательная вплетённость | Noise | **готово** (без аннотаций) |
|
||||
| 7 | имена говорят «я защита» | все артефакты получают данные псевдонимы | Rename | **готово** |
|
||||
| 8 | вторичные компоненты без защиты | весь jar, а не только основной класс | Resources | **готово** |
|
||||
| 9 | шифр без MAC и без per-string salt | salt на строку, уникальный nonce, MAC | Strings | **готово** |
|
||||
|
||||
## 1. Ключ рядом с шифротекстом
|
||||
|
||||
`awc.d("<cipher>", <opaque-выражение>)` — ключ был **константным выражением
|
||||
в вызове**. Свёртка выражения — ~200 строк любого парсера. Итог: 8993 вызова
|
||||
`awc.d("<cipher>", <opaque-выражение>)` - ключ был **константным выражением
|
||||
в вызове**. Свёртка выражения - ~200 строк любого парсера. Итог: 8993 вызова
|
||||
расшифрованы детерминированно, **0 обращений к JVM**.
|
||||
|
||||
Хуже: один шифр на весь продукт (LCG с фиксированными `a/c`) — зная одну
|
||||
Хуже: один шифр на весь продукт (LCG с фиксированными `a/c`) - зная одну
|
||||
пару «шифротекст → текст», остальные раскрываются по обучению.
|
||||
|
||||
**Что делаем иначе.**
|
||||
|
|
@ -41,56 +41,56 @@
|
|||
- MAC: подмена байта не «тихо» портит текст.
|
||||
|
||||
Остаточный риск честно признан: целевой анализ декодера раскрывает текст.
|
||||
Задача — поднять цену, а не сделать невозможным.
|
||||
Задача - поднять цену, а не сделать невозможным.
|
||||
|
||||
## 2. Члены не переименованы
|
||||
|
||||
`onDraw`, `getProjectiles`, `isAuthorized()` — открытые имена. Класс `wb`
|
||||
`onDraw`, `getProjectiles`, `isAuthorized()` - открытые имена. Класс `wb`
|
||||
опознаётся за секунду по трём понятным методам.
|
||||
|
||||
**Что делаем иначе:** переименование полей и методов входит в базовый
|
||||
уровень, а не в «продвинутый». Уже готово, включая:
|
||||
|
||||
- **семейства virtual** — переименование на всю иерархию, чтобы не
|
||||
- **семейства virtual** - переименование на всю иерархию, чтобы не
|
||||
разъединить переопределения и не сломать `invokevirtual`;
|
||||
- **keep-список** — private/static/final можно сразу, `rename_public_members`
|
||||
- **keep-список** - private/static/final можно сразу, `rename_public_members`
|
||||
по умолчанию выключен для библиотек, чтобы не сломать API;
|
||||
- **автодетект рефлексии по имени** — `Class.forName`, `getMethod`,
|
||||
- **автодетект рефлексии по имени** - `Class.forName`, `getMethod`,
|
||||
`getField` с литералами попадают в keep автоматически.
|
||||
|
||||
## 3. Отладочная информация
|
||||
|
||||
`SourceFile` оригинальные у 160 классов, `LineNumberTable` — 10 218,
|
||||
`LocalVariableTable` — имена локальных переменных, `@rn(name, description)` —
|
||||
`SourceFile` оригинальные у 160 классов, `LineNumberTable` - 10 218,
|
||||
`LocalVariableTable` - имена локальных переменных, `@rn(name, description)` -
|
||||
каталог всех модулей с описаниями.
|
||||
|
||||
**Что делаем иначе:** пасс `DebugInfo` — strip `SourceFile`,
|
||||
**Что делаем иначе:** пасс `DebugInfo` - strip `SourceFile`,
|
||||
`LineNumberTable`, `LocalVariableTable`, `Signature`, `InnerClasses` +
|
||||
**ложная** отладочная информация (чужие имена и номера строк, чтобы
|
||||
деобфускатор выдавал правдоподобный, но чужой код).
|
||||
|
||||
Отсюда обязательное требование: **маппинги в отчёте** — иначе мы сами не
|
||||
Отсюда обязательное требование: **маппинги в отчёте** - иначе мы сами не
|
||||
читаем свои stack trace'и.
|
||||
|
||||
## 4. Шаблонный flattening
|
||||
|
||||
Один regex нашёл 3530 автоматов: `while(true) { switch(i + i2) }`, метки —
|
||||
константы, переходы — присваивания в одном basic block, тела блоков не
|
||||
Один regex нашёл 3530 автоматов: `while(true) { switch(i + i2) }`, метки -
|
||||
константы, переходы - присваивания в одном basic block, тела блоков не
|
||||
тронуты.
|
||||
|
||||
**Что делаем иначе:** не одна схема, а **четыре** (`state-spread`,
|
||||
`computed-switch`, `butterfly`, `exception-goto`), с ротацией между методами
|
||||
и с перемешиванием внутри метода. Плюс `butterfly` и `exception-goto`
|
||||
ломают «все переходы — присваивания».
|
||||
ломают «все переходы - присваивания».
|
||||
|
||||
Это единственная техника с **высоким риском**: любая ошибка в CFG =
|
||||
`VerifyError`. Поэтому блокером Flow объявлен не «напишем потом», а
|
||||
`StackMapTable` — см. [`../stack.md`](../stack.md) §3. Без верификатора
|
||||
`StackMapTable` - см. [`../stack.md`](../stack.md) §3. Без верификатора
|
||||
Flow не включается.
|
||||
|
||||
## 5. Opaque-предикаты
|
||||
|
||||
`(A^B)+((A&B)<<1)` — школьное тождество, узнаётся регуляркой и даёт
|
||||
`(A^B)+((A&B)<<1)` - школьное тождество, узнаётся регуляркой и даёт
|
||||
бесплатный фолдинг остальных констант. Настоящих предикатов (зависящих
|
||||
от состояния и всегда истинных) нет.
|
||||
|
||||
|
|
@ -103,26 +103,26 @@ Flow не включается.
|
|||
42 файла, ни одного входящего ссылока, `dangling=0` → BFS от 4
|
||||
entrypoint'ов, которые открыто лежат в `fabric.mod.json`.
|
||||
|
||||
**Что делаем иначе:** `weave_reachable` — junk попадает в граф
|
||||
**Что делаем иначе:** `weave_reachable` - junk попадает в граф
|
||||
достижимости, создаёт ложные рёбра и участвует в вызовах. Это свойство
|
||||
пасса, а не настройка: без вплетённости junk не включается вовсе.
|
||||
|
||||
## 7. Самоописание защиты
|
||||
|
||||
`StringRuntime`, `KeyAssembler`, `AttestClient`, `RuntimeGuard`,
|
||||
`SealedBoot`, `NativeGuard` — названия указывают атакующему, куда смотреть.
|
||||
`SealedBoot`, `NativeGuard` - названия указывают атакующему, куда смотреть.
|
||||
|
||||
**Что делаем иначе:** псевдонимы генерируются одним `NameGenerator` для
|
||||
всех сущностей — классы, пакеты, поля, методы, junk и даже имена атрибутов
|
||||
всех сущностей - классы, пакеты, поля, методы, junk и даже имена атрибутов
|
||||
и схем выглядят как обычные данные. Отдельного «защитного» стиля не
|
||||
существует, поэтому отличить его от остального кода не по чему.
|
||||
|
||||
## 8. Вторичные компоненты
|
||||
|
||||
`evo-agent.jar` вообще без обфускации, .NET-лаунчер — статическая XOR-таблица.
|
||||
`evo-agent.jar` вообще без обфускации, .NET-лаунчер - статическая XOR-таблица.
|
||||
|
||||
**Что делаем иначе:** обфускация применяется ко **всему архиву**, включая
|
||||
ресурсы (`fabric.mod.json`, mixins, refmap, access widener) — см. пасс
|
||||
ресурсы (`fabric.mod.json`, mixins, refmap, access widener) - см. пасс
|
||||
`Resources`. Отдельного «ядро защищено, остальное нет» не бывает.
|
||||
|
||||
## 9. Шифр без аутентификации
|
||||
|
|
@ -131,16 +131,16 @@ entrypoint'ов, которые открыто лежат в `fabric.mod.json`.
|
|||
работает.
|
||||
|
||||
**Уже сделано:** `salt` на строку, уникальный `nonce`, FNV-1a MAC в
|
||||
`noxis-runtime::scheme`. `cipher:tag` лежат рядом — порча обнаруживается.
|
||||
`noxis-runtime::scheme`. `cipher:tag` лежат рядом - порча обнаруживается.
|
||||
|
||||
## Что подтвердилось и не нужно повторять в выводах
|
||||
|
||||
Разбор дал и **опровержения** — важно не строить защиту на мифах:
|
||||
Разбор дал и **опровержения** - важно не строить защиту на мифах:
|
||||
|
||||
- «сдвиги в байткоде бывают отрицательными» — по 1277 классам **0** случаев
|
||||
- «сдвиги в байткоде бывают отрицательными» - по 1277 классам **0** случаев
|
||||
(32 559 сдвигов, константные всегда `1`);
|
||||
- `awc.g(...)` (session-gate) в билде **не вызывался ни разу**.
|
||||
|
||||
Вывод: проверять гипотезу на статистике **до** того, как вкладываться
|
||||
в технику против неё. Отсюда требование к `noxis-verify` — не «красиво
|
||||
в технику против неё. Отсюда требование к `noxis-verify` - не «красиво
|
||||
обфусцировано», а измеримые метрики ([`priorities.md`](priorities.md)).
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
# Что улучшим: стратегия и приоритеты
|
||||
|
||||
Материал собран из `info/evo-improvements.md` (план после разбора EvoVis) и
|
||||
`info/evo-weaknesses.md` (провалы). Здесь — что мы берём, в каком порядке
|
||||
`info/evo-weaknesses.md` (провалы). Здесь - что мы берём, в каком порядке
|
||||
и **что осознанно не делаем**.
|
||||
|
||||
## Стратегия в одном абзаце
|
||||
|
||||
Максимум защиты при минимуме риска сломать рантайм. Дешёвые техники,
|
||||
дающие 80% результата, включаются по умолчанию; дорогие и опасные —
|
||||
дающие 80% результата, включаются по умолчанию; дорогие и опасные -
|
||||
включаются **только** после того, как для них есть верификатор. Каждая
|
||||
техника имеет измеримый критерий «сработала/не сработала», иначе это
|
||||
не защита, а мнение.
|
||||
|
||||
## P0 — базовый уровень (дёшево, снимает главные утечки)
|
||||
## P0 - базовый уровень (дёшево, снимает главные утечки)
|
||||
|
||||
| пункт | что это | пасс | статус |
|
||||
|---|---|---|---|
|
||||
|
|
@ -20,63 +20,63 @@
|
|||
| A2 | вычистить `LineNumberTable`/`LocalVariableTable`/`SourceFile`/`InnerClasses`/`Signature`/`MethodParameters` | DebugInfo | ✅ готово |
|
||||
| A3 | никаких сигнатурных имён у защитного кода, защитный код смешан с обычным | Rename + Noise | ✅ готово |
|
||||
| A4 | обфусцировать **весь** архив, включая ресурсы, mixin-JSON, access widener, service loader'ы | Resources | ✅ готово |
|
||||
| — | маппинг в отчёт (иначе не читаем свои же stack trace) | Rename | ✅ готово |
|
||||
| - | маппинг в отчёт (иначе не читаем свои же stack trace) | Rename | ✅ готово |
|
||||
|
||||
Это самый дешёвый блок и одновременно **самый большой утечка** в разобранном
|
||||
образце: оригинальные `SourceFile` у 160 классов, имена полей и методов,
|
||||
каталог модулей в атрибутах.
|
||||
|
||||
## P1 — строки и константы
|
||||
## P1 - строки и константы
|
||||
|
||||
| пункт | что это | пасс | статус |
|
||||
|---|---|---|---|
|
||||
| B1 | ключ на строку + ротация 3–5 схем + MAC + уникальный nonce | Strings | ✅ готово |
|
||||
| B2 | **вывод** ключа в рантайме, а не константа в вызове | Strings + генератор декодера | ✅ готово |
|
||||
| B3 | константы — не тождества, а вычисления, разнесённые по блокам | Constants | ✅ готово (int/long) |
|
||||
| B3 | константы - не тождества, а вычисления, разнесённые по блокам | Constants | ✅ готово (int/long) |
|
||||
| B4 | **ложная** отладочная информация: чужие имена переменных и строк | DebugInfo | ✅ `SourceFile` |
|
||||
|
||||
B2 — главное отличие от разобранного образца: у него ключ был константным
|
||||
B2 - главное отличие от разобранного образца: у него ключ был константным
|
||||
выражением в вызове и расшифровывался статически, без JVM. Мастер-секрет
|
||||
теперь собирается в рантайме из двух частей (`k1 ^ k2`) декодером класса —
|
||||
теперь собирается в рантайме из двух частей (`k1 ^ k2`) декодером класса -
|
||||
см. [`../stack.md`](../stack.md) §4. B4 пока покрывает только `SourceFile`;
|
||||
ложные номера строк (`fake_line_numbers`) — см. [`../../todo.md`](../../todo.md) §3.6.
|
||||
ложные номера строк (`fake_line_numbers`) - см. [`../../todo.md`](../../todo.md) §3.6.
|
||||
|
||||
## P2 — контрольный поток
|
||||
## P2 - контрольный поток
|
||||
|
||||
| пункт | что это | статус |
|
||||
|---|---|---|
|
||||
| C1 | **несколько схем** флаттенинга вместо одной, с мешаниной внутри метода | план |
|
||||
| C2 | верификатор обязателен: ошибка CFG = `VerifyError` | ✅ готово (кадры + `java -Xverify:all`) |
|
||||
| C3 | реальный объём ≠ раздувание: бюджет на размер и время | ✅ размер; время — план |
|
||||
| C3 | реальный объём ≠ раздувание: бюджет на размер и время | ✅ размер; время - план |
|
||||
|
||||
Приоритет здесь строго подчинён: **C2 раньше C1**. Кадры `StackMapTable`
|
||||
и приёмка `java -Xverify:all` уже готовы (см. [`../stack.md`](../stack.md)
|
||||
§3) — единственное, чего не хватает, это сами схемы флаттенинга (C1).
|
||||
§3) - единственное, чего не хватает, это сами схемы флаттенинга (C1).
|
||||
|
||||
## P3 — измеримость (метрики, без которых ничего не доказано)
|
||||
## P3 - измеримость (метрики, без которых ничего не доказано)
|
||||
|
||||
Обязательный блок, иначе фраза «обфускация есть» ничего не означает.
|
||||
Крейт `noxis-verify` реализован — вот что из списка уже цифра, а что ещё мнение:
|
||||
Крейт `noxis-verify` реализован - вот что из списка уже цифра, а что ещё мнение:
|
||||
|
||||
1. **Утечки имён.** ✅ `collect_names`/`find_leaks` — сверка идентификаторов
|
||||
1. **Утечки имён.** ✅ `collect_names`/`find_leaks` - сверка идентификаторов
|
||||
с учётом keep-списка.
|
||||
2. **Читаемость декомпилята.** ⬜ Прогон JADX/CFR → доля распознанных
|
||||
структур — не начато.
|
||||
структур - не начато.
|
||||
3. **Противник.** ✅ `scan_secrets` + `RED_PATTERNS` держим в репозитории;
|
||||
⬜ полноценный скрипт-деобфускатор (свёртка констант) как тест — план.
|
||||
4. **Семантика.** ⬜ Эталонные программы до/после — не начато
|
||||
⬜ полноценный скрипт-деобфускатор (свёртка констант) как тест - план.
|
||||
4. **Семантика.** ⬜ Эталонные программы до/после - не начато
|
||||
(сейчас семантику подтверждает только `java -Xverify:all`).
|
||||
5. **Бюджеты.** ✅ размер (`inflation_ratio`/`check_inflation`);
|
||||
⬜ время исполнения — не начато.
|
||||
6. **Стабильность между сборками.** ✅ `mappings_changed` — маппинги
|
||||
⬜ время исполнения - не начато.
|
||||
6. **Стабильность между сборками.** ✅ `mappings_changed` - маппинги
|
||||
обязаны отличаться между прогонами с разным seed.
|
||||
|
||||
Итог: `noxis-verify` закрывает красные паттерны, утечки имён,
|
||||
`java -Xverify:all`, бюджет размера и стабильность маппингов. Остаются
|
||||
декомпилят-метрика, бюджет времени и эталонная семантика — три пункта,
|
||||
декомпилят-метрика, бюджет времени и эталонная семантика - три пункта,
|
||||
где «нам кажется защищённым» всё ещё мнение, а не цифра.
|
||||
|
||||
## P4 — долгий горизонт (пока не делаем)
|
||||
## P4 - долгий горизонт (пока не делаем)
|
||||
|
||||
| пункт | что это | почему не сразу |
|
||||
|---|---|---|
|
||||
|
|
@ -86,35 +86,35 @@ B2 — главное отличие от разобранного образц
|
|||
| D4 | честный anti-tamper, а не «зашейрь ключ» | целая инженерная задача, не пасс |
|
||||
| E | собственный нативный/Rust-цель вместо JVM | другой продукт, не наша цель |
|
||||
|
||||
D1 — явно ограничен: включать только для действительно ценных функций,
|
||||
остальное оправдания не имеет. E — отдельный продукт; если он когда-нибудь
|
||||
D1 - явно ограничен: включать только для действительно ценных функций,
|
||||
остальное оправдания не имеет. E - отдельный продукт; если он когда-нибудь
|
||||
понадобится, наш IR как раз делает его возможным (те же пассы, другой
|
||||
writer).
|
||||
|
||||
## Что мы добавили сверх исходного плана
|
||||
|
||||
1. **Структурный гейт при компиляции.** Лимиты «250 строк / 4 файла»
|
||||
гоняются в `build.rs` — см. [`../README.md`](../README.md). Без этого
|
||||
гоняются в `build.rs` - см. [`../README.md`](../README.md). Без этого
|
||||
план P0–P3 превращается в один гигантский файл через месяц.
|
||||
2. **Собственный IR с метками.** Исходный план предполагал работу поверх
|
||||
готового парсера. `Insn::Mark(Label)` убирает пересчёт смещений из
|
||||
всех пассов — это меняет цену каждой техники, меняющей CFG.
|
||||
всех пассов - это меняет цену каждой техники, меняющей CFG.
|
||||
3. **Детерминированный PRNG с доменным разделением.** Без этого
|
||||
пункт «стабильность между сборками» невозможно проверить тестом.
|
||||
4. **Keep как отдельный крейт (`noxis-analysis`), а не флаг CLI.**
|
||||
Рефлексия, JNI, сериализация и entrypoint'ы — это анализ иерархии,
|
||||
Рефлексия, JNI, сериализация и entrypoint'ы - это анализ иерархии,
|
||||
а не настройка, которую надо помнить руками.
|
||||
|
||||
## Порядок выполнения
|
||||
|
||||
Смотрите [`../../todo.md`](../../todo.md). P0–P3 и P3-измеримость уже
|
||||
готовы; единственный крупный пункт, который остался, — схемы `Flow` (C1),
|
||||
готовы; единственный крупный пункт, который остался, - схемы `Flow` (C1),
|
||||
потому что верификатор (C2) для них уже есть:
|
||||
|
||||
```text
|
||||
✅ P0 DebugInfo + Resources → главные утечки сняты
|
||||
✅ P1 Strings (B2) + Constants → ключ выводится, схемы ротируются
|
||||
✅ P1 backend (ristretto) + frames
|
||||
✅ P2 Flow (C1) — готов (09.10.2026): 4 схемы + ротация + мешанина, приёмка настоящим `java`
|
||||
✅ P2 Flow (C1) - готов (09.10.2026): 4 схемы + ротация + мешанина, приёмка настоящим `java`
|
||||
✅ P3 метрики, красные паттерны, CLI
|
||||
```
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@
|
|||
| `DebugInfoStripper` | 69 | strip отладки | пасс `debug` |
|
||||
| `StringFilter` | 35 | что считать строкой | функция внутри `strings` |
|
||||
|
||||
**Вывод:** структура совпадает с нашей почти один в один — это подтверждает
|
||||
**Вывод:** структура совпадает с нашей почти один в один - это подтверждает
|
||||
раскладку крейтов. Ключевое отличие: у них всё живёт в одном jar'е и
|
||||
одном проекте; у нас конвейер отделён от I/O и от CLI.
|
||||
|
||||
|
|
@ -47,11 +47,11 @@
|
|||
|
||||
У них: remap → strings → numbers → flow → junk → debug. Тот же инвариант,
|
||||
что и у нас: **переименование раньше всех техник, порождающих новые ссылки**,
|
||||
а `debug` — последним.
|
||||
а `debug` - последним.
|
||||
|
||||
### 3.2 Наследование как условие безопасности
|
||||
|
||||
`InheritanceAnalyzer` — самый большой после remap'ера модуль. Совпадает с
|
||||
`InheritanceAnalyzer` - самый большой после remap'ера модуль. Совпадает с
|
||||
нашим выводом: переименование virtual-методов **без** иерархии ломает
|
||||
`invokevirtual`. У нас это `hierarchy` + семейства в `rename`.
|
||||
|
||||
|
|
@ -63,13 +63,13 @@
|
|||
|
||||
### 3.4 Подпись срезается
|
||||
|
||||
`*.SF/*.RSA/*.DSA/*.EC` удаляются при перепаковке — после записи подпись
|
||||
`*.SF/*.RSA/*.DSA/*.EC` удаляются при перепаковке - после записи подпись
|
||||
всё равно невалидна, а её наличие заставляет JVM падать. Это входит в
|
||||
`noxis-jar`.
|
||||
|
||||
### 3.5 Маппинг в файл + ложные ограничения
|
||||
|
||||
`--mappings` пишет соответствие «новое → старое» — без него автор сам не
|
||||
`--mappings` пишет соответствие «новое → старое» - без него автор сам не
|
||||
читает stack trace. Плюс честные off-ключи: `--no-strings`, `--no-flow`,
|
||||
`--no-junk`, `--keep-lines`, `--keep-source`, `--keep-vars`, `--no-widen`.
|
||||
|
||||
|
|
@ -78,7 +78,7 @@
|
|||
|
||||
### 3.6 Seed
|
||||
|
||||
`--seed` — детерминированный прогон. У нас это базовое свойство
|
||||
`--seed` - детерминированный прогон. У нас это базовое свойство
|
||||
`noxis-core`, без отдельного флага.
|
||||
|
||||
## 4. Что не берём
|
||||
|
|
@ -86,24 +86,24 @@
|
|||
### 4.1 Java + ASM
|
||||
|
||||
Главное отличие стека. Пара Java+ASM даёт родную JVM, готовые visitor'ы и
|
||||
`CheckClassAdapter` — но и:
|
||||
`CheckClassAdapter` - но и:
|
||||
|
||||
- нужно раскатывать JDK для сборки и запуска тестов;
|
||||
- наш проект — Rust (`deny(unsafe_code)`, свои лимиты, свои гейты);
|
||||
- наш проект - Rust (`deny(unsafe_code)`, свои лимиты, свои гейты);
|
||||
- отсутствует возможность отдавать один бинарь.
|
||||
|
||||
Цена, которую мы платим: `StackMapTable` придётся считать самим (через
|
||||
inference-верификатор — [`../stack.md`](../stack.md) §3). Это осознанно.
|
||||
inference-верификатор - [`../stack.md`](../stack.md) §3). Это осознанно.
|
||||
|
||||
### 4.2 Swing GUI
|
||||
|
||||
Лишняя точка входа: GUI дублирует CLI и удваивает площадь тестирования.
|
||||
У нас — только clap.
|
||||
У нас - только clap.
|
||||
|
||||
### 4.3 `StringDecoderGenerator` как «генератор текста»
|
||||
|
||||
Они генерируют Java-исходник декодера и компилируют. Для нас это означало бы
|
||||
зависимость от `javac` на этапе обфускации. Наш путь — генерировать
|
||||
зависимость от `javac` на этапе обфускации. Наш путь - генерировать
|
||||
**байткод напрямую** через наш IR (`noxis-runtime` держит схемы, генератор
|
||||
собирает `ClassFile`).
|
||||
|
||||
|
|
@ -120,14 +120,14 @@ inference-верификатор — [`../stack.md`](../stack.md) §3). Это
|
|||
|
||||
## 5. Что проверяем на их smoke-тестах
|
||||
|
||||
В `target/` референса лежат готовые проверочные кейсы — их стоит
|
||||
В `target/` референса лежат готовые проверочные кейсы - их стоит
|
||||
переиспользовать как наш regression-набор:
|
||||
|
||||
| кейс | что ломает |
|
||||
|---|---|
|
||||
| `smoke-safe` | базовый round-trip, `Sample`/`MyMixin` |
|
||||
| `smoke-inheritance` | `Base`/`Child`/`InheritedChild`/`ExternalChild` — семейства virtual и мосты |
|
||||
| `smoke-flow` | `LoadAll` — загрузка всех классов, ловит `VerifyError` |
|
||||
| `smoke-inheritance` | `Base`/`Child`/`InheritedChild`/`ExternalChild` - семейства virtual и мосты |
|
||||
| `smoke-flow` | `LoadAll` - загрузка всех классов, ловит `VerifyError` |
|
||||
| `smoke-fields` | переименование полей и статических полей |
|
||||
| `smoke-aes` | шифрование строк, `RunStrings` сверяет расшифровку |
|
||||
| `Probe.java` | точечная проверка одной гипотезы |
|
||||
|
|
@ -144,7 +144,7 @@ inference-верификатор — [`../stack.md`](../stack.md) §3). Это
|
|||
| keep-сканер | генератор Java-исходника |
|
||||
| срез подписей, патч ресурсов | единственная схема шифра |
|
||||
| маппинг в отчёт, честные off-ключи | эмпирическая верификация flow |
|
||||
| их smoke-кейсы как regression | — |
|
||||
| их smoke-кейсы как regression | - |
|
||||
|
||||
Полный перечень провалов защиты, которую они **не** закрывают, — в
|
||||
Полный перечень провалов защиты, которую они **не** закрывают, - в
|
||||
[`lessons.md`](lessons.md).
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Стек: почему именно так
|
||||
|
||||
Документ фиксирует решения, которые трудно отменить. Для каждого —
|
||||
Документ фиксирует решения, которые трудно отменить. Для каждого -
|
||||
альтернативы, критерии, цена и что меняет решение.
|
||||
|
||||
## 1. Собственный IR, а не API чужой библиотеки
|
||||
|
|
@ -10,12 +10,12 @@ Code, Insn}`, а не с типами парсера.
|
|||
|
||||
**Почему не «просто использовать готовую библиотеку целиком»:**
|
||||
|
||||
- библиотека диктует API — каждый пасс привязывается к чужим миграциям
|
||||
- библиотека диктует API - каждый пасс привязывается к чужим миграциям
|
||||
мажорной версии (наш лимит «≤4 файлов» легко разрывается «транзитивно»);
|
||||
- у нас есть конкретное требование, которого нет ни в одном парсере:
|
||||
`Insn::Mark(Label)` вместо смещений;
|
||||
- мы не знаём заранее, захочет ли пользователь backend'а ещё одну
|
||||
библиотеку (например, для `StackMapTable`) — изоляция через IR даёт
|
||||
библиотеку (например, для `StackMapTable`) - изоляция через IR даёт
|
||||
смену backend'а без переписывания пассов.
|
||||
|
||||
**Что IR даёт на практике:**
|
||||
|
|
@ -25,26 +25,26 @@ Code, Insn}`, а не с типами парсера.
|
|||
| метки вместо смещений | пассы никогда не пересчитывают jump-офсеты |
|
||||
| `Attribute::Raw` | round-trip без потерь: неизвестный атрибут возвращается как есть |
|
||||
| `Program` включает ресурсы архива | переименование и патч ресурсов в одном прогоне |
|
||||
| одна точка записи | пересчёт смещений, `max_stack` и `StackMapTable` — ровно один раз |
|
||||
| одна точка записи | пересчёт смещений, `max_stack` и `StackMapTable` - ровно один раз |
|
||||
|
||||
**Цена:** конвертация `model ↔ backend` на границах (чтение и запись) —
|
||||
**Цена:** конвертация `model ↔ backend` на границах (чтение и запись) -
|
||||
примерно один файл на направление. Дёшево.
|
||||
|
||||
## 2. Backend для чтения/записи — `ristretto_classfile`
|
||||
## 2. Backend для чтения/записи - `ristretto_classfile`
|
||||
|
||||
**Решение:** `format::parse` / `format::write` реализуются поверх
|
||||
`ristretto_classfile 0.34`, наш IR остаётся единим API для пассов.
|
||||
Реализация — этап P1, до него обе функции возвращают `Unimplemented`.
|
||||
Реализация - этап P1, до него обе функции возвращают `Unimplemented`.
|
||||
|
||||
### Кандидаты
|
||||
|
||||
| крейт | чтение | запись | верификация | активность | загрузки | MSRV |
|
||||
|---|---|---|---|---|---|---|
|
||||
| **`ristretto_classfile` 0.34** | ✓ | ✓ | ✓ (JVMS §4.10.2) | 2026-09 | 41k | **1.97.1** |
|
||||
| `jclassfile` | ✓ | — (parser) | — | 2026-08 | 28k | — |
|
||||
| `classfile-parser` | ✓ | — (parser) | — | 2024-10 (устарел) | 31k | — |
|
||||
| `java-bytecode` | ✓ | ✓ | ? | 2026-10 | 41 | — |
|
||||
| `jvm-assembler` | — | — | — | **2015, мёртв** | 3k | — |
|
||||
| `jclassfile` | ✓ | - (parser) | - | 2026-08 | 28k | - |
|
||||
| `classfile-parser` | ✓ | - (parser) | - | 2024-10 (устарел) | 31k | - |
|
||||
| `java-bytecode` | ✓ | ✓ | ? | 2026-10 | 41 | - |
|
||||
| `jvm-assembler` | - | - | - | **2015, мёртв** | 3k | - |
|
||||
|
||||
### Критерии и что подтвердил spike
|
||||
|
||||
|
|
@ -56,14 +56,14 @@ MSRV ≤ 1.97.1 (наш тулчейн), лицензия `Apache-2.0 OR MIT` (
|
|||
|
||||
### Что отвергнуто
|
||||
|
||||
- **Писать парсер с нуля** — спецификация JVMS §4 по формату классов
|
||||
плюс MUTF-8, constant pool, модули, records, переполнения — это месяцы
|
||||
- **Писать парсер с нуля** - спецификация JVMS §4 по формату классов
|
||||
плюс MUTF-8, constant pool, модули, records, переполнения - это месяцы
|
||||
работы ради того, что уже написано и покрыто тестами. Это нарушает KISS
|
||||
и не добавляет ни одной защиты.
|
||||
- **`classfile-parser`, `jclassfile`** — только чтение, значит writer всё
|
||||
- **`classfile-parser`, `jclassfile`** - только чтение, значит writer всё
|
||||
равно наш; `classfile-parser` не обновлялся с 2024-10.
|
||||
- **`java-bytecode`** — 41 загрузка: у проекта нет инерции и нет экосистемы.
|
||||
- **`jvm-assembler`** — последний коммит в 2015.
|
||||
- **`java-bytecode`** - 41 загрузка: у проекта нет инерции и нет экосистемы.
|
||||
- **`jvm-assembler`** - последний коммит в 2015.
|
||||
|
||||
## 3. `StackMapTable`: генерируем сами, пользуемся верификатором
|
||||
|
||||
|
|
@ -80,36 +80,36 @@ MSRV ≤ 1.97.1 (наш тулчейн), лицензия `Apache-2.0 OR MIT` (
|
|||
→ StackMapTable → запись
|
||||
```
|
||||
|
||||
`ristretto_classfile` содержит всё нужное **в исходном виде** — но
|
||||
`ristretto_classfile` содержит всё нужное **в исходном виде** - но
|
||||
снаружи, к сожалению, отдаёт не всё (см. результат спайка ниже):
|
||||
|
||||
- `verifiers/bytecode/inference.rs` — итеративный dataflow, LUB, фикспоинт;
|
||||
- `verifiers/bytecode/control_flow.rs` — `compute_successors`, worklist;
|
||||
- `verifiers/bytecode/stackmap.rs` — декодер/кэш `StackMapTable`;
|
||||
- `attributes/{max_stack,max_locals,instruction/stack_effect}.rs` —
|
||||
- `verifiers/bytecode/inference.rs` - итеративный dataflow, LUB, фикспоинт;
|
||||
- `verifiers/bytecode/control_flow.rs` - `compute_successors`, worklist;
|
||||
- `verifiers/bytecode/stackmap.rs` - декодер/кэш `StackMapTable`;
|
||||
- `attributes/{max_stack,max_locals,instruction/stack_effect}.rs` -
|
||||
пересчёт `max_stack`/`max_locals` из инструкций;
|
||||
- `attributes/{stack_frame,verification_type}.rs` — типы для записи.
|
||||
- `attributes/{stack_frame,verification_type}.rs` - типы для записи.
|
||||
|
||||
**Почему это лучше, чем свои кадры:** это ровно та часть JVMS, в которой
|
||||
ошибка невидима в исходниках и всплывает только на `VerifyError` в чужом
|
||||
рантайме. Писать такой верификатор — самая дорогая и самая опасная часть
|
||||
рантайме. Писать такой верификатор - самая дорогая и самая опасная часть
|
||||
всего проекта.
|
||||
|
||||
**Альтернативы, которые отвергнуты:**
|
||||
|
||||
- **Понижать версию класса до 49 и выкинуть `StackMapTable`** — ломает
|
||||
- **Понижать версию класса до 49 и выкинуть `StackMapTable`** - ломает
|
||||
`invokedynamic`/лямбды и включает дорогой fallback-верификатор; на Java 9+
|
||||
это заметно и противоречит цели.
|
||||
- **Не делать Flow вообще** — лишает технику, которая даёт ★★★★ к
|
||||
- **Не делать Flow вообще** - лишает технику, которая даёт ★★★★ к
|
||||
читаемости. Не вариант: вычислим кадры.
|
||||
|
||||
**Результат spike'а (`tests/spike_frames.rs`) — главный вывод раздела:**
|
||||
**Результат spike'а (`tests/spike_frames.rs`) - главный вывод раздела:**
|
||||
|
||||
| что проверяли | что получили |
|
||||
|---|---|
|
||||
| `verify_class` + `FallbackStrategy::AlwaysInference` на классе с ветвлением и без `StackMapTable` | ✅ `success`, `path_used == Inference` — движок вывода типов работает |
|
||||
| `FallbackStrategy::Strict` на том же классе | ✅ `Err("StackMapTable required ... with control flow")` — требование v50+ реально проверяется |
|
||||
| `StackMapTable` с кадром **не на том** сдвиге | ✅ `Err("Merge point at offset N requires StackMapTable frame")` — ошибка локализована точно |
|
||||
| `verify_class` + `FallbackStrategy::AlwaysInference` на классе с ветвлением и без `StackMapTable` | ✅ `success`, `path_used == Inference` - движок вывода типов работает |
|
||||
| `FallbackStrategy::Strict` на том же классе | ✅ `Err("StackMapTable required ... with control flow")` - требование v50+ реально проверяется |
|
||||
| `StackMapTable` с кадром **не на том** сдвиге | ✅ `Err("Merge point at offset N requires StackMapTable frame")` - ошибка локализована точно |
|
||||
| получить сам кадр из крейта | ❌ **нельзя** |
|
||||
|
||||
Причина последней строки: в `ristretto_classfile/src/verifiers/bytecode.rs`
|
||||
|
|
@ -118,7 +118,7 @@ MSRV ≤ 1.97.1 (наш тулчейн), лицензия `Apache-2.0 OR MIT` (
|
|||
`verify_method`, `VerificationResult`, `VerifierConfig` и
|
||||
`VerificationContext`. Диагностика в `VerificationResult.diagnostic`
|
||||
заполняется лишь на отдельных путях отказа, а в строгом режиме крейт
|
||||
возвращает `Err(VerifyError)` целиком — то есть **ожидаемый кадр до нас
|
||||
возвращает `Err(VerifyError)` целиком - то есть **ожидаемый кадр до нас
|
||||
не доходит**, только его смещение.
|
||||
|
||||
**Что это меняет для P1.** Кадры считаем сами, а крейт используем как
|
||||
|
|
@ -132,7 +132,7 @@ MSRV ≤ 1.97.1 (наш тулчейн), лицензия `Apache-2.0 OR MIT` (
|
|||
|
||||
Три способа получить сами кадры, в порядке предпочтения:
|
||||
|
||||
1. ~~**Вендор `verifiers::bytecode::inference`.**~~ — **отвергнут после
|
||||
1. ~~**Вендор `verifiers::bytecode::inference`.**~~ - **отвергнут после
|
||||
оценки объёма.** Один модуль не оторвать: в `inference.rs` 14 ссылок
|
||||
`crate::`, включая private-модули `frame`, `type_system`, `handlers`,
|
||||
`control_flow`, `diagnostics`. Вендорить пришлось бы весь
|
||||
|
|
@ -140,19 +140,19 @@ MSRV ≤ 1.97.1 (наш тулчейн), лицензия `Apache-2.0 OR MIT` (
|
|||
`src/`**, 2.3 MB) ради одной патчи `pub(crate)` → `pub`. Чужой код
|
||||
становится третьей по объёму частью репозитория, отстаёт от апстрима и
|
||||
не проходит наши правила (250 строк / 4 файла).
|
||||
2. **Свой вывод на своём IR — принято.** Кадры считаются по `noxis::model`,
|
||||
2. **Свой вывод на своём IR - принято.** Кадры считаются по `noxis::model`,
|
||||
а не по ристретто: `format::parse` → IR → пассы → `compute_frames(IR)` →
|
||||
`Attribute::StackMapTable` → `format::write`. Ристретто остаётся
|
||||
**парсером, писателем и приёмочным тестом** из crates.io, без форка.
|
||||
3. ~~Итеративная починка через `diagnostic`~~ — **отвергнута спайком**: поля
|
||||
3. ~~Итеративная починка через `diagnostic`~~ - **отвергнута спайком**: поля
|
||||
`expected`/`pre_frame` в этом пути недостижимы.
|
||||
|
||||
**Почему 2, а не 1.** Наш dataflow живёт на типах, которые и так нужны
|
||||
пассам: `Insn`, метки вместо смещений, иерархия классов из
|
||||
`noxis-analysis::hierarchy` для LUB на merge-точках. Кадры — это ровно
|
||||
`noxis-analysis::hierarchy` для LUB на merge-точках. Кадры - это ровно
|
||||
те же инструкции, что уже переписывает `rename::rewrite`, поэтому
|
||||
промежуточная конвертация `model → Instruction[] → model` (требование
|
||||
вендора) добавляет ещё один источник расхождений. Цена — своя реализация
|
||||
вендора) добавляет ещё один источник расхождений. Цена - своя реализация
|
||||
(~1 000–1 500 строк: границы блоков, worklist, merge, LUB, эмиссия
|
||||
`StackFrame`), но она покрывается двумя независимыми судьями:
|
||||
|
||||
|
|
@ -164,7 +164,7 @@ MSRV ≤ 1.97.1 (наш тулчейн), лицензия `Apache-2.0 OR MIT` (
|
|||
java -Xverify:all jar
|
||||
```
|
||||
|
||||
Второй судья — ристретто `verify_class` со стратегией `Strict` — ловит
|
||||
Второй судья - ристретто `verify_class` со стратегией `Strict` - ловит
|
||||
ровно тот класс ошибок, ради которого вендор и рассматривался: неверный
|
||||
сдвиг кадра, пропущенная merge-точка, неверный тип.
|
||||
|
||||
|
|
@ -173,12 +173,12 @@ MSRV ≤ 1.97.1 (наш тулчейн), лицензия `Apache-2.0 OR MIT` (
|
|||
|
||||
## 4. Генерация рантайма, а не вшитая таблица
|
||||
|
||||
**Решение:** `noxis-runtime` — это **заготовки Rust-кода**, из которых
|
||||
**Решение:** `noxis-runtime` - это **заготовки Rust-кода**, из которых
|
||||
собирается класс-декодер (P2), а не готовый бинарь.
|
||||
|
||||
**Почему:** ABI декодера — `d(Ljava/lang/String;I)Ljava/lang/String;`,
|
||||
**Почему:** ABI декодера - `d(Ljava/lang/String;I)Ljava/lang/String;`,
|
||||
но схем шифрования несколько (`xor-splitmix`, `xtea-ctr`) и их число
|
||||
растёт. Константная таблица селекторов рядом с текстом — ровно та ошибка,
|
||||
растёт. Константная таблица селекторов рядом с текстом - ровно та ошибка,
|
||||
которую мы разбирали (`info/evo-weaknesses.md` §1): **ключ рядом с
|
||||
шифротекстом**. Генератор выбирает схемы на этапе сборки артефакта и
|
||||
кодирует выбор в самих вызовах, а мастер-секрет выводится в рантайме.
|
||||
|
|
@ -193,7 +193,7 @@ MSRV ≤ 1.97.1 (наш тулчейн), лицензия `Apache-2.0 OR MIT` (
|
|||
байт-в-байт одинаковый jar. Это свойство, а не удобство: без него
|
||||
воспроизводимая сборка, откат и сравнение версий невозможны.
|
||||
- **Доменное разделение:** `ctx.rng("rename/classes")`,
|
||||
`ctx.rng("noise/junk")` — сид деривается из имени домена, поэтому
|
||||
`ctx.rng("noise/junk")` - сид деривается из имени домена, поэтому
|
||||
случайности разных пассов не статистически коррелированы.
|
||||
- **Зависимость `rand` тянет** OS-энтропию и меняющееся поведение между
|
||||
версиями. Нам нужен ровно обратный эффект.
|
||||
|
|
@ -201,17 +201,17 @@ MSRV ≤ 1.97.1 (наш тулчейн), лицензия `Apache-2.0 OR MIT` (
|
|||
## 6. ZIP: `zip`
|
||||
|
||||
Критерии: чтение, запись, **сохранение порядка и сжатия**, контроль
|
||||
дубликатов. Берём `zip` (стабильный релиз, без `time`/`aes`/`zstd` —
|
||||
дубликатов. Берём `zip` (стабильный релиз, без `time`/`aes`/`zstd` -
|
||||
лишние фичи только раздувают бинарь; `default-features = false,
|
||||
features = ["deflate"]`). Подпись (`*.SF/*.RSA/*.DSA/*.EC`) вырезаем на
|
||||
уровне записей архива — после перепаковки она всё равно невалидна.
|
||||
уровне записей архива - после перепаковки она всё равно невалидна.
|
||||
|
||||
Альтернатива `zip6` — форк того же кода; при равном API он даёт меньше
|
||||
Альтернатива `zip6` - форк того же кода; при равном API он даёт меньше
|
||||
тестов и меньшую инерцию. Нет причины платить за это.
|
||||
|
||||
**MSRV воркспейса поднят до 1.97** — это ровно наш тулчейн (rustc 1.97.1)
|
||||
**MSRV воркспейса поднят до 1.97** - это ровно наш тулчейн (rustc 1.97.1)
|
||||
и минимум `ristretto_classfile 0.34`. Заявлять 1.85, когда зависимость
|
||||
требует 1.97, — враньё в `Cargo.toml`.
|
||||
требует 1.97, - враньё в `Cargo.toml`.
|
||||
|
||||
## 7. CLI: `clap` (derive)
|
||||
|
||||
|
|
@ -221,11 +221,11 @@ features = ["deflate"]`). Подпись (`*.SF/*.RSA/*.DSA/*.EC`) выреза
|
|||
|
||||
## 8. Верификация: `java -Xverify:all` первым делом
|
||||
|
||||
Единственная честная проверка «я ничего не сломал» — запустить настоящую
|
||||
Единственная честная проверка «я ничего не сломал» - запустить настоящую
|
||||
JVM. `noxis-verify` гоняет `java -Xverify:all` на обфусцированном jar
|
||||
и по red-паттернам ищет дыры в защите.
|
||||
|
||||
**Запасной вариант, если JDK не гарантирован:** `ristretto_vm` —
|
||||
**Запасной вариант, если JDK не гарантирован:** `ristretto_vm` -
|
||||
встраиваемый JVM на Rust из того же репозитория. Для нас это означает
|
||||
`noxis-verify` без внешних зависимостей. Стоит проверить в P3, пока
|
||||
используем системный `java` (в окружении есть OpenJDK 21).
|
||||
|
|
|
|||
|
|
@ -1,43 +1,43 @@
|
|||
# Как работает: техники, плюсы, минусы
|
||||
|
||||
Каждая техника описана одинаково: **как работает → что даёт → что стоит /
|
||||
где ломается**. Цена указана честно: обфускация — это всегда обмен
|
||||
где ломается**. Цена указана честно: обфускация - это всегда обмен
|
||||
читаемости и времени против аналитика.
|
||||
|
||||
Итоговая таблица — в конце.
|
||||
Итоговая таблица - в конце.
|
||||
|
||||
## 1. Переименование (`Rename`)
|
||||
|
||||
**Как работает.** Классы получают случайные имена в случайных пакетах
|
||||
(или в исходном пакете, если `spread_packages = false`). Поля и методы
|
||||
переименовываются, если `method_rename_safety` считает их безопасными
|
||||
(private/static/final — сразу; virtual — только «семейством» на всю
|
||||
(private/static/final - сразу; virtual - только «семейством» на всю
|
||||
иерархию, чтобы не разъединить переопределения и мостовые методы).
|
||||
Затем переписываются дескрипторы, `LDC`-классы, вызовы, поля,
|
||||
исключительная таблица, `Signature`/`Exceptions`/`InnerClasses`.
|
||||
|
||||
**Плюсы.**
|
||||
- Самая дешёвая и самая отдающая техника: читает `javap -p` любой анализатор.
|
||||
- Не меняет семантику — риск сломать код минимален.
|
||||
- Не меняет семантику - риск сломать код минимален.
|
||||
- Маппиинг пишется в отчёт → stack trace'и переводятся обратно.
|
||||
|
||||
**Минусы / цена.**
|
||||
- Keep-список обязателен: рефлексия по имени (`Class.forName("pkg.C")`),
|
||||
JNI, `serialVersionUID`, сериализация, entrypoint'ы. Пропустил — получил
|
||||
JNI, `serialVersionUID`, сериализация, entrypoint'ы. Пропустил - получил
|
||||
`ClassNotFoundException` в рантайме.
|
||||
- Рефлексия через строки мы ловим автоматически (анализ `Class.forName`,
|
||||
`getMethod`/`getField` и т.п.), но динамическую конкатенацию — нет.
|
||||
`getMethod`/`getField` и т.п.), но динамическую конкатенацию - нет.
|
||||
- JSON-(де)сериализация по именам полей (Gson/Jackson/`Codec`) не ловится:
|
||||
биндинг идёт через `X.class`-литерал, не строку — см. `todo.md` §2.
|
||||
биндинг идёт через `X.class`-литерал, не строку - см. `todo.md` §2.
|
||||
- Для библиотек `rename_public_members = false` по умолчанию: менять
|
||||
публичный API библиотеки — сломать всех потребителей.
|
||||
публичный API библиотеки - сломать всех потребителей.
|
||||
|
||||
Шесть стилей имён (`ShortLatin`, `IlO0`, `Unicode`, `MixedLatin`,
|
||||
`DecoyWords`, `Plausible`) — компромисс по-разному: `IlO0`/`Unicode` явно
|
||||
`DecoyWords`, `Plausible`) - компромисс по-разному: `IlO0`/`Unicode` явно
|
||||
сигнализируют «это обфускация», `Plausible` маскируется под обычный
|
||||
прикладной код (`TextureCache`, `resolveToken`), ценой того, что при
|
||||
ручном чтении он менее заметно отличается от настоящей логики, чем
|
||||
намеренный мусор — скрыть обфускацию и скрыть её цену ценой чуть большей
|
||||
намеренный мусор - скрыть обфускацию и скрыть её цену ценой чуть большей
|
||||
путаницы для собственной команды при отладке маппингов.
|
||||
|
||||
**Статус: готово.**
|
||||
|
|
@ -51,13 +51,13 @@
|
|||
уникален на строку, MAC ловит порчу.
|
||||
|
||||
**Плюсы.**
|
||||
- Строки — главный источник логики (`"admin"`, URL, SQL, regex).
|
||||
- Строки - главный источник логики (`"admin"`, URL, SQL, regex).
|
||||
- Ротация схем: один скрипт деобфускации не покрывает весь jar.
|
||||
- MAC: подмена шифротекста не «молча» портит текст, а падает.
|
||||
|
||||
**Минусы / цена.**
|
||||
- Декодер добавляет вызов на каждый доступ к строке → оверхед в рантайме.
|
||||
- Целевой анализ декодера всё равно раскроет текст; задача — поднять цену,
|
||||
- Целевой анализ декодера всё равно раскроет текст; задача - поднять цену,
|
||||
а не сделать невозможным.
|
||||
- Мастер-секрет собирается в декодере из двух частей (`k1 ^ k2`).
|
||||
|
||||
|
|
@ -68,7 +68,7 @@
|
|||
|
||||
**Как работает.** Числовая константа не хранится как есть, а собирается из
|
||||
разных выражений: `x ^ k`, `a + b`, `(a << 1) + c`. Плюс настоящие
|
||||
opaque-предикаты — условия, истинность которых зависит от инварианта
|
||||
opaque-предикаты - условия, истинность которых зависит от инварианта
|
||||
(например, `i*(i+1) % 2 == 0`), а не от константы.
|
||||
|
||||
**Плюсы.**
|
||||
|
|
@ -80,7 +80,7 @@ opaque-предикаты — условия, истинность которы
|
|||
(`executor` для `LCONST/ICONST`-цепочек).
|
||||
- Развдувание кода растёт по квадрату от числа вставок.
|
||||
- Шаблонный opaque-предикат (всегда один и тот же `A+B`) узнаётся
|
||||
паттерном — отсюда требование нескольких схем.
|
||||
паттерном - отсюда требование нескольких схем.
|
||||
|
||||
**Статус: готово** для `int`/`long` (`xor`/`add`/`shift` + opaque-предикаты);
|
||||
`float`/`double` не трогаются.
|
||||
|
|
@ -98,22 +98,22 @@ opaque-предикаты — условия, истинность которы
|
|||
|
||||
**Минусы / цена.**
|
||||
- **Самая дорогая и самая опасная техника.** Требует пересчёта
|
||||
`StackMapTable` для классов версии ≥ 50 — иначе JVM не грузит класс.
|
||||
`StackMapTable` для классов версии ≥ 50 - иначе JVM не грузит класс.
|
||||
- Ломает профилировщики, coverage-инструменты и отладку.
|
||||
- Нужен верификатор: любая ошибка в CFG — это `VerifyError` в рантайме.
|
||||
- Нужен верификатор: любая ошибка в CFG - это `VerifyError` в рантайме.
|
||||
- Шаблонная схема (все методы перестроены одинаково) детектируется
|
||||
гистограммой длины блоков.
|
||||
|
||||
**Статус: готов** (`noxis-passes/src/flow/`). Одна схема на метод (ротация
|
||||
через PRNG) плюс нарезка тела `goto`-сплайсами во всех схемах; кадр
|
||||
`StackMapTable` пересчитывается после каждой перестройки. Приёмка —
|
||||
`StackMapTable` пересчитывается после каждой перестройки. Приёмка -
|
||||
настоящий `java -Xverify:all` и исполнение под JVM (`calc` печатает 45
|
||||
после всех схем). Ограничения: `<init>` пропускается (форма
|
||||
super-call-first); методы с `jsr`/`ret`, короче 3 инструкций и длиннее
|
||||
`max_insns` пропускаются; `encrypt_block_bodies` добавляет opaque-guard
|
||||
на входе. Два правила, выученных на крови (см. `StackMapTable`):
|
||||
вставленная метка не должна делить байт-смещение с чужой (иначе кадры
|
||||
мерджатся из несовместимых состояний — после каждой такой метки `nop`),
|
||||
мерджатся из несовместимых состояний - после каждой такой метки `nop`),
|
||||
а инструкция после терминальной обязана иметь свой кадр (поэтому dead-блоки
|
||||
входят через opaque-ветвь и кончаются `athrow`, а не висят недостижимыми).
|
||||
|
||||
|
|
@ -129,32 +129,32 @@ super-call-first); методы с `jsr`/`ret`, короче 3 инструкц
|
|||
|
||||
**Минусы / цена.**
|
||||
- Reachability-анализ отсеивает junk мгновенно: класс, на который никто
|
||||
не ссылается, — мёртвый груз. Отсюда обязательное `weave_reachable`.
|
||||
- Пустые методы/тела — признак, который ищут первым.
|
||||
не ссылается, - мёртвый груз. Отсюда обязательное `weave_reachable`.
|
||||
- Пустые методы/тела - признак, который ищут первым.
|
||||
- Раздувание jar.
|
||||
|
||||
**Статус: готово** (junk-классы + `weave_reachable`; junk-аннотации
|
||||
пропускаются — сырые атрибуты не несут свежие индексы пула).
|
||||
пропускаются - сырые атрибуты не несут свежие индексы пула).
|
||||
|
||||
## 6. Отладочная информация (`DebugInfo`)
|
||||
|
||||
**Как работает.** Strip `LineNumberTable`, `LocalVariableTable`; подмена
|
||||
`SourceFile` на ложное имя; опционально strip `Signature`/`InnerClasses`.
|
||||
Плюс *ложная* отладочная информация — чужие имена переменных и номера строк.
|
||||
Плюс *ложная* отладочная информация - чужие имена переменных и номера строк.
|
||||
|
||||
**Плюсы.**
|
||||
- Практически бесплатно.
|
||||
- Восстанавливает исходные имена переменных и файлов — прямая утечка.
|
||||
- Восстанавливает исходные имена переменных и файлов - прямая утечка.
|
||||
|
||||
**Минусы / цена.**
|
||||
- Убивает нормальные stack trace'и (поэтому маппинги в отчёте обязательны).
|
||||
- Слишком «чистый» класс (`LineNumberTable` есть, а `SourceFile` = `X.java`,
|
||||
которого нет) — тоже признак.
|
||||
которого нет) - тоже признак.
|
||||
|
||||
**Статус: готово** (strip + `spoof_source_file`, детерминированная подмена
|
||||
от мастер-сида). Не реализовано: `fake_line_numbers` (ложные номера строк,
|
||||
а не только `SourceFile`) и `strip_source_file` (полное удаление вместо
|
||||
подмены) — см. [`todo.md`](../todo.md) §3.6.
|
||||
подмены) - см. [`todo.md`](../todo.md) §3.6.
|
||||
|
||||
## 7. Ресурсы (`Resources`)
|
||||
|
||||
|
|
@ -164,7 +164,7 @@ super-call-first); методы с `jsr`/`ret`, короче 3 инструкц
|
|||
|
||||
**Плюсы.**
|
||||
- Без этого обфускация Fabric-мода просто не стартует.
|
||||
- Убранные подписи — иначе jar считается подделанным.
|
||||
- Убранные подписи - иначе jar считается подделанным.
|
||||
|
||||
**Минусы / цена.**
|
||||
- Патч только текстовых ресурсов: бинарные (картинки, классы в `versions/`)
|
||||
|
|
@ -189,6 +189,6 @@ super-call-first); методы с `jsr`/`ret`, короче 3 инструкц
|
|||
| Flow | ★★★★ | **высокий** | высокая | план, кадры готовы |
|
||||
|
||||
Вывод, который стоит держать в голове: **Rename + Resources + DebugInfo**
|
||||
дают большую часть защиты за небольшую цену. Flow — единственная техника,
|
||||
дают большую часть защиты за небольшую цену. Flow - единственная техника,
|
||||
которая может сломать рантайм, и она же единственная, чья отдача
|
||||
не покрывает риск без верификатора.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue