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.
This commit is contained in:
parent
550f62e473
commit
2b19ed6096
4 changed files with 133 additions and 10 deletions
|
|
@ -124,7 +124,8 @@ impl Default for FlowConfig {
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
#[serde(default, deny_unknown_fields)]
|
#[serde(default, deny_unknown_fields)]
|
||||||
pub struct ConstantsConfig {
|
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<String>,
|
pub schemes: Vec<String>,
|
||||||
/// Insert opaque predicates (at most this many per method).
|
/// Insert opaque predicates (at most this many per method).
|
||||||
pub max_predicates: usize,
|
pub max_predicates: usize,
|
||||||
|
|
@ -135,7 +136,12 @@ pub struct ConstantsConfig {
|
||||||
impl Default for ConstantsConfig {
|
impl Default for ConstantsConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
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,
|
max_predicates: 1,
|
||||||
skip: Vec::new(),
|
skip: Vec::new(),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,16 @@
|
||||||
//! Constant-splitting schemes and opaque predicates.
|
//! Constant-splitting schemes and opaque predicates.
|
||||||
//!
|
//!
|
||||||
//! Integer constants outside the `iconst` range are replaced by triples
|
//! Integer constants outside the `iconst` range are replaced by triples
|
||||||
//! (`x^k`, `a+b`, shift); an opaque predicate is an always-true condition with
|
//! (`x^k`, `a+b`, shift) or the `mba` form below; an opaque predicate is an
|
||||||
//! a one-`nop` dead branch (valid, never executed, breaks linear disassembly).
|
//! 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_classfile::{Arith, BranchCond, Cond, Const, Insn, Prim};
|
||||||
use noxis_core::Rng;
|
use noxis_core::Rng;
|
||||||
|
|
@ -13,6 +21,7 @@ pub(super) enum Scheme {
|
||||||
Xor,
|
Xor,
|
||||||
Add,
|
Add,
|
||||||
Shift,
|
Shift,
|
||||||
|
Mba,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Scheme {
|
impl Scheme {
|
||||||
|
|
@ -21,6 +30,7 @@ impl Scheme {
|
||||||
"xor" => Some(Scheme::Xor),
|
"xor" => Some(Scheme::Xor),
|
||||||
"add" => Some(Scheme::Add),
|
"add" => Some(Scheme::Add),
|
||||||
"shift" => Some(Scheme::Shift),
|
"shift" => Some(Scheme::Shift),
|
||||||
|
"mba" => Some(Scheme::Mba),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -36,7 +46,7 @@ pub(super) fn encrypt_int(value: i32, scheme: Scheme, rng: &mut Rng) -> Vec<Insn
|
||||||
}
|
}
|
||||||
(value ^ key, key)
|
(value ^ key, key)
|
||||||
}
|
}
|
||||||
Scheme::Add => {
|
Scheme::Add | Scheme::Mba => {
|
||||||
let part = rng.next_u64() as i32;
|
let part = rng.next_u64() as i32;
|
||||||
(part, value.wrapping_sub(part))
|
(part, value.wrapping_sub(part))
|
||||||
}
|
}
|
||||||
|
|
@ -46,6 +56,7 @@ pub(super) fn encrypt_int(value: i32, scheme: Scheme, rng: &mut Rng) -> Vec<Insn
|
||||||
Scheme::Xor => binary(first, second, Arith::Xor, Prim::Int),
|
Scheme::Xor => binary(first, second, Arith::Xor, Prim::Int),
|
||||||
Scheme::Add => binary(first, second, Arith::Add, Prim::Int),
|
Scheme::Add => binary(first, second, Arith::Add, Prim::Int),
|
||||||
Scheme::Shift => shift_form(value),
|
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<Ins
|
||||||
}
|
}
|
||||||
(value ^ key, key)
|
(value ^ key, key)
|
||||||
}
|
}
|
||||||
Scheme::Add => {
|
Scheme::Add | Scheme::Mba => {
|
||||||
let part = rng.next_u64() as i64;
|
let part = rng.next_u64() as i64;
|
||||||
(part, value.wrapping_sub(part))
|
(part, value.wrapping_sub(part))
|
||||||
}
|
}
|
||||||
|
|
@ -69,6 +80,7 @@ pub(super) fn encrypt_long(value: i64, scheme: Scheme, rng: &mut Rng) -> Vec<Ins
|
||||||
Scheme::Xor => binary_long(first, second, Arith::Xor),
|
Scheme::Xor => binary_long(first, second, Arith::Xor),
|
||||||
Scheme::Add => binary_long(first, second, Arith::Add),
|
Scheme::Add => binary_long(first, second, Arith::Add),
|
||||||
Scheme::Shift => shift_form_long(value),
|
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<Insn> {
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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<Insn> {
|
||||||
|
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<Insn> {
|
||||||
|
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.
|
/// `v = ((v >> 1) << 1) | (v & 1)` - works for any value, including negatives.
|
||||||
fn shift_form(value: i32) -> Vec<Insn> {
|
fn shift_form(value: i32) -> Vec<Insn> {
|
||||||
vec![
|
vec![
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,30 @@ fn eval_int(insns: &[Insn]) -> i32 {
|
||||||
Insn::Const(Const::Int(b)),
|
Insn::Const(Const::Int(b)),
|
||||||
Insn::Binary { .. },
|
Insn::Binary { .. },
|
||||||
] => (a << 1) | (b & 1),
|
] => (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:?}"),
|
_ => panic!("not a form: {insns:?}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -38,7 +62,7 @@ fn eval_int(insns: &[Insn]) -> i32 {
|
||||||
fn int_forms_round_trip() {
|
fn int_forms_round_trip() {
|
||||||
let mut rng = noxis_core::Rng::from_seed(11);
|
let mut rng = noxis_core::Rng::from_seed(11);
|
||||||
for value in [-1000, -7, -6, 6, 7, 42, 65536, i32::MAX, i32::MIN] {
|
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);
|
assert_eq!(eval_int(&encrypt_int(value, scheme, &mut rng)), value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -47,7 +71,7 @@ fn int_forms_round_trip() {
|
||||||
#[test]
|
#[test]
|
||||||
fn long_forms_keep_shape() {
|
fn long_forms_keep_shape() {
|
||||||
let mut rng = noxis_core::Rng::from_seed(12);
|
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);
|
let insns = encrypt_long(1_000_000_000_000, scheme, &mut rng);
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
insns.last(),
|
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 {
|
fn sample() -> Program {
|
||||||
let code = Code {
|
let code = Code {
|
||||||
max_stack: 1,
|
max_stack: 1,
|
||||||
|
|
|
||||||
|
|
@ -82,8 +82,14 @@ opaque-предикаты - условия, истинность которых
|
||||||
- Шаблонный opaque-предикат (всегда один и тот же `A+B`) узнаётся
|
- Шаблонный opaque-предикат (всегда один и тот же `A+B`) узнаётся
|
||||||
паттерном - отсюда требование нескольких схем.
|
паттерном - отсюда требование нескольких схем.
|
||||||
|
|
||||||
**Статус: готово** для `int`/`long` (`xor`/`add`/`shift` + opaque-предикаты);
|
**Статус: готово** для `int`/`long` (`xor`/`add`/`shift`/`mba` +
|
||||||
`float`/`double` не трогаются.
|
opaque-предикаты); `float`/`double` не трогаются. `mba` (`(a&b)+(a|b)=a+b`,
|
||||||
|
13.10.2026) не ломает честную оговорку выше - он раскрывается тем же
|
||||||
|
символьным вычислителем, что и `xor`/`add`/`shift`. Разница в том, что
|
||||||
|
бьёт: `xor`/`add` сворачиваются одним паттерном «два литерала + одна
|
||||||
|
операция»; `mba` требует опознать саму тождество `and+or+add`, а не просто
|
||||||
|
свернуть литералы - отсекает деобфускаторы с жёстко вшитым паттерном на
|
||||||
|
один опкод, но не символьные вычислители.
|
||||||
|
|
||||||
## 4. Обфускация контрольного потока (`Flow`)
|
## 4. Обфускация контрольного потока (`Flow`)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue