FlowPass rebuilds one method per scheme picked from FlowConfig: state-spread (dense tableswitch dispatcher), computed-switch (sparse lookupswitch over a counter local), butterfly (branch inversion), exception-goto (dispatcher under try with a dead catch-all), plus opaque goto-splices inside every method and StackMapTable recomputation afterwards. Now in standard_pipeline (between constants and noise); --no-flow is an honest off-flag. Acceptance is real JVM, not shapes: per-scheme verify, rotation spread, calc-prints-45 execution, paranoid smoke on lorep-1.1.0 (131 methods rebuilt, no new failures over standard). Two verifier rules learned the hard way are now construction invariants: no inserted mark shares a bytecode offset with a foreign one (nop pads), and no frameless gap follows a terminal (dead blocks are opaque-entered and throw). Also fixes a latent frame-emitter bug the pass exposed: wide stack values were written as slot pairs ([Double, Top]) while javac writes one entry ([Double]); HotSpot rejects the pair. Stacks now collapse like locals (emit::stack_vars), with a regression test in classfile/tests/frames.rs.
8.3 KiB
8.3 KiB
Архитектура
Слои
┌──────────────────────────┐
CLI (noxis) │ clap → конфиг → отчёт │
└────────────┬─────────────┘
│
┌─────────────────────┼─────────────────────┐
▼ ▼ ▼
┌─────────────┐ ┌──────────────┐ ┌──────────────┐
│ noxis-jar │ │noxis-passes │ │noxis-verify │
│ ZIP, ресурсы│ │ трансформации│ │ red-паттерны │
└──────┬──────┘ └──────┬───────┘ └──────┬───────┘
│ │ │
│ ┌──────┴───────┐ │
│ │ noxis-core │ │
│ │ конфиг, seed │ │
│ │ маппинги, Pass│ │
│ └──────┬───────┘ │
│ │ │
▼ ▼ ▼
┌────────────────────────────────────────────────────────┐
│ noxis-classfile (IR) │
│ Program → ClassFile → Code → Insn (метки, не смещения)│
│ model/ format/ insn error │
└────────────────────────────────────────────────────────┘
▲
┌──────┴──────┐ ┌───────────────┐ ┌──────────────────┐
│noxis- │ │noxis-runtime │ │noxis-structure- │
│analysis │ │ шифры, ABI │ │check (build-time)│
└─────────────┘ └───────────────┘ └──────────────────┘
Зависимости направлены только вниз. Циклов нет: сборка конвейера живёт в
noxis-passes, потому что noxis-core знает только контракт Pass.
Крейты
| крейт | ответственность |
|---|---|
noxis-classfile |
собственный IR классовых файлов, чтение/запись, StackMapTable |
noxis-core |
конфигурация, детерминированный seed, маппинги, контекст и контракт Pass |
noxis-analysis |
иерархия наследования, entrypoint'ы, рефлексия по имени, keep-правила |
noxis-passes |
сами трансформации + сборка стандартного конвейера |
noxis-runtime |
схемы шифрования строк и ABI класса-декодера |
noxis-jar |
ZIP: чтение, перепаковка, вырезание подписей, патч ресурсов |
noxis-verify |
red-паттерны, java -Xverify:all, метрики |
noxis |
CLI |
noxis-structure-check |
автогейт структурных правил (см. README.md) |
Собственный IR
Пассы не работают с API стороннего парсера — они работают с моделью:
Program все классы + ресурсы архива
├─ ClassFile версия, флаги, this/super, поля, методы, атрибуты
│ ├─ Field
│ └─ Method
│ └─ Code max_stack/max_locals, insns, exception_table, frames
└─ Resource путь + байты внутри архива
Ключевое решение — Insn::Mark(Label) вместо байтовых смещений:
- любой пасс, меняющий порядок или длину инструкций, не пересчитывает jump-офсеты;
- пересчёт делается ровно один раз — в writer'е;
- в IR нет состояния, в котором «смещение уехало, а метка осталась».
Атрибуты, которые мы не понимаем, хранятся как Attribute::Raw { name, data }
и возвращаются на место без изменений — round-trip без потерь.
Контракт пасса
pub trait Pass: Send + Sync {
fn id(&self) -> PassId;
fn apply(&self, ctx: &mut Context, program: &mut Program)
-> Result<PassReport, CoreError>;
}
Context— всё состояние прогона: конфиг, мастер-сид, маппинги, заметки. Из него берётся RNG:ctx.rng("rename/classes")— доменное разделение, чтобы случайности разных пассов не были статистически коррелированы.PassReport— что трансформировано, что сознательно пропущено и почему.Report— собирает отчёты всех пассов + счётчики, сериализуется в JSON.
Детерминированность — свойство, а не пожелание: одинаковый seed на одинаковой версии даёт байт-в-байт одинаковый выход. Это проверяется тестами.
Конвейер
Порядок пассов — не деталь:
Rename → Resources → Strings → Constants → Flow → Noise → DebugInfo
почему именно так:
- Rename первым — все остальные техники порождают новые ссылки
(
LDC-шум, вызовы декодера, junk-классы), и они уже должны быть в обфусцированных именах. - Resources сразу после Rename — патч
fabric.mod.json,*.mixins.json, refmap и access widener должен видеть итоговые имена классов. - Strings и Constants — работают по телам методов, ещё не искажённым control-flow'ем.
- Flow — искажает CFG. Если поставить его раньше, Strings/Constants перестанут находить свои шаблоны в инструкциях.
- Noise — junk-классы и аннотации добавляются в самом конце, когда целевой граф уже финален.
- DebugInfo последним — strip/spoof отладочной информации после всех
трансформаций, чтобы не породить свежие
LineNumberTable.
Статус
| этап | что |
|---|---|
| готово | 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 (4 схемы + ротация + мешанина, приёмка настоящим java) |
| план | фикстуры и regression-тесты |
Детали выбора — stack.md, план — ../todo.md.