obfuskator/crates/noxis-runtime/src/decoder/mod.rs
loki5512344 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

186 lines
6.6 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! Рантайм-декодер: ABI, который получает байткод после подстановки строк.
//!
//! ABI v1 (стабильный, его же нужно повторить на Java side):
//!
//! ```text
//! static String d(String cipher, int salt)
//! ```
//!
//! - `cipher` - base64 шифротекста (схема выбирается по salt: `scheme = all[salt % n]`);
//! - `salt` - индекс строки, по нему детерминированно выводятся key и nonce;
//! - мастер-секрет **не передаётся на вызове** и не лежит в одной константе:
//! он собирается из двух частей (`k1 ^ k2`) прямо в декодере.
//!
//! Класс версии 49: циклы верифицируются выводом типов без `StackMapTable`
//! (проверено загрузкой v49 на Java 21), кадры не генерируются.
mod kdf;
mod xor;
mod xtea;
use noxis_classfile::{
Arith, BranchCond, ClassBuilder, ClassFile, Code, Cond, Insn, InvokeKind, Prim, SlotKind,
Version,
};
use crate::scheme::Scheme;
use kdf::{Gen, c_int, c_long, c_str, call, load, store};
/// Имя метода декодера.
pub const DECODER_METHOD: &str = "d";
/// Дескриптор метода декодера.
pub const DECODER_DESC: &str = "(Ljava/lang/String;I)Ljava/lang/String;";
/// Ошибка генерации рантайма.
#[derive(Debug, thiserror::Error)]
pub enum RuntimeError {
#[error("не реализовано: {0}")]
Unimplemented(&'static str),
#[error("нет активных схем шифрования")]
NoSchemes,
}
/// Сгенерировать класс-декодер (мастер-секрет + расшифровка всех схем).
pub fn decoder_class(
internal_name: &str,
master: u64,
schemes: &[&str],
) -> Result<ClassFile, RuntimeError> {
if schemes.is_empty() {
return Err(RuntimeError::NoSchemes);
}
let mut parsed = Vec::with_capacity(schemes.len());
for name in schemes {
parsed.push(Scheme::parse(name).map_err(|_| RuntimeError::NoSchemes)?);
}
let mut asm = Gen::new();
let master_slot = emit_master(&mut asm, master);
let key = kdf::emit_kdf(&mut asm, master_slot, "string/key");
let nonce = kdf::emit_kdf(&mut asm, master_slot, "string/nonce");
let data = emit_base64(&mut asm);
let len = emit_len(&mut asm, data);
emit_dispatch(&mut asm, &parsed, key, nonce, data, len);
emit_epilogue(&mut asm, data);
let code = Code {
// Точный пик - 10 слотов (замер 08.10.2026); запас без вреда:
// лимит обязан лишь покрывать фактическую глубину.
max_stack: 12,
max_locals: asm.slots,
insns: asm.insns,
exception_table: Vec::new(),
frames: Vec::new(),
attributes: Vec::new(),
};
Ok(ClassBuilder::new(internal_name.to_string(), None)
.version(Version {
minor: 0,
major: 49,
})
.method_with_code(0x0009, DECODER_METHOD, DECODER_DESC, code)
.build())
}
/// Имя класса-декодера для прогона: детерминированно от сида, но выглядит
/// как обычный обфусцированный класс.
pub fn decoder_owner(seed_name: &str) -> String {
seed_name.to_string()
}
/// Пролог: `master = k1 ^ k2`; возвращает слот пары. Ни одна часть не равна
/// мастер-секрету (вторая - `master ^ k1`, первая - потоковая).
fn emit_master(asm: &mut Gen, master: u64) -> u16 {
use noxis_core::Rng;
let mut rng = Rng::from_seed(master ^ 0x9E37_79B9_7F4A_7C15);
let first = rng.next_u64() as i64;
let second = (master ^ first as u64) as i64;
let slot = asm.alloc(true);
asm.emit(c_long(first));
asm.emit(c_long(second));
asm.emit(Insn::Binary {
prim: Prim::Long,
op: Arith::Xor,
});
asm.emit(store(slot, SlotKind::Long));
slot
}
/// `data = Base64.getDecoder().decode(cipher)`; возвращает слот массива.
fn emit_base64(asm: &mut Gen) -> u16 {
let slot = asm.alloc(false);
asm.emit(call(
"java/util/Base64",
"getDecoder",
"()Ljava/util/Base64$Decoder;",
InvokeKind::Static,
));
asm.emit(load(0, SlotKind::Ref));
asm.emit(call(
"java/util/Base64$Decoder",
"decode",
"(Ljava/lang/String;)[B",
InvokeKind::Virtual,
));
asm.emit(store(slot, SlotKind::Ref));
slot
}
/// `len = data.length`; возвращает слот.
fn emit_len(asm: &mut Gen, data: u16) -> u16 {
let slot = asm.alloc(false);
asm.emit(load(data, SlotKind::Ref));
asm.emit(Insn::ArrayLength);
asm.emit(store(slot, SlotKind::Int));
slot
}
/// Диспетчер схем: `salt % n` лестницей `if_icmpne` (n == 1 - без диспетчера).
fn emit_dispatch(asm: &mut Gen, schemes: &[Scheme], key: u16, nonce: u16, data: u16, len: u16) {
if schemes.len() == 1 {
dispatch_branch(asm, schemes[0], key, nonce, data, len);
return;
}
let done = asm.label();
for (index, scheme) in schemes.iter().enumerate() {
if index + 1 == schemes.len() {
break;
}
let next = asm.label();
asm.emit(load(1, SlotKind::Int));
asm.emit(c_int(schemes.len() as i32));
asm.emit(Insn::Binary {
prim: Prim::Int,
op: Arith::Rem,
});
asm.emit(c_int(index as i32));
asm.emit(Insn::Branch(BranchCond::Int(Cond::Ne), next));
dispatch_branch(asm, *scheme, key, nonce, data, len);
asm.emit(Insn::Goto(done));
asm.mark(next);
}
dispatch_branch(asm, schemes[schemes.len() - 1], key, nonce, data, len);
asm.mark(done);
}
fn dispatch_branch(asm: &mut Gen, scheme: Scheme, key: u16, nonce: u16, data: u16, len: u16) {
match scheme {
Scheme::XorSplitmix => xor::emit_xor(asm, key, nonce, data, len),
Scheme::XteaCtr => xtea::emit_xtea(asm, key, nonce, data, len),
}
}
/// Эпилог: `return new String(data, "UTF-8")`.
fn emit_epilogue(asm: &mut Gen, data: u16) {
asm.emit(Insn::New("java/lang/String".to_string()));
asm.emit(Insn::Dup);
asm.emit(load(data, SlotKind::Ref));
asm.emit(c_str("UTF-8"));
asm.emit(call(
"java/lang/String",
"<init>",
"([BLjava/lang/String;)V",
InvokeKind::Special,
));
asm.emit(Insn::AReturn);
}