Initial commit: noxis JVM bytecode obfuscator
Workspace of 9 crates (noxis, noxis-core, noxis-classfile, noxis-analysis, noxis-passes, noxis-runtime, noxis-jar, noxis-verify, noxis-structure-check). AGPL-3.0-only license, Rust 2024 edition, unsafe_code denied.
This commit is contained in:
commit
a641e69455
140 changed files with 17197 additions and 0 deletions
185
info/evo-improvements.md
Normal file
185
info/evo-improvements.md
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
# Как сделать лучше: план улучшений для нашего обфускатора
|
||||
|
||||
Привязка: референс — EvoVis (`evo-obfuscation-full.md`), провалы — `evo-weaknesses.md`.
|
||||
Ниже — что делаем иначе, в порядке отдачи/цены. Для каждой техники — уровень угрозы,
|
||||
которую она закрывает (скрипт-деобфускатор / статический анализ / ручной реверс).
|
||||
|
||||
---
|
||||
|
||||
## A. Базовый уровень (делаем обязательно, дёшево)
|
||||
|
||||
### A1. Обфускация всех символов, не только типов
|
||||
- Имена классов, **полей, методов, локальных переменных, параметров**, конструкторов,
|
||||
record-компонентов, enum-констант, label'ов, атрибутов `SourceFile`.
|
||||
- Индивидуальный алфавит на каждый символ (см. B1), запрет ключевых слов и
|
||||
одинаковых имён в одном scope.
|
||||
- Никаких «readable» ярлыков: у EvoVis `@rn(name=…)` и `description` отдали каталог
|
||||
модулей целиком. Если метаданные нужны рантайму — шифруем их (C2).
|
||||
|
||||
### A2. Вычистить отладочную информацию
|
||||
- `-g` → `LineNumberTable` / `LocalVariableTable` / `SourceFile` / `InnerClasses`
|
||||
/ `EnclosingMethod` / `MethodParameters` / `Signature` — по policy: либо полностью
|
||||
выкинуть, либо заменить на шум (см. B4).
|
||||
- Ресурсы тоже часть кода: mixin-JSON, `fabric.mod.json`, access widener, service
|
||||
loaders, SPI-файлы — шифруем/шифруем имена внутри, раскрываем в рантайме.
|
||||
|
||||
### A3. Никаких сигнатурных имён у защитного кода
|
||||
`KeyAssembler`, `RuntimeGuard`, `NativeGuard` → рандомные имена, а сам защитный код
|
||||
**смешиваем с обычным** (секции, которые выглядят как мусорные утилиты).
|
||||
Рефлексия по именам в открытом виде (`Class.forName("aln")`) — запрещена:
|
||||
имена отдаются только из зашифрованной строки (C2), лучше — через indirection-таблицу.
|
||||
|
||||
### A4. Обфусцировать весь артефакт, а не только ядро
|
||||
Правило: **нет незащищённого компонента**. У EvoVis лаунчер и java-agent были
|
||||
открыты целиком. Пайплайн один на: основной код, агент/плагин, лаунчер/хостер,
|
||||
нативная часть, ресурсы.
|
||||
|
||||
---
|
||||
|
||||
## B. Строки и константы
|
||||
|
||||
### B1. Пер-строчный ключ + нелинейный шифр
|
||||
Вместо общего LCG:
|
||||
- seed = `random_u64` на строку (храним неявно: derive из позиции/ид + master-seed);
|
||||
- алгоритм выбирается из набора (3–5 схем: RC4-like, chacha-с fragments, XTEA-блочный,
|
||||
plain XOR с keystream от BLAKE3, base85+flate перед шифром) — **перемешивание схем
|
||||
между строками**, чтобы один скрипт не покрыл всё;
|
||||
- AAD/MAC: `HMAC(fragment, plaintext_hash)` — повреждение ловится, подмена — тоже;
|
||||
- keystream не повторяется: nonce включает индекс строки.
|
||||
- **против обучения по частотам**: словарь/частотный анализ не работает, если схема
|
||||
и nonce разные.
|
||||
|
||||
### B2. Ключ строки выводится в рантайме, а не лежит рядом
|
||||
Никаких `d(cipher, literal_key)`:
|
||||
- ключ = `KDF(master_key, class_id, method_id, string_index)` либо приходит из
|
||||
защищённого контекста (см. D3);
|
||||
- вычисление — через серию операций, **растворённых в теле вызывающего метода**
|
||||
(не выделенная функция `decrypt(s)` — иначе её найдут и развернут);
|
||||
- мастер-секрет не в константах: собирается из нескольких мест + env/хост-фактора.
|
||||
|
||||
### B3. Константы — не тождества, а вычисления
|
||||
Отказ от `(A^B)+((A&B)<<1)` (опознаётся regex'ом). Вместо этого:
|
||||
- константа собирается из **нескольких узлов**, разнесённых по разным basic block'ам
|
||||
и разным методам (возврат частей, сложение на месте использования);
|
||||
- включаем в цепочку **зависимые от окружения** величины, которые статически вычислимы
|
||||
не полностью: `hashCode()` строки-литерала, `System.identityHashCode` статической
|
||||
константы, значение поля, инициализируемого в `<clinit>`, размер массива;
|
||||
- для `int`/`long`/`float`/`double` — отдельные схемы (bit-cast, разложение по
|
||||
мантиссе), потому что `double`-константы особенно заметны.
|
||||
- opaque-предикаты делать **настоящими**: всегда истинные, но зависящие от состояния,
|
||||
которое статически не выводится (напр. инвариант «массив отсортирован по заполнению»,
|
||||
проверка `System.nanoTime()`-мьютекса, сравнение двух полей, всегда равных после
|
||||
`<clinit>`, но не доказуемых в CFG).
|
||||
|
||||
### B4. Ложная отладочная информация
|
||||
Не только удалить `LineNumberTable`/`LocalVariableTable`, но и **подложить
|
||||
дезориентирующие**: ложные имена локальных переменных, строки из другого проекта,
|
||||
`SourceFile` с вводящим именем. Инструменты (jadx/CFR) и люди верят отладочке сильнее,
|
||||
чем байткоду.
|
||||
|
||||
---
|
||||
|
||||
## C. Контрольный поток
|
||||
|
||||
### C1. Несколько схем флаттенинга вместо одной
|
||||
Минимум набор (применять **случайно и по-разному в разных методах**):
|
||||
1. **Switch по вычислимому индексу**: `table[ (x*31 ^ y) & mask ]` → адрес возврата;
|
||||
переходы через индексы, а не метки.
|
||||
2. **Распределённое состояние**: как у EvoVis (`i + i2`), но одно слагаемое —
|
||||
из поля объекта/статики, второе — из параметра, обновление — через вызов метода;
|
||||
**метки case не константные**, а пересчитанные (opaque).
|
||||
3. **Бинарное дерево/бабочка**: вместо линейного списка состояний — рандомизированный
|
||||
граф переходов, эквивалентный исходному CFG (проверяется верификатором).
|
||||
4. **Вызов через стек/ретурн-адрес**: эмуляция `goto` через `athrow`-как-прыжок
|
||||
(exception-as-goto), `finally`-цепочки, или virtual-dispatch по фейковому интерфейсу.
|
||||
5. **Прячем тела блоков**: каждый блок зашифрован и раскрывается по очереди
|
||||
(см. D2), чтобы деобфускатор не мог читать тело, не эмулируя VM.
|
||||
|
||||
### C2. Обфускация CFG должна сохранять семантику → нужен верификатор
|
||||
Условие приёмки: **анализ эквивалентности до/после** (структурный: разбор в SSA,
|
||||
сравнение支配-графов; либо выполнение тестов на эталонных программах).
|
||||
Плюс автоматические метрики (см. F).
|
||||
|
||||
### C3. Реальный объём ≠ раздувание
|
||||
У EvoVis выигрыш — только в размере декомпилята, но и цена (читаемость для нас же).
|
||||
Оптимальнее: комбинировать обфускацию с **компиляцией в другой целевой код**
|
||||
(см. E), где раздувание не надо — там и так нечем читать.
|
||||
|
||||
---
|
||||
|
||||
## D. Уровень «нужно эмулировать, а не читать»
|
||||
|
||||
### D1. Маленький VM / interpreter для критичных кусков
|
||||
Свой bytecode (10–20 опкодов: арифметика, ветвление, вызов, доступ к полю),
|
||||
инструкции в зашифрованном массиве, диспетчер — обычный код с обфусцированным CFG.
|
||||
Экспортируем в VM только чувствительное: лицензирование, ключи, критическую логику.
|
||||
Это единственный уровень, который реально останавливает ручной реверс.
|
||||
|
||||
### D2. Раскрываемое по шагам тело (split-function / function-splitting)
|
||||
Метод делится на фрагменты (по basic block'ам), каждый зашифрован отдельно,
|
||||
порядок выполнения задаётся таблицей. Деобфускатор обязан строить CFG, прежде чем
|
||||
увидит хоть строку.
|
||||
|
||||
### D3. Анти-анализ в рантайме (мягкий)
|
||||
- self-check целостности: `SHA-256` своих классов/секций → при расхождении
|
||||
**меняем поведение**, а не падаем (у EvoVis падение выдавало мусорный ключ —
|
||||
правильно: не сообщать атакующему, что он пойман);
|
||||
- детект отладчика/трейсера/инструментов эмуляции — реакция: ложные данные,
|
||||
замедление, отключение функций;
|
||||
- **не детектить и не ломать легитимные сценарии** (обновления, IDE, символы) —
|
||||
ложные срабатывания дороже, чем польза защиты.
|
||||
|
||||
### D4. Честный anti-tamper, а не «зашейрь ключ»
|
||||
У EvoVis сильная идея: ключ = изолированная доля в подменённом JVM + HMAC от hwid +
|
||||
порча при детекте. Минус — вся схема работает только на своём окружении и падает
|
||||
на другой ОС/сборке. Для нас: секрет должен переживать **наши** легитимные платформы,
|
||||
а ломаться только при модификации кода/подмене целостности.
|
||||
|
||||
---
|
||||
|
||||
## E. Если цель — не JVM, а свой натив/Rust-бинарник
|
||||
|
||||
Референс JVM-слабости здесь не работают автоматически, но появляются свои:
|
||||
- **строки**: все plaintext-строки в `.rodata` — шифровать, раскрывать лениво;
|
||||
не держать AES-ключ рядом с шифротекстом (та же ошибка, что у `awc.d`);
|
||||
- **имена**: `stripped` бинарник + рандомизация символов; в Rust — макрос, который
|
||||
переименовывает `pub`-символы и убирает `#[doc]`/Debug-имена;
|
||||
- **panic/Debug-вывод**: `{:?}` отдаёт имена типов и полей — для чувствительных типов
|
||||
свой `Debug`, либо не выводить;
|
||||
- **CFG**: обфускация на уровне MIR/LLVM-IR (passes): bogus control flow, flattening
|
||||
на `switch` по вычисленному индексу, вставка недостижимых блоков, размытие
|
||||
границ функций (inlining + outlined);
|
||||
- **константы**: как B3, плюс вынос в вычисляемые таблицы;
|
||||
- **анти-анализ**: `ptrace`-selfcheck, проверка `/proc/self/maps` на свои секции,
|
||||
timing-checks; реакция — молчаливая порча данных;
|
||||
- **обфускация метаданных Rust**: `TypeId`/`Any`, `std::any::type_name`, логи
|
||||
`tracing` — выключать в release.
|
||||
|
||||
---
|
||||
|
||||
## F. Метрики качества (обязательно, иначе «обфускация есть» ничего не значит)
|
||||
|
||||
Автотесты на каждую технику, гоняются в CI:
|
||||
|
||||
1. **Утечки имён**: `strings`/анализ констант — не должно остаться ни одного
|
||||
осмысленного идентификатора из списка-эталона (ошибка = фейл).
|
||||
2. **Читаемость декомпилята**: прогнать JADX/CFR на собранный артефакт →
|
||||
метрики: доля распознанных структур (try/catch, switch, циклы), доля классов
|
||||
с восстановленными именами, число строк «смыслового» кода.
|
||||
3. **Автоматический деобфускатор-противник**: минимальный скрипт-анализатор
|
||||
(тот же `expr`-свёртка, regex на паттерны) — если он снимает >X% строк/констант,
|
||||
техника провалена. Держим этот «красный список паттернов» в репозитории.
|
||||
4. **Семантика**: тесты/эталонные программы до и после — 100% совпадение.
|
||||
5. **Размер и скорость**: бюджет (≤ N% размера, ≤ M% времени исполнения),
|
||||
отдельно — бюджет на рантайм-раскрытие строк (кэш, чтобы не платить на каждый вызов).
|
||||
6. **Стабильность между сборками**: имена/схемы меняются от билда к билду
|
||||
(у EvoVis шифр один на все версии — окно для обучения).
|
||||
|
||||
## Приоритет
|
||||
|
||||
1. A1+A2+A4 (символы, отладка, всё артефакты) — дёшево, biggest leak у EvoVis.
|
||||
2. B1+B2 (пер-строчные ключи, вывод ключа) — снимает главный провал `awc.d`.
|
||||
3. C1 несколько схем флаттенинга + C2 верификатор.
|
||||
4. F метрики — чтобы не спорить на глаз.
|
||||
5. C3/E (другой целевой код) — как долгосрочный путь.
|
||||
6. D1 VM — только для действительно ценных кусков, остальное не оправдывает цену.
|
||||
Loading…
Add table
Add a link
Reference in a new issue