Commit graph

40 commits

Author SHA1 Message Date
1ddc4afbfc
examples: add frame_repro - recompute-frames repro over an untouched jar 2026-10-15 12:53:22 +02:00
a4450b69ba
classfile: fix condy ldc category (ldc_w vs ldc2_w by descriptor), split attrs tests 2026-10-15 12:53:22 +02:00
6dd9c220d4
namegen: reword plausible-style comment to not cite local decompiled paths 2026-10-15 12:53:22 +02:00
d19e4f784d
deps: drop regex, toml, indexmap - std-only minirx, toml_lite, inferred IndexMap 2026-10-15 12:53:22 +02:00
59637e50dc
verify: add adversary module - generic constant-folding script
noxis-verify::adversary::fold_constants walks maximal straight-line
Const(Int/Long)+Binary{Int,Long} runs, modeling the baseline attack
any deobfuscator runs first (and the one raised on the forum: two
adjacent literals and one op, foldable in your head). Each stack slot
carries its own provenance span, so several split constants placed
back to back without a separator are reported as separate findings
instead of merging into one run.

Two uses: an honest §7 measurement (answer: every Constants scheme,
mba included, folds under a generic evaluator - only pattern matchers
hardcoded to one opcode are defeated, documented already in
constants/forms.rs) and a correctness oracle (every scheme must fold
back to the exact source value, now pinned by a regression test).

Tests: bare_literals_are_not_folded,
every_constants_scheme_folds_back_to_the_source_value.
2026-10-14 01:35:21 +02:00
2b19ed6096
passes: add mba scheme to Constants (and+or+add identity)
(a&b)+(a|b) = a+b, documented honestly: falls to the same symbolic
evaluator as xor/add/shift (every sub-expression is a foldable
LDC,LDC,BINOP triple), but defeats a pattern matcher hardcoded to one
opcode ("ldc,ldc,ixor = split constant"). Added to ConstantsConfig
default schemes.

Tests: mba_form_uses_and_or_add_not_a_single_binop, round-trip coverage
in int_forms_round_trip/long_forms_keep_shape.
2026-10-14 01:30:10 +02:00
550f62e473
docs: drop stale learnings notes (lessons/priorities/reference) 2026-10-13 13:28:03 +02:00
1ebbca10fa
analysis: lock mixin classes and plugin again (revert mixin rename)
Renaming mixin classes and patching mixins.json (90506a4) breaks Fabric
mods at runtime, proven on LoVisual (2026-10-13):

- When obfuscated packages are randomized per class the config patcher
  computes an empty common prefix and writes `package: ""`. Fabric/Mixin
  then drops every mixin as "orphaned", so `RenderPipelineMixin
  implements IRenderPipeline` never applies, the interface is not
  injected into `RenderPipeline`, and the entrypoint dies with
  `ClassCastException: RenderPipeline cannot be cast to IRenderPipeline`.
- `IMixinConfigPlugin.shouldApplyMixin` compares the runtime mixin class
  name against string literals (`Set.of("dev.loki...Mixin")`); renaming
  desyncs those sets and optional sodium/iris/xaero mixins get applied
  when the target mod is absent.

Re-enable absorb_mixins: mixin entry classes and the config plugin keep
their names, and locking a class freezes its members too (needed for
@Accessor/@Invoker/@Overwrite name lookups). Calls from locked mixin
bytecode are still rewritten consistently, and the family mechanism keeps
interface method names (IRenderPipeline) in sync with the frozen mixin
methods. patch_mixins_json stays but is identity for locked classes.

Verified: LoVisual obf jar loads all 221 mixins (0 orphaned), the main
menu renders, the entrypoint runs.
2026-10-13 13:18:13 +02:00
01afa6af37
jar: pass module-info.class through as a raw resource
JPMS descriptors are never loaded by a class loader; parsing one and
rebuilding its pool left the unmodeled Module attribute with stale
indices, and ASM died in readModule on the rewritten jar (caught by the
ASM probe on Loreport, which ships META-INF/versions module-infos).
Byte-for-byte resource passthrough keeps the descriptor valid.
2026-10-13 10:23:19 +02:00
15cd31252e
classfile,passes: model RuntimeVisibleTypeAnnotations and AnnotationDefault
Type annotations (method- and code-level) and annotation defaults were
passed through as Raw bytes with the original pool indices; the writer
rebuilds the pool, so those indices pointed at the wrong utf8 or past
the pool end. HotSpot tolerates the garbage (lazy reflection parsing),
but ASM's ClassReader dies with ArrayIndexOutOfBoundsException while
Paper's plugin converter reads every method - 76 of 275 LoParkour
classes failed to convert. Target_info and type_path carry no pool
references and stay opaque bytes; type descriptors and element values
are modeled and rewritten by rename like plain annotations.
2026-10-13 10:17:10 +02:00
3f92b72c8b
remove stray luac.out 2026-10-12 14:19:46 +02:00
90506a4736
passes: rename mixin classes + patch mixins.json/refmap like OBF
- Remove absorb_mixins keep rule (mixin classes now renameable)
- Add mixins.rs: patch_mixins_json rewrites package + entries via
  common prefix of mapped names; patch_refmap_json rewrites all strings
- Resources pass routes *.mixins.json and *refmap.json to specialized
  patchers, falls back to generic text remap

Reference: OBF fabric-obfuscator patchMixinsJson/patchRefmapJson.
2026-10-12 14:18:19 +02:00
b73c72d56d
classfile: model RuntimeVisible/InvisibleParameterAnnotations (fixes ASM AIOOBE on write)
Parameter annotations were unmodeled and fell through to Raw with stale
constant-pool indices after the pool rebuild. ASM (Paper converter, Fabric
mixin transformer) then failed with ArrayIndexOutOfBoundsException in
readParameterAnnotations. Now parsed and rewritten like class-level
annotations.
2026-10-12 13:19:12 +02:00
045cd3ed9c
passes: gate step-2 renames on the virtual family index (fixes override-chain split) 2026-10-12 10:38:55 +02:00
2069aa6646
analysis: drop shaded-package lock heuristic (superseded by the family-split fix) 2026-10-12 10:38:55 +02:00
3709d3ad35
analysis: lock shaded reflective packages (gson TypeAdapter-style dispatch) 2026-10-11 13:42:08 +02:00
48a5bc23a9
classfile,passes: model runtime-visible annotations, rewrite their descriptors 2026-10-11 13:02:26 +02:00
4b14eeb40f
classfile: writer honors itf flag for invokestatic on interfaces 2026-10-11 12:24:50 +02:00
5901c9eb69
passes: rename rewrites EnclosingMethod and array-owner method refs 2026-10-11 11:22:11 +02:00
8fcecee849
passes: rename rewrites invokedynamic descriptors and BootstrapMethods refs 2026-10-11 10:34:42 +02:00
9a33cef9b1
classfile,passes,jar: model PermittedSubclasses, normalize interface flags, name class in write errors 2026-10-10 14:22:03 +02:00
e9d5e44b9e
verify,cli: jadx decompile-quality metric with analyze --decompile 2026-10-10 13:32:23 +02:00
2655f83d3c
passes: generic signatures rewrite nested references; rename tests split 2026-10-10 13:13:58 +02:00
63630dd223
frames: JDK hierarchy fallback, array-operand rewrite, aaload fixes; LoVisual 131 to 0 FAIL 2026-10-10 13:00:58 +02:00
1c417f596c
classfile: aaload on 1-dim ref arrays yields element type; passes: minimizer example 2026-10-10 11:47:06 +02:00
21a7c3df1a
analysis: keep-lock mixin classes and plugins from mixins.json configs 2026-10-10 10:02:38 +02:00
6a68c9d69b
passes,core: debug fakes - line numbers and source removal
DebugConfig gains fake_line_numbers (deterministic upward walk
replacing LineNumberTable rows when stripping is off; strip wins)
and strip_source_file (drops SourceFile entirely, beating spoof).
Debug tests moved to passes/tests/debug_info.rs with new cases
for both flags and their precedence.
2026-10-09 14:08:08 +02:00
2cbb9db31e
passes: junk classes with real bodies and no fingerprint
NoisePass now builds branching helper methods (counted int loop,
random arithmetic, even/odd merge, non-throwing ops only) whose
results the entry sums into a private static field, instead of an
empty run()V. Names come from the same NameGenerator styles as
real code and live in a random real package (no more shadow/decoy
uniformity for grep to catch); every generated method gets fresh
StackMapTable frames. weave_first_reachable takes (class, entry)
pairs. New NoiseConfig.junk_methods_per_class (default 3).

Tests assert invariants (unique valid identifiers, calls, field
store, determinism) plus real-JVM verification of junk classes;
paranoid+junk_ratio=1 smoke on lorep: 18 junk classes, all load.
2026-10-09 14:04:24 +02:00
74ad4552e4
analysis,verify: databind field lock and locked-class share
KeepRules::absorb_databind finds X.class literals before Gson /
Jackson / fastjson2 / Moshi calls and locks every field of X
(reflection-bound names never reach the string-based detection).
The class itself still renames. Test in analysis/tests/dto.rs;
keep unit tests moved to tests/keep.rs.

ProgramStats gains locked_classes + locked_share() so analyst-bait
source names are loud in the report instead of surfacing later.
2026-10-09 13:53:30 +02:00
c3acc0baf4
analysis: lock DTO fields passed as X.class to databind calls
Gson/Jackson/fastjson2/Moshi bind fields by reflection from a Class
literal, with no field-name string for the string-based detection:
renaming such a field silently breaks deserialization. Absorb_databind
finds the nearest Const::Class before fromJson/readValue/parseObject/
adapter calls (10-insn window, control-flow boundaries stop the search)
and keeps every field of that class; the class itself still renames.

Tests: noxis-analysis/tests/dto.rs (red first), keep unit tests moved
to tests/keep.rs (file budget). Code comments migrated to English.
2026-10-09 13:51:35 +02:00
139aea9a8f
frames,jar,passes: verifier-driven hardening from real jars
Proven by paranoid runs over lorep (17 classes, fully green),
LoVisual (4081 classes) and oc2r (1278 classes) with per-class
java -Xverify:all, each fix first reproduced as a red test:

- merge: Top absorbs instead of Top-wins (javac declares Top for
  slots unwritten on some inflow; precise types reject those edges).
  Regression: loop_head_keeps_top_for_loop_local.
- emit: collapse [Long|Double, Top] pairs on operand stacks
  (javac writes one entry; the pair is rejected). Regression:
  wide_stack_join_emits_single_entries.
- xfer ArrayLoad: push the wide pair, fixing lcmp underflow.
  Regression: array_load_wide_pushes_pair.
- xfer <init>: convert only the receiver's own allocation site
  instead of every Uninitialized (nested construction clobbered
  the outer object). Regression: nested_construction_keeps_outer.
- xfer ANewArray: array operands gain one dimension ([B] -> [[B)
  instead of an illegal L-wrap ([L[B;). Covered by the same test.
- jar load: duplicate class names (multi-release module-info
  twins) keep the first occurrence instead of crashing save.
  Test: duplicate_class_names_keep_first.
- strings/constants: skip methods past MAX_METHOD_INSNS (8000)
  so giants cannot overflow the 64KB writer ceiling. Tests:
  oversized_methods_are_skipped in both passes.
- merge errors now name both types being merged.

Also: em-dash removed repo-wide (724 spots, rule added to
todo.md), allow-lints only in tests (scratch example deleted),
code comments migrated to English in every file this change
touches.
2026-10-09 13:47:27 +02:00
2172206e83
passes: implement control-flow pass (4 schemes + rotation)
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.
2026-10-09 11:11:55 +02:00
39178420e4
structure-check: gate counts code lines, docs and comments in English
Gate ignores blank and full-line comment lines so the 250-line limit
measures code bulk, not documentation weight; docs/README.md synced.
Translate code comments to English in classfile, core config, namegen,
rename assign, constants forms. Expand Plausible word bank with
client-ecosystem terms (Module, Setting, Mixin, ...). cargo fmt clean,
check.sh green.
2026-10-09 10:31:02 +02:00
ec28ec82e3
gitignore: stop tracking todo.md, keep it local-only 2026-10-08 23:12:37 +02:00
f08bf60635 classfile: intern constant-pool entries on write (fix LVTT/LVT ClassFormatError)
ristretto's ConstantPool never deduplicates: every add_* pushes a new entry.
HotSpot matches each LocalVariableTypeTable entry to a LocalVariableTable
entry by raw name_index, so when a name (e.g. a local's) was interned twice
the two tables got different indices and the class failed to load:
'ClassFormatError: LVTT entry ... does not match any LVT entry'.

The writer now memoizes every pool builder by its semantic key and rebuilds
composite constants (Class/NameAndType/Fieldref/Methodref/MethodHandle/
Dynamic) from our own utf8/class primitives so ristretto never mints a
shadow Utf8. This also shrinks the pool (no more ldc -> ldc_w widening that
changed code length on a pure round-trip).

Split pool.rs into a pool/ module (refs in pool/refs.rs) to respect the
file-size gate. Regression test: tests/pool asserts the written pool has no
duplicate CONSTANT_Utf8. Verified: pure parse->write of the neoforge jar
(92 classes) now loads under java -Xverify:all with 0 errors.
2026-10-08 22:55:49 +02:00
a2e4c69210 classfile,core: pick frame compact form by byte delta; honor --no-* flags
StackMapTable compact-vs-extended selection used the instruction delta,
but JVM validity requires the byte offset delta <= 63. Track byte offsets
in convert_frames and branch on byte_delta while keeping the instruction
delta stored (ristretto re-derives bytes).

Pipeline::run ignored config.disable, so --no-* CLI flags had no effect;
gate each pass on config.is_enabled.
2026-10-08 22:39:22 +02:00
c3b8d4dd62 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
2026-10-08 22:34:00 +02:00
2674d7fc89 classfile,passes: model Nest attributes as IR (fix ClassFormatError)
NestHost/NestMembers were carried as Attribute::Raw with constant-pool
indices baked in. During writing the pool is rebuilt in a different order,
so those indices pointed at the wrong entries and the JVM rejected the
class: ClassFormatError: Nest member/host class_info_index N has bad
constant type.

Model them explicitly in the IR, convert on parse and write (resolving
class names through the new pool), and remap their class names during
rename. Add a NestMembers round-trip regression test. Verified on a real
jar: all Nest ClassFormatErrors gone.
2026-10-08 22:26:58 +02:00
8b107cf1b3 Fix formatting and example lint after shroud->noxis rename
- cargo fmt --all: shorter crate identifiers reflowed `use {...}` lists
- noxis/examples/dbg.rs: allow unwrap_used/expect_used (scratch harness),
  add module doc
2026-10-08 22:04:02 +02:00
a641e69455 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.
2026-10-08 22:01:03 +02:00