diff --git a/crates/noxis-core/src/config/sections.rs b/crates/noxis-core/src/config/sections.rs index 8379889..d11ec33 100644 --- a/crates/noxis-core/src/config/sections.rs +++ b/crates/noxis-core/src/config/sections.rs @@ -124,7 +124,8 @@ impl Default for FlowConfig { #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(default, deny_unknown_fields)] pub struct ConstantsConfig { - /// Splitting schemes (`xor`, `add`, `shift`), picked by RNG per constant. + /// Splitting schemes (`xor`, `add`, `shift`, `mba`), picked by RNG per + /// constant. pub schemes: Vec, /// Insert opaque predicates (at most this many per method). pub max_predicates: usize, @@ -135,7 +136,12 @@ pub struct ConstantsConfig { impl Default for ConstantsConfig { fn default() -> Self { Self { - schemes: vec!["xor".to_string(), "add".to_string(), "shift".to_string()], + schemes: vec![ + "xor".to_string(), + "add".to_string(), + "shift".to_string(), + "mba".to_string(), + ], max_predicates: 1, skip: Vec::new(), } diff --git a/crates/noxis-passes/src/constants/forms.rs b/crates/noxis-passes/src/constants/forms.rs index bb9926e..a03af9a 100644 --- a/crates/noxis-passes/src/constants/forms.rs +++ b/crates/noxis-passes/src/constants/forms.rs @@ -1,8 +1,16 @@ //! Constant-splitting schemes and opaque predicates. //! //! Integer constants outside the `iconst` range are replaced by triples -//! (`x^k`, `a+b`, shift); an opaque predicate is an always-true condition with -//! a one-`nop` dead branch (valid, never executed, breaks linear disassembly). +//! (`x^k`, `a+b`, shift) or the `mba` form below; an opaque predicate is an +//! always-true condition with a one-`nop` dead branch (valid, never +//! executed, breaks linear disassembly). +//! +//! None of this survives a real constant-propagation pass (every operand is +//! a literal, so a general evaluator folds the whole expression regardless +//! of depth) - that is a known, documented limit, not a claim otherwise. +//! `mba` raises the bar against a *pattern* matcher instead: `xor`/`add` +//! hardcode one opcode triple as "this is a split constant", `mba` needs the +//! `(a&b)+(a|b) = a+b` identity specifically. use noxis_classfile::{Arith, BranchCond, Cond, Const, Insn, Prim}; use noxis_core::Rng; @@ -13,6 +21,7 @@ pub(super) enum Scheme { Xor, Add, Shift, + Mba, } impl Scheme { @@ -21,6 +30,7 @@ impl Scheme { "xor" => Some(Scheme::Xor), "add" => Some(Scheme::Add), "shift" => Some(Scheme::Shift), + "mba" => Some(Scheme::Mba), _ => None, } } @@ -36,7 +46,7 @@ pub(super) fn encrypt_int(value: i32, scheme: Scheme, rng: &mut Rng) -> Vec { + Scheme::Add | Scheme::Mba => { let part = rng.next_u64() as i32; (part, value.wrapping_sub(part)) } @@ -46,6 +56,7 @@ pub(super) fn encrypt_int(value: i32, scheme: Scheme, rng: &mut Rng) -> Vec binary(first, second, Arith::Xor, Prim::Int), Scheme::Add => binary(first, second, Arith::Add, Prim::Int), Scheme::Shift => shift_form(value), + Scheme::Mba => mba_form(first, second), } } @@ -59,7 +70,7 @@ pub(super) fn encrypt_long(value: i64, scheme: Scheme, rng: &mut Rng) -> Vec { + Scheme::Add | Scheme::Mba => { let part = rng.next_u64() as i64; (part, value.wrapping_sub(part)) } @@ -69,6 +80,7 @@ pub(super) fn encrypt_long(value: i64, scheme: Scheme, rng: &mut Rng) -> Vec binary_long(first, second, Arith::Xor), Scheme::Add => binary_long(first, second, Arith::Add), Scheme::Shift => shift_form_long(value), + Scheme::Mba => mba_form_long(first, second), } } @@ -92,6 +104,53 @@ fn binary_long(first: i64, second: i64, op: Arith) -> Vec { ] } +/// MBA identity: `a+b == (a&b) + (a|b)` (exact, bit-exact under wrapping). +/// `a`/`b` are pushed twice (no stack juggling needed) so each sub-expression +/// stays a plain, correct `and`/`or` - the shape, not a single opcode, is +/// what a matcher has to recognize. +fn mba_form(a: i32, b: i32) -> Vec { + vec![ + Insn::Const(Const::Int(a)), + Insn::Const(Const::Int(b)), + Insn::Binary { + prim: Prim::Int, + op: Arith::And, + }, + Insn::Const(Const::Int(a)), + Insn::Const(Const::Int(b)), + Insn::Binary { + prim: Prim::Int, + op: Arith::Or, + }, + Insn::Binary { + prim: Prim::Int, + op: Arith::Add, + }, + ] +} + +/// The same for `long`. +fn mba_form_long(a: i64, b: i64) -> Vec { + vec![ + Insn::Const(Const::Long(a)), + Insn::Const(Const::Long(b)), + Insn::Binary { + prim: Prim::Long, + op: Arith::And, + }, + Insn::Const(Const::Long(a)), + Insn::Const(Const::Long(b)), + Insn::Binary { + prim: Prim::Long, + op: Arith::Or, + }, + Insn::Binary { + prim: Prim::Long, + op: Arith::Add, + }, + ] +} + /// `v = ((v >> 1) << 1) | (v & 1)` - works for any value, including negatives. fn shift_form(value: i32) -> Vec { vec![ diff --git a/crates/noxis-passes/src/constants/tests.rs b/crates/noxis-passes/src/constants/tests.rs index 8d55d61..bce6e97 100644 --- a/crates/noxis-passes/src/constants/tests.rs +++ b/crates/noxis-passes/src/constants/tests.rs @@ -30,6 +30,30 @@ fn eval_int(insns: &[Insn]) -> i32 { Insn::Const(Const::Int(b)), Insn::Binary { .. }, ] => (a << 1) | (b & 1), + // MBA: `(a&b) + (a|b) == a+b`. Each sub-expression is still a + // foldable `LDC,LDC,BINOP` triple - a generic constant evaluator + // still resolves this instantly. What it defeats is a *pattern* + // matcher hardcoded to one opcode (`ldc,ldc,ixor` = "this is a + // split constant"): it would need to recognize the `and+or+add` + // identity specifically, not just fold a literal pair. + [ + Insn::Const(Const::Int(a1)), + Insn::Const(Const::Int(b1)), + Insn::Binary { + op: noxis_classfile::Arith::And, + .. + }, + Insn::Const(Const::Int(a2)), + Insn::Const(Const::Int(b2)), + Insn::Binary { + op: noxis_classfile::Arith::Or, + .. + }, + Insn::Binary { + op: noxis_classfile::Arith::Add, + .. + }, + ] if a1 == a2 && b1 == b2 => (a1 & b1).wrapping_add(a1 | b1), _ => panic!("not a form: {insns:?}"), } } @@ -38,7 +62,7 @@ fn eval_int(insns: &[Insn]) -> i32 { fn int_forms_round_trip() { let mut rng = noxis_core::Rng::from_seed(11); for value in [-1000, -7, -6, 6, 7, 42, 65536, i32::MAX, i32::MIN] { - for scheme in [Scheme::Xor, Scheme::Add, Scheme::Shift] { + for scheme in [Scheme::Xor, Scheme::Add, Scheme::Shift, Scheme::Mba] { assert_eq!(eval_int(&encrypt_int(value, scheme, &mut rng)), value); } } @@ -47,7 +71,7 @@ fn int_forms_round_trip() { #[test] fn long_forms_keep_shape() { let mut rng = noxis_core::Rng::from_seed(12); - for scheme in [Scheme::Xor, Scheme::Add, Scheme::Shift] { + for scheme in [Scheme::Xor, Scheme::Add, Scheme::Shift, Scheme::Mba] { let insns = encrypt_long(1_000_000_000_000, scheme, &mut rng); assert!(matches!( insns.last(), @@ -59,6 +83,34 @@ fn long_forms_keep_shape() { } } +/// MBA shape: `and`/`or`/`add` on two reused operands, not `xor`/`add` on a +/// single pair - a matcher hardcoded to one opcode triple (`ldc,ldc,ixor`) +/// doesn't recognize this as "a split constant" without the identity. +#[test] +fn mba_form_uses_and_or_add_not_a_single_binop() { + let mut rng = noxis_core::Rng::from_seed(13); + for value in [-1000, -6, 6, 42, 65536, i32::MAX, i32::MIN] { + let insns = encrypt_int(value, Scheme::Mba, &mut rng); + let ops: Vec<_> = insns + .iter() + .filter_map(|i| match i { + Insn::Binary { op, .. } => Some(*op), + _ => None, + }) + .collect(); + assert_eq!( + ops, + [ + noxis_classfile::Arith::And, + noxis_classfile::Arith::Or, + noxis_classfile::Arith::Add, + ], + "unexpected mba shape: {insns:?}" + ); + assert_eq!(insns.len(), 7, "unexpected mba insn count: {insns:?}"); + } +} + fn sample() -> Program { let code = Code { max_stack: 1, diff --git a/docs/techniques.md b/docs/techniques.md index b1c7397..5ed3c24 100644 --- a/docs/techniques.md +++ b/docs/techniques.md @@ -82,8 +82,14 @@ opaque-предикаты - условия, истинность которых - Шаблонный opaque-предикат (всегда один и тот же `A+B`) узнаётся паттерном - отсюда требование нескольких схем. -**Статус: готово** для `int`/`long` (`xor`/`add`/`shift` + opaque-предикаты); -`float`/`double` не трогаются. +**Статус: готово** для `int`/`long` (`xor`/`add`/`shift`/`mba` + +opaque-предикаты); `float`/`double` не трогаются. `mba` (`(a&b)+(a|b)=a+b`, +13.10.2026) не ломает честную оговорку выше - он раскрывается тем же +символьным вычислителем, что и `xor`/`add`/`shift`. Разница в том, что +бьёт: `xor`/`add` сворачиваются одним паттерном «два литерала + одна +операция»; `mba` требует опознать саму тождество `and+or+add`, а не просто +свернуть литералы - отсекает деобфускаторы с жёстко вшитым паттерном на +один опкод, но не символьные вычислители. ## 4. Обфускация контрольного потока (`Flow`)