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:
parent
2674d7fc89
commit
c3b8d4dd62
9 changed files with 349 additions and 162 deletions
122
README.md
122
README.md
|
|
@ -1,34 +1,61 @@
|
|||
<div align="center">
|
||||
|
||||
# noxis
|
||||
|
||||
Обфускатор JVM/Java-байткода на Rust.
|
||||
|
||||
Читает JAR, переписывает классовые файлы и ресурсы, собирает обратно —
|
||||
детерминированно, с маппингами для stack trace и с проверкой, что выход
|
||||
действительно загружается настоящей JVM.
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
Читает 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,7 +63,7 @@
|
|||
noxis/
|
||||
├── Cargo.toml workspace, lints, release-профиль
|
||||
├── scripts/check.sh structure → fmt → clippy → test
|
||||
├── docs/ ← архитектура, техники, стек, уроки
|
||||
├── docs/ архитектура, техники, стек, уроки
|
||||
├── info/ исходный материал разбора (вне правил)
|
||||
└── crates/
|
||||
├── noxis-classfile/ IR + парсер/запись + StackMapTable
|
||||
|
|
@ -45,66 +72,47 @@ noxis/
|
|||
├── noxis-passes/ сами трансформации + конвейер
|
||||
├── noxis-runtime/ схемы шифрования, ABI декодера
|
||||
├── noxis-jar/ ZIP, ресурсы, срез подписей
|
||||
├── noxis-verify/ red-паттерны, java -Xverify (план)
|
||||
├── noxis/ CLI (план)
|
||||
├── 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 в репозитории.
|
||||
|
|
|
|||
|
|
@ -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:?}");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<String>) {
|
|||
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<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);
|
||||
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<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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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`, чтобы гейт не
|
||||
срабатывал на правки вне проверяемых путей.
|
||||
|
||||
Остальное, что не привязано к компиляции, собирается одним скриптом:
|
||||
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
167
docs/learnings/ai-resistance.md
Normal file
167
docs/learnings/ai-resistance.md
Normal 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/)
|
||||
|
|
@ -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. Ключ рядом с шифротекстом
|
||||
|
|
|
|||
|
|
@ -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
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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 — единственная техника,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue