docs: sync status with code, add AI-resistance research, restyle README

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
This commit is contained in:
Loki 2026-10-08 22:34:00 +02:00
parent 2674d7fc89
commit c3b8d4dd62
9 changed files with 349 additions and 162 deletions

122
README.md
View file

@ -1,34 +1,61 @@
<div align="center">
# noxis # noxis
Обфускатор JVM/Java-байткода на Rust. Обфускатор JVM/Java-байткода на Rust.
Читает JAR, переписывает классовые файлы и ресурсы, собирает обратно — ![Rust](https://img.shields.io/badge/Rust-2024_edition-orange?style=flat-square&logo=rust&logoColor=white)
детерминированно, с маппингами для stack trace и с проверкой, что выход ![MSRV](https://img.shields.io/badge/MSRV-1.97-informational?style=flat-square)
действительно загружается настоящей JVM. ![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)
</div>
---
Читает JAR, переписывает классовые файлы и ресурсы, собирает обратно: детерминированно,
с маппингами для stack trace и с проверкой, что выход действительно загружается настоящей JVM.
## Статус ## Статус
В работе. Ядро построено, часть техник реализована, часть — по плану. Ядро построено, большая часть техник реализована.
| блок | состояние | | блок | состояние |
|---|---| |---|---|
| IR классовых файлов, метки вместо смещений | ✅ | | IR классовых файлов, метки вместо смещений | ✅ |
| конфиг, детерминированный PRNG, маппинги, отчёт | ✅ | | конфиг, детерминированный PRNG, маппинги, отчёт | ✅ |
| анализ: иерархия, entrypoint'ы, keep-правила | ✅ | | анализ: иерархия, entrypoint'ы, keep-правила | ✅ |
| переименование всех символов | ✅ | | `format::parse`/`write` + кадры `StackMapTable` (backend `ristretto_classfile`) | ✅ |
| схемы шифрования строк (salt, nonce, MAC) | ✅ | | пассы: rename, resources, strings, constants, noise, debug | ✅ |
| JAR: чтение/запись, ресурсы, срез подписей | ✅ | | верификация: red-паттерны, `java -Xverify:all`, метрики | ✅ |
| гейт структурных правил при компиляции | ✅ | | CLI (`obfuscate` / `analyze` / `patterns`) | ✅ |
| пассы: rename, resources, debug | ✅ | | пасс flow (кадры готовы, схем флаттенинга пока нет) | 🚧 |
| spike backend'а `ristretto_classfile` | ✅ |
| `format::parse`/`write` + кадры `StackMapTable` | ✅ |
| пассы: strings, constants, noise (+ декодер v49) | ✅ |
| пасс flow | 🚧 |
| верификация и метрики | 🚧 |
| CLI | 🚧 |
Подробности — [`docs/architecture.md`](docs/architecture.md), Подробности в [`docs/architecture.md`](docs/architecture.md), план в [`todo.md`](todo.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,7 +63,7 @@
noxis/ noxis/
├── Cargo.toml workspace, lints, release-профиль ├── Cargo.toml workspace, lints, release-профиль
├── scripts/check.sh structure → fmt → clippy → test ├── scripts/check.sh structure → fmt → clippy → test
├── docs/ ← архитектура, техники, стек, уроки ├── docs/ архитектура, техники, стек, уроки
├── info/ исходный материал разбора (вне правил) ├── info/ исходный материал разбора (вне правил)
└── crates/ └── crates/
├── noxis-classfile/ IR + парсер/запись + StackMapTable ├── noxis-classfile/ IR + парсер/запись + StackMapTable
@ -45,66 +72,47 @@ noxis/
├── noxis-passes/ сами трансформации + конвейер ├── noxis-passes/ сами трансформации + конвейер
├── noxis-runtime/ схемы шифрования, ABI декодера ├── noxis-runtime/ схемы шифрования, ABI декодера
├── noxis-jar/ ZIP, ресурсы, срез подписей ├── noxis-jar/ ZIP, ресурсы, срез подписей
├── noxis-verify/ red-паттерны, java -Xverify (план) ├── noxis-verify/ red-паттерны, java -Xverify, метрики
├── noxis/ CLI (план) ├── noxis/ CLI: obfuscate, analyze, patterns
└── noxis-structure-check/ автогейт структуры └── 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/README.md`](docs/README.md) | индекс и структурные правила проекта |
| [`docs/architecture.md`](docs/architecture.md) | слои, крейты, IR, конвейер | | [`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/stack.md`](docs/stack.md) | почему свой IR, backend, `StackMapTable`, PRNG |
| [`docs/learnings/lessons.md`](docs/learnings/lessons.md) | уроки разбора: чего делать нельзя | | [`docs/learnings/lessons.md`](docs/learnings/lessons.md) | уроки разбора: чего делать нельзя |
| [`docs/learnings/reference.md`](docs/learnings/reference.md) | разбор референсного обфускатора | | [`docs/learnings/reference.md`](docs/learnings/reference.md) | разбор референсного обфускатора |
| [`docs/learnings/priorities.md`](docs/learnings/priorities.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). Исходный материал разбора в [`info/README.md`](info/README.md).
## Принципы
- **KISS / DRY / SOLID.** Файл — одна ответственность. Не больше 250 строк,
не больше 4 файлов в директории.
- **Детерминированность.** Один seed → байт-в-байт одинаковый артефакт.
- **Ничего не ломаем.** Техника, которая может дать `VerifyError`,
включается только после верификатора.
- **Измеримо.** Защита без метрик — это мнение, а не свойство.
## Лицензия ## Лицензия
AGPL-3.0-only — см. [`LICENSE`](LICENSE). AGPL-3.0-only, см. [`LICENSE`](LICENSE).
Проект остаётся полностью открытым: его можно изучать, использовать Проект остаётся полностью открытым: его можно изучать, использовать (в том числе
(в том числе коммерчески) и изменять. Условие AGPL — любые производные коммерчески) и изменять. Условие AGPL: любые производные работы, которые
работы, которые распространяются или доступны пользователям по сети, распространяются или доступны пользователям по сети, обязаны публиковаться под
обязаны публиковаться под той же лицензией. Это защищает `noxis` от той же лицензией. Это защищает `noxis` от закрытых форков, которые перепродают
закрытых форков, которые перепродают его как готовый продукт. его как готовый продукт.
Сторонние зависимости и их лицензии — в [`NOTICE.md`](NOTICE.md). Сторонние зависимости и их лицензии в [`NOTICE.md`](NOTICE.md).
## Ответственное использование ## Ответственное использование
`noxis` — инструмент двойного назначения. Он одинаково полезен для `noxis` это инструмент двойного назначения, одинаково полезный для защиты
защиты собственного байткода и способен усложнять анализ вредоносного собственного байткода и способен усложнять анализ вредоносного ПО. Лицензия
ПО. Лицензия это не регламентирует, но это норма сообщества проекта: это не регламентирует, но это норма сообщества проекта:
- используйте обфускацию, чтобы защищать **свой** код и код тех, кто - используйте обфускацию, чтобы защищать **свой** код и код тех, кто вас об этом попросил;
вас об этом попросил; - не применяйте инструмент для упаковки вредоносов, обхода средств защиты
- не применяйте инструмент для упаковки вредоносов, обхода средств и антивирусов или иной деятельности, запрещённой законом.
защиты и антивирусов или иной деятельности, запрещённой законом.
Нарушение этих норм не влечёт юридических последствий по лицензии, но Нарушение этих норм не влечёт юридических последствий по лицензии, но приведёт
приведёт к отказу в поддержке и закрытию issue/PR в репозитории. к отказу в поддержке и закрытию issue/PR в репозитории.

View file

@ -76,4 +76,16 @@ mod tests {
let found = violations(&tmp.0); let found = violations(&tmp.0);
assert!(found.is_empty(), "{found:?}"); 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:?}");
}
} }

View file

@ -1,31 +1,32 @@
//! Project structure rules, invoked from every crate's `build.rs`. //! Project structure rules, invoked from every crate's `build.rs`.
//! //!
//! Rules: //! Rules apply to Rust code only, not to documentation:
//! * every `.rs` and `.md` file is at most [`MAX_FILE_LINES`] lines long; //! * every `.rs` file is at most [`MAX_FILE_LINES`] lines long;
//! * every directory inside a crate `src/` or the workspace `docs/` holds at //! * every directory inside a crate's `src/`, `tests/` or `benches/` holds
//! most [`MAX_DIR_FILES`] files — subdirectories are unlimited. //! at most [`MAX_DIR_FILES`] files — subdirectories are unlimited.
//! //!
//! Deliberately excluded: `Cargo.lock`, `.gitignore`, the legacy `info/` tree, //! Deliberately excluded: `Cargo.lock`, `.gitignore`, `docs/`, the legacy
//! and crate roots (they always own `Cargo.toml` plus `build.rs`). //! `info/` tree, and crate roots (they always own `Cargo.toml` plus
//! `build.rs`).
use std::ffi::OsStr; use std::ffi::OsStr;
use std::fs; use std::fs;
use std::path::{Path, PathBuf}; 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; pub const MAX_FILE_LINES: usize = 250;
/// Maximum number of files per directory; subdirectories are not counted. /// Maximum number of files per directory; subdirectories are not counted.
pub const MAX_DIR_FILES: usize = 4; pub const MAX_DIR_FILES: usize = 4;
/// Extensions subject to [`MAX_FILE_LINES`]. /// 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. /// Directory names the scan never descends into.
const SKIPPED_DIRS: [&str; 2] = ["target", ".git"]; const SKIPPED_DIRS: [&str; 2] = ["target", ".git"];
/// Scans the calling crate's `src/`/`tests/`/`benches/` plus the workspace /// Scans the calling crate's `src/`/`tests/`/`benches/` and panics with a
/// `docs/` tree and panics with a report on the first violation. /// report on the first violation.
/// ///
/// # Panics /// # Panics
/// ///
@ -35,13 +36,13 @@ pub fn check() {
let mut violations = Vec::new(); let mut violations = Vec::new();
// `src/` обязателен; `tests/` и `benches/` проверяются, если созданы — // `src/` обязателен; `tests/` и `benches/` проверяются, если созданы —
// иначе правило «250 строк» обходится выносом кода в тесты. // иначе правило «250 строк» обходится выносом кода в тесты. `docs/` вне
let mut roots = vec![manifest.join("src"), manifest.join("tests")]; // гейта: правила — только про Rust-код.
roots.push(manifest.join("benches")); let roots = [
manifest.join("src"),
if let Some(root) = workspace_root(&manifest) { manifest.join("tests"),
roots.push(root.join("docs")); manifest.join("benches"),
} ];
for root in &roots { for root in &roots {
watch(root); watch(root);
@ -64,8 +65,9 @@ pub fn check() {
/// Re-runs the build script whenever a checked tree changes. /// Re-runs the build script whenever a checked tree changes.
/// ///
/// Without this cargo only invalidates `build.rs` on crate-local changes, so /// Without an explicit `rerun-if-changed`, cargo falls back to its default
/// editing `docs/` would never be re-validated. /// (rerun on any change anywhere in the package); naming the trees keeps the
/// gate from firing on unrelated edits.
fn watch(root: &Path) { fn watch(root: &Path) {
if root.is_dir() { if root.is_dir() {
println!("cargo:rerun-if-changed={}", root.display()); println!("cargo:rerun-if-changed={}", root.display());
@ -101,6 +103,10 @@ pub fn scan(root: &Path, violations: &mut Vec<String>) {
subdirs.push(path); subdirs.push(path);
continue; continue;
} }
let ext = path.extension().and_then(OsStr::to_str).unwrap_or_default();
if !LINE_LIMITED_EXTS.contains(&ext) {
continue;
}
files += 1; files += 1;
check_line_limit(&path, violations); check_line_limit(&path, violations);
} }
@ -118,10 +124,6 @@ pub fn scan(root: &Path, violations: &mut Vec<String>) {
} }
fn check_line_limit(path: &Path, violations: &mut Vec<String>) { fn check_line_limit(path: &Path, violations: &mut Vec<String>) {
let ext = path.extension().and_then(OsStr::to_str).unwrap_or_default();
if !LINE_LIMITED_EXTS.contains(&ext) {
return;
}
let lines = count_lines(path); let lines = count_lines(path);
if lines > MAX_FILE_LINES { if lines > MAX_FILE_LINES {
violations.push(format!( 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"), 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<PathBuf> {
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
}

View file

@ -12,22 +12,24 @@
| [`learnings/lessons.md`](learnings/lessons.md) | уроки из разбора EvoVis: что защита сделала плохо и чего мы не делаем | | [`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/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). [`../info/`](../info/README.md).
## Структурные правила проекта ## Структурные правила проекта
Правила действуют на **весь наш код и документацию**: `crates/**` и `docs/**`. Правила — **только про Rust-код**: `crates/*/src`, `crates/*/tests`,
Не учитываются: `Cargo.lock`, `.gitignore`, `info/`, корень крейта `crates/*/benches`. Документация (`docs/**`, корневые `.md`) и `info/` вне
гейта: писать подробно там, где это документация, а не ограничивать объём
искусственно. Не учитываются также `Cargo.lock`, `.gitignore`, корень крейта
(в нём всегда лежат `Cargo.toml` + `build.rs`). (в нём всегда лежат `Cargo.toml` + `build.rs`).
1. **Не больше 250 строк** в одном `.rs` или `.md` файле. 1. **Не больше 250 строк** в одном `.rs` файле.
2. **Не больше 4 файлов** в одной директории. Подпапки не ограничены: 2. **Не больше 4 `.rs`-файлов** в одной директории. Подпапки не ограничены:
`src/` может содержать `lib.rs` и три модуля-директории, каждая из которых `src/` может содержать `lib.rs` и три модуля-директории, каждая из которых
в свою очередь содержит не больше четырёх файлов. в свою очередь содержит не больше четырёх файлов.
Проверяются: `crates/*/src`, `crates/*/tests`, `crates/*/benches` и `docs/`.
`tests/` и `benches/` не исключены намеренно — иначе правило `tests/` и `benches/` не исключены намеренно — иначе правило
«250 строк» обходится выносом кода в тесты. «250 строк» обходится выносом кода в тесты.
@ -45,11 +47,11 @@ fn main() {
} }
``` ```
`noxis-structure-check` находит корень воркспейста, обходит деревья `src/`, `noxis-structure-check` обходит деревья `src/`, `tests/` и `benches/` **этого
`tests/` и `benches/` всех крейтов плюс `docs/`, и **падает со стопкой ошибок**, крейта** и **падает со стопкой ошибок**, если лимит нарушен. Поэтому
если лимит нарушен. Поэтому `cargo build` не соберёт нарушение — специально `cargo build` не соберёт нарушение — специально запускать проверку не нужно.
запускать проверку не нужно. Деревья регистрируются через Деревья регистрируются через `cargo:rerun-if-changed`, чтобы гейт не
`cargo:rerun-if-changed`, так что правка `docs/` тоже перезапускает гейт. срабатывал на правки вне проверяемых путей.
Остальное, что не привязано к компиляции, собирается одним скриптом: Остальное, что не привязано к компиляции, собирается одним скриптом:

View file

@ -120,9 +120,9 @@ Rename → Resources → Strings → Constants → Flow → Noise → DebugInfo
| этап | что | | этап | что |
|---|---| |---|---|
| **готово** | IR, модель, конфиг, seed/PRNG, маппинги, анализ, keep, переименование, шифры строк, гейт правил, `format::parse`/`write` поверх ристретто, вывод кадров `StackMapTable` | | **готово** | IR, модель, конфиг, seed/PRNG, маппинги, анализ, keep, гейт правил, `format::parse`/`write` поверх ристретто, вывод кадров `StackMapTable` |
| **P2** | пассы строк/констант/шума/потока, генератор декодера, CLI | | **готово** | пассы rename/resources/strings/constants/noise/debug, генератор декодера, вывод мастер-секрета в рантайме |
| **P2** | генерация класса-декодера, вывод мастер-секрета, остальные пассы | | **готово** | `noxis-verify` (red-паттерны, `java -Xverify:all`, метрики), CLI (`obfuscate`/`analyze`/`patterns`) |
| **P3** | CLI, метрики, интеграция с `java -Xverify:all` | | **план** | схемы пасса `flow` (кадры для них уже считаются), фикстуры и regression-тесты |
Детали выбора — [`stack.md`](stack.md), план — [`../todo.md`](../todo.md). Детали выбора — [`stack.md`](stack.md), план — [`../todo.md`](../todo.md).

View file

@ -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/)

View file

@ -11,14 +11,14 @@
| № | провал EvoVis | наше решение | пасс | статус | | № | провал EvoVis | наше решение | пасс | статус |
|---|---|---|---|---| |---|---|---|---|---|
| 1 | ключ лежит рядом с шифротекстом | вывод мастер-секрета в декодере, ключ из `master‖salt` | Strings | частично | | 1 | ключ лежит рядом с шифротекстом | вывод мастер-секрета в декодере, ключ из `master‖salt` | Strings | **готово** |
| 2 | переименованы типы, но не члены | rename полей/методов + семейства virtual | Rename | **готово** | | 2 | переименованы типы, но не члены | rename полей/методов + семейства virtual | Rename | **готово** |
| 3 | отладочная информация нетронута | strip + ложный `SourceFile`/номера строк | DebugInfo | план | | 3 | отладочная информация нетронута | strip + ложный `SourceFile`/номера строк | DebugInfo | **готово** (`SourceFile`; номера строк — план) |
| 4 | шаблонный flattening | 4 схемы CFG + мешанина внутри метода | Flow | план | | 4 | шаблонный flattening | 4 схемы CFG + мешанина внутри метода | Flow | план (кадры готовы) |
| 5 | opaque-предикаты = `A+B` | ротация тождеств + предикаты от состояния | Constants | план | | 5 | opaque-предикаты = `A+B` | ротация тождеств + предикаты от состояния | Constants | **готово** (int/long) |
| 6 | junk-классы вне графа reachability | `weave_reachable` — обязательная вплетённость | Noise | план | | 6 | junk-классы вне графа reachability | `weave_reachable` — обязательная вплетённость | Noise | **готово** (без аннотаций) |
| 7 | имена говорят «я защита» | все артефакты получают данные псевдонимы | Rename | **готово** | | 7 | имена говорят «я защита» | все артефакты получают данные псевдонимы | Rename | **готово** |
| 8 | вторичные компоненты без защиты | весь jar, а не только основной класс | Resources | план | | 8 | вторичные компоненты без защиты | весь jar, а не только основной класс | Resources | **готово** |
| 9 | шифр без MAC и без per-string salt | salt на строку, уникальный nonce, MAC | Strings | **готово** | | 9 | шифр без MAC и без per-string salt | salt на строку, уникальный nonce, MAC | Strings | **готово** |
## 1. Ключ рядом с шифротекстом ## 1. Ключ рядом с шифротекстом

View file

@ -17,9 +17,9 @@
| пункт | что это | пасс | статус | | пункт | что это | пасс | статус |
|---|---|---|---| |---|---|---|---|
| A1 | переименование **всех** символов: классы, поля, методы, локальные переменные, параметры, record-компоненты, enum-константы, `SourceFile` | Rename | ✅ готово | | A1 | переименование **всех** символов: классы, поля, методы, локальные переменные, параметры, record-компоненты, enum-константы, `SourceFile` | Rename | ✅ готово |
| A2 | вычистить `LineNumberTable`/`LocalVariableTable`/`SourceFile`/`InnerClasses`/`Signature`/`MethodParameters` | DebugInfo | план | | A2 | вычистить `LineNumberTable`/`LocalVariableTable`/`SourceFile`/`InnerClasses`/`Signature`/`MethodParameters` | DebugInfo | ✅ готово |
| A3 | никаких сигнатурных имён у защитного кода, защитный код смешан с обычным | Rename + Noise | ✅ / план | | A3 | никаких сигнатурных имён у защитного кода, защитный код смешан с обычным | Rename + Noise | ✅ готово |
| A4 | обфусцировать **весь** архив, включая ресурсы, mixin-JSON, access widener, service loader'ы | Resources | план | | A4 | обфусцировать **весь** архив, включая ресурсы, mixin-JSON, access widener, service loader'ы | Resources | ✅ готово |
| — | маппинг в отчёт (иначе не читаем свои же stack trace) | Rename | ✅ готово | | — | маппинг в отчёт (иначе не читаем свои же stack trace) | Rename | ✅ готово |
Это самый дешёвый блок и одновременно **самый большой утечка** в разобранном Это самый дешёвый блок и одновременно **самый большой утечка** в разобранном
@ -30,49 +30,51 @@
| пункт | что это | пасс | статус | | пункт | что это | пасс | статус |
|---|---|---|---| |---|---|---|---|
| B1 | ключ на строку + ротация 3–5 схем + MAC + уникальный nonce | Strings | ✅ схемы готовы | | B1 | ключ на строку + ротация 3–5 схем + MAC + уникальный nonce | Strings | ✅ готово |
| B2 | **вывод** ключа в рантайме, а не константа в вызове | Strings + генератор декодера | ⛔ блокер | | B2 | **вывод** ключа в рантайме, а не константа в вызове | Strings + генератор декодера | ✅ готово |
| B3 | константы — не тождества, а вычисления, разнесённые по блокам | Constants | план | | B3 | константы — не тождества, а вычисления, разнесённые по блокам | Constants | ✅ готово (int/long) |
| B4 | **ложная** отладочная информация: чужие имена переменных и строк | DebugInfo | план | | B4 | **ложная** отладочная информация: чужие имена переменных и строк | DebugInfo | ✅ `SourceFile` |
B2 — главное отличие от разобранного образца: у него ключ был константным B2 — главное отличие от разобранного образца: у него ключ был константным
выражением в вызове и расшифровывался статически, без JVM. Мастер-секрет выражением в вызове и расшифровывался статически, без JVM. Мастер-секрет
должен собираться в рантайме из нескольких мест. Это требует сначала теперь собирается в рантайме из двух частей (`k1 ^ k2`) декодером класса —
собрать класс-декодер (P2) — потому и блокер, а не «сделаем потом». см. [`../stack.md`](../stack.md) §4. B4 пока покрывает только `SourceFile`;
ложные номера строк (`fake_line_numbers`) — см. [`../../todo.md`](../../todo.md) §3.6.
## P2 — контрольный поток ## P2 — контрольный поток
| пункт | что это | статус | | пункт | что это | статус |
|---|---|---| |---|---|---|
| C1 | **несколько схем** флаттенинга вместо одной, с мешаниной внутри метода | план | | C1 | **несколько схем** флаттенинга вместо одной, с мешаниной внутри метода | план |
| C2 | верификатор обязателен: ошибка CFG = `VerifyError` | план | | C2 | верификатор обязателен: ошибка CFG = `VerifyError` | ✅ готово (кадры + `java -Xverify:all`) |
| C3 | реальный объём ≠ раздувание: бюджет на размер и время | план | | C3 | реальный объём ≠ раздувание: бюджет на размер и время | ✅ размер; время — план |
Приоритет здесь строго подчинён: **C2 раньше C1**. Включать Flow без Приоритет здесь строго подчинён: **C2 раньше C1**. Кадры `StackMapTable`
проверки кадров `StackMapTable` — это гарантированный битый jar. и приёмка `java -Xverify:all` уже готовы (см. [`../stack.md`](../stack.md)
Подробности: [`../stack.md`](../stack.md) §3. §3) — единственное, чего не хватает, это сами схемы флаттенинга (C1).
## P3 — измеримость (метрики, без которых ничего не доказано) ## P3 — измеримость (метрики, без которых ничего не доказано)
Обязательный блок, иначе фраза «обфускация есть» ничего не означает: Обязательный блок, иначе фраза «обфускация есть» ничего не означает.
Крейт `noxis-verify` реализован — вот что из списка уже цифра, а что ещё мнение:
1. **Утечки имён.** Список эталонных осмысленных идентификаторов; 1. **Утечки имён.** ✅ `collect_names`/`find_leaks` — сверка идентификаторов
если хоть один остался в `strings`/константах — тест падает. с учётом keep-списка.
2. **Читаемость декомпилята.** Прогон JADX/CFR → доля распознанных структур 2. **Читаемость декомпилята.** ⬜ Прогон JADX/CFR → доля распознанных
(try/catch, switch, циклы), доля классов с восстановленными именами. структур — не начато.
3. **Противник.** Минимальный скрипт-деобфускатор (свёртка констант, 3. **Противник.** ✅ `scan_secrets` + `RED_PATTERNS` держим в репозитории;
regex-паттерны). Если он снимает больше порога — техника провалена. ⬜ полноценный скрипт-деобфускатор (свёртка констант) как тест — план.
**Красный список паттернов держим в репозитории.** 4. **Семантика.** ⬜ Эталонные программы до/после — не начато
4. **Семантика.** Эталонные программы до и после — 100% совпадение. (сейчас семантику подтверждает только `java -Xverify:all`).
5. **Бюджеты.** Размер ≤ N%, время исполнения ≤ M%, отдельно — бюджет 5. **Бюджеты.** ✅ размер (`inflation_ratio`/`check_inflation`);
раскрытия строк (кэш, чтобы не платить на каждый вызов). ⬜ время исполнения — не начато.
6. **Стабильность между сборками.** Имена и схемы **меняются** от билда 6. **Стабильность между сборками.** ✅ `mappings_changed` — маппинги
к билду: у разобранного образца шифр был один на все версии — готовое обязаны отличаться между прогонами с разным seed.
окно для обучения по частотам.
Это блок `noxis-verify`: красные паттерны + `java -Xverify:all` + Итог: `noxis-verify` закрывает красные паттерны, утечки имён,
метрики. Единственное место, где «нам кажется защищённым» заменяется `java -Xverify:all`, бюджет размера и стабильность маппингов. Остаются
цифрой. декомпилят-метрика, бюджет времени и эталонная семантика — три пункта,
где «нам кажется защищённым» всё ещё мнение, а не цифра.
## P4 — долгий горизонт (пока не делаем) ## P4 — долгий горизонт (пока не делаем)
@ -105,12 +107,14 @@ writer).
## Порядок выполнения ## Порядок выполнения
Смотрите [`../../todo.md`](../../todo.md). Кратко: Смотрите [`../../todo.md`](../../todo.md). P0–P3 и P3-измеримость уже
готовы; единственный крупный пункт, который остался, — схемы `Flow` (C1),
потому что верификатор (C2) для них уже есть:
```text ```text
P0 DebugInfo + Resources → снимает главные утечки, почти бесплатно ✅ P0 DebugInfo + Resources → главные утечки сняты
P1 Strings (B2) + Constants → ключ выводится, схемы ротируются ✅ P1 Strings (B2) + Constants → ключ выводится, схемы ротируются
P1 backend (ristretto) + frames ✅ P1 backend (ristretto) + frames
P2 Flow (C1) после верификатора ⬜ P2 Flow (C1) — единственное, что ждёт реализации; верификатор готов
P3 метрики (F) и красные паттерны ✅ P3 метрики, красные паттерны, CLI
``` ```

View file

@ -130,7 +130,10 @@ opaque-предикаты — условия, истинность которы
- Слишком «чистый» класс (`LineNumberTable` есть, а `SourceFile` = `X.java`, - Слишком «чистый» класс (`LineNumberTable` есть, а `SourceFile` = `X.java`,
которого нет) — тоже признак. которого нет) — тоже признак.
**Статус: не реализован.** **Статус: готово** (strip + `spoof_source_file`, детерминированная подмена
от мастер-сида). Не реализовано: `fake_line_numbers` (ложные номера строк,
а не только `SourceFile`) и `strip_source_file` (полное удаление вместо
подмены) — см. [`todo.md`](../todo.md) §3.6.
## 7. Ресурсы (`Resources`) ## 7. Ресурсы (`Resources`)
@ -147,7 +150,10 @@ opaque-предикаты — условия, истинность которы
трогать нельзя. трогать нельзя.
- Промах в патче → тихо неработающий refmap, ловится только запуском. - Промах в патче → тихо неработающий refmap, ловится только запуском.
**Статус: не реализован (крейт `noxis-jar` — план).** **Статус: готово** (патч текстовых ресурсов по маппингу, включая
`fabric.mod.json`/`*.mixins.json`/`META-INF/services/*`; срез подписей
в `noxis-jar`). Не реализовано: типизированный патч бинарных/специфичных
форматов (refmap, access widener) сверх общего boundary-replace.
## Сводная таблица ## Сводная таблица
@ -155,11 +161,11 @@ opaque-предикаты — условия, истинность которы
|---|---|---|---|---| |---|---|---|---|---|
| Rename | ★★★★★ | низкий | низкая | готово | | Rename | ★★★★★ | низкий | низкая | готово |
| Strings | ★★★★ | средний | средняя | готово | | Strings | ★★★★ | средний | средняя | готово |
| Resources | ★★★★★ | низкий | низкая | план | | Resources | ★★★★★ | низкий | низкая | готово |
| DebugInfo | ★★★ | низкий | низкая | план | | DebugInfo | ★★★ | низкий | низкая | готово |
| Constants | ★★ | низкий | низкая | готово (int/long) | | Constants | ★★ | низкий | низкая | готово (int/long) |
| Noise | ★ | низкий | раздувание | готово (без аннотаций) | | Noise | ★ | низкий | раздувание | готово (без аннотаций) |
| Flow | ★★★★ | **высокий** | высокая | план, ждёт frames | | Flow | ★★★★ | **высокий** | высокая | план, кадры готовы |
Вывод, который стоит держать в голове: **Rename + Resources + DebugInfo** Вывод, который стоит держать в голове: **Rename + Resources + DebugInfo**
дают большую часть защиты за небольшую цену. Flow — единственная техника, дают большую часть защиты за небольшую цену. Flow — единственная техника,