From c3b8d4dd628132efdc1ac88dc3fc4c99cfd1f1a3 Mon Sep 17 00:00:00 2001 From: loki5512344 Date: Thu, 8 Oct 2026 22:34:00 +0200 Subject: [PATCH] docs: sync status with code, add AI-resistance research, restyle README MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Documentation had drifted from the actual implementation: the noxis (CLI) and noxis-verify crates were fully built and tested but every doc called them "not started". Verified against the code (build + test run) and corrected status across README.md, todo.md, architecture.md, techniques.md and learnings/{lessons,priorities}.md. Also: - restyled README.md (badges, no em dashes, usage examples, dropped the redundant "Принципы" section already covered elsewhere) - added docs/learnings/ai-resistance.md: a sourced writeup of what actually works against LLM-driven reverse engineering (Elastic Security Labs' Opus 4.6 vs Tigress study, the Acoda paper) and what of it applies to noxis, plus concrete todo.md §7 items - noxis-structure-check: the 250-line/4-file gate was scanning docs/**, which fought against writing documentation freely; it now only scans crates/*/{src,tests,benches} and only counts .rs files --- README.md | 134 +++++++++-------- crates/noxis-structure-check/src/lib.rs | 12 ++ crates/noxis-structure-check/src/rules.rs | 62 ++++---- docs/README.md | 22 +-- docs/architecture.md | 8 +- docs/learnings/ai-resistance.md | 167 ++++++++++++++++++++++ docs/learnings/lessons.md | 12 +- docs/learnings/priorities.md | 78 +++++----- docs/techniques.md | 16 ++- 9 files changed, 349 insertions(+), 162 deletions(-) create mode 100644 docs/learnings/ai-resistance.md diff --git a/README.md b/README.md index 76e15e0..4d592b6 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,61 @@ +
+ # noxis Обфускатор JVM/Java-байткода на Rust. -Читает JAR, переписывает классовые файлы и ресурсы, собирает обратно — -детерминированно, с маппингами для stack trace и с проверкой, что выход -действительно загружается настоящей JVM. +![Rust](https://img.shields.io/badge/Rust-2024_edition-orange?style=flat-square&logo=rust&logoColor=white) +![MSRV](https://img.shields.io/badge/MSRV-1.97-informational?style=flat-square) +![License](https://img.shields.io/badge/license-AGPL--3.0-blue?style=flat-square&logo=gnu&logoColor=white) +![Status](https://img.shields.io/badge/status-в_разработке-yellow?style=flat-square) + +
+ +--- + +Читает JAR, переписывает классовые файлы и ресурсы, собирает обратно: детерминированно, +с маппингами для stack trace и с проверкой, что выход действительно загружается настоящей JVM. ## Статус -В работе. Ядро построено, часть техник реализована, часть — по плану. +Ядро построено, большая часть техник реализована. | блок | состояние | |---|---| | IR классовых файлов, метки вместо смещений | ✅ | | конфиг, детерминированный PRNG, маппинги, отчёт | ✅ | | анализ: иерархия, entrypoint'ы, keep-правила | ✅ | -| переименование всех символов | ✅ | -| схемы шифрования строк (salt, nonce, MAC) | ✅ | -| JAR: чтение/запись, ресурсы, срез подписей | ✅ | -| гейт структурных правил при компиляции | ✅ | -| пассы: rename, resources, debug | ✅ | -| spike backend'а `ristretto_classfile` | ✅ | -| `format::parse`/`write` + кадры `StackMapTable` | ✅ | -| пассы: strings, constants, noise (+ декодер v49) | ✅ | -| пасс flow | 🚧 | -| верификация и метрики | 🚧 | -| CLI | 🚧 | +| `format::parse`/`write` + кадры `StackMapTable` (backend `ristretto_classfile`) | ✅ | +| пассы: rename, resources, strings, constants, noise, debug | ✅ | +| верификация: red-паттерны, `java -Xverify:all`, метрики | ✅ | +| CLI (`obfuscate` / `analyze` / `patterns`) | ✅ | +| пасс flow (кадры готовы, схем флаттенинга пока нет) | 🚧 | -Подробности — [`docs/architecture.md`](docs/architecture.md), -план — [`todo.md`](todo.md). +Подробности в [`docs/architecture.md`](docs/architecture.md), план в [`todo.md`](todo.md). + +## Быстрый старт + +```sh +./scripts/check.sh # fmt + clippy -D warnings + тесты + +# обфускация jar: конвейер, маппинги, JSON-отчёт +cargo run -p noxis -- obfuscate input.jar -o output.jar --seed 42 + +# что внутри jar: классы, entrypoint'ы, keep-правила (без изменений) +cargo run -p noxis -- analyze input.jar + +# красный список паттернов: показать или прогнать по jar'у +cargo run -p noxis -- patterns --list +cargo run -p noxis -- patterns --input output.jar +``` + +`obfuscate` по умолчанию включает все готовые пассы. Флаги `--no-rename`, +`--no-strings`, `--no-constants`, `--no-noise`, `--no-resources`, `--no-debug-info` +выключают их по отдельности. `--no-flow` честно падает с ошибкой: пасса пока нет. + +Одно `cargo build` уже гоняет проверку структуры кода: `.rs`-файл длиннее +250 строк или директория с больше чем 4 `.rs`-файлами ломают сборку. +Документация (`docs/**`) под это правило не попадает. Правила в [`docs/README.md`](docs/README.md). ## Структура @@ -36,75 +63,56 @@ noxis/ ├── Cargo.toml workspace, lints, release-профиль ├── scripts/check.sh structure → fmt → clippy → test -├── docs/ ← архитектура, техники, стек, уроки +├── docs/ архитектура, техники, стек, уроки ├── info/ исходный материал разбора (вне правил) └── crates/ - ├── noxis-classfile/ IR + парсер/запись + StackMapTable - ├── noxis-core/ конфиг, seed, маппинги, контракт Pass - ├── noxis-analysis/ иерархия, entrypoint'ы, keep - ├── noxis-passes/ сами трансформации + конвейер - ├── noxis-runtime/ схемы шифрования, ABI декодера - ├── noxis-jar/ ZIP, ресурсы, срез подписей - ├── noxis-verify/ red-паттерны, java -Xverify (план) - ├── noxis/ CLI (план) + ├── noxis-classfile/ IR + парсер/запись + StackMapTable + ├── noxis-core/ конфиг, seed, маппинги, контракт Pass + ├── noxis-analysis/ иерархия, entrypoint'ы, keep + ├── noxis-passes/ сами трансформации + конвейер + ├── noxis-runtime/ схемы шифрования, ABI декодера + ├── noxis-jar/ ZIP, ресурсы, срез подписей + ├── noxis-verify/ red-паттерны, java -Xverify, метрики + ├── noxis/ CLI: obfuscate, analyze, patterns └── noxis-structure-check/ автогейт структуры ``` -## Быстрый старт - -```sh -./scripts/check.sh # fmt + clippy -D warnings + тесты -``` - -Одно `cargo build` уже гоняет проверку структуры проекта: если файл -длиннее 250 строк или в директории больше 4 файлов, сборка падает. -Правила — в [`docs/README.md`](docs/README.md). - ## Документация | документ | что внутри | |---|---| | [`docs/README.md`](docs/README.md) | индекс и структурные правила проекта | | [`docs/architecture.md`](docs/architecture.md) | слои, крейты, IR, конвейер | -| [`docs/techniques.md`](docs/techniques.md) | как работает каждая техника: плюсы и минусы | +| [`docs/techniques.md`](docs/techniques.md) | как работает каждая техника, плюсы и минусы | | [`docs/stack.md`](docs/stack.md) | почему свой IR, backend, `StackMapTable`, PRNG | | [`docs/learnings/lessons.md`](docs/learnings/lessons.md) | уроки разбора: чего делать нельзя | | [`docs/learnings/reference.md`](docs/learnings/reference.md) | разбор референсного обфускатора | | [`docs/learnings/priorities.md`](docs/learnings/priorities.md) | что улучшаем и в каком порядке | +| [`docs/learnings/ai-resistance.md`](docs/learnings/ai-resistance.md) | новые техники (MBA, виртуализация) и устойчивость к LLM-анализу | -Исходный материал разбора — [`info/README.md`](info/README.md). - -## Принципы - -- **KISS / DRY / SOLID.** Файл — одна ответственность. Не больше 250 строк, - не больше 4 файлов в директории. -- **Детерминированность.** Один seed → байт-в-байт одинаковый артефакт. -- **Ничего не ломаем.** Техника, которая может дать `VerifyError`, - включается только после верификатора. -- **Измеримо.** Защита без метрик — это мнение, а не свойство. +Исходный материал разбора в [`info/README.md`](info/README.md). ## Лицензия -AGPL-3.0-only — см. [`LICENSE`](LICENSE). +AGPL-3.0-only, см. [`LICENSE`](LICENSE). -Проект остаётся полностью открытым: его можно изучать, использовать -(в том числе коммерчески) и изменять. Условие AGPL — любые производные -работы, которые распространяются или доступны пользователям по сети, -обязаны публиковаться под той же лицензией. Это защищает `noxis` от -закрытых форков, которые перепродают его как готовый продукт. +Проект остаётся полностью открытым: его можно изучать, использовать (в том числе +коммерчески) и изменять. Условие AGPL: любые производные работы, которые +распространяются или доступны пользователям по сети, обязаны публиковаться под +той же лицензией. Это защищает `noxis` от закрытых форков, которые перепродают +его как готовый продукт. -Сторонние зависимости и их лицензии — в [`NOTICE.md`](NOTICE.md). +Сторонние зависимости и их лицензии в [`NOTICE.md`](NOTICE.md). ## Ответственное использование -`noxis` — инструмент двойного назначения. Он одинаково полезен для -защиты собственного байткода и способен усложнять анализ вредоносного -ПО. Лицензия это не регламентирует, но это норма сообщества проекта: +`noxis` это инструмент двойного назначения, одинаково полезный для защиты +собственного байткода и способен усложнять анализ вредоносного ПО. Лицензия +это не регламентирует, но это норма сообщества проекта: -- используйте обфускацию, чтобы защищать **свой** код и код тех, кто - вас об этом попросил; -- не применяйте инструмент для упаковки вредоносов, обхода средств - защиты и антивирусов или иной деятельности, запрещённой законом. +- используйте обфускацию, чтобы защищать **свой** код и код тех, кто вас об этом попросил; +- не применяйте инструмент для упаковки вредоносов, обхода средств защиты + и антивирусов или иной деятельности, запрещённой законом. -Нарушение этих норм не влечёт юридических последствий по лицензии, но -приведёт к отказу в поддержке и закрытию issue/PR в репозитории. +Нарушение этих норм не влечёт юридических последствий по лицензии, но приведёт +к отказу в поддержке и закрытию issue/PR в репозитории. diff --git a/crates/noxis-structure-check/src/lib.rs b/crates/noxis-structure-check/src/lib.rs index 347afdc..7cde776 100644 --- a/crates/noxis-structure-check/src/lib.rs +++ b/crates/noxis-structure-check/src/lib.rs @@ -76,4 +76,16 @@ mod tests { let found = violations(&tmp.0); assert!(found.is_empty(), "{found:?}"); } + + #[test] + fn ignores_markdown_size_and_count() { + // Правила — только про Rust-код: `.md` не лимитирован ни по + // строкам, ни в счётчике файлов директории. + let tmp = TempDir::new("md"); + for index in 0..super::MAX_DIR_FILES + 3 { + fs::write(tmp.0.join(format!("doc{index}.md")), "line\n".repeat(300)).expect("write"); + } + let found = violations(&tmp.0); + assert!(found.is_empty(), "{found:?}"); + } } diff --git a/crates/noxis-structure-check/src/rules.rs b/crates/noxis-structure-check/src/rules.rs index 8cf1ada..d6b9b9a 100644 --- a/crates/noxis-structure-check/src/rules.rs +++ b/crates/noxis-structure-check/src/rules.rs @@ -1,31 +1,32 @@ //! Project structure rules, invoked from every crate's `build.rs`. //! -//! Rules: -//! * every `.rs` and `.md` file is at most [`MAX_FILE_LINES`] lines long; -//! * every directory inside a crate `src/` or the workspace `docs/` holds at -//! most [`MAX_DIR_FILES`] files — subdirectories are unlimited. +//! Rules apply to Rust code only, not to documentation: +//! * every `.rs` file is at most [`MAX_FILE_LINES`] lines long; +//! * every directory inside a crate's `src/`, `tests/` or `benches/` holds +//! at most [`MAX_DIR_FILES`] files — subdirectories are unlimited. //! -//! Deliberately excluded: `Cargo.lock`, `.gitignore`, the legacy `info/` tree, -//! and crate roots (they always own `Cargo.toml` plus `build.rs`). +//! Deliberately excluded: `Cargo.lock`, `.gitignore`, `docs/`, the legacy +//! `info/` tree, and crate roots (they always own `Cargo.toml` plus +//! `build.rs`). use std::ffi::OsStr; use std::fs; use std::path::{Path, PathBuf}; -/// Maximum number of lines in a source (`rs`) or documentation (`md`) file. +/// Maximum number of lines in a Rust source file. pub const MAX_FILE_LINES: usize = 250; /// Maximum number of files per directory; subdirectories are not counted. pub const MAX_DIR_FILES: usize = 4; /// Extensions subject to [`MAX_FILE_LINES`]. -const LINE_LIMITED_EXTS: [&str; 2] = ["rs", "md"]; +const LINE_LIMITED_EXTS: [&str; 1] = ["rs"]; /// Directory names the scan never descends into. const SKIPPED_DIRS: [&str; 2] = ["target", ".git"]; -/// Scans the calling crate's `src/`/`tests/`/`benches/` plus the workspace -/// `docs/` tree and panics with a report on the first violation. +/// Scans the calling crate's `src/`/`tests/`/`benches/` and panics with a +/// report on the first violation. /// /// # Panics /// @@ -35,13 +36,13 @@ pub fn check() { let mut violations = Vec::new(); // `src/` обязателен; `tests/` и `benches/` проверяются, если созданы — - // иначе правило «250 строк» обходится выносом кода в тесты. - let mut roots = vec![manifest.join("src"), manifest.join("tests")]; - roots.push(manifest.join("benches")); - - if let Some(root) = workspace_root(&manifest) { - roots.push(root.join("docs")); - } + // иначе правило «250 строк» обходится выносом кода в тесты. `docs/` вне + // гейта: правила — только про Rust-код. + let roots = [ + manifest.join("src"), + manifest.join("tests"), + manifest.join("benches"), + ]; for root in &roots { watch(root); @@ -64,8 +65,9 @@ pub fn check() { /// Re-runs the build script whenever a checked tree changes. /// -/// Without this cargo only invalidates `build.rs` on crate-local changes, so -/// editing `docs/` would never be re-validated. +/// Without an explicit `rerun-if-changed`, cargo falls back to its default +/// (rerun on any change anywhere in the package); naming the trees keeps the +/// gate from firing on unrelated edits. fn watch(root: &Path) { if root.is_dir() { println!("cargo:rerun-if-changed={}", root.display()); @@ -101,6 +103,10 @@ pub fn scan(root: &Path, violations: &mut Vec) { subdirs.push(path); continue; } + let ext = path.extension().and_then(OsStr::to_str).unwrap_or_default(); + if !LINE_LIMITED_EXTS.contains(&ext) { + continue; + } files += 1; check_line_limit(&path, violations); } @@ -118,10 +124,6 @@ pub fn scan(root: &Path, violations: &mut Vec) { } fn check_line_limit(path: &Path, violations: &mut Vec) { - let ext = path.extension().and_then(OsStr::to_str).unwrap_or_default(); - if !LINE_LIMITED_EXTS.contains(&ext) { - return; - } let lines = count_lines(path); if lines > MAX_FILE_LINES { violations.push(format!( @@ -149,17 +151,3 @@ fn manifest_dir() -> PathBuf { None => panic!("CARGO_MANIFEST_DIR is unset; run this check from a build script"), } } - -/// Walks up from `start` until a manifest declaring `[workspace]` is found. -fn workspace_root(start: &Path) -> Option { - let mut current = Some(start); - while let Some(dir) = current { - if let Ok(text) = fs::read_to_string(dir.join("Cargo.toml")) - && text.lines().any(|line| line.trim() == "[workspace]") - { - return Some(dir.to_path_buf()); - } - current = dir.parent(); - } - None -} diff --git a/docs/README.md b/docs/README.md index 7236664..6f18984 100644 --- a/docs/README.md +++ b/docs/README.md @@ -12,22 +12,24 @@ | [`learnings/lessons.md`](learnings/lessons.md) | уроки из разбора EvoVis: что защита сделала плохо и чего мы не делаем | | [`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). Исходный материал разбора — [`../info/`](../info/README.md). ## Структурные правила проекта -Правила действуют на **весь наш код и документацию**: `crates/**` и `docs/**`. -Не учитываются: `Cargo.lock`, `.gitignore`, `info/`, корень крейта +Правила — **только про Rust-код**: `crates/*/src`, `crates/*/tests`, +`crates/*/benches`. Документация (`docs/**`, корневые `.md`) и `info/` вне +гейта: писать подробно там, где это документация, а не ограничивать объём +искусственно. Не учитываются также `Cargo.lock`, `.gitignore`, корень крейта (в нём всегда лежат `Cargo.toml` + `build.rs`). -1. **Не больше 250 строк** в одном `.rs` или `.md` файле. -2. **Не больше 4 файлов** в одной директории. Подпапки не ограничены: +1. **Не больше 250 строк** в одном `.rs` файле. +2. **Не больше 4 `.rs`-файлов** в одной директории. Подпапки не ограничены: `src/` может содержать `lib.rs` и три модуля-директории, каждая из которых в свою очередь содержит не больше четырёх файлов. -Проверяются: `crates/*/src`, `crates/*/tests`, `crates/*/benches` и `docs/`. `tests/` и `benches/` не исключены намеренно — иначе правило «250 строк» обходится выносом кода в тесты. @@ -45,11 +47,11 @@ fn main() { } ``` -`noxis-structure-check` находит корень воркспейста, обходит деревья `src/`, -`tests/` и `benches/` всех крейтов плюс `docs/`, и **падает со стопкой ошибок**, -если лимит нарушен. Поэтому `cargo build` не соберёт нарушение — специально -запускать проверку не нужно. Деревья регистрируются через -`cargo:rerun-if-changed`, так что правка `docs/` тоже перезапускает гейт. +`noxis-structure-check` обходит деревья `src/`, `tests/` и `benches/` **этого +крейта** и **падает со стопкой ошибок**, если лимит нарушен. Поэтому +`cargo build` не соберёт нарушение — специально запускать проверку не нужно. +Деревья регистрируются через `cargo:rerun-if-changed`, чтобы гейт не +срабатывал на правки вне проверяемых путей. Остальное, что не привязано к компиляции, собирается одним скриптом: diff --git a/docs/architecture.md b/docs/architecture.md index 108456e..91a31ad 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -120,9 +120,9 @@ Rename → Resources → Strings → Constants → Flow → Noise → DebugInfo | этап | что | |---|---| -| **готово** | IR, модель, конфиг, seed/PRNG, маппинги, анализ, keep, переименование, шифры строк, гейт правил, `format::parse`/`write` поверх ристретто, вывод кадров `StackMapTable` | -| **P2** | пассы строк/констант/шума/потока, генератор декодера, CLI | -| **P2** | генерация класса-декодера, вывод мастер-секрета, остальные пассы | -| **P3** | CLI, метрики, интеграция с `java -Xverify:all` | +| **готово** | IR, модель, конфиг, seed/PRNG, маппинги, анализ, keep, гейт правил, `format::parse`/`write` поверх ристретто, вывод кадров `StackMapTable` | +| **готово** | пассы rename/resources/strings/constants/noise/debug, генератор декодера, вывод мастер-секрета в рантайме | +| **готово** | `noxis-verify` (red-паттерны, `java -Xverify:all`, метрики), CLI (`obfuscate`/`analyze`/`patterns`) | +| **план** | схемы пасса `flow` (кадры для них уже считаются), фикстуры и regression-тесты | Детали выбора — [`stack.md`](stack.md), план — [`../todo.md`](../todo.md). diff --git a/docs/learnings/ai-resistance.md b/docs/learnings/ai-resistance.md new file mode 100644 index 0000000..1e3881f --- /dev/null +++ b/docs/learnings/ai-resistance.md @@ -0,0 +1,167 @@ +# Устойчивость к ИИ-анализу и новые техники (октябрь 2026) + +Полный разбор двух источников по запросу: что конкретно работает против LLM +при реверсе кода и что из этого переносится на JVM-байткод. Плюс отдельно: +что нового в классической обфускации (MBA, виртуализация). + +Короткий вывод: специальной "защиты от нейросетей" как отдельного свойства +кода не существует. Есть два разных, непересекающихся приёма, которые +работают по разным причинам, и переносится на наш проект только один из них. + +## 1. Elastic Security Labs: агент с IDA Pro против Tigress (апрель 2026) + +[«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"-ила) три защиты, нацеленные именно на модель. Это ближе всего +к нашей реальной угрозе: не чат с текстом исходника, а агент, читающий +бинарник/байткод инструментами. + +### Benchmark на Tigress + +22 цели, 4 уровня сложности: без трансформаций → одиночные → пары → тяжёлые +комбинации. Трансформации: `EncodeArithmetic`, `EncodeLiterals` (MBA), +`Flatten` (control-flow flattening), `JIT`/`JitDynamic` (XTEA-шифрование кода +с рантайм-расшифровкой), `Virtualize` (byte-code VM, варианты +indirect/regs и switch/stack). + +- Opus решил ~40% из 20 валидных задач. Успех стоил в среднем **$2.39**, + провал — **$4.83**. Ни одна задача уровня "многослойные комбинации" не + была решена. +- **`JIT` (XTEA + рантайм-расшифровка) оказался самой сложной одиночной + трансформацией** — модель понимала структуру движка, но не смогла + восстановить ключевые константы. Это ровно наш подход в `Strings` + (ключ выводится в рантайме, не лежит в вызове), только применённый ко + **всему коду метода**, а не к строкам. +- На самых жёстких комбинациях цена и время выросли в ~4.5× и ~4.3×, но + задачи, которые Opus уже умел решать, он решил всё равно. **Flatten + 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 | + +### Что из этого реально работает, по механизму + +1. **Контекстное окно.** Длинные тупиковые пути (decoy-функции, ложные + переходы) забивают контекст и портят рассуждение модели. +2. **Бюджет шагов/денег.** Каждый впустую потраченный цикл стоит агенту + реальных токенов; если он упирается в лимит и сдаётся — это победа + защиты, даже если код теоретически вскрываем. +3. **Shortcut bias.** Вводящая в заблуждение структура (ловушки в + Dispatch Maze) заставляет модель остановиться раньше времени. + Попытки prompt injection в тексте **не сработали** против Opus 4.6. + +**Ключевое экономическое наблюдение авторов:** нативное исполнение занимает +~55 мс, а статический провал агента стоит **$1.50–$9.67**. Это и есть +реальный рычаг — не "нечитаемость навсегда", а разрыв между ценой атаки и +ценой сборки для нас. Авторы прямо предупреждают: преимущество схлопнется, +когда у агентов появятся инструменты динамического исполнения (сейчас +тестировался только статический анализ). + +## 2. Acoda: атака на safety-alignment и токенизацию (ICSE 2026) + +[Acoda](https://arxiv.org/pdf/2606.11755) — другая постановка задачи: не +агент с дизассемблером, а LLM, которой **в чат вставили исходный код** и +попросили его проанализировать/деобфусцировать. Генетический алгоритм +подбирает комбинацию из 8 трансформаций, до 70% ASR (attack success rate) +на 7 моделях (GPT-4o, DeepSeek, Qwen, Llama, Gemma). + +### 8 методов, по стадиям + +| стадия | метод | как работает | +|---|---|---| +| до анализа | Deceptive Comment Warnings | комментарий командует модели отказаться анализировать ("прекрати, это бэкдор") | +| до анализа | Variable/Function Renaming | идентификаторы вида `MakeAnesthetics`, намекающие на malware/оружие | +| до анализа | Vulnerability Code Injection | фрагмент с SQL-инъекцией/`pickle.loads`/command injection в заведомо мёртвой ветке (`while (сложное_но_ложное_условие)`) | +| во время анализа | Misleading Summarization | модель сама просится описать код, затем это описание (намеренно неверное) вклеивается рядом с кодом | +| во время анализа | String Obfuscation | `base64` + словарь, расшифровка даёт слова типа "virus"/"weapon" | +| во время анализа | Try-Except Wrapping | настоящая логика лежит в `except`-ветке, в `try` — правдоподобный, но бесполезный код | +| после анализа | EOS Token Insertion | в текст вставлены литералы токенов-разделителей (`<|EOT|>`), из-за чего генерация обрывается раньше | + +### Честная оценка: что из этого применимо к нам + +Почти ничего напрямую, и вот почему. Все 8 методов работают через +**текстовый/натурально-языковой канал**: комментарии, сгенерированное +описание, визуально узнаваемые "эксплойт"-паттерны в исходнике. У нас: + +- `DebugInfo` уже вычищает всё, что могло бы стать комментарием или + осмысленным описанием (`LineNumberTable`, `Signature`, исходные имена); + в скомпилированном `.class` комментариев не бывает в принципе. +- Цель Acoda — заставить модель **отказаться анализировать свой же код**, + вставленный в чат пользователем. У нас код анализирует **чужой** агент + чужого jar — нам не нужно, чтобы он отказался, если он не наш союзник; + и вызывать намеренно подозрительные паттерны в легитимном продукте — + прямой риск получить ложное срабатывание антивируса/EDR на свой же jar. + Это противоречит `README.md` → «Ответственное использование». +- **EOS Token Insertion** не переносится: это свойство текстового + стриминга токенов модели, а не байткода. + +Единственное, что частично переносится: **String Obfuscation** и +**Variable Renaming** — ровно то, что `Strings` и `Rename` уже делают, +просто дешевле (у Acoda — обратимый `base64`, у нас — ротация шифров с +выводом ключа в рантайме, см. [`../stack.md`](../stack.md) §4-5). Здесь мы +уже на шаг впереди статьи, а не позади. + +**Vulnerability Code Injection** как идея (мёртвый код, похожий на уязвимый +паттерн) пересекается с нашим `Noise`, но внедрять **реально похожие на +эксплойт сигнатуры** в выдаваемый клиентам jar — плохая идея для легального +продукта защиты: это именно то, от чего предостерегает +[`info/evo-weaknesses.md`](../../info/evo-weaknesses.md) с другой стороны +(не подставлять защищённый продукт под чужие red-листы). + +## 3. Что добавить в noxis практически + +Экономический рычаг из Elastic (цена агента > цены нашей сборки) ложится на +архитектуру без переделок, конкретные пункты — в [`../../todo.md`](../../todo.md) +§7: + +1. **Вложенные слои шифрования строк** (аналог Matryoshka Wall) — расширить + `StringsConfig` полем "глубина", decoder разворачивает N слоёв вместо + одного. Цена агента растёт с глубиной почти линейно, цена сборки — нет. +2. **Больше decoy-точек ветвления ключа** (аналог Dispatch Maze) — наш + декодер уже собирает мастер-секрет из `k1^k2` (`stack.md` §4); можно + поднять число частей и раскидать их сборку по случайным junk-методам + из `Noise`, а не держать в одном месте. +3. **MBA вместо простых тождеств в `Constants`** — то же решение, что ловит + и символьный исполнитель, и SMT-решатель, и, по данным Elastic, + "Flatten + MBA" бьёт сильнее, чем просто Flatten. +4. **Не делать:** имитацию вредоносных паттернов, prompt injection в + строках, EOS-токены. Либо не переносится на байткод, либо создаёт риск + ложного срабатывания защитных систем на легитимном продукте. + +Критерий "сработало/не сработало" для всего этого — как и для остальных +техник: прогон конкретного агента на конкретной сборке, а не вера в +методичку. Эффект устаревает с каждой новой версией модели. + +## 4. Классическая обфускация: что нового, помимо LLM + +- [Систематическое исследование control-flow обфускации в Java](https://arxiv.org/pdf/1809.11037) + называет **виртуализацию** (table interpretation) самой мощной техникой: + декомпилятор не восстанавливает структуру, потому что исходного байткода + метода в классе физически больше нет, есть только интерпретатор и таблица + операций. Коммерческие инструменты (например, + [Promon Shield](https://promon.io/security-news/virtual-machine-obfuscation-sdk-protection)) + уже уносят это на уровень нативной библиотеки. +- **Для нас это ломает собственное допущение** из + [`../stack.md`](../stack.md) §3: `noxis-verify` верифицирует метод через + `java -Xverify:all`, потому что это *обычный* байткод с валидным + `StackMapTable`. Виртуализированный метод — уже не обычный байткод, + верификатор проверит только обвязку интерпретатора, не тело. Big-ticket + item, не для ближайшего спринта. +- **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) +- [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/) diff --git a/docs/learnings/lessons.md b/docs/learnings/lessons.md index ae1fee1..0fe7e1e 100644 --- a/docs/learnings/lessons.md +++ b/docs/learnings/lessons.md @@ -11,14 +11,14 @@ | № | провал EvoVis | наше решение | пасс | статус | |---|---|---|---|---| -| 1 | ключ лежит рядом с шифротекстом | вывод мастер-секрета в декодере, ключ из `master‖salt` | Strings | частично | +| 1 | ключ лежит рядом с шифротекстом | вывод мастер-секрета в декодере, ключ из `master‖salt` | Strings | **готово** | | 2 | переименованы типы, но не члены | rename полей/методов + семейства virtual | Rename | **готово** | -| 3 | отладочная информация нетронута | strip + ложный `SourceFile`/номера строк | DebugInfo | план | -| 4 | шаблонный flattening | 4 схемы CFG + мешанина внутри метода | Flow | план | -| 5 | opaque-предикаты = `A+B` | ротация тождеств + предикаты от состояния | Constants | план | -| 6 | junk-классы вне графа reachability | `weave_reachable` — обязательная вплетённость | Noise | план | +| 3 | отладочная информация нетронута | strip + ложный `SourceFile`/номера строк | DebugInfo | **готово** (`SourceFile`; номера строк — план) | +| 4 | шаблонный flattening | 4 схемы CFG + мешанина внутри метода | Flow | план (кадры готовы) | +| 5 | opaque-предикаты = `A+B` | ротация тождеств + предикаты от состояния | Constants | **готово** (int/long) | +| 6 | junk-классы вне графа reachability | `weave_reachable` — обязательная вплетённость | Noise | **готово** (без аннотаций) | | 7 | имена говорят «я защита» | все артефакты получают данные псевдонимы | Rename | **готово** | -| 8 | вторичные компоненты без защиты | весь jar, а не только основной класс | Resources | план | +| 8 | вторичные компоненты без защиты | весь jar, а не только основной класс | Resources | **готово** | | 9 | шифр без MAC и без per-string salt | salt на строку, уникальный nonce, MAC | Strings | **готово** | ## 1. Ключ рядом с шифротекстом diff --git a/docs/learnings/priorities.md b/docs/learnings/priorities.md index d3fb815..57352c7 100644 --- a/docs/learnings/priorities.md +++ b/docs/learnings/priorities.md @@ -17,9 +17,9 @@ | пункт | что это | пасс | статус | |---|---|---|---| | A1 | переименование **всех** символов: классы, поля, методы, локальные переменные, параметры, record-компоненты, enum-константы, `SourceFile` | Rename | ✅ готово | -| A2 | вычистить `LineNumberTable`/`LocalVariableTable`/`SourceFile`/`InnerClasses`/`Signature`/`MethodParameters` | DebugInfo | план | -| A3 | никаких сигнатурных имён у защитного кода, защитный код смешан с обычным | Rename + Noise | ✅ / план | -| A4 | обфусцировать **весь** архив, включая ресурсы, mixin-JSON, access widener, service loader'ы | Resources | план | +| A2 | вычистить `LineNumberTable`/`LocalVariableTable`/`SourceFile`/`InnerClasses`/`Signature`/`MethodParameters` | DebugInfo | ✅ готово | +| A3 | никаких сигнатурных имён у защитного кода, защитный код смешан с обычным | Rename + Noise | ✅ готово | +| A4 | обфусцировать **весь** архив, включая ресурсы, mixin-JSON, access widener, service loader'ы | Resources | ✅ готово | | — | маппинг в отчёт (иначе не читаем свои же stack trace) | Rename | ✅ готово | Это самый дешёвый блок и одновременно **самый большой утечка** в разобранном @@ -30,49 +30,51 @@ | пункт | что это | пасс | статус | |---|---|---|---| -| B1 | ключ на строку + ротация 3–5 схем + MAC + уникальный nonce | Strings | ✅ схемы готовы | -| B2 | **вывод** ключа в рантайме, а не константа в вызове | Strings + генератор декодера | ⛔ блокер | -| B3 | константы — не тождества, а вычисления, разнесённые по блокам | Constants | план | -| B4 | **ложная** отладочная информация: чужие имена переменных и строк | DebugInfo | план | +| B1 | ключ на строку + ротация 3–5 схем + MAC + уникальный nonce | Strings | ✅ готово | +| B2 | **вывод** ключа в рантайме, а не константа в вызове | Strings + генератор декодера | ✅ готово | +| B3 | константы — не тождества, а вычисления, разнесённые по блокам | Constants | ✅ готово (int/long) | +| B4 | **ложная** отладочная информация: чужие имена переменных и строк | DebugInfo | ✅ `SourceFile` | B2 — главное отличие от разобранного образца: у него ключ был константным выражением в вызове и расшифровывался статически, без JVM. Мастер-секрет -должен собираться в рантайме из нескольких мест. Это требует сначала -собрать класс-декодер (P2) — потому и блокер, а не «сделаем потом». +теперь собирается в рантайме из двух частей (`k1 ^ k2`) декодером класса — +см. [`../stack.md`](../stack.md) §4. B4 пока покрывает только `SourceFile`; +ложные номера строк (`fake_line_numbers`) — см. [`../../todo.md`](../../todo.md) §3.6. ## P2 — контрольный поток | пункт | что это | статус | |---|---|---| | C1 | **несколько схем** флаттенинга вместо одной, с мешаниной внутри метода | план | -| C2 | верификатор обязателен: ошибка CFG = `VerifyError` | план | -| C3 | реальный объём ≠ раздувание: бюджет на размер и время | план | +| C2 | верификатор обязателен: ошибка CFG = `VerifyError` | ✅ готово (кадры + `java -Xverify:all`) | +| C3 | реальный объём ≠ раздувание: бюджет на размер и время | ✅ размер; время — план | -Приоритет здесь строго подчинён: **C2 раньше C1**. Включать Flow без -проверки кадров `StackMapTable` — это гарантированный битый jar. -Подробности: [`../stack.md`](../stack.md) §3. +Приоритет здесь строго подчинён: **C2 раньше C1**. Кадры `StackMapTable` +и приёмка `java -Xverify:all` уже готовы (см. [`../stack.md`](../stack.md) +§3) — единственное, чего не хватает, это сами схемы флаттенинга (C1). ## P3 — измеримость (метрики, без которых ничего не доказано) -Обязательный блок, иначе фраза «обфускация есть» ничего не означает: +Обязательный блок, иначе фраза «обфускация есть» ничего не означает. +Крейт `noxis-verify` реализован — вот что из списка уже цифра, а что ещё мнение: -1. **Утечки имён.** Список эталонных осмысленных идентификаторов; - если хоть один остался в `strings`/константах — тест падает. -2. **Читаемость декомпилята.** Прогон JADX/CFR → доля распознанных структур - (try/catch, switch, циклы), доля классов с восстановленными именами. -3. **Противник.** Минимальный скрипт-деобфускатор (свёртка констант, - regex-паттерны). Если он снимает больше порога — техника провалена. - **Красный список паттернов держим в репозитории.** -4. **Семантика.** Эталонные программы до и после — 100% совпадение. -5. **Бюджеты.** Размер ≤ N%, время исполнения ≤ M%, отдельно — бюджет - раскрытия строк (кэш, чтобы не платить на каждый вызов). -6. **Стабильность между сборками.** Имена и схемы **меняются** от билда - к билду: у разобранного образца шифр был один на все версии — готовое - окно для обучения по частотам. +1. **Утечки имён.** ✅ `collect_names`/`find_leaks` — сверка идентификаторов + с учётом keep-списка. +2. **Читаемость декомпилята.** ⬜ Прогон JADX/CFR → доля распознанных + структур — не начато. +3. **Противник.** ✅ `scan_secrets` + `RED_PATTERNS` держим в репозитории; + ⬜ полноценный скрипт-деобфускатор (свёртка констант) как тест — план. +4. **Семантика.** ⬜ Эталонные программы до/после — не начато + (сейчас семантику подтверждает только `java -Xverify:all`). +5. **Бюджеты.** ✅ размер (`inflation_ratio`/`check_inflation`); + ⬜ время исполнения — не начато. +6. **Стабильность между сборками.** ✅ `mappings_changed` — маппинги + обязаны отличаться между прогонами с разным seed. -Это блок `noxis-verify`: красные паттерны + `java -Xverify:all` + -метрики. Единственное место, где «нам кажется защищённым» заменяется -цифрой. +Итог: `noxis-verify` закрывает красные паттерны, утечки имён, +`java -Xverify:all`, бюджет размера и стабильность маппингов. Остаются +декомпилят-метрика, бюджет времени и эталонная семантика — три пункта, +где «нам кажется защищённым» всё ещё мнение, а не цифра. ## P4 — долгий горизонт (пока не делаем) @@ -105,12 +107,14 @@ writer). ## Порядок выполнения -Смотрите [`../../todo.md`](../../todo.md). Кратко: +Смотрите [`../../todo.md`](../../todo.md). P0–P3 и P3-измеримость уже +готовы; единственный крупный пункт, который остался, — схемы `Flow` (C1), +потому что верификатор (C2) для них уже есть: ```text -P0 DebugInfo + Resources → снимает главные утечки, почти бесплатно -P1 Strings (B2) + Constants → ключ выводится, схемы ротируются -P1 backend (ristretto) + frames -P2 Flow (C1) после верификатора -P3 метрики (F) и красные паттерны +✅ P0 DebugInfo + Resources → главные утечки сняты +✅ P1 Strings (B2) + Constants → ключ выводится, схемы ротируются +✅ P1 backend (ristretto) + frames +⬜ P2 Flow (C1) — единственное, что ждёт реализации; верификатор готов +✅ P3 метрики, красные паттерны, CLI ``` diff --git a/docs/techniques.md b/docs/techniques.md index 7359eba..dfde703 100644 --- a/docs/techniques.md +++ b/docs/techniques.md @@ -130,7 +130,10 @@ opaque-предикаты — условия, истинность которы - Слишком «чистый» класс (`LineNumberTable` есть, а `SourceFile` = `X.java`, которого нет) — тоже признак. -**Статус: не реализован.** +**Статус: готово** (strip + `spoof_source_file`, детерминированная подмена +от мастер-сида). Не реализовано: `fake_line_numbers` (ложные номера строк, +а не только `SourceFile`) и `strip_source_file` (полное удаление вместо +подмены) — см. [`todo.md`](../todo.md) §3.6. ## 7. Ресурсы (`Resources`) @@ -147,7 +150,10 @@ opaque-предикаты — условия, истинность которы трогать нельзя. - Промах в патче → тихо неработающий refmap, ловится только запуском. -**Статус: не реализован (крейт `noxis-jar` — план).** +**Статус: готово** (патч текстовых ресурсов по маппингу, включая +`fabric.mod.json`/`*.mixins.json`/`META-INF/services/*`; срез подписей +в `noxis-jar`). Не реализовано: типизированный патч бинарных/специфичных +форматов (refmap, access widener) сверх общего boundary-replace. ## Сводная таблица @@ -155,11 +161,11 @@ opaque-предикаты — условия, истинность которы |---|---|---|---|---| | Rename | ★★★★★ | низкий | низкая | готово | | Strings | ★★★★ | средний | средняя | готово | -| Resources | ★★★★★ | низкий | низкая | план | -| DebugInfo | ★★★ | низкий | низкая | план | +| Resources | ★★★★★ | низкий | низкая | готово | +| DebugInfo | ★★★ | низкий | низкая | готово | | Constants | ★★ | низкий | низкая | готово (int/long) | | Noise | ★ | низкий | раздувание | готово (без аннотаций) | -| Flow | ★★★★ | **высокий** | высокая | план, ждёт frames | +| Flow | ★★★★ | **высокий** | высокая | план, кадры готовы | Вывод, который стоит держать в голове: **Rename + Resources + DebugInfo** дают большую часть защиты за небольшую цену. Flow — единственная техника,