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.
This commit is contained in:
parent
2172206e83
commit
139aea9a8f
96 changed files with 1053 additions and 831 deletions
|
|
@ -1,8 +1,8 @@
|
||||||
# NOTICE — сторонние компоненты
|
# NOTICE - сторонние компоненты
|
||||||
|
|
||||||
Проект `noxis` распространяется по лицензии AGPL-3.0-only
|
Проект `noxis` распространяется по лицензии AGPL-3.0-only
|
||||||
(см. [`LICENSE`](LICENSE)).
|
(см. [`LICENSE`](LICENSE)).
|
||||||
Ниже — компоненты third-party, которые используются напрямую и чьи
|
Ниже - компоненты third-party, которые используются напрямую и чьи
|
||||||
лицензионные условия обязательны к сохранению.
|
лицензионные условия обязательны к сохранению.
|
||||||
|
|
||||||
## ristretto_classfile
|
## ristretto_classfile
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ pub struct Entrypoints {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Entrypoints {
|
impl Entrypoints {
|
||||||
/// Собрать entrypoint'ы по программе (без keep-правил — их добавляет [`Self::merge`]).
|
/// Собрать entrypoint'ы по программе (без keep-правил - их добавляет [`Self::merge`]).
|
||||||
pub fn collect(program: &Program) -> Self {
|
pub fn collect(program: &Program) -> Self {
|
||||||
let mut out = Self::default();
|
let mut out = Self::default();
|
||||||
|
|
||||||
|
|
@ -101,7 +101,7 @@ fn implements_serializable(program: &Program, start: &str) -> bool {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
let Some(class) = program.class(&name) else {
|
let Some(class) = program.class(&name) else {
|
||||||
// Внешний предок: считаем, что не знаем — консервативно не keep'им.
|
// Внешний предок: считаем, что не знаем - консервативно не keep'им.
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
if let Some(sup) = &class.super_class {
|
if let Some(sup) = &class.super_class {
|
||||||
|
|
@ -130,15 +130,15 @@ fn collect_service_interfaces(program: &Program) -> BTreeSet<String> {
|
||||||
/// Линейный эвристический поиск имён в рефлексивных API.
|
/// Линейный эвристический поиск имён в рефлексивных API.
|
||||||
///
|
///
|
||||||
/// Полноценный dataflow не нужен: если рядом с вызовом `Class.forName(...)` /
|
/// Полноценный dataflow не нужен: если рядом с вызовом `Class.forName(...)` /
|
||||||
/// `getMethod(...)` лежит строковый литерал — это имя видно статически,
|
/// `getMethod(...)` лежит строковый литерал - это имя видно статически,
|
||||||
/// значит, его нельзя переименовывать.
|
/// значит, его нельзя переименовывать.
|
||||||
fn collect_reflective_names(out: &mut Entrypoints, code: &noxis_classfile::Code) {
|
fn collect_reflective_names(out: &mut Entrypoints, code: &noxis_classfile::Code) {
|
||||||
/// API, чьи аргументы — **имена классов**.
|
/// API, чьи аргументы - **имена классов**.
|
||||||
const CLASS_API: &[(&str, &str)] = &[
|
const CLASS_API: &[(&str, &str)] = &[
|
||||||
("java/lang/Class", "forName"),
|
("java/lang/Class", "forName"),
|
||||||
("java/lang/ClassLoader", "loadClass"),
|
("java/lang/ClassLoader", "loadClass"),
|
||||||
];
|
];
|
||||||
/// API, чьи аргументы — **имена членов** (поле/метод).
|
/// API, чьи аргументы - **имена членов** (поле/метод).
|
||||||
const MEMBER_API: &[(&str, &str)] = &[
|
const MEMBER_API: &[(&str, &str)] = &[
|
||||||
("java/lang/Class", "getMethod"),
|
("java/lang/Class", "getMethod"),
|
||||||
("java/lang/Class", "getDeclaredMethod"),
|
("java/lang/Class", "getDeclaredMethod"),
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ impl ClassHierarchy {
|
||||||
.and_then(|n| n.super_class.clone())
|
.and_then(|n| n.super_class.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Есть ли класс в иерархии (в том числе из внешнего classpath — нет).
|
/// Есть ли класс в иерархии (в том числе из внешнего classpath - нет).
|
||||||
pub fn contains(&self, internal_name: &str) -> bool {
|
pub fn contains(&self, internal_name: &str) -> bool {
|
||||||
self.nodes.contains_key(internal_name)
|
self.nodes.contains_key(internal_name)
|
||||||
}
|
}
|
||||||
|
|
@ -57,7 +57,7 @@ impl ClassHierarchy {
|
||||||
|
|
||||||
/// Является ли `child` подклассом `parent` (в пределах программы).
|
/// Является ли `child` подклассом `parent` (в пределах программы).
|
||||||
///
|
///
|
||||||
/// Неполная иерархия (внешние зависимости не грузим) — это осознанно:
|
/// Неполная иерархия (внешние зависимости не грузим) - это осознанно:
|
||||||
/// обфускатор работает по артефакту, а не по всему classpath.
|
/// обфускатор работает по артефакту, а не по всему classpath.
|
||||||
pub fn is_subclass_of(&self, child: &str, parent: &str) -> bool {
|
pub fn is_subclass_of(&self, child: &str, parent: &str) -> bool {
|
||||||
if child == parent {
|
if child == parent {
|
||||||
|
|
@ -137,10 +137,10 @@ pub enum RenameSafety {
|
||||||
/// Оценка безопасности переименования метода.
|
/// Оценка безопасности переименования метода.
|
||||||
///
|
///
|
||||||
/// Правила (сознательно консервативные, как в `OBF/InheritanceAnalyzer`):
|
/// Правила (сознательно консервативные, как в `OBF/InheritanceAnalyzer`):
|
||||||
/// - `<init>`/`<clinit>` — никогда;
|
/// - `<init>`/`<clinit>` - никогда;
|
||||||
/// - `private`/`static`/`final`/`<package-private>` внутри финального класса — можно;
|
/// - `private`/`static`/`final`/`<package-private>` внутри финального класса - можно;
|
||||||
/// - `public`/`protected`/`package-private`, если класс не `final` и есть
|
/// - `public`/`protected`/`package-private`, если класс не `final` и есть
|
||||||
/// наследники (внутри артефакта или нет) — нельзя, пока нет доказательства.
|
/// наследники (внутри артефакта или нет) - нельзя, пока нет доказательства.
|
||||||
pub fn method_rename_safety(
|
pub fn method_rename_safety(
|
||||||
program: &Program,
|
program: &Program,
|
||||||
hierarchy: &ClassHierarchy,
|
hierarchy: &ClassHierarchy,
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ use regex::Regex;
|
||||||
use crate::entrypoints::Entrypoints;
|
use crate::entrypoints::Entrypoints;
|
||||||
use noxis_core::Config;
|
use noxis_core::Config;
|
||||||
|
|
||||||
/// Скомпилированный glob: `*` — любые символы кроме `/`, `**` — всё, `?` — один.
|
/// Скомпилированный glob: `*` - любые символы кроме `/`, `**` - всё, `?` - один.
|
||||||
fn compile(pattern: &str) -> Result<Regex, regex::Error> {
|
fn compile(pattern: &str) -> Result<Regex, regex::Error> {
|
||||||
let mut out = String::from("^");
|
let mut out = String::from("^");
|
||||||
let chars: Vec<char> = pattern.chars().collect();
|
let chars: Vec<char> = pattern.chars().collect();
|
||||||
|
|
@ -107,7 +107,7 @@ impl KeepRules {
|
||||||
/// Методы, на которые ссылаются `BootstrapMethods` (`invokedynamic`).
|
/// Методы, на которые ссылаются `BootstrapMethods` (`invokedynamic`).
|
||||||
///
|
///
|
||||||
/// Переименование такого метода без правки bootstrap-записи ломает класс,
|
/// Переименование такого метода без правки bootstrap-записи ломает класс,
|
||||||
/// а правка — задача P2 (см. `todo.md`): пока просто не трогаем имена.
|
/// а правка - задача P2 (см. `todo.md`): пока просто не трогаем имена.
|
||||||
pub fn absorb_bootstrap(&mut self, program: &Program) {
|
pub fn absorb_bootstrap(&mut self, program: &Program) {
|
||||||
for class in &program.classes {
|
for class in &program.classes {
|
||||||
for method in &class.methods {
|
for method in &class.methods {
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
//!
|
//!
|
||||||
//! Из этих данных пасс переименования узнаёт, **что** трогать нельзя.
|
//! Из этих данных пасс переименования узнаёт, **что** трогать нельзя.
|
||||||
//! Урок из `info/evo-weaknesses.md`: у EvoVis рефлексия по имени была
|
//! Урок из `info/evo-weaknesses.md`: у EvoVis рефлексия по имени была
|
||||||
//! открытым текстом, а entrypoint'ы — в `fabric.mod.json`; здесь они
|
//! открытым текстом, а entrypoint'ы - в `fabric.mod.json`; здесь они
|
||||||
//! обязаны попадать в keep-набор автоматически.
|
//! обязаны попадать в keep-набор автоматически.
|
||||||
|
|
||||||
pub mod entrypoints;
|
pub mod entrypoints;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
//! Emitting compact frames at the points where the verifier expects them.
|
//! Emitting compact frames at the points where the verifier expects them.
|
||||||
//!
|
//!
|
||||||
//! A frame's kind is chosen by comparing with the previous frame
|
//! A frame's kind is chosen by comparing with the previous frame
|
||||||
//! (`Same`/`Append`/`Chop`/`SameLocals`, otherwise `Full`) — like `javac`.
|
//! (`Same`/`Append`/`Chop`/`SameLocals`, otherwise `Full`) - like `javac`.
|
||||||
//! Trailing `Top`s are trimmed both when comparing and when writing: an empty
|
//! Trailing `Top`s are trimmed both when comparing and when writing: an empty
|
||||||
//! slot is not a type.
|
//! slot is not a type.
|
||||||
|
|
||||||
|
|
@ -12,7 +12,7 @@ use super::xfer::State;
|
||||||
use super::{StackMapFrame, VerificationType};
|
use super::{StackMapFrame, VerificationType};
|
||||||
use crate::error::ClassfileError;
|
use crate::error::ClassfileError;
|
||||||
|
|
||||||
/// Frames at every point in `targets` (sorted — deltas in order).
|
/// Frames at every point in `targets` (sorted - deltas in order).
|
||||||
///
|
///
|
||||||
/// Deltas are in instruction numbers (like the backend after reading); the writer
|
/// Deltas are in instruction numbers (like the backend after reading); the writer
|
||||||
/// recomputes them into bytes itself.
|
/// recomputes them into bytes itself.
|
||||||
|
|
@ -50,7 +50,7 @@ fn select(
|
||||||
previous: &[VerificationType],
|
previous: &[VerificationType],
|
||||||
stack: &[VerificationType],
|
stack: &[VerificationType],
|
||||||
) -> StackMapFrame {
|
) -> StackMapFrame {
|
||||||
// Comparison is at variable granularity (`long` — one entry, like `javac`);
|
// Comparison is at variable granularity (`long` - one entry, like `javac`);
|
||||||
// otherwise halves would produce `Append{[Top, Long]}`.
|
// otherwise halves would produce `Append{[Top, Long]}`.
|
||||||
let current = vars(locals);
|
let current = vars(locals);
|
||||||
let base = vars(previous);
|
let base = vars(previous);
|
||||||
|
|
@ -98,7 +98,7 @@ fn select(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stack entries → values: `[Double, Top]` — one entry (like `javac`).
|
/// Stack entries → values: `[Double, Top]` - one entry (like `javac`).
|
||||||
///
|
///
|
||||||
/// Unlike [`vars`], no trailing-`Top` trimming: every stack entry is live by
|
/// Unlike [`vars`], no trailing-`Top` trimming: every stack entry is live by
|
||||||
/// construction, and dropping one would corrupt the depth.
|
/// construction, and dropping one would corrupt the depth.
|
||||||
|
|
@ -122,7 +122,7 @@ fn stack_vars(stack: &[VerificationType]) -> Vec<VerificationType> {
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Slots → variables: `[Long, Top]` — one entry; trailing `Top`s — gone.
|
/// Slots → variables: `[Long, Top]` - one entry; trailing `Top`s - gone.
|
||||||
///
|
///
|
||||||
/// Single `Top`s inside (holes) are kept: in `Full` they are a legal type, and we
|
/// Single `Top`s inside (holes) are kept: in `Full` they are a legal type, and we
|
||||||
/// don't allow them in `Append` (see `select`).
|
/// don't allow them in `Append` (see `select`).
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ pub(crate) fn split_array(descriptor: &str) -> Option<(usize, &str)> {
|
||||||
Some((dims, descriptor[dims..].trim_end_matches(';')))
|
Some((dims, descriptor[dims..].trim_end_matches(';')))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Slots for a single field descriptor (`long`/`double` — pair with `Top`).
|
/// Slots for a single field descriptor (`long`/`double` - pair with `Top`).
|
||||||
pub(crate) fn field_slots(descriptor: &str) -> Result<Vec<VerificationType>, DescError> {
|
pub(crate) fn field_slots(descriptor: &str) -> Result<Vec<VerificationType>, DescError> {
|
||||||
let mut chars = descriptor.chars();
|
let mut chars = descriptor.chars();
|
||||||
match chars.next() {
|
match chars.next() {
|
||||||
|
|
@ -57,10 +57,10 @@ pub(crate) fn field_slots(descriptor: &str) -> Result<Vec<VerificationType>, Des
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parameter and return slots (`None` — `void`).
|
/// Parameter and return slots (`None` - `void`).
|
||||||
type MethodSlots = (Vec<Vec<VerificationType>>, Option<Vec<VerificationType>>);
|
type MethodSlots = (Vec<Vec<VerificationType>>, Option<Vec<VerificationType>>);
|
||||||
|
|
||||||
/// Method descriptor: parameter slots and return slots (`None` — `void`).
|
/// Method descriptor: parameter slots and return slots (`None` - `void`).
|
||||||
pub(crate) fn method_slots(descriptor: &str) -> Result<MethodSlots, DescError> {
|
pub(crate) fn method_slots(descriptor: &str) -> Result<MethodSlots, DescError> {
|
||||||
let (params, ret) = descriptor
|
let (params, ret) = descriptor
|
||||||
.split_once('(')
|
.split_once('(')
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
//! Type merging (LUB) and descriptor parsing.
|
//! Type merging (LUB) and descriptor parsing.
|
||||||
//!
|
//!
|
||||||
//! The lattice: `Top` is "empty" (the neighbour wins), `Null` is absorbed into a
|
//! The lattice: `Top` absorbs (a slot unwritten on any inflow stays `Top`,
|
||||||
//! reference, equal types map to themselves, references take the least upper
|
//! like `javac` - a precise type would reject the inflow that never wrote
|
||||||
//! bound over the hierarchy. Incompatible initialized types (`int` vs a reference
|
//! it). `Null` is absorbed into a reference, equal types map to themselves,
|
||||||
//! after `astore` in a handler) yield `Top`, like `javac`'s truncated frames: a
|
//! references take the least upper bound over the hierarchy. Incompatible
|
||||||
//! `Top` in a frame accepts any actual value, so this is always sound. The
|
//! initialized types (`int` vs a reference) yield `Top`. The exception is
|
||||||
//! exception is uninitialized types: merging them means a broken construction
|
//! uninitialized types: merging them means a broken construction flow -
|
||||||
//! flow — here an honest error, not a silent `Top`.
|
//! here an honest error, not a silent `Top`.
|
||||||
|
|
||||||
pub(super) mod descs;
|
pub(super) mod descs;
|
||||||
|
|
||||||
|
|
@ -19,7 +19,7 @@ use descs::write_local;
|
||||||
|
|
||||||
/// The class hierarchy for LUB: the superclass chain is enough.
|
/// The class hierarchy for LUB: the superclass chain is enough.
|
||||||
pub trait Hierarchy {
|
pub trait Hierarchy {
|
||||||
/// The direct superclass (`None` — `java/lang/Object` or unknown).
|
/// The direct superclass (`None` - `java/lang/Object` or unknown).
|
||||||
fn superclass(&self, class: &str) -> Option<String>;
|
fn superclass(&self, class: &str) -> Option<String>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -38,7 +38,7 @@ impl Merger<'_> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Merge an incoming state; `true` — the slot changed.
|
/// Merge an incoming state; `true` - the slot changed.
|
||||||
pub fn merge_into(
|
pub fn merge_into(
|
||||||
&self,
|
&self,
|
||||||
slot: &mut Option<State>,
|
slot: &mut Option<State>,
|
||||||
|
|
@ -75,11 +75,11 @@ impl Merger<'_> {
|
||||||
VerificationType::Object(self.class.clone())
|
VerificationType::Object(self.class.clone())
|
||||||
};
|
};
|
||||||
slot = write_local(&mut locals, slot, vec![this])
|
slot = write_local(&mut locals, slot, vec![this])
|
||||||
.map_err(|_| self.invalid("параметры не влезают в locals"))?;
|
.map_err(|_| self.invalid("parameters do not fit into locals"))?;
|
||||||
}
|
}
|
||||||
for param in params {
|
for param in params {
|
||||||
slot = write_local(&mut locals, slot, param)
|
slot = write_local(&mut locals, slot, param)
|
||||||
.map_err(|_| self.invalid("параметры не влезают в locals"))?;
|
.map_err(|_| self.invalid("parameters do not fit into locals"))?;
|
||||||
}
|
}
|
||||||
Ok(State {
|
Ok(State {
|
||||||
locals,
|
locals,
|
||||||
|
|
@ -89,10 +89,10 @@ impl Merger<'_> {
|
||||||
|
|
||||||
fn lub_states(&self, first: &State, second: &State) -> Result<State, ClassfileError> {
|
fn lub_states(&self, first: &State, second: &State) -> Result<State, ClassfileError> {
|
||||||
if first.locals.len() != second.locals.len() {
|
if first.locals.len() != second.locals.len() {
|
||||||
return Err(self.invalid("разная длина locals при слиянии"));
|
return Err(self.invalid("locals length differs across the merge"));
|
||||||
}
|
}
|
||||||
if first.stack.len() != second.stack.len() {
|
if first.stack.len() != second.stack.len() {
|
||||||
return Err(self.invalid("разная глубина стека при слиянии"));
|
return Err(self.invalid("stack depth differs across the merge"));
|
||||||
}
|
}
|
||||||
Ok(State {
|
Ok(State {
|
||||||
locals: first
|
locals: first
|
||||||
|
|
@ -120,7 +120,10 @@ impl Merger<'_> {
|
||||||
return Ok(first.clone());
|
return Ok(first.clone());
|
||||||
}
|
}
|
||||||
match (first, second) {
|
match (first, second) {
|
||||||
(Top, other) | (other, Top) => Ok(other.clone()),
|
// A slot unwritten on any inflow stays `Top` (like `javac`): the
|
||||||
|
// frame keeps accepting every inflow, while a precise type would
|
||||||
|
// reject the one that never wrote the slot.
|
||||||
|
(Top, _) | (_, Top) => Ok(Top),
|
||||||
(Null, other @ Object(_)) | (other @ Object(_), Null) => Ok(other.clone()),
|
(Null, other @ Object(_)) | (other @ Object(_), Null) => Ok(other.clone()),
|
||||||
(Null, Null) => Ok(Null),
|
(Null, Null) => Ok(Null),
|
||||||
(Integer, Integer) | (Float, Float) | (Long, Long) | (Double, Double) => {
|
(Integer, Integer) | (Float, Float) | (Long, Long) | (Double, Double) => {
|
||||||
|
|
@ -128,10 +131,12 @@ impl Merger<'_> {
|
||||||
}
|
}
|
||||||
(Object(a), Object(b)) => Ok(Object(self.common_superclass(a, b))),
|
(Object(a), Object(b)) => Ok(Object(self.common_superclass(a, b))),
|
||||||
// Equal ones are handled above; here uninitialized are always
|
// Equal ones are handled above; here uninitialized are always
|
||||||
// different — that's a broken construction flow.
|
// different - that's a broken construction flow.
|
||||||
(VerificationType::UninitializedThis | VerificationType::Uninitialized(_), _)
|
(VerificationType::UninitializedThis | VerificationType::Uninitialized(_), _)
|
||||||
| (_, VerificationType::UninitializedThis | VerificationType::Uninitialized(_)) => {
|
| (_, VerificationType::UninitializedThis | VerificationType::Uninitialized(_)) => {
|
||||||
Err(self.invalid("слияние неинициализированных типов"))
|
Err(self.invalid(&format!(
|
||||||
|
"merging uninitialized types ({first:?} with {second:?})"
|
||||||
|
)))
|
||||||
}
|
}
|
||||||
// Incompatible initialized (e.g. `int` from the `try` body and
|
// Incompatible initialized (e.g. `int` from the `try` body and
|
||||||
// `Throwable` from a handler in one slot): `Top`, like `javac`.
|
// `Throwable` from a handler in one slot): `Top`, like `javac`.
|
||||||
|
|
@ -139,7 +144,7 @@ impl Merger<'_> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Least common ancestor: climb the chains, fallback — `Object`.
|
/// Least common ancestor: climb the chains, fallback - `Object`.
|
||||||
fn common_superclass(&self, first: &str, second: &str) -> String {
|
fn common_superclass(&self, first: &str, second: &str) -> String {
|
||||||
if first == second {
|
if first == second {
|
||||||
return first.to_string();
|
return first.to_string();
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,11 @@
|
||||||
//!
|
//!
|
||||||
//! Frames are the most expensive part of any Rust obfuscator: the JVM verifier
|
//! Frames are the most expensive part of any Rust obfuscator: the JVM verifier
|
||||||
//! (class version ≥ 50) demands an exact `StackMapTable`, or the class won't load.
|
//! (class version ≥ 50) demands an exact `StackMapTable`, or the class won't load.
|
||||||
//! Implementation options and their costs — `docs/stack.md`.
|
//! Implementation options and their costs - `docs/stack.md`.
|
||||||
//!
|
//!
|
||||||
//! Method: CFG over labels → worklist with types (`xfer`) → LUB merge (`merge`)
|
//! Method: CFG over labels → worklist with types (`xfer`) → LUB merge (`merge`)
|
||||||
//! → emit compact frames (`emit`). Byte offsets come from the same encoding the
|
//! → emit compact frames (`emit`). Byte offsets come from the same encoding the
|
||||||
//! writer uses — otherwise frames would drift apart from the bytes.
|
//! writer uses - otherwise frames would drift apart from the bytes.
|
||||||
|
|
||||||
mod emit;
|
mod emit;
|
||||||
mod flow;
|
mod flow;
|
||||||
|
|
@ -113,7 +113,7 @@ pub fn compute_frames(
|
||||||
emit::emit_frames(&states, &targets, &initial.locals, &merger)
|
emit::emit_frames(&states, &targets, &initial.locals, &merger)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The byte offset of each instruction — using the same encoding as the writer.
|
/// The byte offset of each instruction - using the same encoding as the writer.
|
||||||
fn layout(
|
fn layout(
|
||||||
bare: &[&Insn],
|
bare: &[&Insn],
|
||||||
labels: &BTreeMap<Label, u32>,
|
labels: &BTreeMap<Label, u32>,
|
||||||
|
|
@ -188,7 +188,7 @@ fn run_worklist(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Merge a state into an instruction; `true` — changed, keep walking.
|
/// Merge a state into an instruction; `true` - changed, keep walking.
|
||||||
fn flow_into(
|
fn flow_into(
|
||||||
merger: &Merger<'_>,
|
merger: &Merger<'_>,
|
||||||
states: &mut [Option<State>],
|
states: &mut [Option<State>],
|
||||||
|
|
@ -220,7 +220,7 @@ fn flow_into(
|
||||||
}
|
}
|
||||||
|
|
||||||
/// CFG edges without exceptions: fallthrough + branch targets.
|
/// CFG edges without exceptions: fallthrough + branch targets.
|
||||||
/// The exception type at a handler's entry (`catch-all` — any throwable).
|
/// The exception type at a handler's entry (`catch-all` - any throwable).
|
||||||
fn catch_type(catch: &Option<String>) -> VerificationType {
|
fn catch_type(catch: &Option<String>) -> VerificationType {
|
||||||
match catch {
|
match catch {
|
||||||
Some(name) => VerificationType::Object(name.clone()),
|
Some(name) => VerificationType::Object(name.clone()),
|
||||||
|
|
|
||||||
191
crates/noxis-classfile/src/format/frames/tests/joins.rs
Normal file
191
crates/noxis-classfile/src/format/frames/tests/joins.rs
Normal file
|
|
@ -0,0 +1,191 @@
|
||||||
|
//! Join-shape tests: loop heads, wide array loads, nested construction.
|
||||||
|
|
||||||
|
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
||||||
|
|
||||||
|
use super::super::{StackMapFrame, VerificationType, compute_frames};
|
||||||
|
use super::{NoHierarchy, code_method, load, load_ref, store, store_ref};
|
||||||
|
use crate::insn::{BranchCond, Cond, Const, Insn, Prim};
|
||||||
|
|
||||||
|
/// `anewarray [B` (a partially bound multi-dim array) yields its descriptor
|
||||||
|
/// verbatim: wrapping it again (`[L[B;`) is not a class and HotSpot rejects
|
||||||
|
/// the pool entry.
|
||||||
|
#[test]
|
||||||
|
fn anewarray_of_array_keeps_descriptor() {
|
||||||
|
let method = code_method(
|
||||||
|
"make",
|
||||||
|
"()Ljava/lang/Object;",
|
||||||
|
2,
|
||||||
|
1,
|
||||||
|
vec![
|
||||||
|
Insn::Mark(0),
|
||||||
|
Insn::Const(Const::Int(3)),
|
||||||
|
Insn::ANewArray("[B".to_string()),
|
||||||
|
store_ref(0),
|
||||||
|
load_ref(0),
|
||||||
|
Insn::Branch(BranchCond::Null { is_null: false }, 1),
|
||||||
|
Insn::AconstNull,
|
||||||
|
store_ref(0),
|
||||||
|
Insn::Goto(2),
|
||||||
|
Insn::Mark(1),
|
||||||
|
Insn::Mark(2),
|
||||||
|
load_ref(0),
|
||||||
|
Insn::AReturn,
|
||||||
|
],
|
||||||
|
vec![],
|
||||||
|
);
|
||||||
|
let frames = compute_frames("demo/Make", &method, &NoHierarchy).expect("frames");
|
||||||
|
assert_eq!(
|
||||||
|
frames,
|
||||||
|
vec![StackMapFrame::Append {
|
||||||
|
offset_delta: 8,
|
||||||
|
locals: vec![VerificationType::Object("[[B".to_string())],
|
||||||
|
},]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A loop head keeps `Top` for a slot written only inside the loop.
|
||||||
|
///
|
||||||
|
/// Inflows: entry (never wrote it) and back edge (wrote `Integer`). Like
|
||||||
|
/// `javac`, the join declares `Top`: a precise type would reject the inflow
|
||||||
|
/// that never wrote the slot (`Type top is not assignable`).
|
||||||
|
#[test]
|
||||||
|
fn loop_head_keeps_top_for_loop_local() {
|
||||||
|
let method = code_method(
|
||||||
|
"sum",
|
||||||
|
"()I",
|
||||||
|
2,
|
||||||
|
3,
|
||||||
|
vec![
|
||||||
|
Insn::Mark(0),
|
||||||
|
Insn::Const(Const::Int(0)),
|
||||||
|
store(0),
|
||||||
|
Insn::Const(Const::Int(0)),
|
||||||
|
store(1),
|
||||||
|
Insn::Mark(1),
|
||||||
|
load(1),
|
||||||
|
Insn::Const(Const::Int(10)),
|
||||||
|
Insn::Branch(BranchCond::Int(Cond::Ge), 2),
|
||||||
|
load(1),
|
||||||
|
store(2),
|
||||||
|
load(2),
|
||||||
|
store(0),
|
||||||
|
Insn::Iinc { index: 1, delta: 1 },
|
||||||
|
Insn::Goto(1),
|
||||||
|
Insn::Mark(2),
|
||||||
|
load(0),
|
||||||
|
Insn::Return(Some(Prim::Int)),
|
||||||
|
],
|
||||||
|
vec![],
|
||||||
|
);
|
||||||
|
let frames = compute_frames("demo/Sum", &method, &NoHierarchy).expect("frames");
|
||||||
|
assert_eq!(
|
||||||
|
frames,
|
||||||
|
vec![
|
||||||
|
StackMapFrame::Append {
|
||||||
|
offset_delta: 4,
|
||||||
|
locals: vec![VerificationType::Integer, VerificationType::Integer],
|
||||||
|
},
|
||||||
|
StackMapFrame::Same { offset_delta: 8 },
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A nested construction (`new Outer(new Inner())`) initializes only the
|
||||||
|
/// inner object: the outer `Uninitialized` survives with its own offset, so
|
||||||
|
/// a later join still sees it instead of a clobbered `Object(Inner)`.
|
||||||
|
#[test]
|
||||||
|
fn nested_construction_keeps_outer_uninitialized() {
|
||||||
|
let method = code_method(
|
||||||
|
"make",
|
||||||
|
"()I",
|
||||||
|
4,
|
||||||
|
1,
|
||||||
|
vec![
|
||||||
|
Insn::Mark(0),
|
||||||
|
Insn::New("demo/Outer".to_string()),
|
||||||
|
Insn::Dup,
|
||||||
|
Insn::New("demo/Inner".to_string()),
|
||||||
|
Insn::Dup,
|
||||||
|
Insn::Invoke {
|
||||||
|
kind: crate::insn::InvokeKind::Special,
|
||||||
|
owner: "demo/Inner".to_string(),
|
||||||
|
name: "<init>".to_string(),
|
||||||
|
descriptor: "()V".to_string(),
|
||||||
|
itf: false,
|
||||||
|
},
|
||||||
|
Insn::Invoke {
|
||||||
|
kind: crate::insn::InvokeKind::Special,
|
||||||
|
owner: "demo/Outer".to_string(),
|
||||||
|
name: "<init>".to_string(),
|
||||||
|
descriptor: "(Ldemo/Inner;)V".to_string(),
|
||||||
|
itf: false,
|
||||||
|
},
|
||||||
|
store_ref(0),
|
||||||
|
load_ref(0),
|
||||||
|
Insn::Branch(BranchCond::Null { is_null: false }, 1),
|
||||||
|
Insn::Const(Const::Int(0)),
|
||||||
|
Insn::Goto(2),
|
||||||
|
Insn::Mark(1),
|
||||||
|
Insn::Const(Const::Int(1)),
|
||||||
|
Insn::Mark(2),
|
||||||
|
Insn::Return(Some(Prim::Int)),
|
||||||
|
],
|
||||||
|
vec![],
|
||||||
|
);
|
||||||
|
let frames = compute_frames("demo/Make", &method, &NoHierarchy).expect("frames");
|
||||||
|
assert_eq!(
|
||||||
|
frames,
|
||||||
|
vec![
|
||||||
|
StackMapFrame::Append {
|
||||||
|
offset_delta: 11,
|
||||||
|
locals: vec![VerificationType::Object("demo/Outer".to_string())],
|
||||||
|
},
|
||||||
|
StackMapFrame::SameLocals {
|
||||||
|
offset_delta: 0,
|
||||||
|
local: VerificationType::Integer,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `laload` pushes a pair (`long` + `Top`): a following `lcmp` needs four
|
||||||
|
/// entries and must not underflow.
|
||||||
|
#[test]
|
||||||
|
fn array_load_wide_pushes_pair() {
|
||||||
|
let method = code_method(
|
||||||
|
"get",
|
||||||
|
"([JI)J",
|
||||||
|
4,
|
||||||
|
2,
|
||||||
|
vec![
|
||||||
|
Insn::Mark(0),
|
||||||
|
load_ref(0),
|
||||||
|
load(1),
|
||||||
|
Insn::ArrayLoad(crate::insn::ArrayKind::Long),
|
||||||
|
Insn::Const(Const::Long(0)),
|
||||||
|
Insn::Cmp {
|
||||||
|
prim: Prim::Long,
|
||||||
|
nan_great: false,
|
||||||
|
},
|
||||||
|
Insn::Branch(BranchCond::Zero(Cond::Ne), 1),
|
||||||
|
Insn::Const(Const::Long(1)),
|
||||||
|
Insn::Goto(2),
|
||||||
|
Insn::Mark(1),
|
||||||
|
Insn::Const(Const::Long(0)),
|
||||||
|
Insn::Mark(2),
|
||||||
|
Insn::Return(Some(Prim::Long)),
|
||||||
|
],
|
||||||
|
vec![],
|
||||||
|
);
|
||||||
|
let frames = compute_frames("demo/Wide", &method, &NoHierarchy).expect("frames");
|
||||||
|
assert_eq!(
|
||||||
|
frames,
|
||||||
|
vec![
|
||||||
|
StackMapFrame::Same { offset_delta: 8 },
|
||||||
|
StackMapFrame::SameLocals {
|
||||||
|
offset_delta: 0,
|
||||||
|
local: VerificationType::Long,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
//! Unit-тесты `compute_frames` на синтезированном IR.
|
//! Unit tests for `compute_frames` on synthesized IR.
|
||||||
|
|
||||||
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
||||||
|
|
||||||
|
|
@ -7,8 +7,8 @@ use crate::Hierarchy;
|
||||||
use crate::insn::{BranchCond, Cond, Const, Insn, Prim, SlotKind};
|
use crate::insn::{BranchCond, Cond, Const, Insn, Prim, SlotKind};
|
||||||
use crate::model::{Code, ExceptionHandler, Method};
|
use crate::model::{Code, ExceptionHandler, Method};
|
||||||
|
|
||||||
/// Пустая иерархия: всё неизвестное — наследники `Object` (для LUB хватает).
|
/// Empty hierarchy: everything unknown descends from `Object` (enough for LUB).
|
||||||
struct NoHierarchy;
|
pub(super) struct NoHierarchy;
|
||||||
|
|
||||||
impl Hierarchy for NoHierarchy {
|
impl Hierarchy for NoHierarchy {
|
||||||
fn superclass(&self, _class: &str) -> Option<String> {
|
fn superclass(&self, _class: &str) -> Option<String> {
|
||||||
|
|
@ -16,7 +16,7 @@ impl Hierarchy for NoHierarchy {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn code_method(
|
pub(super) fn code_method(
|
||||||
name: &str,
|
name: &str,
|
||||||
descriptor: &str,
|
descriptor: &str,
|
||||||
max_stack: u16,
|
max_stack: u16,
|
||||||
|
|
@ -40,7 +40,7 @@ fn code_method(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ветвь → два кадра (`Same`, `SameLocals`).
|
/// A branch emits two frames (`Same`, `SameLocals`).
|
||||||
#[test]
|
#[test]
|
||||||
fn branch_emits_same_frames() {
|
fn branch_emits_same_frames() {
|
||||||
let mut method = code_method(
|
let mut method = code_method(
|
||||||
|
|
@ -76,7 +76,11 @@ fn branch_emits_same_frames() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `try/catch`: handler получает `Full`, продолжение — `Append`.
|
/// `try/catch`: the handler gets `Full`, the continuation - `Same`.
|
||||||
|
///
|
||||||
|
/// Slot 1 is written only inside the handler, so the join keeps `Top` there
|
||||||
|
/// (like `javac`): a precise type would reject the inflow that never wrote
|
||||||
|
/// it (`Type top is not assignable`).
|
||||||
#[test]
|
#[test]
|
||||||
fn handler_emits_full_frame() {
|
fn handler_emits_full_frame() {
|
||||||
let method = code_method(
|
let method = code_method(
|
||||||
|
|
@ -115,15 +119,12 @@ fn handler_emits_full_frame() {
|
||||||
locals: vec![VerificationType::Integer],
|
locals: vec![VerificationType::Integer],
|
||||||
stack: vec![VerificationType::Object("java/lang/Exception".to_string())],
|
stack: vec![VerificationType::Object("java/lang/Exception".to_string())],
|
||||||
},
|
},
|
||||||
StackMapFrame::Append {
|
StackMapFrame::Same { offset_delta: 2 },
|
||||||
offset_delta: 2,
|
|
||||||
locals: vec![VerificationType::Object("java/lang/Exception".to_string())],
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Два `new` на разных ветвях нельзя слить — честная ошибка, не `Top`.
|
/// Two `new` on different branches cannot merge - an honest error, not `Top`.
|
||||||
#[test]
|
#[test]
|
||||||
fn uninitialized_merge_is_rejected() {
|
fn uninitialized_merge_is_rejected() {
|
||||||
let method = code_method(
|
let method = code_method(
|
||||||
|
|
@ -150,28 +151,37 @@ fn uninitialized_merge_is_rejected() {
|
||||||
],
|
],
|
||||||
vec![],
|
vec![],
|
||||||
);
|
);
|
||||||
let err = compute_frames("demo/Pick", &method, &NoHierarchy).expect_err("слияние");
|
let err = compute_frames("demo/Pick", &method, &NoHierarchy).expect_err("merge");
|
||||||
assert!(
|
assert!(
|
||||||
err.to_string().contains("неинициализированных"),
|
err.to_string().contains("uninitialized"),
|
||||||
"не тот текст: {err}"
|
"wrong message: {err}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load(slot: u16) -> Insn {
|
mod joins;
|
||||||
|
|
||||||
|
pub(super) fn load(slot: u16) -> Insn {
|
||||||
Insn::Load {
|
Insn::Load {
|
||||||
slot,
|
slot,
|
||||||
kind: SlotKind::Int,
|
kind: SlotKind::Int,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn store(slot: u16) -> Insn {
|
pub(super) fn load_ref(slot: u16) -> Insn {
|
||||||
|
Insn::Load {
|
||||||
|
slot,
|
||||||
|
kind: SlotKind::Ref,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn store(slot: u16) -> Insn {
|
||||||
Insn::Store {
|
Insn::Store {
|
||||||
slot,
|
slot,
|
||||||
kind: SlotKind::Int,
|
kind: SlotKind::Int,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn store_ref(slot: u16) -> Insn {
|
pub(super) fn store_ref(slot: u16) -> Insn {
|
||||||
Insn::Store {
|
Insn::Store {
|
||||||
slot,
|
slot,
|
||||||
kind: SlotKind::Ref,
|
kind: SlotKind::Ref,
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
//! Вызовы и константы: дескрипторы, `<init>`, слоты значений.
|
//! Calls and constants: descriptors, `<init>`, value slots.
|
||||||
|
|
||||||
use super::super::VerificationType;
|
use super::super::VerificationType;
|
||||||
use super::super::merge::descs::{DescError, field_slots, method_slots};
|
use super::super::merge::descs::{DescError, field_slots, method_slots};
|
||||||
|
|
@ -34,18 +34,12 @@ pub(super) fn invoke(
|
||||||
descriptor: &str,
|
descriptor: &str,
|
||||||
) -> Result<(), ClassfileError> {
|
) -> Result<(), ClassfileError> {
|
||||||
let (params, ret) = method_slots(descriptor).map_err(|e| context(class, method, e))?;
|
let (params, ret) = method_slots(descriptor).map_err(|e| context(class, method, e))?;
|
||||||
super::stack::pop_slots(
|
super::stack::pop_slots(state, count_slots(¶ms), class, method, "call arguments")?;
|
||||||
state,
|
|
||||||
count_slots(¶ms),
|
|
||||||
class,
|
|
||||||
method,
|
|
||||||
"аргументы вызова",
|
|
||||||
)?;
|
|
||||||
let mut receiver = None;
|
let mut receiver = None;
|
||||||
if kind != InvokeKind::Static {
|
if kind != InvokeKind::Static {
|
||||||
receiver = super::stack::pop_slots(state, 1, class, method, "objectref")?.pop();
|
receiver = super::stack::pop_slots(state, 1, class, method, "objectref")?.pop();
|
||||||
}
|
}
|
||||||
// `<init>` инициализирует получателя во ВСЕХ его копиях.
|
// `<init>` initializes the receiver in ALL its copies.
|
||||||
if name == "<init>" {
|
if name == "<init>" {
|
||||||
match receiver {
|
match receiver {
|
||||||
Some(VerificationType::UninitializedThis) => {
|
Some(VerificationType::UninitializedThis) => {
|
||||||
|
|
@ -55,9 +49,11 @@ pub(super) fn invoke(
|
||||||
&initialized(class),
|
&initialized(class),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Some(VerificationType::Uninitialized(_)) => {
|
// Only the receiver's own allocation site: an enclosing `new`
|
||||||
|
// whose `<init>` has not run yet must stay uninitialized.
|
||||||
|
Some(VerificationType::Uninitialized(offset)) => {
|
||||||
let fresh = VerificationType::Object(owner.to_string());
|
let fresh = VerificationType::Object(owner.to_string());
|
||||||
replace_uninit(state, &fresh);
|
replace(state, &VerificationType::Uninitialized(offset), &fresh);
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
@ -68,12 +64,12 @@ pub(super) fn invoke(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Число слотов всех параметров.
|
/// Slot count across all parameters.
|
||||||
pub(super) fn count_slots(params: &[Vec<VerificationType>]) -> usize {
|
pub(super) fn count_slots(params: &[Vec<VerificationType>]) -> usize {
|
||||||
params.iter().map(Vec::len).sum()
|
params.iter().map(Vec::len).sum()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Заменить все вхождения типа новым (инициализация через `<init>`).
|
/// Replace every occurrence of a type with a new one (init via `<init>`).
|
||||||
fn replace(state: &mut State, from: &VerificationType, to: &VerificationType) {
|
fn replace(state: &mut State, from: &VerificationType, to: &VerificationType) {
|
||||||
for slot in state.locals.iter_mut().chain(state.stack.iter_mut()) {
|
for slot in state.locals.iter_mut().chain(state.stack.iter_mut()) {
|
||||||
if slot == from {
|
if slot == from {
|
||||||
|
|
@ -82,15 +78,6 @@ fn replace(state: &mut State, from: &VerificationType, to: &VerificationType) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Любой `Uninitialized` (кроме `this`) → тип владельца (созданный `new`).
|
|
||||||
fn replace_uninit(state: &mut State, to: &VerificationType) {
|
|
||||||
for slot in state.locals.iter_mut().chain(state.stack.iter_mut()) {
|
|
||||||
if matches!(slot, VerificationType::Uninitialized(_)) {
|
|
||||||
*slot = to.clone();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn initialized(class: &str) -> VerificationType {
|
fn initialized(class: &str) -> VerificationType {
|
||||||
VerificationType::Object(class.to_string())
|
VerificationType::Object(class.to_string())
|
||||||
}
|
}
|
||||||
|
|
@ -115,7 +102,7 @@ pub(super) fn prim_push(prim: Prim) -> Vec<VerificationType> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `(слоты аргумента, примитив результата)` для конверсии.
|
/// `(argument slots, result primitive)` for a conversion.
|
||||||
pub(super) fn convert_shapes(op: Convert) -> (usize, Prim) {
|
pub(super) fn convert_shapes(op: Convert) -> (usize, Prim) {
|
||||||
use Convert::{D2F, D2I, D2L, F2D, F2I, F2L, I2B, I2C, I2D, I2F, I2L, I2S, L2D, L2F, L2I};
|
use Convert::{D2F, D2I, D2L, F2D, F2I, F2L, I2B, I2C, I2D, I2F, I2L, I2S, L2D, L2F, L2I};
|
||||||
use Prim::{Double, Float, Int, Long};
|
use Prim::{Double, Float, Int, Long};
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
//! Состояние типов и transfer-функции инструкций.
|
//! Type states and per-instruction transfer functions.
|
||||||
//!
|
//!
|
||||||
//! Стек растёт вправо (`push` — в конец). `long`/`double` занимают два слота:
|
//! The stack grows right (`push` appends). `long`/`double` take two slots:
|
||||||
//! значение + `Top`. Проверяется только ФОРМА (глубина, категории), не
|
//! value + `Top`. Only the SHAPE is checked (depth, categories), not
|
||||||
//! соответствие типов — неверные типы честно упадут на верификаторе JVM.
|
//! type agreement - wrong types honestly fail on the JVM verifier.
|
||||||
|
|
||||||
pub(super) mod calls;
|
pub(super) mod calls;
|
||||||
pub(super) mod stack;
|
pub(super) mod stack;
|
||||||
|
|
@ -15,14 +15,14 @@ use crate::insn::{Arith, ArrayKind, BranchCond, Insn, SlotKind};
|
||||||
use calls::{const_slots, context, convert_shapes, count_slots, invoke, prim_push, prim_slots};
|
use calls::{const_slots, context, convert_shapes, count_slots, invoke, prim_push, prim_slots};
|
||||||
use stack::{dup, dup2, local_get, pop_slots, push, store_local, wide_push};
|
use stack::{dup, dup2, local_get, pop_slots, push, store_local, wide_push};
|
||||||
|
|
||||||
/// Типы locals + operand stack в одной точке программы.
|
/// Local types plus the operand stack at one program point.
|
||||||
#[derive(Debug, Clone, Default, PartialEq)]
|
#[derive(Debug, Clone, Default, PartialEq)]
|
||||||
pub(super) struct State {
|
pub(super) struct State {
|
||||||
pub locals: Vec<VerificationType>,
|
pub locals: Vec<VerificationType>,
|
||||||
pub stack: Vec<VerificationType>,
|
pub stack: Vec<VerificationType>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Применить инструкцию к состоянию (на месте).
|
/// Apply one instruction to the state (in place).
|
||||||
pub(super) fn transfer(
|
pub(super) fn transfer(
|
||||||
class: &str,
|
class: &str,
|
||||||
method: &str,
|
method: &str,
|
||||||
|
|
@ -31,7 +31,11 @@ pub(super) fn transfer(
|
||||||
state: &mut State,
|
state: &mut State,
|
||||||
) -> Result<(), ClassfileError> {
|
) -> Result<(), ClassfileError> {
|
||||||
match insn {
|
match insn {
|
||||||
Insn::Mark(label) => Err(invalid(class, method, &format!("маркер {label} не снят"))),
|
Insn::Mark(label) => Err(invalid(
|
||||||
|
class,
|
||||||
|
method,
|
||||||
|
&format!("marker {label} was not stripped"),
|
||||||
|
)),
|
||||||
Insn::Nop => Ok(()),
|
Insn::Nop => Ok(()),
|
||||||
Insn::AconstNull => push(state, vec![VerificationType::Null]),
|
Insn::AconstNull => push(state, vec![VerificationType::Null]),
|
||||||
Insn::Const(value) => {
|
Insn::Const(value) => {
|
||||||
|
|
@ -51,29 +55,29 @@ pub(super) fn transfer(
|
||||||
Insn::Iinc { index, .. } => {
|
Insn::Iinc { index, .. } => {
|
||||||
let current = local_get(state, *index, class, method)?;
|
let current = local_get(state, *index, class, method)?;
|
||||||
if current != VerificationType::Integer {
|
if current != VerificationType::Integer {
|
||||||
return Err(invalid(class, method, "iinc не по int"));
|
return Err(invalid(class, method, "iinc over a non-int"));
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Insn::ArrayLoad(kind) => {
|
Insn::ArrayLoad(kind) => {
|
||||||
pop_slots(state, 1, class, method, "индекс массива")?;
|
pop_slots(state, 1, class, method, "array index")?;
|
||||||
let array = pop_slots(state, 1, class, method, "массив")?.remove(0);
|
let array = pop_slots(state, 1, class, method, "array")?.remove(0);
|
||||||
push(state, vec![array_element(&array, *kind)])
|
push(state, wide_push(array_element(&array, *kind)))
|
||||||
}
|
}
|
||||||
Insn::ArrayStore(kind) => {
|
Insn::ArrayStore(kind) => {
|
||||||
let count = match kind {
|
let count = match kind {
|
||||||
ArrayKind::Long | ArrayKind::Double => 2,
|
ArrayKind::Long | ArrayKind::Double => 2,
|
||||||
_ => 1,
|
_ => 1,
|
||||||
};
|
};
|
||||||
pop_slots(state, count, class, method, "значение")?;
|
pop_slots(state, count, class, method, "value")?;
|
||||||
pop_slots(state, 1, class, method, "индекс массива")?;
|
pop_slots(state, 1, class, method, "array index")?;
|
||||||
pop_slots(state, 1, class, method, "массив")?;
|
pop_slots(state, 1, class, method, "array")?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Insn::Pop => {
|
Insn::Pop => {
|
||||||
let top = pop_slots(state, 1, class, method, "pop")?.remove(0);
|
let top = pop_slots(state, 1, class, method, "pop")?.remove(0);
|
||||||
if top == VerificationType::Top {
|
if top == VerificationType::Top {
|
||||||
return Err(invalid(class, method, "pop половины long/double"));
|
return Err(invalid(class, method, "pop of a long/double half"));
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -90,7 +94,7 @@ pub(super) fn transfer(
|
||||||
Insn::Swap => {
|
Insn::Swap => {
|
||||||
let mut taken = pop_slots(state, 2, class, method, "swap")?;
|
let mut taken = pop_slots(state, 2, class, method, "swap")?;
|
||||||
if taken.contains(&VerificationType::Top) {
|
if taken.contains(&VerificationType::Top) {
|
||||||
return Err(invalid(class, method, "swap с половиной"));
|
return Err(invalid(class, method, "swap with a half"));
|
||||||
}
|
}
|
||||||
taken.reverse();
|
taken.reverse();
|
||||||
push(state, taken)
|
push(state, taken)
|
||||||
|
|
@ -100,7 +104,7 @@ pub(super) fn transfer(
|
||||||
if *op == Arith::Neg {
|
if *op == Arith::Neg {
|
||||||
pop_slots(state, slots as usize, class, method, "neg")?;
|
pop_slots(state, slots as usize, class, method, "neg")?;
|
||||||
} else if matches!(op, Arith::Shl | Arith::Shr | Arith::Ushr) {
|
} else if matches!(op, Arith::Shl | Arith::Shr | Arith::Ushr) {
|
||||||
// Сдвиг берёт (значение, int), а не два значения.
|
// A shift takes (value, int), not two values.
|
||||||
pop_slots(state, slots as usize + 1, class, method, "shift")?;
|
pop_slots(state, slots as usize + 1, class, method, "shift")?;
|
||||||
} else {
|
} else {
|
||||||
pop_slots(state, 2 * slots as usize, class, method, "arith")?;
|
pop_slots(state, 2 * slots as usize, class, method, "arith")?;
|
||||||
|
|
@ -128,7 +132,7 @@ pub(super) fn transfer(
|
||||||
Insn::Jsr(_) | Insn::Ret(_) => Err(invalid(
|
Insn::Jsr(_) | Insn::Ret(_) => Err(invalid(
|
||||||
class,
|
class,
|
||||||
method,
|
method,
|
||||||
"подпрограммы jsr/ret не поддерживаются",
|
"jsr/ret subroutines are not supported",
|
||||||
)),
|
)),
|
||||||
Insn::TableSwitch { .. } | Insn::LookupSwitch { .. } => {
|
Insn::TableSwitch { .. } | Insn::LookupSwitch { .. } => {
|
||||||
pop_slots(state, 1, class, method, "switch")?;
|
pop_slots(state, 1, class, method, "switch")?;
|
||||||
|
|
@ -192,7 +196,7 @@ pub(super) fn transfer(
|
||||||
ArrayKind::Int => "[I",
|
ArrayKind::Int => "[I",
|
||||||
ArrayKind::Long => "[J",
|
ArrayKind::Long => "[J",
|
||||||
ArrayKind::Reference => {
|
ArrayKind::Reference => {
|
||||||
return Err(invalid(class, method, "newarray ссылочного типа"));
|
return Err(invalid(class, method, "newarray of a reference type"));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
push(
|
push(
|
||||||
|
|
@ -202,7 +206,16 @@ pub(super) fn transfer(
|
||||||
}
|
}
|
||||||
Insn::ANewArray(name) => {
|
Insn::ANewArray(name) => {
|
||||||
pop_slots(state, 1, class, method, "anewarray")?;
|
pop_slots(state, 1, class, method, "anewarray")?;
|
||||||
push(state, vec![VerificationType::Object(format!("[L{name};"))])
|
// `anewarray` adds one dimension: a plain class becomes
|
||||||
|
// `[L...;`, an already-array operand (`anewarray [B` for a
|
||||||
|
// partially bound multi-dim) gains one more `[`. Wrapping an
|
||||||
|
// array operand in `L...;` (`[L[B;`) is not a class.
|
||||||
|
let descriptor = if name.starts_with('[') {
|
||||||
|
format!("[{name}")
|
||||||
|
} else {
|
||||||
|
format!("[L{name};")
|
||||||
|
};
|
||||||
|
push(state, vec![VerificationType::Object(descriptor)])
|
||||||
}
|
}
|
||||||
Insn::MultiANewArray { descriptor, dims } => {
|
Insn::MultiANewArray { descriptor, dims } => {
|
||||||
pop_slots(state, *dims as usize, class, method, "multianewarray")?;
|
pop_slots(state, *dims as usize, class, method, "multianewarray")?;
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ use super::State;
|
||||||
use crate::error::ClassfileError;
|
use crate::error::ClassfileError;
|
||||||
use crate::insn::SlotKind;
|
use crate::insn::SlotKind;
|
||||||
|
|
||||||
/// Запись в local с учётом ширины (`long`/`double` — парой с `Top`).
|
/// Запись в local с учётом ширины (`long`/`double` - парой с `Top`).
|
||||||
pub(super) fn store_local(
|
pub(super) fn store_local(
|
||||||
state: &mut State,
|
state: &mut State,
|
||||||
slot: u16,
|
slot: u16,
|
||||||
|
|
@ -38,7 +38,7 @@ pub(super) fn push(state: &mut State, slots: Vec<VerificationType>) -> Result<()
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Снять `n` слотов (снизу вверх); нехватка — честная ошибка.
|
/// Снять `n` слотов (снизу вверх); нехватка - честная ошибка.
|
||||||
pub(super) fn pop_slots(
|
pub(super) fn pop_slots(
|
||||||
state: &mut State,
|
state: &mut State,
|
||||||
n: usize,
|
n: usize,
|
||||||
|
|
@ -84,7 +84,7 @@ pub(super) fn local_set(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Значение local'а целиком: `long`/`double` — парой с `Top`.
|
/// Значение local'а целиком: `long`/`double` - парой с `Top`.
|
||||||
pub(super) fn wide_push(value: VerificationType) -> Vec<VerificationType> {
|
pub(super) fn wide_push(value: VerificationType) -> Vec<VerificationType> {
|
||||||
match value {
|
match value {
|
||||||
VerificationType::Long | VerificationType::Double => vec![value, VerificationType::Top],
|
VerificationType::Long | VerificationType::Double => vec![value, VerificationType::Top],
|
||||||
|
|
@ -112,7 +112,7 @@ pub(super) fn dup(
|
||||||
push(state, out)
|
push(state, out)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `dup2*`: форма выбирается по вершине (`Top` — вторая половина `long`).
|
/// `dup2*`: форма выбирается по вершине (`Top` - вторая половина `long`).
|
||||||
pub(super) fn dup2(
|
pub(super) fn dup2(
|
||||||
state: &mut State,
|
state: &mut State,
|
||||||
class: &str,
|
class: &str,
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
//! `BootstrapMethods`, `EnclosingMethod`, `MethodParameters` and `ConstantValue`
|
//! `BootstrapMethods`, `EnclosingMethod`, `MethodParameters` and `ConstantValue`
|
||||||
//! are modeled explicitly (they hold pool references that survive rebuilding);
|
//! are modeled explicitly (they hold pool references that survive rebuilding);
|
||||||
//! everything else that is structured but unneeded by us goes to `Raw` as-is.
|
//! everything else that is structured but unneeded by us goes to `Raw` as-is.
|
||||||
//! `Code` and `StackMapTable` are not handled here — the calling method code
|
//! `Code` and `StackMapTable` are not handled here - the calling method code
|
||||||
//! parses them (see `super::class`).
|
//! parses them (see `super::class`).
|
||||||
|
|
||||||
use ristretto_classfile::ConstantPool;
|
use ristretto_classfile::ConstantPool;
|
||||||
|
|
@ -131,7 +131,7 @@ fn inner(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The enclosing class's method (`0` — class only, no method).
|
/// The enclosing class's method (`0` - class only, no method).
|
||||||
fn enclosing_method(
|
fn enclosing_method(
|
||||||
pool: &ConstantPool<'_>,
|
pool: &ConstantPool<'_>,
|
||||||
index: u16,
|
index: u16,
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ use super::super::pool::class_name;
|
||||||
use crate::error::ClassfileError;
|
use crate::error::ClassfileError;
|
||||||
use crate::format::{StackMapFrame, VerificationType};
|
use crate::format::{StackMapFrame, VerificationType};
|
||||||
|
|
||||||
/// Backend frames → ours (index deltas — as-is).
|
/// Backend frames → ours (index deltas - as-is).
|
||||||
pub(super) fn to_frames(
|
pub(super) fn to_frames(
|
||||||
frames: &[StackFrame],
|
frames: &[StackFrame],
|
||||||
pool: &ConstantPool<'_>,
|
pool: &ConstantPool<'_>,
|
||||||
|
|
@ -100,7 +100,7 @@ fn vtype(value: &RtType, pool: &ConstantPool<'_>) -> Result<VerificationType, Cl
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The catch type (`0` — catch-all for `finally`/`any`).
|
/// The catch type (`0` - catch-all for `finally`/`any`).
|
||||||
pub(super) fn catch_type(
|
pub(super) fn catch_type(
|
||||||
pool: &ConstantPool<'_>,
|
pool: &ConstantPool<'_>,
|
||||||
index: u16,
|
index: u16,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
//! Method-body labels: where they must stand and how they resolve.
|
//! Method-body labels: where they must stand and how they resolve.
|
||||||
//!
|
//!
|
||||||
//! The backend numbers instructions in order — labels are placed directly on the
|
//! The backend numbers instructions in order - labels are placed directly on the
|
||||||
//! numbers, without byte offsets.
|
//! numbers, without byte offsets.
|
||||||
|
|
||||||
use std::collections::{BTreeMap, BTreeSet};
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
|
|
@ -13,7 +13,7 @@ use crate::insn::Label;
|
||||||
|
|
||||||
/// Instruction numbers that must carry a label.
|
/// Instruction numbers that must carry a label.
|
||||||
///
|
///
|
||||||
/// `extra` — debug-table positions (already as numbers): pass insertions must
|
/// `extra` - debug-table positions (already as numbers): pass insertions must
|
||||||
/// not leave them without a label.
|
/// not leave them without a label.
|
||||||
pub(super) fn label_indexes(
|
pub(super) fn label_indexes(
|
||||||
code: &[Instruction],
|
code: &[Instruction],
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
//! Backend `Code` → our IR method body: labels, exceptions, frames.
|
//! Backend `Code` → our IR method body: labels, exceptions, frames.
|
||||||
//!
|
//!
|
||||||
//! The backend already turned byte offsets into instruction indices — the
|
//! The backend already turned byte offsets into instruction indices - the
|
||||||
//! converters work directly with them: labels are placed on branch targets,
|
//! converters work directly with them: labels are placed on branch targets,
|
||||||
//! try-block boundaries and debug-table positions; frames are carried over as-is
|
//! try-block boundaries and debug-table positions; frames are carried over as-is
|
||||||
//! (index deltas). `LineNumberTable`/`LocalVariableTable` positions are labels
|
//! (index deltas). `LineNumberTable`/`LocalVariableTable` positions are labels
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ use super::super::pool::const_from;
|
||||||
use crate::error::ClassfileError;
|
use crate::error::ClassfileError;
|
||||||
use crate::insn::{Arith, ArrayKind, Const, Convert, Insn, Prim, SlotKind};
|
use crate::insn::{Arith, ArrayKind, Const, Convert, Insn, Prim, SlotKind};
|
||||||
|
|
||||||
/// `Some` — the instruction is covered; `None` — hand it to the next converters.
|
/// `Some` - the instruction is covered; `None` - hand it to the next converters.
|
||||||
pub(super) fn convert_basic(
|
pub(super) fn convert_basic(
|
||||||
instruction: &Instruction,
|
instruction: &Instruction,
|
||||||
pool: &ConstantPool<'_>,
|
pool: &ConstantPool<'_>,
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ use super::Resolver;
|
||||||
use crate::error::ClassfileError;
|
use crate::error::ClassfileError;
|
||||||
use crate::insn::{BranchCond, Cond, Insn, Label, Prim};
|
use crate::insn::{BranchCond, Cond, Insn, Label, Prim};
|
||||||
|
|
||||||
/// `Some` — the instruction is covered; `None` — hand it to the next converters.
|
/// `Some` - the instruction is covered; `None` - hand it to the next converters.
|
||||||
pub(super) fn convert_flow(
|
pub(super) fn convert_flow(
|
||||||
instruction: &Instruction,
|
instruction: &Instruction,
|
||||||
index: u16,
|
index: u16,
|
||||||
|
|
@ -52,7 +52,7 @@ fn branch(cond: BranchCond, target: u16, resolve: &Resolver<'_>) -> Result<Insn,
|
||||||
Ok(Insn::Branch(cond, resolve(target)?))
|
Ok(Insn::Branch(cond, resolve(target)?))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A `switch` target — an instruction number: base plus the backend's relative delta.
|
/// A `switch` target - an instruction number: base plus the backend's relative delta.
|
||||||
fn abs(index: u16, delta: i32, resolve: &Resolver<'_>) -> Result<Label, ClassfileError> {
|
fn abs(index: u16, delta: i32, resolve: &Resolver<'_>) -> Result<Label, ClassfileError> {
|
||||||
let absolute = i32::from(index)
|
let absolute = i32::from(index)
|
||||||
.checked_add(delta)
|
.checked_add(delta)
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
//!
|
//!
|
||||||
//! Branch offsets come from the backend (absolute for `if*`/`goto`, relative to
|
//! Branch offsets come from the backend (absolute for `if*`/`goto`, relative to
|
||||||
//! the instruction start for `switch`), so conversion takes a resolver from an
|
//! the instruction start for `switch`), so conversion takes a resolver from an
|
||||||
//! offset to a [`Label`](crate::insn::Label) — the calling code assigns labels.
|
//! offset to a [`Label`](crate::insn::Label) - the calling code assigns labels.
|
||||||
|
|
||||||
mod basic;
|
mod basic;
|
||||||
mod flow;
|
mod flow;
|
||||||
|
|
@ -17,7 +17,7 @@ use crate::insn::{Insn, Label};
|
||||||
/// Resolves an absolute bytecode offset into a label.
|
/// Resolves an absolute bytecode offset into a label.
|
||||||
pub(super) type Resolver<'a> = dyn Fn(u16) -> Result<Label, ClassfileError> + 'a;
|
pub(super) type Resolver<'a> = dyn Fn(u16) -> Result<Label, ClassfileError> + 'a;
|
||||||
|
|
||||||
/// Convert one instruction. `index` — its number in the method body (`switch` needs it).
|
/// Convert one instruction. `index` - its number in the method body (`switch` needs it).
|
||||||
pub(super) fn convert(
|
pub(super) fn convert(
|
||||||
instruction: &Instruction,
|
instruction: &Instruction,
|
||||||
index: u16,
|
index: u16,
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ use super::super::pool::{class_name, dynamic_parts, expect_kind, is_interface, r
|
||||||
use crate::error::ClassfileError;
|
use crate::error::ClassfileError;
|
||||||
use crate::insn::{ArrayKind, Insn, InvokeKind};
|
use crate::insn::{ArrayKind, Insn, InvokeKind};
|
||||||
|
|
||||||
/// `Some` — the instruction is covered; `None` — hand it to the next converters.
|
/// `Some` - the instruction is covered; `None` - hand it to the next converters.
|
||||||
pub(super) fn convert_ref(
|
pub(super) fn convert_ref(
|
||||||
instruction: &Instruction,
|
instruction: &Instruction,
|
||||||
pool: &ConstantPool<'_>,
|
pool: &ConstantPool<'_>,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
//! Our IR attributes → backend attributes.
|
//! Our IR attributes → backend attributes.
|
||||||
//!
|
//!
|
||||||
//! Attribute names (`SourceFile`, …) are interned into the pool right here — the
|
//! Attribute names (`SourceFile`, …) are interned into the pool right here - the
|
||||||
//! backend stores only `name_index`, and the string is recovered on read.
|
//! backend stores only `name_index`, and the string is recovered on read.
|
||||||
|
|
||||||
use ristretto_classfile::attributes::{
|
use ristretto_classfile::attributes::{
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
//!
|
//!
|
||||||
//! Our IR stores `offset_delta` in instruction numbers; the backend recomputes
|
//! Our IR stores `offset_delta` in instruction numbers; the backend recomputes
|
||||||
//! them into bytes on `to_bytes`. The compact form (`Same`/`SameLocals`) is valid
|
//! them into bytes on `to_bytes`. The compact form (`Same`/`SameLocals`) is valid
|
||||||
//! only when the **byte** delta is ≤ 63 — otherwise the extended form is needed.
|
//! only when the **byte** delta is ≤ 63 - otherwise the extended form is needed.
|
||||||
//! We choose by the byte delta: bytes are computed from the current method's layout.
|
//! We choose by the byte delta: bytes are computed from the current method's layout.
|
||||||
|
|
||||||
use ristretto_classfile::attributes::{StackFrame as RtFrame, VerificationType as RtType};
|
use ristretto_classfile::attributes::{StackFrame as RtFrame, VerificationType as RtType};
|
||||||
|
|
@ -14,7 +14,7 @@ use crate::format::{StackMapFrame, VerificationType};
|
||||||
/// The byte offset of an instruction's start (for computing frame deltas).
|
/// The byte offset of an instruction's start (for computing frame deltas).
|
||||||
type ByteOf<'a> = &'a (dyn Fn(u32) -> Result<u16, ClassfileError> + 'a);
|
type ByteOf<'a> = &'a (dyn Fn(u32) -> Result<u16, ClassfileError> + 'a);
|
||||||
|
|
||||||
/// IR frames → backend frames (index deltas — as-is).
|
/// IR frames → backend frames (index deltas - as-is).
|
||||||
pub(super) fn convert_frames(
|
pub(super) fn convert_frames(
|
||||||
frames: &[StackMapFrame],
|
frames: &[StackMapFrame],
|
||||||
pool: &mut Pool,
|
pool: &mut Pool,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
//! Our [`Insn`](crate::insn::Insn) → the backend's `Instruction`.
|
//! Our [`Insn`](crate::insn::Insn) → the backend's `Instruction`.
|
||||||
//!
|
//!
|
||||||
//! The reverse side of `super::super::parse::insns`: labels resolve into
|
//! The reverse side of `super::super::parse::insns`: labels resolve into
|
||||||
//! absolute instruction numbers (`switch` — into deltas from itself), constants
|
//! absolute instruction numbers (`switch` - into deltas from itself), constants
|
||||||
//! and references go to the pool via [`Pool`](super::super::pool::Pool).
|
//! and references go to the pool via [`Pool`](super::super::pool::Pool).
|
||||||
|
|
||||||
use indexmap::IndexMap;
|
use indexmap::IndexMap;
|
||||||
|
|
@ -13,9 +13,9 @@ use super::{branch, delta, fit_index, ret_op};
|
||||||
use crate::error::ClassfileError;
|
use crate::error::ClassfileError;
|
||||||
use crate::insn::{Insn, Label, SlotKind};
|
use crate::insn::{Insn, Label, SlotKind};
|
||||||
|
|
||||||
/// One IR instruction → a backend instruction (`None` — a label marker).
|
/// One IR instruction → a backend instruction (`None` - a label marker).
|
||||||
///
|
///
|
||||||
/// `index` — the instruction's number in the method (needed for `switch` deltas).
|
/// `index` - the instruction's number in the method (needed for `switch` deltas).
|
||||||
pub(crate) fn convert(
|
pub(crate) fn convert(
|
||||||
insn: &Insn,
|
insn: &Insn,
|
||||||
index: u32,
|
index: u32,
|
||||||
|
|
|
||||||
|
|
@ -132,7 +132,7 @@ pub(crate) fn measure(insns: &[RtInsn]) -> Result<BTreeMap<u32, u16>, ClassfileE
|
||||||
Ok(map)
|
Ok(map)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Branch targets don't affect size — substitute the current position for sizing.
|
/// Branch targets don't affect size - substitute the current position for sizing.
|
||||||
fn smash_targets(insn: &mut RtInsn, position: u16) {
|
fn smash_targets(insn: &mut RtInsn, position: u16) {
|
||||||
match insn {
|
match insn {
|
||||||
RtInsn::Ifeq(target)
|
RtInsn::Ifeq(target)
|
||||||
|
|
|
||||||
|
|
@ -229,7 +229,7 @@ fn arg_count(descriptor: &str) -> Result<u8, ClassfileError> {
|
||||||
.map_err(|_| ClassfileError::Write(format!("слишком много аргументов в {descriptor}")))
|
.map_err(|_| ClassfileError::Write(format!("слишком много аргументов в {descriptor}")))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Skip characters up to the separator (inclusive); `false` — the string ended.
|
/// Skip characters up to the separator (inclusive); `false` - the string ended.
|
||||||
fn skip_to(chars: &mut std::str::Chars<'_>, end: char) -> bool {
|
fn skip_to(chars: &mut std::str::Chars<'_>, end: char) -> bool {
|
||||||
for char in chars {
|
for char in chars {
|
||||||
if char == end {
|
if char == end {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
//! Helpers building `InnerClasses`/`BootstrapMethods`/`MethodParameters`.
|
//! Helpers building `InnerClasses`/`BootstrapMethods`/`MethodParameters`.
|
||||||
//!
|
//!
|
||||||
//! Moved out of `attrs` so it stays within the 250-line limit; the logic is
|
//! Moved out of `attrs` so it stays within the 250-line limit; the logic is
|
||||||
//! unchanged — only the location. Each builds a backend entry from an IR
|
//! unchanged - only the location. Each builds a backend entry from an IR
|
||||||
//! element, interning names/descriptors and resolving `CONSTANT_Class` indices
|
//! element, interning names/descriptors and resolving `CONSTANT_Class` indices
|
||||||
//! through the pool.
|
//! through the pool.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,10 @@
|
||||||
//!
|
//!
|
||||||
//! The pool is built from scratch by a deterministic walk (classes, fields,
|
//! The pool is built from scratch by a deterministic walk (classes, fields,
|
||||||
//! methods, attributes, code), so identical IR yields identical bytes. `Raw`
|
//! methods, attributes, code), so identical IR yields identical bytes. `Raw`
|
||||||
//! attribute indices are not carried over — see `super::attrs`.
|
//! attribute indices are not carried over - see `super::attrs`.
|
||||||
//!
|
//!
|
||||||
//! The backend does not deduplicate: `add_*` always pushes a new entry. We cache
|
//! The backend does not deduplicate: `add_*` always pushes a new entry. We cache
|
||||||
//! semantic keys ourselves — otherwise `LocalVariableTable` and
|
//! semantic keys ourselves - otherwise `LocalVariableTable` and
|
||||||
//! `LocalVariableTypeTable` would get different `name_index` values for the same
|
//! `LocalVariableTypeTable` would get different `name_index` values for the same
|
||||||
//! name, and HotSpot would reject the class (`LVTT entry ... does not match any
|
//! name, and HotSpot would reject the class (`LVTT entry ... does not match any
|
||||||
//! LVT entry`). The walk order is deterministic, so the cache only changes pool
|
//! LVT entry`). The walk order is deterministic, so the cache only changes pool
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
//!
|
//!
|
||||||
//! This is an `impl Pool` from [`super`], split out so the parent file stays
|
//! This is an `impl Pool` from [`super`], split out so the parent file stays
|
||||||
//! within the structure limit. Each builder is memoized by a semantic key
|
//! within the structure limit. Each builder is memoized by a semantic key
|
||||||
//! (owner/name/descriptor/…) — an identical reference always gets the same index,
|
//! (owner/name/descriptor/…) - an identical reference always gets the same index,
|
||||||
//! and the pool doesn't bloat with duplicates (bloat would otherwise widen `ldc`
|
//! and the pool doesn't bloat with duplicates (bloat would otherwise widen `ldc`
|
||||||
//! into `ldc_w` and change code length on relocation).
|
//! into `ldc_w` and change code length on relocation).
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -88,7 +88,7 @@ pub enum InvokeKind {
|
||||||
Interface,
|
Interface,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Type conversion (`i2l`, `f2b`, ... — JVMS §6.5.i2<b|c|s|l|f|d>).
|
/// Type conversion (`i2l`, `f2b`, ... - JVMS §6.5.i2<b|c|s|l|f|d>).
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum Convert {
|
pub enum Convert {
|
||||||
I2L,
|
I2L,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
//! IR instructions: no offsets, branches address a [`Label`].
|
//! IR instructions: no offsets, branches address a [`Label`].
|
||||||
//!
|
//!
|
||||||
//! This is fundamental: any pass that changes instruction order/length need not
|
//! This is fundamental: any pass that changes instruction order/length need not
|
||||||
//! recompute jump offsets — the writer does that once at the end of the pipeline.
|
//! recompute jump offsets - the writer does that once at the end of the pipeline.
|
||||||
|
|
||||||
mod kinds;
|
mod kinds;
|
||||||
|
|
||||||
|
|
@ -27,7 +27,7 @@ pub enum Insn {
|
||||||
slot: u16,
|
slot: u16,
|
||||||
kind: SlotKind,
|
kind: SlotKind,
|
||||||
},
|
},
|
||||||
/// `iinc` — increase a local variable by a constant.
|
/// `iinc` - increase a local variable by a constant.
|
||||||
Iinc {
|
Iinc {
|
||||||
index: u16,
|
index: u16,
|
||||||
delta: i32,
|
delta: i32,
|
||||||
|
|
@ -45,7 +45,7 @@ pub enum Insn {
|
||||||
Dup2X1,
|
Dup2X1,
|
||||||
Dup2X2,
|
Dup2X2,
|
||||||
Swap,
|
Swap,
|
||||||
/// `iadd`, `lmul`, `lneg`, ... — type + operation.
|
/// `iadd`, `lmul`, `lneg`, ... - type + operation.
|
||||||
Binary {
|
Binary {
|
||||||
prim: Prim,
|
prim: Prim,
|
||||||
op: Arith,
|
op: Arith,
|
||||||
|
|
@ -59,9 +59,9 @@ pub enum Insn {
|
||||||
Convert(Convert),
|
Convert(Convert),
|
||||||
Branch(BranchCond, Label),
|
Branch(BranchCond, Label),
|
||||||
Goto(Label),
|
Goto(Label),
|
||||||
/// `jsr` — subroutine call (only version ≤ 50 classes).
|
/// `jsr` - subroutine call (only version ≤ 50 classes).
|
||||||
Jsr(Label),
|
Jsr(Label),
|
||||||
/// `ret` — return from a subroutine (`jsr`/`ret` come in pairs).
|
/// `ret` - return from a subroutine (`jsr`/`ret` come in pairs).
|
||||||
Ret(u16),
|
Ret(u16),
|
||||||
TableSwitch {
|
TableSwitch {
|
||||||
default: Label,
|
default: Label,
|
||||||
|
|
@ -73,9 +73,9 @@ pub enum Insn {
|
||||||
default: Label,
|
default: Label,
|
||||||
pairs: Vec<(i32, Label)>,
|
pairs: Vec<(i32, Label)>,
|
||||||
},
|
},
|
||||||
/// `return`/`ireturn`/...; `None` — void return.
|
/// `return`/`ireturn`/...; `None` - void return.
|
||||||
Return(Option<Prim>),
|
Return(Option<Prim>),
|
||||||
/// `areturn` — return a reference ([`Prim`] has no reference type).
|
/// `areturn` - return a reference ([`Prim`] has no reference type).
|
||||||
AReturn,
|
AReturn,
|
||||||
GetStatic {
|
GetStatic {
|
||||||
owner: String,
|
owner: String,
|
||||||
|
|
@ -112,7 +112,7 @@ pub enum Insn {
|
||||||
New(String),
|
New(String),
|
||||||
NewArray(ArrayKind),
|
NewArray(ArrayKind),
|
||||||
ANewArray(String),
|
ANewArray(String),
|
||||||
/// `multianewarray` — array of any dimension; `dims` — how many dimensions from the stack.
|
/// `multianewarray` - array of any dimension; `dims` - how many dimensions from the stack.
|
||||||
MultiANewArray {
|
MultiANewArray {
|
||||||
descriptor: String,
|
descriptor: String,
|
||||||
dims: u8,
|
dims: u8,
|
||||||
|
|
|
||||||
|
|
@ -27,3 +27,11 @@ pub use model::{
|
||||||
LocalVariable, LocalVariableType, Method, MethodParameter, Program, RecordComponent, Resource,
|
LocalVariable, LocalVariableType, Method, MethodParameter, Program, RecordComponent, Resource,
|
||||||
Version,
|
Version,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Safety budget: a method body that would grow past this many instructions
|
||||||
|
/// is left alone by the expanding passes (`strings`, `constants`).
|
||||||
|
///
|
||||||
|
/// The writer caps a method body at 64KB; at a pessimistic 8 bytes per
|
||||||
|
/// instruction this count can never overflow it, while real methods
|
||||||
|
/// (hundreds of instructions) never come close.
|
||||||
|
pub const MAX_METHOD_INSNS: usize = 8_000;
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ pub struct ClassBuilder {
|
||||||
|
|
||||||
impl ClassBuilder {
|
impl ClassBuilder {
|
||||||
/// A new `public` class with the given superclass (`None` = `java/lang/Object`,
|
/// A new `public` class with the given superclass (`None` = `java/lang/Object`,
|
||||||
/// except for `java/lang/Object` itself — it has no superclass).
|
/// except for `java/lang/Object` itself - it has no superclass).
|
||||||
pub fn new(this_class: impl Into<String>, super_class: Option<&str>) -> Self {
|
pub fn new(this_class: impl Into<String>, super_class: Option<&str>) -> Self {
|
||||||
let this_class = this_class.into();
|
let this_class = this_class.into();
|
||||||
let super_class = match super_class {
|
let super_class = match super_class {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
//! The "program" structure — the unit of work for the pipeline.
|
//! The "program" structure - the unit of work for the pipeline.
|
||||||
|
|
||||||
pub mod builder;
|
pub mod builder;
|
||||||
pub mod version;
|
pub mod version;
|
||||||
|
|
@ -8,7 +8,7 @@ pub use version::Version;
|
||||||
use crate::format::StackMapFrame;
|
use crate::format::StackMapFrame;
|
||||||
use crate::insn::{Const, Insn, Label};
|
use crate::insn::{Const, Insn, Label};
|
||||||
|
|
||||||
/// Unknown/specific attributes are stored as bytes — lossless round-trip.
|
/// Unknown/specific attributes are stored as bytes - lossless round-trip.
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub enum Attribute {
|
pub enum Attribute {
|
||||||
SourceFile(String),
|
SourceFile(String),
|
||||||
|
|
@ -32,7 +32,7 @@ pub enum Attribute {
|
||||||
class: String,
|
class: String,
|
||||||
method: Option<(String, String)>,
|
method: Option<(String, String)>,
|
||||||
},
|
},
|
||||||
/// `MethodParameters`: formal parameter names (`None` — unnamed).
|
/// `MethodParameters`: formal parameter names (`None` - unnamed).
|
||||||
MethodParameters(Vec<MethodParameter>),
|
MethodParameters(Vec<MethodParameter>),
|
||||||
/// `Record`: the record class's components.
|
/// `Record`: the record class's components.
|
||||||
Record(Vec<RecordComponent>),
|
Record(Vec<RecordComponent>),
|
||||||
|
|
@ -47,7 +47,7 @@ pub enum Attribute {
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
pub struct LineNumber {
|
pub struct LineNumber {
|
||||||
/// Start label (the writer computes bytes — survives pass insertions).
|
/// Start label (the writer computes bytes - survives pass insertions).
|
||||||
pub start: Label,
|
pub start: Label,
|
||||||
pub line: u16,
|
pub line: u16,
|
||||||
}
|
}
|
||||||
|
|
@ -159,7 +159,7 @@ impl Code {
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub struct ExceptionHandler {
|
pub struct ExceptionHandler {
|
||||||
/// `None` — catch-all (`finally`).
|
/// `None` - catch-all (`finally`).
|
||||||
pub start: Label,
|
pub start: Label,
|
||||||
pub end: Label,
|
pub end: Label,
|
||||||
pub handler: Label,
|
pub handler: Label,
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
//! Frames are dropped and recomputed. Judges: determinism + IR round-trip +
|
//! Frames are dropped and recomputed. Judges: determinism + IR round-trip +
|
||||||
//! real `java -Xverify:all` for classes without external dependencies.
|
//! real `java -Xverify:all` for classes without external dependencies.
|
||||||
//! (The backend's `verify_class` is unfit for acceptance: it expects frames
|
//! (The backend's `verify_class` is unfit for acceptance: it expects frames
|
||||||
//! in bytes while `from_bytes` hands out indices — see `todo.md`.)
|
//! in bytes while `from_bytes` hands out indices - see `todo.md`.)
|
||||||
|
|
||||||
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
||||||
|
|
||||||
|
|
@ -28,7 +28,7 @@ impl Hierarchy for MapHierarchy {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// JDK-only classes — `java` checks them fully.
|
/// JDK-only classes - `java` checks them fully.
|
||||||
const JDK_ONLY: [&str; 2] = [
|
const JDK_ONLY: [&str; 2] = [
|
||||||
"dev/loki/lorep/util/TimeUtil",
|
"dev/loki/lorep/util/TimeUtil",
|
||||||
"dev/loki/lorep/util/PaginationUtil",
|
"dev/loki/lorep/util/PaginationUtil",
|
||||||
|
|
@ -95,7 +95,7 @@ fn fixtures_recompute_and_verify() {
|
||||||
/// A join with a `double` below on the stack must emit it as ONE entry.
|
/// A join with a `double` below on the stack must emit it as ONE entry.
|
||||||
///
|
///
|
||||||
/// Regression: the emitter used to write the slot pair (`[Double, Top]`),
|
/// Regression: the emitter used to write the slot pair (`[Double, Top]`),
|
||||||
/// like `javac` does for locals — but on the operand stack `javac` writes a
|
/// like `javac` does for locals - but on the operand stack `javac` writes a
|
||||||
/// bare `[Double]`, and HotSpot rejects the pair with
|
/// bare `[Double]`, and HotSpot rejects the pair with
|
||||||
/// `Inconsistent stackmap frames`.
|
/// `Inconsistent stackmap frames`.
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
//! сопоставляет запись `LocalVariableTypeTable` с `LocalVariableTable` по
|
//! сопоставляет запись `LocalVariableTypeTable` с `LocalVariableTable` по
|
||||||
//! сырому `name_index`. Дублирующий `Utf8` для одного имени даёт разные индексы
|
//! сырому `name_index`. Дублирующий `Utf8` для одного имени даёт разные индексы
|
||||||
//! и рушит класс: `ClassFormatError: LVTT entry ... does not match any LVT
|
//! и рушит класс: `ClassFormatError: LVTT entry ... does not match any LVT
|
||||||
//! entry`. Писатель обязан интернировать строки — поэтому в записанном пуле
|
//! entry`. Писатель обязан интернировать строки - поэтому в записанном пуле
|
||||||
//! каждый `CONSTANT_Utf8` уникален.
|
//! каждый `CONSTANT_Utf8` уникален.
|
||||||
|
|
||||||
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
//!
|
//!
|
||||||
//! Пул при записи строится заново (порядок другой), поэтому сравнивается IR,
|
//! Пул при записи строится заново (порядок другой), поэтому сравнивается IR,
|
||||||
//! а не байты. Записанные байты обязаны проходить структурный `verify`
|
//! а не байты. Записанные байты обязаны проходить структурный `verify`
|
||||||
//! backend'а — это ловит битые ссылки пула и кривые атрибуты.
|
//! backend'а - это ловит битые ссылки пула и кривые атрибуты.
|
||||||
|
|
||||||
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
//! Spike, часть 2: доступна ли **генерация** кадров `StackMapTable`.
|
//! Spike, часть 2: доступна ли **генерация** кадров `StackMapTable`.
|
||||||
//!
|
//!
|
||||||
//! Ключевой вопрос `docs/stack.md` §3: считает ли `ristretto_classfile` кадры
|
//! Ключевой вопрос `docs/stack.md` §3: считает ли `ristretto_classfile` кадры
|
||||||
//! сам и отдаёт ли их наружу. Если да — наша работа сводится к записи; если
|
//! сам и отдаёт ли их наружу. Если да - наша работа сводится к записи; если
|
||||||
//! нет — кадры придётся считать (или вендорить их `verifiers::bytecode`).
|
//! нет - кадры придётся считать (или вендорить их `verifiers::bytecode`).
|
||||||
|
|
||||||
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
||||||
|
|
||||||
|
|
@ -82,7 +82,7 @@ fn inference_config() -> VerifierConfig {
|
||||||
VerifierConfig::default().with_fallback_strategy(FallbackStrategy::AlwaysInference)
|
VerifierConfig::default().with_fallback_strategy(FallbackStrategy::AlwaysInference)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Без `StackMapTable` inference обязан пройти сам — значит, движок кадров у
|
/// Без `StackMapTable` inference обязан пройти сам - значит, движок кадров у
|
||||||
/// крейта есть, просто его результат не отдаётся наружу.
|
/// крейта есть, просто его результат не отдаётся наружу.
|
||||||
#[test]
|
#[test]
|
||||||
fn inference_path_verifies_without_stack_map_table() {
|
fn inference_path_verifies_without_stack_map_table() {
|
||||||
|
|
@ -94,7 +94,7 @@ fn inference_path_verifies_without_stack_map_table() {
|
||||||
assert_eq!(results[0].path_used, VerificationPath::Inference);
|
assert_eq!(results[0].path_used, VerificationPath::Inference);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Строгий режим без кадров обязан упасть — иначе требование `StackMapTable`
|
/// Строгий режим без кадров обязан упасть - иначе требование `StackMapTable`
|
||||||
/// для классов ≥ 50 не проверяется вовсе.
|
/// для классов ≥ 50 не проверяется вовсе.
|
||||||
#[test]
|
#[test]
|
||||||
fn strict_mode_rejects_missing_stack_map_table() {
|
fn strict_mode_rejects_missing_stack_map_table() {
|
||||||
|
|
@ -102,7 +102,7 @@ fn strict_mode_rejects_missing_stack_map_table() {
|
||||||
let config = VerifierConfig::default().with_fallback_strategy(FallbackStrategy::Strict);
|
let config = VerifierConfig::default().with_fallback_strategy(FallbackStrategy::Strict);
|
||||||
let outcome = verify_class(&class_file, &Permissive, &config);
|
let outcome = verify_class(&class_file, &Permissive, &config);
|
||||||
|
|
||||||
// Строгий режим падает либо `Err`, либо `success == false` — оба варианта
|
// Строгий режим падает либо `Err`, либо `success == false` - оба варианта
|
||||||
// приемлемы, важен сам отказ.
|
// приемлемы, важен сам отказ.
|
||||||
let rejected = match &outcome {
|
let rejected = match &outcome {
|
||||||
Err(_) => true,
|
Err(_) => true,
|
||||||
|
|
@ -113,13 +113,13 @@ fn strict_mode_rejects_missing_stack_map_table() {
|
||||||
|
|
||||||
/// Негативный результат spike'а: кадр не на том сдвиге не отдаёт диагностику.
|
/// Негативный результат spike'а: кадр не на том сдвиге не отдаёт диагностику.
|
||||||
///
|
///
|
||||||
/// Ждали `Ok(vec![result])` с заполненным `diagnostic.expected` — тогда
|
/// Ждали `Ok(vec![result])` с заполненным `diagnostic.expected` - тогда
|
||||||
/// генерация кадров шла бы итеративным путём через публичный API. Фактически
|
/// генерация кадров шла бы итеративным путём через публичный API. Фактически
|
||||||
/// `verify_class` в строгом режиме возвращает `Err(VerifyError)`, и ожидаемый
|
/// `verify_class` в строгом режиме возвращает `Err(VerifyError)`, и ожидаемый
|
||||||
/// кадр до нас не доходит: из крейта достаётся только место ошибки
|
/// кадр до нас не доходит: из крейта достаётся только место ошибки
|
||||||
/// («merge point at offset N»), но не её содержимое.
|
/// («merge point at offset N»), но не её содержимое.
|
||||||
///
|
///
|
||||||
/// Вывод для `docs/stack.md` §3: проверять кадры крейт умеет, отдавать — нет.
|
/// Вывод для `docs/stack.md` §3: проверять кадры крейт умеет, отдавать - нет.
|
||||||
#[test]
|
#[test]
|
||||||
fn diagnostic_exposes_expected_frame_on_failure() {
|
fn diagnostic_exposes_expected_frame_on_failure() {
|
||||||
let mut class_file = branch_class();
|
let mut class_file = branch_class();
|
||||||
|
|
@ -127,7 +127,7 @@ fn diagnostic_exposes_expected_frame_on_failure() {
|
||||||
.constant_pool
|
.constant_pool
|
||||||
.add_utf8("StackMapTable")
|
.add_utf8("StackMapTable")
|
||||||
.expect("utf8");
|
.expect("utf8");
|
||||||
// Кадр на сдвиге 0, а настоящий якорь — на сдвиге 3.
|
// Кадр на сдвиге 0, а настоящий якорь - на сдвиге 3.
|
||||||
class_file.attributes.push(Attribute::StackMapTable {
|
class_file.attributes.push(Attribute::StackMapTable {
|
||||||
name_index,
|
name_index,
|
||||||
frames: vec![StackFrame::SameFrame { frame_type: 0 }],
|
frames: vec![StackFrame::SameFrame { frame_type: 0 }],
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
//! Spike: пригоден ли `ristretto_classfile` как backend для чтения/записи.
|
//! Spike: пригоден ли `ristretto_classfile` как backend для чтения/записи.
|
||||||
//!
|
//!
|
||||||
//! Критерии — `docs/stack.md` §2. Тест существует, чтобы ответы на вопросы
|
//! Критерии - `docs/stack.md` §2. Тест существует, чтобы ответы на вопросы
|
||||||
//! spike'а не устаревали: если крейт сломает хотя бы один пункт, сборка упадёт,
|
//! spike'а не устаревали: если крейт сломает хотя бы один пункт, сборка упадёт,
|
||||||
//! и решение о backend'е придётся пересмотреть.
|
//! и решение о backend'е придётся пересмотреть.
|
||||||
//!
|
//!
|
||||||
|
|
@ -59,7 +59,7 @@ fn round_trip_preserves_identity() {
|
||||||
assert_eq!(restored.constant_pool.len(), source.constant_pool.len());
|
assert_eq!(restored.constant_pool.len(), source.constant_pool.len());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Кадры `StackMapTable` конструируются и сериализуются публичным API —
|
/// Кадры `StackMapTable` конструируются и сериализуются публичным API -
|
||||||
/// значит, мы можем **записывать** кадры, а не только читать.
|
/// значит, мы можем **записывать** кадры, а не только читать.
|
||||||
#[test]
|
#[test]
|
||||||
fn stack_map_frames_are_constructible() {
|
fn stack_map_frames_are_constructible() {
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@ pub enum Profile {
|
||||||
Standard,
|
Standard,
|
||||||
/// Всё включено, включая агрессивный control-flow.
|
/// Всё включено, включая агрессивный control-flow.
|
||||||
Paranoid,
|
Paranoid,
|
||||||
/// Ничего не включено явно — только `enable` из конфига.
|
/// Ничего не включено явно - только `enable` из конфига.
|
||||||
Manual,
|
Manual,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -97,7 +97,7 @@ impl Profile {
|
||||||
#[serde(default, deny_unknown_fields)]
|
#[serde(default, deny_unknown_fields)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub profile: Profile,
|
pub profile: Profile,
|
||||||
/// Мастер-сид; `None` — случайный, но обязательно попадает в отчёт.
|
/// Мастер-сид; `None` - случайный, но обязательно попадает в отчёт.
|
||||||
pub seed: Option<u64>,
|
pub seed: Option<u64>,
|
||||||
/// Принудительно включить пассы вне профиля.
|
/// Принудительно включить пассы вне профиля.
|
||||||
pub enable: Vec<PassId>,
|
pub enable: Vec<PassId>,
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,9 @@ use serde::{Deserialize, Serialize};
|
||||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
#[serde(rename_all = "kebab-case")]
|
#[serde(rename_all = "kebab-case")]
|
||||||
pub enum NameStyle {
|
pub enum NameStyle {
|
||||||
/// `a`, `b`, ... `aa` — short Latin.
|
/// `a`, `b`, ... `aa` - short Latin.
|
||||||
ShortLatin,
|
ShortLatin,
|
||||||
/// `I`, `l`, `O`, `0` — visually identical.
|
/// `I`, `l`, `O`, `0` - visually identical.
|
||||||
IlO0,
|
IlO0,
|
||||||
/// Random Unicode names (surrogates are valid in modified UTF-8).
|
/// Random Unicode names (surrogates are valid in modified UTF-8).
|
||||||
Unicode,
|
Unicode,
|
||||||
|
|
@ -17,7 +17,7 @@ pub enum NameStyle {
|
||||||
MixedLatin,
|
MixedLatin,
|
||||||
/// Pseudo-meaningful words from another project (misdirection).
|
/// Pseudo-meaningful words from another project (misdirection).
|
||||||
DecoyWords,
|
DecoyWords,
|
||||||
/// Plausible names: `TextureCache`, `resolveToken` — blends in as
|
/// Plausible names: `TextureCache`, `resolveToken` - blends in as
|
||||||
/// ordinary application code instead of obvious noise.
|
/// ordinary application code instead of obvious noise.
|
||||||
Plausible,
|
Plausible,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
//! Ядро обфускатора: конфигурация, детерминированный seed, маппинги,
|
//! Ядро обфускатора: конфигурация, детерминированный seed, маппинги,
|
||||||
//! контекст прогона и контракт пассов ([`pipeline::Pass`]).
|
//! контекст прогона и контракт пассов ([`pipeline::Pass`]).
|
||||||
//!
|
//!
|
||||||
//! Ядро ничего не знает о бинарном формате `.class` и о ZIP — только о модели
|
//! Ядро ничего не знает о бинарном формате `.class` и о ZIP - только о модели
|
||||||
//! [`noxis_classfile::Program`]. Сборка конвейера из пассов живёт в
|
//! [`noxis_classfile::Program`]. Сборка конвейера из пассов живёт в
|
||||||
//! `noxis-passes`, чтобы не было цикла зависимостей.
|
//! `noxis-passes`, чтобы не было цикла зависимостей.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ pub fn field_key(owner: &str, name: &str, descriptor: &str) -> String {
|
||||||
/// Таблицы «оригинал → обфусцированное».
|
/// Таблицы «оригинал → обфусцированное».
|
||||||
///
|
///
|
||||||
/// Нужна по двум причинам: (1) перевод stack trace'ей обратно в читаемые имена
|
/// Нужна по двум причинам: (1) перевод stack trace'ей обратно в читаемые имена
|
||||||
/// (аналог ZKM Stack Trace Translate), (2) аудит — что именно поменялось.
|
/// (аналог ZKM Stack Trace Translate), (2) аудит - что именно поменялось.
|
||||||
#[derive(Debug, Clone, Default, PartialEq, Serialize)]
|
#[derive(Debug, Clone, Default, PartialEq, Serialize)]
|
||||||
pub struct Mapping {
|
pub struct Mapping {
|
||||||
/// internal name класса → новый internal name
|
/// internal name класса → новый internal name
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ impl Context {
|
||||||
/// Детерминированный RNG для домена (`"rename"`, `"strings/0"`, ...).
|
/// Детерминированный RNG для домена (`"rename"`, `"strings/0"`, ...).
|
||||||
///
|
///
|
||||||
/// Доменное разделение: разные пассы не должны вытягивать одну
|
/// Доменное разделение: разные пассы не должны вытягивать одну
|
||||||
/// последовательность — иначе их «случайности» статистически коррелируют.
|
/// последовательность - иначе их «случайности» статистически коррелируют.
|
||||||
pub fn rng(&self, domain: &str) -> Rng {
|
pub fn rng(&self, domain: &str) -> Rng {
|
||||||
Rng::from_seed(derive_seed(self.master_seed, domain, 0))
|
Rng::from_seed(derive_seed(self.master_seed, domain, 0))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ pub trait Pass: Send + Sync {
|
||||||
|
|
||||||
/// Упорядоченная цепочка пассов.
|
/// Упорядоченная цепочка пассов.
|
||||||
///
|
///
|
||||||
/// Порядок — не деталь: см. `docs/architecture.md` (rename → resources →
|
/// Порядок - не деталь: см. `docs/architecture.md` (rename → resources →
|
||||||
/// strings → constants → flow → noise → debug), иначе часть техник ломает
|
/// strings → constants → flow → noise → debug), иначе часть техник ломает
|
||||||
/// предыдущие или работает по уже мёртвым именам.
|
/// предыдущие или работает по уже мёртвым именам.
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
|
|
@ -88,7 +88,7 @@ impl Pipeline {
|
||||||
for pass in &self.passes {
|
for pass in &self.passes {
|
||||||
let id = pass.id();
|
let id = pass.id();
|
||||||
if !ctx.config.is_enabled(id) {
|
if !ctx.config.is_enabled(id) {
|
||||||
tracing::info!(target: "noxis", pass = %id, "пасс выключен — пропущен");
|
tracing::info!(target: "noxis", pass = %id, "пасс выключен - пропущен");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
tracing::info!(target: "noxis", pass = %id, "пасс запущен");
|
tracing::info!(target: "noxis", pass = %id, "пасс запущен");
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
//! Итог прогона: пассы + счётчики — сериализуется в JSON.
|
//! Итог прогона: пассы + счётчики - сериализуется в JSON.
|
||||||
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
/// Итог прогона: пассы + счётчики — сериализуется в JSON.
|
/// Итог прогона: пассы + счётчики - сериализуется в JSON.
|
||||||
#[derive(Debug, Clone, Default, Serialize)]
|
#[derive(Debug, Clone, Default, Serialize)]
|
||||||
pub struct Report {
|
pub struct Report {
|
||||||
pub seed: u64,
|
pub seed: u64,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
//! Детерминированный PRNG и вывод производных seed'ов.
|
//! Детерминированный PRNG и вывод производных seed'ов.
|
||||||
//!
|
//!
|
||||||
//! Свой генератор, а не `rand`: обфускация должна быть **воспроизводимой между
|
//! Свой генератор, а не `rand`: обфускация должна быть **воспроизводимой между
|
||||||
//! версиями инструмента** — алгоритм замирает в формате, а не в изменении API
|
//! версиями инструмента** - алгоритм замирает в формате, а не в изменении API
|
||||||
//! чужого крейта. Схема: SplitMix64 для инициализации + xoshiro256++ для генерации.
|
//! чужого крейта. Схема: SplitMix64 для инициализации + xoshiro256++ для генерации.
|
||||||
|
|
||||||
/// Генератор xoshiro256++.
|
/// Генератор xoshiro256++.
|
||||||
|
|
@ -97,7 +97,7 @@ impl Rng {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// FNV-1a 64 — детерминированный вывод производного сида.
|
/// FNV-1a 64 - детерминированный вывод производного сида.
|
||||||
///
|
///
|
||||||
/// Разделение доменов обязательно: пассы не должны делить одну и ту же
|
/// Разделение доменов обязательно: пассы не должны делить одну и ту же
|
||||||
/// последовательность (иначе переименование и шифрование «случайно»
|
/// последовательность (иначе переименование и шифрование «случайно»
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
//! Чтение и запись ZIP-архива JAR.
|
//! Чтение и запись ZIP-архива JAR.
|
||||||
//!
|
//!
|
||||||
//! Единственная задача модуля — переместить записи из/в вектор [`Entry`],
|
//! Единственная задача модуля - переместить записи из/в вектор [`Entry`],
|
||||||
//! сохранив порядок и не потеряв дубликатов молча. Всё, что связано с
|
//! сохранив порядок и не потеряв дубликатов молча. Всё, что связано с
|
||||||
//! содержимым (`Program`, маппинги, подпись), живёт в соседних модулях.
|
//! содержимым (`Program`, маппинги, подпись), живёт в соседних модулях.
|
||||||
|
|
||||||
|
|
@ -30,7 +30,7 @@ pub enum JarError {
|
||||||
|
|
||||||
/// Одна запись архива: имя + байты.
|
/// Одна запись архива: имя + байты.
|
||||||
///
|
///
|
||||||
/// Порядок в `[`Entry`]` — это порядок в архиве, он сохраняется при
|
/// Порядок в `[`Entry`]` - это порядок в архиве, он сохраняется при
|
||||||
/// чтении и записи. Метод сжатия не хранится: всё пишется deflate'ом,
|
/// чтении и записи. Метод сжатия не хранится: всё пишется deflate'ом,
|
||||||
/// что детерминированно и одинаково для одинаковых данных.
|
/// что детерминированно и одинаково для одинаковых данных.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
|
@ -51,7 +51,7 @@ impl Entry {
|
||||||
|
|
||||||
/// Читает все не-каталожные записи из `reader`, сохраняя порядок.
|
/// Читает все не-каталожные записи из `reader`, сохраняя порядок.
|
||||||
///
|
///
|
||||||
/// Повторяющееся имя — ошибка, а не тихое предупреждение: битый архив
|
/// Повторяющееся имя - ошибка, а не тихое предупреждение: битый архив
|
||||||
/// с двумя `META-INF/MANIFEST.MF` собрать дороже, чем отказать.
|
/// с двумя `META-INF/MANIFEST.MF` собрать дороже, чем отказать.
|
||||||
pub fn read_entries<R: Read + Seek>(reader: R) -> Result<Vec<Entry>, JarError> {
|
pub fn read_entries<R: Read + Seek>(reader: R) -> Result<Vec<Entry>, JarError> {
|
||||||
let mut archive = zip::ZipArchive::new(reader)?;
|
let mut archive = zip::ZipArchive::new(reader)?;
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,13 @@
|
||||||
//!
|
//!
|
||||||
//! Слой разделён на три несвязанные задачи:
|
//! Слой разделён на три несвязанные задачи:
|
||||||
//!
|
//!
|
||||||
//! * [`archive`] — чистая работа с ZIP: порядок записей, сжатие, дубликаты;
|
//! * [`archive`] - чистая работа с ZIP: порядок записей, сжатие, дубликаты;
|
||||||
//! * [`signatures`] — срез невалидных подписей `META-INF/*.SF|RSA|DSA|EC`;
|
//! * [`signatures`] - срез невалидных подписей `META-INF/*.SF|RSA|DSA|EC`;
|
||||||
//! * [`loader`] — перевод архива в [`Program`](noxis_classfile::Program)
|
//! * [`loader`] - перевод архива в [`Program`](noxis_classfile::Program)
|
||||||
//! и обратно (классы через [`format`](noxis_classfile::format), остальное
|
//! и обратно (классы через [`format`](noxis_classfile::format), остальное
|
||||||
//! как ресурсы).
|
//! как ресурсы).
|
||||||
//!
|
//!
|
||||||
//! Обфускация ресурсов — пасс `resources` в `noxis-passes`; сюда она не
|
//! Обфускация ресурсов - пасс `resources` в `noxis-passes`; сюда она не
|
||||||
//! входит, потому что ей нужен маппинг имён, а не I/O.
|
//! входит, потому что ей нужен маппинг имён, а не I/O.
|
||||||
|
|
||||||
pub mod archive;
|
pub mod archive;
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
//! Перевод архива в [`Program`] и обратно.
|
//! Archive to [`Program`] and back.
|
||||||
//!
|
//!
|
||||||
//! Правило простое: `*.class` уходит в парсер, всё остальное — в ресурсы.
|
//! The rule is simple: `*.class` goes to the parser, everything else -
|
||||||
//! Обратно классы сериализуются первыми, затем ресурсы: порядок внутри
|
//! to resources. Classes serialize first, then resources: the order inside
|
||||||
//! архива на поведение не влияет, а фиксированный порядок даёт
|
//! the archive does not affect behaviour, and a fixed order gives
|
||||||
//! воспроизводимую запись.
|
//! reproducible writes.
|
||||||
|
|
||||||
|
use std::collections::BTreeSet;
|
||||||
|
|
||||||
use noxis_classfile::Program;
|
use noxis_classfile::Program;
|
||||||
use noxis_classfile::format;
|
use noxis_classfile::format;
|
||||||
|
|
@ -11,13 +13,22 @@ use noxis_classfile::model::Resource;
|
||||||
|
|
||||||
use crate::archive::{Entry, JarError};
|
use crate::archive::{Entry, JarError};
|
||||||
|
|
||||||
/// Разбирает записи архива: классы — через парсер, остальное — как ресурсы.
|
/// Parses archive entries: classes - through the parser, the rest - as resources.
|
||||||
|
///
|
||||||
|
/// A name that repeats (multi-release jars carry the same class at the root
|
||||||
|
/// and under `META-INF/versions/*`) is kept once, first occurrence wins:
|
||||||
|
/// the flat model cannot hold two classes under one name, and later passes
|
||||||
|
/// (rename maps, jar writing) assume uniqueness.
|
||||||
pub fn load(entries: Vec<Entry>) -> Result<Program, JarError> {
|
pub fn load(entries: Vec<Entry>) -> Result<Program, JarError> {
|
||||||
let mut program = Program::default();
|
let mut program = Program::default();
|
||||||
|
let mut seen: BTreeSet<String> = BTreeSet::new();
|
||||||
|
|
||||||
for entry in entries {
|
for entry in entries {
|
||||||
if is_class_entry(&entry.name) {
|
if is_class_entry(&entry.name) {
|
||||||
program.classes.push(format::parse(&entry.data)?);
|
let class = format::parse(&entry.data)?;
|
||||||
|
if seen.insert(class.this_class.clone()) {
|
||||||
|
program.classes.push(class);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
program.resources.push(Resource {
|
program.resources.push(Resource {
|
||||||
path: entry.name,
|
path: entry.name,
|
||||||
|
|
@ -29,7 +40,7 @@ pub fn load(entries: Vec<Entry>) -> Result<Program, JarError> {
|
||||||
Ok(program)
|
Ok(program)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Собирает записи архива из [`Program`]: сначала классы, потом ресурсы.
|
/// Assembles archive entries from [`Program`]: classes first, then resources.
|
||||||
pub fn save(program: &Program) -> Result<Vec<Entry>, JarError> {
|
pub fn save(program: &Program) -> Result<Vec<Entry>, JarError> {
|
||||||
let mut entries = Vec::with_capacity(program.classes.len() + program.resources.len());
|
let mut entries = Vec::with_capacity(program.classes.len() + program.resources.len());
|
||||||
|
|
||||||
|
|
@ -44,7 +55,7 @@ pub fn save(program: &Program) -> Result<Vec<Entry>, JarError> {
|
||||||
Ok(entries)
|
Ok(entries)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Классовый файл внутри архива: путь оканчивается на `.class`.
|
/// A class file inside the archive: the path ends in `.class`.
|
||||||
fn is_class_entry(name: &str) -> bool {
|
fn is_class_entry(name: &str) -> bool {
|
||||||
name.len() > ".class".len() && name.ends_with(".class")
|
name.len() > ".class".len() && name.ends_with(".class")
|
||||||
}
|
}
|
||||||
|
|
@ -78,7 +89,7 @@ mod tests {
|
||||||
assert_eq!(save(&program).expect("save"), source);
|
assert_eq!(save(&program).expect("save"), source);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// До P1 `format::parse` — заглушка; после P1 ожидается успешный разбор.
|
/// Class entries go through the parser (round-trip keeps them intact).
|
||||||
#[test]
|
#[test]
|
||||||
fn class_entries_go_through_the_parser() {
|
fn class_entries_go_through_the_parser() {
|
||||||
let entries = vec![
|
let entries = vec![
|
||||||
|
|
@ -97,4 +108,22 @@ mod tests {
|
||||||
Err(other) => panic!("unexpected error: {other}"),
|
Err(other) => panic!("unexpected error: {other}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Multi-release twins (`demo/App.class` + `META-INF/versions/9/...`):
|
||||||
|
/// the flat model keeps the first occurrence so no two classes ever
|
||||||
|
/// share one name downstream.
|
||||||
|
#[test]
|
||||||
|
fn duplicate_class_names_keep_first() {
|
||||||
|
use noxis_classfile::ClassBuilder;
|
||||||
|
|
||||||
|
let class = ClassBuilder::new("demo/App", None).build();
|
||||||
|
let bytes = format::write(&class).expect("write");
|
||||||
|
let entries = vec![
|
||||||
|
Entry::new("demo/App.class", bytes.clone()),
|
||||||
|
Entry::new("META-INF/versions/9/demo/App.class", bytes),
|
||||||
|
];
|
||||||
|
let program = load(entries).expect("load");
|
||||||
|
assert_eq!(program.classes.len(), 1);
|
||||||
|
assert_eq!(program.classes[0].this_class, "demo/App");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
//!
|
//!
|
||||||
//! После перепаковки архива `META-INF/*.SF` и `*.RSA|DSA|EC` описывают
|
//! После перепаковки архива `META-INF/*.SF` и `*.RSA|DSA|EC` описывают
|
||||||
//! **старые** байты: JVM либо отказывается запускать jar, либо ругается на
|
//! **старые** байты: JVM либо отказывается запускать jar, либо ругается на
|
||||||
//! подпись. Вырезать их обязательно — и это делается на уровне записей,
|
//! подпись. Вырезать их обязательно - и это делается на уровне записей,
|
||||||
//! потому что `MANIFEST.MF` остаётся, а его секции `Name:` тоже нужно
|
//! потому что `MANIFEST.MF` остаётся, а его секции `Name:` тоже нужно
|
||||||
//! выкинуть (ими занимается пасс `resources`).
|
//! выкинуть (ими занимается пасс `resources`).
|
||||||
|
|
||||||
|
|
@ -19,10 +19,10 @@ const META_INF: &str = "META-INF/";
|
||||||
|
|
||||||
/// Проверяет, является ли запись архива файлом подписи.
|
/// Проверяет, является ли запись архива файлом подписи.
|
||||||
///
|
///
|
||||||
/// Совпадение — только для файлов, лежащих **непосредственно** в
|
/// Совпадение - только для файлов, лежащих **непосредственно** в
|
||||||
/// `META-INF/`: вложенный `META-INF/certs/x.SF` — обычный ресурс.
|
/// `META-INF/`: вложенный `META-INF/certs/x.SF` - обычный ресурс.
|
||||||
///
|
///
|
||||||
/// Регистр не важен (`meta-inf/x.sf` — тоже подпись), `MANIFEST.MF`
|
/// Регистр не важен (`meta-inf/x.sf` - тоже подпись), `MANIFEST.MF`
|
||||||
/// никогда не является подписью.
|
/// никогда не является подписью.
|
||||||
pub fn is_signature_entry(name: &str) -> bool {
|
pub fn is_signature_entry(name: &str) -> bool {
|
||||||
let Some(relative) = name.strip_prefix(META_INF) else {
|
let Some(relative) = name.strip_prefix(META_INF) else {
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,7 @@ fn binary_long(first: i64, second: i64, op: Arith) -> Vec<Insn> {
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `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![
|
||||||
Insn::Const(Const::Int(value >> 1)),
|
Insn::Const(Const::Int(value >> 1)),
|
||||||
|
|
@ -137,7 +137,7 @@ fn shift_form_long(value: i64) -> Vec<Insn> {
|
||||||
pub(super) fn opaque_true(dead: u32, merge: u32, rng: &mut Rng) -> Vec<Insn> {
|
pub(super) fn opaque_true(dead: u32, merge: u32, rng: &mut Rng) -> Vec<Insn> {
|
||||||
let constant = rng.next_u64() as i32;
|
let constant = rng.next_u64() as i32;
|
||||||
vec![
|
vec![
|
||||||
// P = ((C*C) + C) & 1 — even, so always 0.
|
// P = ((C*C) + C) & 1 - even, so always 0.
|
||||||
Insn::Const(Const::Int(constant)),
|
Insn::Const(Const::Int(constant)),
|
||||||
Insn::Dup,
|
Insn::Dup,
|
||||||
Insn::Binary {
|
Insn::Binary {
|
||||||
|
|
@ -154,7 +154,7 @@ pub(super) fn opaque_true(dead: u32, merge: u32, rng: &mut Rng) -> Vec<Insn> {
|
||||||
prim: Prim::Int,
|
prim: Prim::Int,
|
||||||
op: Arith::And,
|
op: Arith::And,
|
||||||
},
|
},
|
||||||
// P != 0 never holds — the branch into DEAD is not taken, value popped.
|
// P != 0 never holds - the branch into DEAD is not taken, value popped.
|
||||||
Insn::Branch(BranchCond::Zero(Cond::Ne), dead),
|
Insn::Branch(BranchCond::Zero(Cond::Ne), dead),
|
||||||
Insn::Goto(merge),
|
Insn::Goto(merge),
|
||||||
Insn::Mark(dead),
|
Insn::Mark(dead),
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
//! Пасс шифрования констант: числа → арифметика, плюс opaque-предикаты.
|
//! The constants pass: numbers become arithmetic, plus opaque predicates.
|
||||||
//!
|
//!
|
||||||
//! `float`/`double` не трогаются (битовая точность через целые — отдельно).
|
//! `float`/`double` are left alone (bit-exact paths through integers are
|
||||||
//! Мелочь из `iconst`-диапазона (`-1..=5`, `0L/1L`) остаётся как есть:
|
//! separate). Trivia from the `iconst` range (`-1..=5`, `0L/1L`) stays as is:
|
||||||
//! её шифрование только раздувает горячий код.
|
//! encrypting it only bloats hot code.
|
||||||
|
|
||||||
mod forms;
|
mod forms;
|
||||||
|
|
||||||
|
|
@ -10,12 +10,12 @@ mod forms;
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|
||||||
use noxis_analysis::ClassHierarchy;
|
use noxis_analysis::ClassHierarchy;
|
||||||
use noxis_classfile::{Const, Insn, Program, compute_frames};
|
use noxis_classfile::{Const, Insn, MAX_METHOD_INSNS, Program, compute_frames};
|
||||||
use noxis_core::{Context, CoreError, Pass, PassId, PassReport};
|
use noxis_core::{Context, CoreError, Pass, PassId, PassReport};
|
||||||
|
|
||||||
use forms::{Scheme, encrypt_int, encrypt_long, opaque_true};
|
use forms::{Scheme, encrypt_int, encrypt_long, opaque_true};
|
||||||
|
|
||||||
/// Пасс шифрования констант.
|
/// The constants encryption pass.
|
||||||
#[derive(Debug, Default, Clone, Copy)]
|
#[derive(Debug, Default, Clone, Copy)]
|
||||||
pub struct ConstantsPass;
|
pub struct ConstantsPass;
|
||||||
|
|
||||||
|
|
@ -30,17 +30,18 @@ impl Pass for ConstantsPass {
|
||||||
for name in &cfg.schemes {
|
for name in &cfg.schemes {
|
||||||
schemes.push(
|
schemes.push(
|
||||||
Scheme::parse(name)
|
Scheme::parse(name)
|
||||||
.ok_or_else(|| pass_error(format!("неизвестная схема `{name}`")))?,
|
.ok_or_else(|| pass_error(format!("unknown scheme `{name}`")))?,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if schemes.is_empty() {
|
if schemes.is_empty() {
|
||||||
return Err(pass_error("нет схем шифрования"));
|
return Err(pass_error("no encryption schemes"));
|
||||||
}
|
}
|
||||||
let mut rng = ctx.rng("constants");
|
let mut rng = ctx.rng("constants");
|
||||||
let mut replaced = 0usize;
|
let mut replaced = 0usize;
|
||||||
let mut predicates = 0usize;
|
let mut predicates = 0usize;
|
||||||
let mut skipped = 0usize;
|
let mut skipped = 0usize;
|
||||||
let mut mutated: Vec<(usize, usize)> = Vec::new();
|
let mut mutated: Vec<(usize, usize)> = Vec::new();
|
||||||
|
let mut oversized: Vec<(usize, usize)> = Vec::new();
|
||||||
|
|
||||||
for (ci, class) in program.classes.iter_mut().enumerate() {
|
for (ci, class) in program.classes.iter_mut().enumerate() {
|
||||||
if cfg
|
if cfg
|
||||||
|
|
@ -55,25 +56,34 @@ impl Pass for ConstantsPass {
|
||||||
let Some(code) = method.code.as_mut() else {
|
let Some(code) = method.code.as_mut() else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let mut fresh = Vec::with_capacity(code.insns.len());
|
let original = std::mem::take(&mut code.insns);
|
||||||
|
let mut fresh = Vec::with_capacity(original.len());
|
||||||
let mut touched = false;
|
let mut touched = false;
|
||||||
for insn in code.insns.drain(..) {
|
for insn in &original {
|
||||||
match insn {
|
match insn {
|
||||||
Insn::Const(Const::Int(value)) if !is_tiny_int(value) => {
|
Insn::Const(Const::Int(value)) if !is_tiny_int(*value) => {
|
||||||
let scheme = schemes[rng.below(schemes.len() as u64) as usize];
|
let scheme = schemes[rng.below(schemes.len() as u64) as usize];
|
||||||
fresh.extend(encrypt_int(value, scheme, &mut rng));
|
fresh.extend(encrypt_int(*value, scheme, &mut rng));
|
||||||
replaced += 1;
|
replaced += 1;
|
||||||
touched = true;
|
touched = true;
|
||||||
}
|
}
|
||||||
Insn::Const(Const::Long(value)) if value != 0 && value != 1 => {
|
Insn::Const(Const::Long(value)) if *value != 0 && *value != 1 => {
|
||||||
let scheme = schemes[rng.below(schemes.len() as u64) as usize];
|
let scheme = schemes[rng.below(schemes.len() as u64) as usize];
|
||||||
fresh.extend(encrypt_long(value, scheme, &mut rng));
|
fresh.extend(encrypt_long(*value, scheme, &mut rng));
|
||||||
replaced += 1;
|
replaced += 1;
|
||||||
touched = true;
|
touched = true;
|
||||||
}
|
}
|
||||||
other => fresh.push(other),
|
other => fresh.push(other.clone()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if fresh.len() > MAX_METHOD_INSNS {
|
||||||
|
// Would risk the 64KB writer ceiling: keep the original
|
||||||
|
// code untouched (predicates included) and count a skip.
|
||||||
|
code.insns = original;
|
||||||
|
skipped += 1;
|
||||||
|
oversized.push((ci, mi));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
code.insns = fresh;
|
code.insns = fresh;
|
||||||
if touched {
|
if touched {
|
||||||
code.max_stack = code.max_stack.saturating_add(2);
|
code.max_stack = code.max_stack.saturating_add(2);
|
||||||
|
|
@ -84,6 +94,7 @@ impl Pass for ConstantsPass {
|
||||||
insert_predicates(
|
insert_predicates(
|
||||||
program,
|
program,
|
||||||
&mut mutated,
|
&mut mutated,
|
||||||
|
&oversized,
|
||||||
cfg.max_predicates,
|
cfg.max_predicates,
|
||||||
&mut rng,
|
&mut rng,
|
||||||
&mut predicates,
|
&mut predicates,
|
||||||
|
|
@ -96,15 +107,17 @@ impl Pass for ConstantsPass {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Мелочь, которую шифровать невыгодно (уже `iconst`).
|
/// Trivia that is not worth encrypting (already `iconst`).
|
||||||
fn is_tiny_int(value: i32) -> bool {
|
fn is_tiny_int(value: i32) -> bool {
|
||||||
(-1..=5).contains(&value)
|
(-1..=5).contains(&value)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Opaque-предикаты в начало методов с кодом (не больше лимита на метод).
|
/// Opaque predicates at the top of methods with code (at most the limit).
|
||||||
|
/// Oversized methods stay fully untouched, predicates included.
|
||||||
fn insert_predicates(
|
fn insert_predicates(
|
||||||
program: &mut Program,
|
program: &mut Program,
|
||||||
mutated: &mut Vec<(usize, usize)>,
|
mutated: &mut Vec<(usize, usize)>,
|
||||||
|
oversized: &[(usize, usize)],
|
||||||
max_predicates: usize,
|
max_predicates: usize,
|
||||||
rng: &mut noxis_core::Rng,
|
rng: &mut noxis_core::Rng,
|
||||||
predicates: &mut usize,
|
predicates: &mut usize,
|
||||||
|
|
@ -116,6 +129,9 @@ fn insert_predicates(
|
||||||
for ci in 0..classes {
|
for ci in 0..classes {
|
||||||
let methods = program.classes[ci].methods.len();
|
let methods = program.classes[ci].methods.len();
|
||||||
for mi in 0..methods {
|
for mi in 0..methods {
|
||||||
|
if oversized.contains(&(ci, mi)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
let empty_or_missing = program.classes[ci].methods[mi]
|
let empty_or_missing = program.classes[ci].methods[mi]
|
||||||
.code
|
.code
|
||||||
.as_ref()
|
.as_ref()
|
||||||
|
|
@ -123,7 +139,7 @@ fn insert_predicates(
|
||||||
if empty_or_missing {
|
if empty_or_missing {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let missing = || pass_error("метод пропал при вставке предиката");
|
let missing = || pass_error("method vanished while inserting a predicate");
|
||||||
let mut top = program.classes[ci]
|
let mut top = program.classes[ci]
|
||||||
.methods
|
.methods
|
||||||
.get(mi)
|
.get(mi)
|
||||||
|
|
@ -166,7 +182,7 @@ fn insert_predicates(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Пересчитать фреймы затронутого метода.
|
/// Recompute the frames of a touched method.
|
||||||
fn refresh_frames(
|
fn refresh_frames(
|
||||||
program: &mut Program,
|
program: &mut Program,
|
||||||
ci: usize,
|
ci: usize,
|
||||||
|
|
@ -177,14 +193,14 @@ fn refresh_frames(
|
||||||
let class = program
|
let class = program
|
||||||
.classes
|
.classes
|
||||||
.get(ci)
|
.get(ci)
|
||||||
.ok_or_else(|| pass_error("класс пропал"))?;
|
.ok_or_else(|| pass_error("class vanished"))?;
|
||||||
(
|
(
|
||||||
class.this_class.clone(),
|
class.this_class.clone(),
|
||||||
class
|
class
|
||||||
.methods
|
.methods
|
||||||
.get(mi)
|
.get(mi)
|
||||||
.cloned()
|
.cloned()
|
||||||
.ok_or_else(|| pass_error("метод пропал"))?,
|
.ok_or_else(|| pass_error("method vanished"))?,
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
let frames =
|
let frames =
|
||||||
|
|
@ -194,7 +210,7 @@ fn refresh_frames(
|
||||||
.get_mut(ci)
|
.get_mut(ci)
|
||||||
.and_then(|c| c.methods.get_mut(mi))
|
.and_then(|c| c.methods.get_mut(mi))
|
||||||
.and_then(|m| m.code.as_mut())
|
.and_then(|m| m.code.as_mut())
|
||||||
.ok_or_else(|| pass_error("метод пропал"))?
|
.ok_or_else(|| pass_error("method vanished"))?
|
||||||
.frames = frames;
|
.frames = frames;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -205,7 +221,7 @@ fn report(replaced: usize, predicates: usize, skipped: usize) -> PassReport {
|
||||||
transformed: replaced + predicates,
|
transformed: replaced + predicates,
|
||||||
skipped,
|
skipped,
|
||||||
notes: vec![format!(
|
notes: vec![format!(
|
||||||
"констант разбито: {replaced}, предикатов: {predicates}"
|
"constants split: {replaced}, predicates: {predicates}"
|
||||||
)],
|
)],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
//! Тесты пасса констант: формы вычисляют исходник, мелочь цела.
|
//! Constant-pass tests: forms compute the source value, trivia survives.
|
||||||
|
|
||||||
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
||||||
|
|
||||||
use noxis_classfile::{ClassBuilder, Code, Const, Insn, Prim, Program, Version};
|
use noxis_classfile::{ClassBuilder, Code, Const, Insn, MAX_METHOD_INSNS, Prim, Program, Version};
|
||||||
use noxis_core::{Config, Context, Pass};
|
use noxis_core::{Config, Context, Pass};
|
||||||
|
|
||||||
use super::ConstantsPass;
|
use super::ConstantsPass;
|
||||||
use super::forms::{Scheme, encrypt_int, encrypt_long};
|
use super::forms::{Scheme, encrypt_int, encrypt_long};
|
||||||
|
|
||||||
/// Формы обратимы арифметически (интерпретатор троек/пятёрок).
|
/// Forms round-trip arithmetically (an interpreter for triples/quintuples).
|
||||||
fn eval_int(insns: &[Insn]) -> i32 {
|
fn eval_int(insns: &[Insn]) -> i32 {
|
||||||
match insns {
|
match insns {
|
||||||
[
|
[
|
||||||
|
|
@ -20,7 +20,7 @@ fn eval_int(insns: &[Insn]) -> i32 {
|
||||||
match op {
|
match op {
|
||||||
Xor => a ^ b,
|
Xor => a ^ b,
|
||||||
Add => a.wrapping_add(*b),
|
Add => a.wrapping_add(*b),
|
||||||
_ => panic!("не та тройка: {insns:?}"),
|
_ => panic!("not a triple: {insns:?}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
[
|
[
|
||||||
|
|
@ -30,7 +30,7 @@ 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),
|
||||||
_ => panic!("не форма: {insns:?}"),
|
_ => panic!("not a form: {insns:?}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -84,7 +84,7 @@ fn sample() -> Program {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Пасс: большие константы разбиты, мелочь и структура целы.
|
/// The pass: big constants are split, trivia and structure survive.
|
||||||
#[test]
|
#[test]
|
||||||
fn constants_split_and_predicated() {
|
fn constants_split_and_predicated() {
|
||||||
let mut program = sample();
|
let mut program = sample();
|
||||||
|
|
@ -96,16 +96,54 @@ fn constants_split_and_predicated() {
|
||||||
.apply(&mut ctx, &mut program)
|
.apply(&mut ctx, &mut program)
|
||||||
.expect("constants");
|
.expect("constants");
|
||||||
|
|
||||||
let code = program.classes[0].methods[0].code.as_ref().expect("код");
|
let code = program.classes[0].methods[0].code.as_ref().expect("code");
|
||||||
let has = |v: i32| code.insns.contains(&Insn::Const(Const::Int(v)));
|
let has = |v: i32| code.insns.contains(&Insn::Const(Const::Int(v)));
|
||||||
assert!(!has(100), "исходная константа уцелела");
|
assert!(!has(100), "source constant survived");
|
||||||
assert!(has(3), "мелочь тронута");
|
assert!(has(3), "trivia touched");
|
||||||
assert!(
|
assert!(
|
||||||
!code.insns.contains(&Insn::Const(Const::Long(10))),
|
!code.insns.contains(&Insn::Const(Const::Long(10))),
|
||||||
"long уцелел"
|
"long survived"
|
||||||
);
|
);
|
||||||
// Opaque-предикат: мёртвая ветвь из `nop`.
|
// Opaque predicate: a dead branch made of `nop`.
|
||||||
assert!(code.insns.contains(&Insn::Nop), "нет предиката");
|
assert!(code.insns.contains(&Insn::Nop), "no predicate");
|
||||||
assert!(report.transformed >= 3, "мало трансформаций: {report:?}");
|
assert!(report.transformed >= 3, "few transforms: {report:?}");
|
||||||
assert_eq!(report.skipped, 0);
|
assert_eq!(report.skipped, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A method that would blow past the 64KB ceiling stays byte-identical.
|
||||||
|
#[test]
|
||||||
|
fn oversized_methods_are_skipped() {
|
||||||
|
let code = Code {
|
||||||
|
max_stack: 2,
|
||||||
|
max_locals: 0,
|
||||||
|
insns: vec![Insn::Const(Const::Int(1_000_000)); MAX_METHOD_INSNS + 1],
|
||||||
|
exception_table: Vec::new(),
|
||||||
|
frames: Vec::new(),
|
||||||
|
attributes: Vec::new(),
|
||||||
|
};
|
||||||
|
let mut program = Program {
|
||||||
|
classes: vec![
|
||||||
|
ClassBuilder::new("demo/App", Some("java/lang/Object"))
|
||||||
|
.version(Version::JAVA_21)
|
||||||
|
.method_with_code(0x0009, "run", "()V", code)
|
||||||
|
.build(),
|
||||||
|
],
|
||||||
|
resources: Vec::new(),
|
||||||
|
};
|
||||||
|
let mut ctx = Context::new(Config {
|
||||||
|
seed: Some(9),
|
||||||
|
..Config::default()
|
||||||
|
});
|
||||||
|
let report = ConstantsPass
|
||||||
|
.apply(&mut ctx, &mut program)
|
||||||
|
.expect("constants");
|
||||||
|
|
||||||
|
let code = program.classes[0].methods[0].code.as_ref().expect("code");
|
||||||
|
assert!(
|
||||||
|
code.insns
|
||||||
|
.iter()
|
||||||
|
.all(|i| matches!(i, Insn::Const(Const::Int(1_000_000)))),
|
||||||
|
"oversized method was rewritten"
|
||||||
|
);
|
||||||
|
assert_eq!(report.skipped, 1);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
//! Пасс отладочной информации: удаление и ложная подмена.
|
//! Пасс отладочной информации: удаление и ложная подмена.
|
||||||
//!
|
//!
|
||||||
//! Источник требований — `info/evo-weaknesses.md` §3: у разобранного
|
//! Источник требований - `info/evo-weaknesses.md` §3: у разобранного
|
||||||
//! образца оригинальные `SourceFile` у 160 классов, 10 218 `LineNumberTable`
|
//! образца оригинальные `SourceFile` у 160 классов, 10 218 `LineNumberTable`
|
||||||
//! и имена локальных переменных. Это дешевле всего чинить и дороже всего
|
//! и имена локальных переменных. Это дешевле всего чинить и дороже всего
|
||||||
//! отдавать.
|
//! отдавать.
|
||||||
//!
|
//!
|
||||||
//! Важно: удалять недостаточно. Инструменты (JADX/CFR) верят отладке сильнее,
|
//! Важно: удалять недостаточно. Инструменты (JADX/CFR) верят отладке сильнее,
|
||||||
//! чем байткоду, поэтому `spoof_source_file` подставляет **ложное** имя —
|
//! чем байткоду, поэтому `spoof_source_file` подставляет **ложное** имя -
|
||||||
//! консистентное, но не связанное с реальным исходником.
|
//! консистентное, но не связанное с реальным исходником.
|
||||||
|
|
||||||
use noxis_classfile::{Attribute, ClassFile, Code, Method, Program};
|
use noxis_classfile::{Attribute, ClassFile, Code, Method, Program};
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
//! Every emitter here is stack-neutral as a whole: whatever the prologue
|
//! Every emitter here is stack-neutral as a whole: whatever the prologue
|
||||||
//! pushes it also consumes before falling through to `body`. That is what
|
//! pushes it also consumes before falling through to `body`. That is what
|
||||||
//! makes prepending safe at method entry (empty stack) without any dataflow
|
//! makes prepending safe at method entry (empty stack) without any dataflow
|
||||||
//! analysis — all joins at `body` carry an empty stack, so any computed
|
//! analysis - all joins at `body` carry an empty stack, so any computed
|
||||||
//! `StackMapTable` stays consistent.
|
//! `StackMapTable` stays consistent.
|
||||||
|
|
||||||
use noxis_classfile::{Arith, BranchCond, Cond, Const, Insn, Prim, SlotKind};
|
use noxis_classfile::{Arith, BranchCond, Cond, Const, Insn, Prim, SlotKind};
|
||||||
|
|
@ -14,7 +14,7 @@ use noxis_core::Rng;
|
||||||
/// The opaque key is computed as `a + b` (two constants, never the key
|
/// The opaque key is computed as `a + b` (two constants, never the key
|
||||||
/// itself) and dispatched over `[key-2, key+2]`; only the middle slot reaches
|
/// itself) and dispatched over `[key-2, key+2]`; only the middle slot reaches
|
||||||
/// `body`, the rest fall into a dead block that jumps back to `body`.
|
/// `body`, the rest fall into a dead block that jumps back to `body`.
|
||||||
/// Does not emit `Mark(body)` — the caller places it.
|
/// Does not emit `Mark(body)` - the caller places it.
|
||||||
pub(super) fn table_dispatch(body: u32, dead: u32, rng: &mut Rng) -> Vec<Insn> {
|
pub(super) fn table_dispatch(body: u32, dead: u32, rng: &mut Rng) -> Vec<Insn> {
|
||||||
let key = 100 + (rng.next_u64() % 10_000) as i32;
|
let key = 100 + (rng.next_u64() % 10_000) as i32;
|
||||||
let half = (rng.next_u64() as i32) & 0x3FFF_FFFF;
|
let half = (rng.next_u64() as i32) & 0x3FFF_FFFF;
|
||||||
|
|
@ -41,9 +41,9 @@ pub(super) fn table_dispatch(body: u32, dead: u32, rng: &mut Rng) -> Vec<Insn> {
|
||||||
/// Sparse `lookupswitch` dispatcher over a counter local (`computed-switch`).
|
/// Sparse `lookupswitch` dispatcher over a counter local (`computed-switch`).
|
||||||
///
|
///
|
||||||
/// The counter lives in `slot` (a fresh local owned by the caller): the shape
|
/// The counter lives in `slot` (a fresh local owned by the caller): the shape
|
||||||
/// differs from `table_dispatch` on purpose — a sparse switch over a variable
|
/// differs from `table_dispatch` on purpose - a sparse switch over a variable
|
||||||
/// instead of a dense range over a stack value. Junk keys never equal `key`.
|
/// instead of a dense range over a stack value. Junk keys never equal `key`.
|
||||||
/// Does not emit `Mark(body)` — the caller places it.
|
/// Does not emit `Mark(body)` - the caller places it.
|
||||||
pub(super) fn lookup_dispatch(body: u32, dead: u32, slot: u16, rng: &mut Rng) -> Vec<Insn> {
|
pub(super) fn lookup_dispatch(body: u32, dead: u32, slot: u16, rng: &mut Rng) -> Vec<Insn> {
|
||||||
let key = 100 + (rng.next_u64() % 10_000) as i32;
|
let key = 100 + (rng.next_u64() % 10_000) as i32;
|
||||||
let mut keys = vec![key];
|
let mut keys = vec![key];
|
||||||
|
|
@ -82,7 +82,7 @@ pub(super) fn lookup_dispatch(body: u32, dead: u32, slot: u16, rng: &mut Rng) ->
|
||||||
/// Always-true opaque guard `(C*C + C) & 1 == 0` with a dead `nop` branch.
|
/// Always-true opaque guard `(C*C + C) & 1 == 0` with a dead `nop` branch.
|
||||||
///
|
///
|
||||||
/// Stack-neutral: the computed value is consumed by the branch that never
|
/// Stack-neutral: the computed value is consumed by the branch that never
|
||||||
/// fires. Ends with `Mark(merge)` — execution continues there.
|
/// fires. Ends with `Mark(merge)` - execution continues there.
|
||||||
/// Same construction as the constants pass uses; the predicate core is shared
|
/// Same construction as the constants pass uses; the predicate core is shared
|
||||||
/// with the flow splices (`rewire::opaque_compute`).
|
/// with the flow splices (`rewire::opaque_compute`).
|
||||||
pub(super) fn opaque_guard(dead: u32, merge: u32, rng: &mut Rng) -> Vec<Insn> {
|
pub(super) fn opaque_guard(dead: u32, merge: u32, rng: &mut Rng) -> Vec<Insn> {
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
//!
|
//!
|
||||||
//! Every scheme also cuts the stream with a few `goto` splices (intra-method
|
//! Every scheme also cuts the stream with a few `goto` splices (intra-method
|
||||||
//! mixing) and recomputes `StackMapTable` afterwards, so the only thing a
|
//! mixing) and recomputes `StackMapTable` afterwards, so the only thing a
|
||||||
//! scheme may assume is an empty stack at method entry — joins stay consistent
|
//! scheme may assume is an empty stack at method entry - joins stay consistent
|
||||||
//! by construction (see `dispatch`/`rewire` for why each shape is safe).
|
//! by construction (see `dispatch`/`rewire` for why each shape is safe).
|
||||||
|
|
||||||
mod dispatch;
|
mod dispatch;
|
||||||
|
|
@ -120,7 +120,7 @@ fn skippable(name: &str, code: &Code, max_insns: usize) -> bool {
|
||||||
if name == "<init>" {
|
if name == "<init>" {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// Frames are never computed for subroutines — keep them byte-identical.
|
// Frames are never computed for subroutines - keep them byte-identical.
|
||||||
if code
|
if code
|
||||||
.insns
|
.insns
|
||||||
.iter()
|
.iter()
|
||||||
|
|
@ -167,7 +167,7 @@ fn apply_scheme(code: &mut Code, scheme: Scheme, cfg: &noxis_core::FlowConfig, r
|
||||||
prologue.push(Insn::Mark(body));
|
prologue.push(Insn::Mark(body));
|
||||||
}
|
}
|
||||||
Scheme::ComputedSwitch if code.max_locals == u16::MAX => {
|
Scheme::ComputedSwitch if code.max_locals == u16::MAX => {
|
||||||
// No free local slot — fall back to the stack-based dispatcher.
|
// No free local slot - fall back to the stack-based dispatcher.
|
||||||
prologue.extend(table_dispatch(body, dead, rng));
|
prologue.extend(table_dispatch(body, dead, rng));
|
||||||
prologue.push(Insn::Mark(body));
|
prologue.push(Insn::Mark(body));
|
||||||
}
|
}
|
||||||
|
|
@ -189,7 +189,7 @@ fn apply_scheme(code: &mut Code, scheme: Scheme, cfg: &noxis_core::FlowConfig, r
|
||||||
// No skip-goto after the dead block: the dispatcher's dead
|
// No skip-goto after the dead block: the dispatcher's dead
|
||||||
// `Goto(body)` already jumps over the handler. A second goto
|
// `Goto(body)` already jumps over the handler. A second goto
|
||||||
// would sit after a terminal with no frame of its own, and
|
// would sit after a terminal with no frame of its own, and
|
||||||
// HotSpot requires a frame at every block start — even an
|
// HotSpot requires a frame at every block start - even an
|
||||||
// unreachable one. try_e IS the dead mark for the same reason.
|
// unreachable one. try_e IS the dead mark for the same reason.
|
||||||
let (mut handler_code, entry) = dead_catch(try_start, dead, handler, body);
|
let (mut handler_code, entry) = dead_catch(try_start, dead, handler, body);
|
||||||
guarded.append(&mut handler_code);
|
guarded.append(&mut handler_code);
|
||||||
|
|
|
||||||
|
|
@ -77,8 +77,8 @@ pub(super) fn invert_branches(
|
||||||
/// Each splice computes an always-false opaque predicate `(C*C + C) & 1 != 0`
|
/// Each splice computes an always-false opaque predicate `(C*C + C) & 1 != 0`
|
||||||
/// and branches on it: the fallthrough continues the stream, the dead block
|
/// and branches on it: the fallthrough continues the stream, the dead block
|
||||||
/// throws instead of rejoining. Throwing (rather than jumping back) keeps the
|
/// throws instead of rejoining. Throwing (rather than jumping back) keeps the
|
||||||
/// splice valid at *any* stack depth — `athrow` has no successor, so there is
|
/// splice valid at *any* stack depth - `athrow` has no successor, so there is
|
||||||
/// nothing to merge — and the dead block still owns a frame through its entry
|
/// nothing to merge - and the dead block still owns a frame through its entry
|
||||||
/// branch, which HotSpot requires even for code that never executes.
|
/// branch, which HotSpot requires even for code that never executes.
|
||||||
///
|
///
|
||||||
/// Cuts land at or after `start` (the entry prologue must stay first) and
|
/// Cuts land at or after `start` (the entry prologue must stay first) and
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ use noxis_core::{Config, Context, NameStyle, Rng};
|
||||||
use super::{ACC_PRIVATE, ACC_PROTECTED, ACC_PUBLIC, ACC_STATIC};
|
use super::{ACC_PRIVATE, ACC_PROTECTED, ACC_PUBLIC, ACC_STATIC};
|
||||||
use crate::namegen::NameGenerator;
|
use crate::namegen::NameGenerator;
|
||||||
|
|
||||||
/// `(owner, name, descriptor)` — matches a JVM reference.
|
/// `(owner, name, descriptor)` - matches a JVM reference.
|
||||||
pub(super) type MemberKey = (String, String, String);
|
pub(super) type MemberKey = (String, String, String);
|
||||||
|
|
||||||
/// Rename maps for one run: `declared` is where a member was declared,
|
/// Rename maps for one run: `declared` is where a member was declared,
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,11 @@
|
||||||
//! у EvoVis члены не переименовывались вообще, и класс читался по `javap -p`).
|
//! у EvoVis члены не переименовывались вообще, и класс читался по `javap -p`).
|
||||||
//!
|
//!
|
||||||
//! Тонкости, которые здесь учитываются:
|
//! Тонкости, которые здесь учитываются:
|
||||||
//! - вызов `Sub.staticMethod()` может указывать владельцем подкласс — нужны
|
//! - вызов `Sub.staticMethod()` может указывать владельцем подкласс - нужны
|
||||||
//! псевдонимы (`alias`) для всей иерархии;
|
//! псевдонимы (`alias`) для всей иерархии;
|
||||||
//! - мостовые методы (`ACC_BRIDGE`) и ковариантные возвраты живут в одном
|
//! - мостовые методы (`ACC_BRIDGE`) и ковариантные возвраты живут в одном
|
||||||
//! «семействе» метода — переименовывать их нужно разом;
|
//! «семействе» метода - переименовывать их нужно разом;
|
||||||
//! - `serialVersionUID`, JNI, рефлексия, сериализация — не трогаем.
|
//! - `serialVersionUID`, JNI, рефлексия, сериализация - не трогаем.
|
||||||
|
|
||||||
/// Карты и стратегия назначения имён.
|
/// Карты и стратегия назначения имён.
|
||||||
mod assign;
|
mod assign;
|
||||||
|
|
@ -179,7 +179,7 @@ impl Pass for RenamePass {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Виртуальные семейства — только в агрессивном режиме.
|
// 3. Виртуальные семейства - только в агрессивном режиме.
|
||||||
if cfg.rename.rename_public_members {
|
if cfg.rename.rename_public_members {
|
||||||
renamed_methods += rename_virtual_families(program, &analysis, &mut maps, &cfg, ctx);
|
renamed_methods += rename_virtual_families(program, &analysis, &mut maps, &cfg, ctx);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
//! Пасс ресурсов: патч текстовых файлов архива по маппингу классов.
|
//! Пасс ресурсов: патч текстовых файлов архива по маппингу классов.
|
||||||
//!
|
//!
|
||||||
//! Без этого обфускация Fabric-мода просто не стартует: `fabric.mod.json`
|
//! Без этого обфускация Fabric-мода просто не стартует: `fabric.mod.json`
|
||||||
//! хранит main-class точкой, `*.mixins.json` — пути внутренними именами,
|
//! хранит main-class точкой, `*.mixins.json` - пути внутренними именами,
|
||||||
//! refmap и access widener — тоже. Материал: `info/evo-weaknesses.md` §3/§8
|
//! refmap и access widener - тоже. Материал: `info/evo-weaknesses.md` §3/§8
|
||||||
//! (у разобранного образца ресурсы вообще не трогали).
|
//! (у разобранного образца ресурсы вообще не трогали).
|
||||||
//!
|
//!
|
||||||
//! Замена **границная**, а не подстрочная: `org/example/Foo` не должен
|
//! Замена **границная**, а не подстрочная: `org/example/Foo` не должен
|
||||||
|
|
@ -33,7 +33,7 @@ const TEXT_EXTS: [&str; 9] = [
|
||||||
".accesswidener",
|
".accesswidener",
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Service loader'ы: файла без расширения, но каждая строка — имя класса.
|
/// Service loader'ы: файла без расширения, но каждая строка - имя класса.
|
||||||
const SERVICES_PREFIX: &str = "META-INF/services/";
|
const SERVICES_PREFIX: &str = "META-INF/services/";
|
||||||
|
|
||||||
/// Пасс патча ресурсов.
|
/// Пасс патча ресурсов.
|
||||||
|
|
@ -52,7 +52,7 @@ impl Pass for ResourcesPass {
|
||||||
pass: PassId::Resources,
|
pass: PassId::Resources,
|
||||||
transformed: 0,
|
transformed: 0,
|
||||||
skipped: program.resources.len(),
|
skipped: program.resources.len(),
|
||||||
notes: vec!["маппинг классов пуст — ресурсы не тронуты".into()],
|
notes: vec!["маппинг классов пуст - ресурсы не тронуты".into()],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -142,7 +142,7 @@ fn replace_delimited(text: &str, old: &str, new: &str, dotted: bool) -> String {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Совпадение частичное — пропускаем первый символ и ищем дальше.
|
// Совпадение частичное - пропускаем первый символ и ищем дальше.
|
||||||
let mut chars = rest[at..].chars();
|
let mut chars = rest[at..].chars();
|
||||||
out.push_str(before);
|
out.push_str(before);
|
||||||
match chars.next() {
|
match chars.next() {
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,7 @@ fn empty_map_is_a_noop() {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn unicode_text_survives_unrelated_substitution() {
|
fn unicode_text_survives_unrelated_substitution() {
|
||||||
let text = "комментарий: Лорем_ипсум — org/example/Bar";
|
let text = "комментарий: Лорем_ипсум - org/example/Bar";
|
||||||
assert_eq!(remap_text(text, &mapping()), text);
|
assert_eq!(remap_text(text, &mapping()), text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
//! Шифрование строк: чанки, salt под схему, последовательность вызова.
|
//! Шифрование строк: чанки, salt под схему, последовательность вызова.
|
||||||
//!
|
//!
|
||||||
//! Один `Const::String` превращается в `ldc cipher; ldc salt; invokestatic
|
//! Один `Const::String` превращается в `ldc cipher; ldc salt; invokestatic
|
||||||
//! d` на чанк; чанки склеиваются `String.concat` (без `StringBuilder` —
|
//! d` на чанк; чанки склеиваются `String.concat` (без `StringBuilder` -
|
||||||
//! меньше инструкций и нет вызовов конструктора).
|
//! меньше инструкций и нет вызовов конструктора).
|
||||||
|
|
||||||
use noxis_classfile::{Const, Insn, InvokeKind};
|
use noxis_classfile::{Const, Insn, InvokeKind};
|
||||||
|
|
@ -24,7 +24,7 @@ pub(super) fn split_chunks(text: &str, split_above: usize) -> Vec<String> {
|
||||||
|
|
||||||
/// Salt, попадающий в нужную схему: `salt % total == index`.
|
/// Salt, попадающий в нужную схему: `salt % total == index`.
|
||||||
///
|
///
|
||||||
/// Salt всегда неотрицательный (Java `%` сохраняет знак — отрицательный salt
|
/// Salt всегда неотрицательный (Java `%` сохраняет знак - отрицательный salt
|
||||||
/// выбрал бы не ту ветку декодера). `index` обязан быть меньше `total`.
|
/// выбрал бы не ту ветку декодера). `index` обязан быть меньше `total`.
|
||||||
pub(super) fn salt_for_scheme(raw: u64, index: usize, total: usize) -> i32 {
|
pub(super) fn salt_for_scheme(raw: u64, index: usize, total: usize) -> i32 {
|
||||||
let total = total.max(1) as u64;
|
let total = total.max(1) as u64;
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
//! Пасс шифрования строк: `Const::String` → вызовы рантайм-декодера.
|
//! The strings pass: `Const::String` becomes runtime-decoder calls.
|
||||||
//!
|
//!
|
||||||
//! Схема строки выбирается ротацией (`salt % n`), salt детерминирован
|
//! The scheme rotates per string (`salt % n`), the salt is deterministic
|
||||||
//! (KDF или RNG-поток — см. `derive_key`). Декодер генерируется один на
|
//! (KDF or RNG stream - see `derive_key`). The decoder is generated once per
|
||||||
//! программу и только если была зашифрована хотя бы одна строка.
|
//! program and only if at least one string was encrypted.
|
||||||
|
|
||||||
mod crypt;
|
mod crypt;
|
||||||
|
|
||||||
|
|
@ -12,14 +12,14 @@ mod tests;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
use noxis_analysis::ClassHierarchy;
|
use noxis_analysis::ClassHierarchy;
|
||||||
use noxis_classfile::{Const, Insn, Program, compute_frames};
|
use noxis_classfile::{Const, Insn, MAX_METHOD_INSNS, Program, compute_frames};
|
||||||
use noxis_core::{Context, CoreError, NameStyle, Pass, PassId, PassReport, derive_seed};
|
use noxis_core::{Context, CoreError, NameStyle, Pass, PassId, PassReport, derive_seed};
|
||||||
use noxis_runtime::{Scheme, decoder_class, decoder_owner, encrypt};
|
use noxis_runtime::{Scheme, decoder_class, decoder_owner, encrypt};
|
||||||
|
|
||||||
use crate::namegen::NameGenerator;
|
use crate::namegen::NameGenerator;
|
||||||
use crypt::{decode_sequence, salt_for_scheme, split_chunks};
|
use crypt::{decode_sequence, salt_for_scheme, split_chunks};
|
||||||
|
|
||||||
/// Пасс шифрования строк.
|
/// The string encryption pass.
|
||||||
#[derive(Debug, Default, Clone, Copy)]
|
#[derive(Debug, Default, Clone, Copy)]
|
||||||
pub struct StringsPass;
|
pub struct StringsPass;
|
||||||
|
|
||||||
|
|
@ -35,9 +35,9 @@ impl Pass for StringsPass {
|
||||||
schemes.push(Scheme::parse(name).map_err(|e| pass_error(e.to_string()))?);
|
schemes.push(Scheme::parse(name).map_err(|e| pass_error(e.to_string()))?);
|
||||||
}
|
}
|
||||||
if schemes.is_empty() {
|
if schemes.is_empty() {
|
||||||
return Err(pass_error("нет схем шифрования"));
|
return Err(pass_error("no encryption schemes"));
|
||||||
}
|
}
|
||||||
// Фаза 1: собрать все шифруемые строки (позиция + текст).
|
// Phase 1: collect every encryptable string (position + text).
|
||||||
let mut targets: Vec<(usize, usize, usize, String)> = Vec::new();
|
let mut targets: Vec<(usize, usize, usize, String)> = Vec::new();
|
||||||
let mut skipped = 0usize;
|
let mut skipped = 0usize;
|
||||||
for (ci, class) in program.classes.iter().enumerate() {
|
for (ci, class) in program.classes.iter().enumerate() {
|
||||||
|
|
@ -60,10 +60,10 @@ impl Pass for StringsPass {
|
||||||
return Ok(report(
|
return Ok(report(
|
||||||
0,
|
0,
|
||||||
skipped,
|
skipped,
|
||||||
vec!["нет строк для шифрования".to_string()],
|
vec!["no strings to encrypt".to_string()],
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
// Фаза 2: зашифровать и подставить последовательности вызовов.
|
// Phase 2: encrypt and substitute call sequences.
|
||||||
let master = ctx.master_seed;
|
let master = ctx.master_seed;
|
||||||
let mut rng = ctx.rng("strings");
|
let mut rng = ctx.rng("strings");
|
||||||
let owner = decoder_name(ctx, program);
|
let owner = decoder_name(ctx, program);
|
||||||
|
|
@ -83,49 +83,64 @@ impl Pass for StringsPass {
|
||||||
replacements.insert((*ci, *mi, *ii), decode_sequence(&owner, &parts));
|
replacements.insert((*ci, *mi, *ii), decode_sequence(&owner, &parts));
|
||||||
}
|
}
|
||||||
let mut mutated: Vec<(usize, usize)> = Vec::new();
|
let mut mutated: Vec<(usize, usize)> = Vec::new();
|
||||||
|
let mut applied = 0usize;
|
||||||
|
let mut per_method: BTreeMap<(usize, usize), usize> = BTreeMap::new();
|
||||||
|
for (ci, mi, _, _) in &targets {
|
||||||
|
*per_method.entry((*ci, *mi)).or_default() += 1;
|
||||||
|
}
|
||||||
for (ci, class) in program.classes.iter_mut().enumerate() {
|
for (ci, class) in program.classes.iter_mut().enumerate() {
|
||||||
for (mi, method) in class.methods.iter_mut().enumerate() {
|
for (mi, method) in class.methods.iter_mut().enumerate() {
|
||||||
let Some(code) = method.code.as_mut() else {
|
let Some(code) = method.code.as_mut() else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let mut fresh = Vec::with_capacity(code.insns.len());
|
let original = std::mem::take(&mut code.insns);
|
||||||
|
let method_targets = per_method.get(&(ci, mi)).copied().unwrap_or(0);
|
||||||
|
let mut fresh = Vec::with_capacity(original.len());
|
||||||
let mut touched = false;
|
let mut touched = false;
|
||||||
for (ii, insn) in code.insns.drain(..).enumerate() {
|
for (ii, insn) in original.iter().enumerate() {
|
||||||
if let Some(sequence) = replacements.remove(&(ci, mi, ii)) {
|
if let Some(sequence) = replacements.remove(&(ci, mi, ii)) {
|
||||||
fresh.extend(sequence);
|
fresh.extend(sequence);
|
||||||
touched = true;
|
touched = true;
|
||||||
} else {
|
} else {
|
||||||
fresh.push(insn);
|
fresh.push(insn.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if fresh.len() > MAX_METHOD_INSNS {
|
||||||
|
// Would risk the 64KB writer ceiling: keep the original
|
||||||
|
// code (and its strings) untouched.
|
||||||
|
code.insns = original;
|
||||||
|
skipped += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
code.insns = fresh;
|
||||||
if touched {
|
if touched {
|
||||||
|
applied += method_targets;
|
||||||
code.max_stack = code.max_stack.saturating_add(2);
|
code.max_stack = code.max_stack.saturating_add(2);
|
||||||
mutated.push((ci, mi));
|
mutated.push((ci, mi));
|
||||||
}
|
}
|
||||||
code.insns = fresh;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Фаза 3: класс-декодер один на программу.
|
// Phase 3: one decoder class per program.
|
||||||
let scheme_names: Vec<&str> = cfg.schemes.iter().map(String::as_str).collect();
|
let scheme_names: Vec<&str> = cfg.schemes.iter().map(String::as_str).collect();
|
||||||
let decoder =
|
let decoder =
|
||||||
decoder_class(&owner, master, &scheme_names).map_err(|e| pass_error(e.to_string()))?;
|
decoder_class(&owner, master, &scheme_names).map_err(|e| pass_error(e.to_string()))?;
|
||||||
program.classes.push(decoder);
|
program.classes.push(decoder);
|
||||||
ctx.note(format!("декодер строк: {owner}"));
|
ctx.note(format!("string decoder: {owner}"));
|
||||||
// Фаза 4: фреймы затронутых методов.
|
// Phase 4: frames of the touched methods.
|
||||||
let hierarchy = ClassHierarchy::build(program);
|
let hierarchy = ClassHierarchy::build(program);
|
||||||
for (ci, mi) in &mutated {
|
for (ci, mi) in &mutated {
|
||||||
let (name, snapshot) = {
|
let (name, snapshot) = {
|
||||||
let class = program
|
let class = program
|
||||||
.classes
|
.classes
|
||||||
.get(*ci)
|
.get(*ci)
|
||||||
.ok_or_else(|| pass_error("класс пропал"))?;
|
.ok_or_else(|| pass_error("class vanished"))?;
|
||||||
(
|
(
|
||||||
class.this_class.clone(),
|
class.this_class.clone(),
|
||||||
class
|
class
|
||||||
.methods
|
.methods
|
||||||
.get(*mi)
|
.get(*mi)
|
||||||
.cloned()
|
.cloned()
|
||||||
.ok_or_else(|| pass_error("метод пропал"))?,
|
.ok_or_else(|| pass_error("method vanished"))?,
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
let frames = compute_frames(&name, &snapshot, &hierarchy)
|
let frames = compute_frames(&name, &snapshot, &hierarchy)
|
||||||
|
|
@ -135,16 +150,13 @@ impl Pass for StringsPass {
|
||||||
.get_mut(*ci)
|
.get_mut(*ci)
|
||||||
.and_then(|c| c.methods.get_mut(*mi))
|
.and_then(|c| c.methods.get_mut(*mi))
|
||||||
.and_then(|m| m.code.as_mut())
|
.and_then(|m| m.code.as_mut())
|
||||||
.ok_or_else(|| pass_error("метод пропал"))?
|
.ok_or_else(|| pass_error("method vanished"))?
|
||||||
.frames = frames;
|
.frames = frames;
|
||||||
}
|
}
|
||||||
Ok(report(
|
Ok(report(
|
||||||
targets.len(),
|
applied,
|
||||||
skipped,
|
skipped,
|
||||||
vec![format!(
|
vec![format!("strings encrypted: {applied}, decoder: {owner}")],
|
||||||
"зашифровано строк: {}, декодер: {owner}",
|
|
||||||
targets.len()
|
|
||||||
)],
|
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -162,12 +174,12 @@ fn pass_error(msg: impl Into<String>) -> CoreError {
|
||||||
CoreError::pass(PassId::Strings, msg)
|
CoreError::pass(PassId::Strings, msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Строка подпадает под `skip`-паттерны конфига.
|
/// A string matches the `skip` patterns of the config.
|
||||||
fn is_skipped(text: &str, skip: &[String]) -> bool {
|
fn is_skipped(text: &str, skip: &[String]) -> bool {
|
||||||
skip.iter().any(|pattern| text.contains(pattern))
|
skip.iter().any(|pattern| text.contains(pattern))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Salt чанка: KDF от позиции или RNG-поток (см. `derive_key`).
|
/// A chunk salt: KDF from the position or an RNG stream (see `derive_key`).
|
||||||
fn next_salt(rng: &mut noxis_core::Rng, master: u64, counter: &mut u64, derive: bool) -> u64 {
|
fn next_salt(rng: &mut noxis_core::Rng, master: u64, counter: &mut u64, derive: bool) -> u64 {
|
||||||
let value = if derive {
|
let value = if derive {
|
||||||
derive_seed(master, "strings/salt", *counter)
|
derive_seed(master, "strings/salt", *counter)
|
||||||
|
|
@ -178,7 +190,7 @@ fn next_salt(rng: &mut noxis_core::Rng, master: u64, counter: &mut u64, derive:
|
||||||
value
|
value
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Детерминированное имя декодера без коллизий с классами программы.
|
/// A collision-free deterministic decoder name.
|
||||||
fn decoder_name(ctx: &mut Context, program: &Program) -> String {
|
fn decoder_name(ctx: &mut Context, program: &Program) -> String {
|
||||||
let mut names = NameGenerator::new(ctx.rng("strings/decoder"), NameStyle::ShortLatin);
|
let mut names = NameGenerator::new(ctx.rng("strings/decoder"), NameStyle::ShortLatin);
|
||||||
for class in &program.classes {
|
for class in &program.classes {
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
//! Тесты пасса строк: подстановка, чанки, детерминизм шифротекста.
|
//! String-pass tests: substitution, chunks, ciphertext determinism.
|
||||||
|
|
||||||
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
||||||
|
|
||||||
use noxis_classfile::{ClassBuilder, Code, Const, Insn, Program, Version};
|
use noxis_classfile::{ClassBuilder, Code, Const, Insn, MAX_METHOD_INSNS, Program, Version};
|
||||||
use noxis_core::{Config, Context};
|
use noxis_core::{Config, Context};
|
||||||
use noxis_runtime::{Scheme, encrypt};
|
use noxis_runtime::{Scheme, encrypt};
|
||||||
|
|
||||||
|
|
@ -36,7 +36,7 @@ fn sample() -> Program {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Короткие строки не режутся, длинные — на границе символов.
|
/// Short strings stay whole, long ones split on char boundaries.
|
||||||
#[test]
|
#[test]
|
||||||
fn chunks_split_by_chars() {
|
fn chunks_split_by_chars() {
|
||||||
assert_eq!(split_chunks("abc", 64), vec!["abc".to_string()]);
|
assert_eq!(split_chunks("abc", 64), vec!["abc".to_string()]);
|
||||||
|
|
@ -45,19 +45,19 @@ fn chunks_split_by_chars() {
|
||||||
assert_eq!(chunks.concat().len(), 100);
|
assert_eq!(chunks.concat().len(), 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Salt всегда неотрицателен и попадает в схему.
|
/// Salt is always non-negative and lands in its scheme.
|
||||||
#[test]
|
#[test]
|
||||||
fn salt_hits_scheme() {
|
fn salt_hits_scheme() {
|
||||||
for raw in [0u64, 1, 7, u64::MAX, 123456789] {
|
for raw in [0u64, 1, 7, u64::MAX, 123456789] {
|
||||||
for (index, total) in [(0, 1), (0, 2), (1, 2), (2, 3)] {
|
for (index, total) in [(0, 1), (0, 2), (1, 2), (2, 3)] {
|
||||||
let salt = salt_for_scheme(raw, index, total);
|
let salt = salt_for_scheme(raw, index, total);
|
||||||
assert!(salt >= 0, "отрицательный salt");
|
assert!(salt >= 0, "negative salt");
|
||||||
assert_eq!(salt as usize % total, index);
|
assert_eq!(salt as usize % total, index);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Пасс: строки заменены вызовами, шифротекст воспроизводим, декодер есть.
|
/// The pass: strings become calls, ciphertext is reproducible, decoder exists.
|
||||||
#[test]
|
#[test]
|
||||||
fn strings_replaced_and_deterministic() {
|
fn strings_replaced_and_deterministic() {
|
||||||
let mut program = sample();
|
let mut program = sample();
|
||||||
|
|
@ -70,14 +70,14 @@ fn strings_replaced_and_deterministic() {
|
||||||
|
|
||||||
assert_eq!(report.transformed, 2);
|
assert_eq!(report.transformed, 2);
|
||||||
assert_eq!(report.skipped, 1);
|
assert_eq!(report.skipped, 1);
|
||||||
// Декодер добавлен последним.
|
// The decoder is appended last.
|
||||||
assert_eq!(program.classes.len(), 2);
|
assert_eq!(program.classes.len(), 2);
|
||||||
let decoder = program.classes.last().expect("декодер");
|
let decoder = program.classes.last().expect("decoder");
|
||||||
assert!(decoder.methods.iter().any(|m| m.name == "d"));
|
assert!(decoder.methods.iter().any(|m| m.name == "d"));
|
||||||
|
|
||||||
// Исходных строк не осталось (шифры — длинные Const::String — на месте);
|
// No source strings left (ciphers - long Const::String - are in place);
|
||||||
// короткая на месте.
|
// the short one stays.
|
||||||
let code = program.classes[0].methods[0].code.as_ref().expect("код");
|
let code = program.classes[0].methods[0].code.as_ref().expect("code");
|
||||||
let kept: Vec<&String> = code
|
let kept: Vec<&String> = code
|
||||||
.insns
|
.insns
|
||||||
.iter()
|
.iter()
|
||||||
|
|
@ -90,7 +90,7 @@ fn strings_replaced_and_deterministic() {
|
||||||
assert!(!kept.iter().any(|s| *s == &"y".repeat(100)));
|
assert!(!kept.iter().any(|s| *s == &"y".repeat(100)));
|
||||||
assert!(kept.contains(&&"x".to_string()));
|
assert!(kept.contains(&&"x".to_string()));
|
||||||
|
|
||||||
// Каждый вызов сверяется с прямым шифрованием (тот же сид → те же блобы).
|
// Every call checks out against direct encryption (same seed, same blobs).
|
||||||
let schemes = [Scheme::XorSplitmix, Scheme::XteaCtr];
|
let schemes = [Scheme::XorSplitmix, Scheme::XteaCtr];
|
||||||
let mut triples = Vec::new();
|
let mut triples = Vec::new();
|
||||||
let mut window: Vec<&Insn> = Vec::new();
|
let mut window: Vec<&Insn> = Vec::new();
|
||||||
|
|
@ -109,7 +109,7 @@ fn strings_replaced_and_deterministic() {
|
||||||
triples.push((cipher.clone(), *salt));
|
triples.push((cipher.clone(), *salt));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// "hello world" (1 чанк) + "y"*100 (2 чанка).
|
// "hello world" (1 chunk) + "y"*100 (2 chunks).
|
||||||
assert_eq!(triples.len(), 3);
|
assert_eq!(triples.len(), 3);
|
||||||
let originals = ["hello world".to_string(), "y".repeat(50), "y".repeat(50)];
|
let originals = ["hello world".to_string(), "y".repeat(50), "y".repeat(50)];
|
||||||
for ((cipher, salt), original) in triples.iter().zip(originals) {
|
for ((cipher, salt), original) in triples.iter().zip(originals) {
|
||||||
|
|
@ -117,7 +117,7 @@ fn strings_replaced_and_deterministic() {
|
||||||
let blob = encrypt(scheme, 7, *salt, &original);
|
let blob = encrypt(scheme, 7, *salt, &original);
|
||||||
assert_eq!(cipher, &blob.cipher);
|
assert_eq!(cipher, &blob.cipher);
|
||||||
}
|
}
|
||||||
// Склейка чанков через `concat` — ровно одна; конструктор жив.
|
// Chunk joins through `concat` - exactly one; the constructor is alive.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
code.insns
|
code.insns
|
||||||
.iter()
|
.iter()
|
||||||
|
|
@ -127,3 +127,39 @@ fn strings_replaced_and_deterministic() {
|
||||||
);
|
);
|
||||||
assert!(!decode_sequence("o", &[("c".to_string(), 1)]).is_empty());
|
assert!(!decode_sequence("o", &[("c".to_string(), 1)]).is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A method that would blow past the 64KB ceiling keeps its strings.
|
||||||
|
#[test]
|
||||||
|
fn oversized_methods_are_skipped() {
|
||||||
|
let code = Code {
|
||||||
|
max_stack: 2,
|
||||||
|
max_locals: 0,
|
||||||
|
insns: vec![Insn::Const(Const::String("oversized".to_string())); MAX_METHOD_INSNS + 1],
|
||||||
|
exception_table: Vec::new(),
|
||||||
|
frames: Vec::new(),
|
||||||
|
attributes: Vec::new(),
|
||||||
|
};
|
||||||
|
let mut program = Program {
|
||||||
|
classes: vec![
|
||||||
|
ClassBuilder::new("demo/App", Some("java/lang/Object"))
|
||||||
|
.version(Version::JAVA_21)
|
||||||
|
.method_with_code(0x0009, "run", "()V", code)
|
||||||
|
.build(),
|
||||||
|
],
|
||||||
|
resources: Vec::new(),
|
||||||
|
};
|
||||||
|
let mut ctx = Context::new(Config {
|
||||||
|
seed: Some(7),
|
||||||
|
..Config::default()
|
||||||
|
});
|
||||||
|
let report = StringsPass.apply(&mut ctx, &mut program).expect("strings");
|
||||||
|
|
||||||
|
let code = program.classes[0].methods[0].code.as_ref().expect("code");
|
||||||
|
assert!(
|
||||||
|
code.insns
|
||||||
|
.iter()
|
||||||
|
.all(|i| matches!(i, Insn::Const(Const::String(s)) if s == "oversized")),
|
||||||
|
"oversized method was rewritten"
|
||||||
|
);
|
||||||
|
assert_eq!(report.skipped, 1);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
//! Вход собирается `ByteBuffer`, чтобы не материализовывать 26 байт
|
//! Вход собирается `ByteBuffer`, чтобы не материализовывать 26 байт
|
||||||
//! поштучно: `allocate.putLong(master).put(domain).putLong(salt).array()`.
|
//! поштучно: `allocate.putLong(master).put(domain).putLong(salt).array()`.
|
||||||
//!
|
//!
|
||||||
//! Здесь же — строитель тела метода и короткие эмиссии (общие для всех
|
//! Здесь же - строитель тела метода и короткие эмиссии (общие для всех
|
||||||
//! ветвей декодера).
|
//! ветвей декодера).
|
||||||
|
|
||||||
use noxis_classfile::{Arith, BranchCond, Cond, Const, Insn, InvokeKind, Label, Prim, SlotKind};
|
use noxis_classfile::{Arith, BranchCond, Cond, Const, Insn, InvokeKind, Label, Prim, SlotKind};
|
||||||
|
|
@ -21,7 +21,7 @@ impl Gen {
|
||||||
Self {
|
Self {
|
||||||
insns: Vec::new(),
|
insns: Vec::new(),
|
||||||
labels: 0,
|
labels: 0,
|
||||||
// Слоты 0 (`cipher`) и 1 (`salt`) — параметры метода.
|
// Слоты 0 (`cipher`) и 1 (`salt`) - параметры метода.
|
||||||
slots: 2,
|
slots: 2,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -32,7 +32,7 @@ impl Gen {
|
||||||
label
|
label
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Занять слоты (`wide` — пара под `long`).
|
/// Занять слоты (`wide` - пара под `long`).
|
||||||
pub(super) fn alloc(&mut self, wide: bool) -> u16 {
|
pub(super) fn alloc(&mut self, wide: bool) -> u16 {
|
||||||
let slot = self.slots;
|
let slot = self.slots;
|
||||||
self.slots += if wide { 2 } else { 1 };
|
self.slots += if wide { 2 } else { 1 };
|
||||||
|
|
@ -134,7 +134,7 @@ pub(super) fn emit_kdf(asm: &mut Gen, master: u16, domain: &str) -> u16 {
|
||||||
|
|
||||||
/// Собрать вход `master || domain || salt` в слот `input`.
|
/// Собрать вход `master || domain || salt` в слот `input`.
|
||||||
///
|
///
|
||||||
/// Rust считает FNV по little-endian байтам, а `putLong` кладёт big-endian —
|
/// Rust считает FNV по little-endian байтам, а `putLong` кладёт big-endian -
|
||||||
/// поэтому оба `long` разворачиваются (`Long.reverseBytes`).
|
/// поэтому оба `long` разворачиваются (`Long.reverseBytes`).
|
||||||
fn build_input(asm: &mut Gen, master: u16, domain: &str, input: u16) {
|
fn build_input(asm: &mut Gen, master: u16, domain: &str, input: u16) {
|
||||||
asm.emit(c_int(16 + domain.len() as i32));
|
asm.emit(c_int(16 + domain.len() as i32));
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,8 @@
|
||||||
//! static String d(String cipher, int salt)
|
//! static String d(String cipher, int salt)
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
//! - `cipher` — base64 шифротекста (схема выбирается по salt: `scheme = all[salt % n]`);
|
//! - `cipher` - base64 шифротекста (схема выбирается по salt: `scheme = all[salt % n]`);
|
||||||
//! - `salt` — индекс строки, по нему детерминированно выводятся key и nonce;
|
//! - `salt` - индекс строки, по нему детерминированно выводятся key и nonce;
|
||||||
//! - мастер-секрет **не передаётся на вызове** и не лежит в одной константе:
|
//! - мастер-секрет **не передаётся на вызове** и не лежит в одной константе:
|
||||||
//! он собирается из двух частей (`k1 ^ k2`) прямо в декодере.
|
//! он собирается из двух частей (`k1 ^ k2`) прямо в декодере.
|
||||||
//!
|
//!
|
||||||
|
|
@ -63,7 +63,7 @@ pub fn decoder_class(
|
||||||
emit_dispatch(&mut asm, &parsed, key, nonce, data, len);
|
emit_dispatch(&mut asm, &parsed, key, nonce, data, len);
|
||||||
emit_epilogue(&mut asm, data);
|
emit_epilogue(&mut asm, data);
|
||||||
let code = Code {
|
let code = Code {
|
||||||
// Точный пик — 10 слотов (замер 08.10.2026); запас без вреда:
|
// Точный пик - 10 слотов (замер 08.10.2026); запас без вреда:
|
||||||
// лимит обязан лишь покрывать фактическую глубину.
|
// лимит обязан лишь покрывать фактическую глубину.
|
||||||
max_stack: 12,
|
max_stack: 12,
|
||||||
max_locals: asm.slots,
|
max_locals: asm.slots,
|
||||||
|
|
@ -88,7 +88,7 @@ pub fn decoder_owner(seed_name: &str) -> String {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Пролог: `master = k1 ^ k2`; возвращает слот пары. Ни одна часть не равна
|
/// Пролог: `master = k1 ^ k2`; возвращает слот пары. Ни одна часть не равна
|
||||||
/// мастер-секрету (вторая — `master ^ k1`, первая — потоковая).
|
/// мастер-секрету (вторая - `master ^ k1`, первая - потоковая).
|
||||||
fn emit_master(asm: &mut Gen, master: u64) -> u16 {
|
fn emit_master(asm: &mut Gen, master: u64) -> u16 {
|
||||||
use noxis_core::Rng;
|
use noxis_core::Rng;
|
||||||
|
|
||||||
|
|
@ -135,7 +135,7 @@ fn emit_len(asm: &mut Gen, data: u16) -> u16 {
|
||||||
slot
|
slot
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Диспетчер схем: `salt % n` лестницей `if_icmpne` (n == 1 — без диспетчера).
|
/// Диспетчер схем: `salt % n` лестницей `if_icmpne` (n == 1 - без диспетчера).
|
||||||
fn emit_dispatch(asm: &mut Gen, schemes: &[Scheme], key: u16, nonce: u16, data: u16, len: u16) {
|
fn emit_dispatch(asm: &mut Gen, schemes: &[Scheme], key: u16, nonce: u16, data: u16, len: u16) {
|
||||||
if schemes.len() == 1 {
|
if schemes.len() == 1 {
|
||||||
dispatch_branch(asm, schemes[0], key, nonce, data, len);
|
dispatch_branch(asm, schemes[0], key, nonce, data, len);
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ use noxis_classfile::{Arith, ArrayKind, BranchCond, Cond, Convert, Insn, Prim, S
|
||||||
|
|
||||||
use super::kdf::{Gen, c_int, c_long, load, store};
|
use super::kdf::{Gen, c_int, c_long, load, store};
|
||||||
|
|
||||||
/// Расшифровать `data` на месте; `len` — длина массива.
|
/// Расшифровать `data` на месте; `len` - длина массива.
|
||||||
pub(super) fn emit_xor(asm: &mut Gen, key: u16, nonce: u16, data: u16, len: u16) {
|
pub(super) fn emit_xor(asm: &mut Gen, key: u16, nonce: u16, data: u16, len: u16) {
|
||||||
let liga = Slots::alloc(asm);
|
let liga = Slots::alloc(asm);
|
||||||
emit_seed(asm, key, nonce, &liga);
|
emit_seed(asm, key, nonce, &liga);
|
||||||
|
|
@ -76,7 +76,7 @@ fn emit_states(asm: &mut Gen, slots: &Slots) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Один `splitmix64`: `sm += PHI`, финализатор — в `out`.
|
/// Один `splitmix64`: `sm += PHI`, финализатор - в `out`.
|
||||||
fn splitmix(asm: &mut Gen, sm: u16, out: u16) {
|
fn splitmix(asm: &mut Gen, sm: u16, out: u16) {
|
||||||
const PHI: i64 = 0x9E37_79B9_7F4A_7C15u64 as i64;
|
const PHI: i64 = 0x9E37_79B9_7F4A_7C15u64 as i64;
|
||||||
const C1: i64 = 0xBF58_476D_1CE4_E5B9u64 as i64;
|
const C1: i64 = 0xBF58_476D_1CE4_E5B9u64 as i64;
|
||||||
|
|
@ -105,7 +105,7 @@ fn finalize(asm: &mut Gen, amount: i32, constant: i64) {
|
||||||
asm.emit(long_op(Arith::Mul));
|
asm.emit(long_op(Arith::Mul));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Цикл по чанкам: свежий `next_u64` на 8 байт, внутренний — по байтам.
|
/// Цикл по чанкам: свежий `next_u64` на 8 байт, внутренний - по байтам.
|
||||||
///
|
///
|
||||||
/// Без `skip`-джойна: стрим всегда свеж на входе во внутренний цикл.
|
/// Без `skip`-джойна: стрим всегда свеж на входе во внутренний цикл.
|
||||||
fn emit_loop(asm: &mut Gen, data: u16, len: u16, slots: &Slots) {
|
fn emit_loop(asm: &mut Gen, data: u16, len: u16, slots: &Slots) {
|
||||||
|
|
@ -188,7 +188,7 @@ fn emit_next(asm: &mut Gen, slots: &Slots) {
|
||||||
asm.emit(c_int(17));
|
asm.emit(c_int(17));
|
||||||
asm.emit(shift(Arith::Shl));
|
asm.emit(shift(Arith::Shl));
|
||||||
asm.emit(store(slots.tmp_c, SlotKind::Long));
|
asm.emit(store(slots.tmp_c, SlotKind::Long));
|
||||||
// s2 ^= s0; s3 ^= s1; s1 ^= s2; s0 ^= s3 — строго по порядку.
|
// s2 ^= s0; s3 ^= s1; s1 ^= s2; s0 ^= s3 - строго по порядку.
|
||||||
xor_into(asm, s[2], s[0]);
|
xor_into(asm, s[2], s[0]);
|
||||||
xor_into(asm, s[3], s[1]);
|
xor_into(asm, s[3], s[1]);
|
||||||
xor_into(asm, s[1], s[2]);
|
xor_into(asm, s[1], s[2]);
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,16 @@
|
||||||
//! Ветвь `xtea-ctr`: XTEA в режиме счётчика, побайтовый XOR на месте.
|
//! Ветвь `xtea-ctr`: XTEA в режиме счётчика, побайтовый XOR на месте.
|
||||||
//!
|
//!
|
||||||
//! Зеркало `scheme::xtea_ctr` побитово: ключ — четыре `u32` из key/nonce,
|
//! Зеркало `scheme::xtea_ctr` побитово: ключ - четыре `u32` из key/nonce,
|
||||||
//! счётчик — номер чанка, все правые сдвиги — логические (`u32` в Rust).
|
//! счётчик - номер чанка, все правые сдвиги - логические (`u32` в Rust).
|
||||||
|
|
||||||
use noxis_classfile::{Arith, ArrayKind, BranchCond, Cond, Convert, Insn, Prim, SlotKind};
|
use noxis_classfile::{Arith, ArrayKind, BranchCond, Cond, Convert, Insn, Prim, SlotKind};
|
||||||
|
|
||||||
use super::kdf::{Gen, c_int, load, store};
|
use super::kdf::{Gen, c_int, load, store};
|
||||||
|
|
||||||
/// XTEA delta — знаковое представление для `ldc_w`.
|
/// XTEA delta - знаковое представление для `ldc_w`.
|
||||||
const DELTA: i32 = 0x9E37_79B9u32 as i32;
|
const DELTA: i32 = 0x9E37_79B9u32 as i32;
|
||||||
|
|
||||||
/// Расшифровать `data` на месте; `len` — длина массива.
|
/// Расшифровать `data` на месте; `len` - длина массива.
|
||||||
pub(super) fn emit_xtea(asm: &mut Gen, key: u16, nonce: u16, data: u16, len: u16) {
|
pub(super) fn emit_xtea(asm: &mut Gen, key: u16, nonce: u16, data: u16, len: u16) {
|
||||||
let slots = Slots::alloc(asm);
|
let slots = Slots::alloc(asm);
|
||||||
emit_key_array(asm, key, nonce, &slots);
|
emit_key_array(asm, key, nonce, &slots);
|
||||||
|
|
@ -168,7 +168,7 @@ fn mixing(asm: &mut Gen, dst: u16, src: u16, key_shift: i32, slots: &Slots) {
|
||||||
asm.emit(store(dst, SlotKind::Int));
|
asm.emit(store(dst, SlotKind::Int));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// XOR 8 байт чанка со стримом `(v0, v1)`; хвост короче 8 — частично.
|
/// XOR 8 байт чанка со стримом `(v0, v1)`; хвост короче 8 - частично.
|
||||||
fn emit_xor_bytes(asm: &mut Gen, data: u16, len: u16, slots: &Slots) {
|
fn emit_xor_bytes(asm: &mut Gen, data: u16, len: u16, slots: &Slots) {
|
||||||
asm.emit(c_int(0));
|
asm.emit(c_int(0));
|
||||||
asm.emit(store(slots.index, SlotKind::Int));
|
asm.emit(store(slots.index, SlotKind::Int));
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
//! Схемы шифрования строк: пер-строчный ключ, nonce и несколько алгоритмов.
|
//! Схемы шифрования строк: пер-строчный ключ, nonce и несколько алгоритмов.
|
||||||
//!
|
//!
|
||||||
//! Требования (см. `info/evo-weaknesses.md` и `docs/techniques.md`):
|
//! Требования (см. `info/evo-weaknesses.md` и `docs/techniques.md`):
|
||||||
//! - **разные схемы между строками** — один скрипт не покрывает весь jar;
|
//! - **разные схемы между строками** - один скрипт не покрывает весь jar;
|
||||||
//! - **ключ не рядом с шифротекстом** — на вызов передаётся только salt,
|
//! - **ключ не рядом с шифротекстом** - на вызов передаётся только salt,
|
||||||
//! мастер-секрет живёт в рантайм-декодере (сборка мастер-секрета — P2);
|
//! мастер-секрет живёт в рантайм-декодере (сборка мастер-секрета - P2);
|
||||||
//! - **nonce уникален на строку** — keystream не повторяется;
|
//! - **nonce уникален на строку** - keystream не повторяется;
|
||||||
//! - **MAC** — повреждение/подмена шифротекста ловится, а не молча портит текст.
|
//! - **MAC** - повреждение/подмена шифротекста ловится, а не молча портит текст.
|
||||||
|
|
||||||
use noxis_core::{Rng, derive_seed};
|
use noxis_core::{Rng, derive_seed};
|
||||||
|
|
||||||
|
|
@ -58,7 +58,7 @@ impl Scheme {
|
||||||
pub struct Blob {
|
pub struct Blob {
|
||||||
/// base64 шифротекста.
|
/// base64 шифротекста.
|
||||||
pub cipher: String,
|
pub cipher: String,
|
||||||
/// 32-битный MAC по ключу от plaintext — ловит подмену/порчу.
|
/// 32-битный MAC по ключу от plaintext - ловит подмену/порчу.
|
||||||
pub tag: u32,
|
pub tag: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -67,7 +67,7 @@ pub fn string_key(master: u64, salt: i32) -> u64 {
|
||||||
derive_seed(master, "string/key", u64::from(salt as u32))
|
derive_seed(master, "string/key", u64::from(salt as u32))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Nonce строки: детерминированный от salt, но другой домен — keystream не
|
/// Nonce строки: детерминированный от salt, но другой домен - keystream не
|
||||||
/// пересекается даже при совпадении ключей.
|
/// пересекается даже при совпадении ключей.
|
||||||
pub fn string_nonce(master: u64, salt: i32) -> u64 {
|
pub fn string_nonce(master: u64, salt: i32) -> u64 {
|
||||||
derive_seed(master, "string/nonce", u64::from(salt as u32))
|
derive_seed(master, "string/nonce", u64::from(salt as u32))
|
||||||
|
|
@ -123,7 +123,7 @@ fn xor_splitmix(key: u64, nonce: u64, data: &mut [u8]) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// XTEA в режиме счётчика: keystream = XTEA(k, counter), XOR — на месте.
|
/// XTEA в режиме счётчика: keystream = XTEA(k, counter), XOR - на месте.
|
||||||
fn xtea_ctr(key: u64, nonce: u64, data: &mut [u8]) {
|
fn xtea_ctr(key: u64, nonce: u64, data: &mut [u8]) {
|
||||||
let k = [
|
let k = [
|
||||||
key as u32,
|
key as u32,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
//! Сквозная проверка декодера: шифруем в Rust, расшифровываем настоящим `java`.
|
//! Сквозная проверка декодера: шифруем в Rust, расшифровываем настоящим `java`.
|
||||||
//!
|
//!
|
||||||
//! Собираются два класса (декодер + вызывающий `main`), пишутся на диск,
|
//! Собираются два класса (декодер + вызывающий `main`), пишутся на диск,
|
||||||
//! `java` исполняет — stdout обязан совпасть с исходными строками. Без `java`
|
//! `java` исполняет - stdout обязан совпасть с исходными строками. Без `java`
|
||||||
//! тест пропускается (graceful skip, как в `noxis-classfile/tests/frames.rs`).
|
//! тест пропускается (graceful skip, как в `noxis-classfile/tests/frames.rs`).
|
||||||
|
|
||||||
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
//! Project structure rules, invoked from every crate's `build.rs`.
|
//! Project structure rules, invoked from every crate's `build.rs`.
|
||||||
//!
|
//!
|
||||||
//! Rules apply to Rust code only, not to documentation:
|
//! Rules apply to Rust code only, not to documentation:
|
||||||
//! * every `.rs` file holds at most [`MAX_FILE_LINES`] *code* lines — blank
|
//! * every `.rs` file holds at most [`MAX_FILE_LINES`] *code* lines - blank
|
||||||
//! lines and full-line comments (`//`, `///`, `//!`) are not counted;
|
//! lines and full-line comments (`//`, `///`, `//!`) are not counted;
|
||||||
//! * every directory inside a crate's `src/`, `tests/` or `benches/` holds
|
//! * every directory inside a crate's `src/`, `tests/` or `benches/` holds
|
||||||
//! at most [`MAX_DIR_FILES`] files — subdirectories are unlimited.
|
//! at most [`MAX_DIR_FILES`] files - subdirectories are unlimited.
|
||||||
//!
|
//!
|
||||||
//! Deliberately excluded: `Cargo.lock`, `.gitignore`, `docs/`, the legacy
|
//! Deliberately excluded: `Cargo.lock`, `.gitignore`, `docs/`, the legacy
|
||||||
//! `info/` tree, and crate roots (they always own `Cargo.toml` plus
|
//! `info/` tree, and crate roots (they always own `Cargo.toml` plus
|
||||||
|
|
@ -34,13 +34,13 @@ const SKIPPED_DIRS: [&str; 2] = ["target", ".git"];
|
||||||
///
|
///
|
||||||
/// # Panics
|
/// # Panics
|
||||||
///
|
///
|
||||||
/// Panics whenever a rule is violated — that is the point of the gate.
|
/// Panics whenever a rule is violated - that is the point of the gate.
|
||||||
pub fn check() {
|
pub fn check() {
|
||||||
let manifest = manifest_dir();
|
let manifest = manifest_dir();
|
||||||
let mut violations = Vec::new();
|
let mut violations = Vec::new();
|
||||||
|
|
||||||
// `src/` is required; `tests/` and `benches/` are checked only when they
|
// `src/` is required; `tests/` and `benches/` are checked only when they
|
||||||
// exist — otherwise the line rule is bypassed by moving code into tests.
|
// exist - otherwise the line rule is bypassed by moving code into tests.
|
||||||
// `docs/` stays out of the gate: the rules are about Rust code only.
|
// `docs/` stays out of the gate: the rules are about Rust code only.
|
||||||
let roots = [
|
let roots = [
|
||||||
manifest.join("src"),
|
manifest.join("src"),
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
//!
|
//!
|
||||||
//! Каждый класс грузится отдельной JVM в дереве пакетов: изоляция важнее
|
//! Каждый класс грузится отдельной JVM в дереве пакетов: изоляция важнее
|
||||||
//! скорости (один битый класс не тянет за собой остальные). Классы, которые
|
//! скорости (один битый класс не тянет за собой остальные). Классы, которые
|
||||||
//! не грузятся из-за отсутствующих зависимостей, — `Skipped`, а не провал:
|
//! не грузятся из-за отсутствующих зависимостей, - `Skipped`, а не провал:
|
||||||
//! судить о них верификатор не может.
|
//! судить о них верификатор не может.
|
||||||
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
@ -13,7 +13,7 @@ use std::process::Command;
|
||||||
pub enum JavaVerdict {
|
pub enum JavaVerdict {
|
||||||
/// Загрузился без ошибок верификации (`main` при этом может отсутствовать).
|
/// Загрузился без ошибок верификации (`main` при этом может отсутствовать).
|
||||||
Verified,
|
Verified,
|
||||||
/// Верификатор отверг класс (текст ошибки — для отчёта).
|
/// Верификатор отверг класс (текст ошибки - для отчёта).
|
||||||
Failed(String),
|
Failed(String),
|
||||||
/// Судить нельзя: нет `java` или не хватает зависимостей.
|
/// Судить нельзя: нет `java` или не хватает зависимостей.
|
||||||
Skipped(String),
|
Skipped(String),
|
||||||
|
|
@ -51,7 +51,7 @@ pub fn verify_class_bytes(internal_name: &str, bytes: &[u8]) -> JavaVerdict {
|
||||||
verdict
|
verdict
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Проверить все классы jar'а (фильтр — префиксы внутренних имён).
|
/// Проверить все классы jar'а (фильтр - префиксы внутренних имён).
|
||||||
pub fn verify_jar(jar: &Path, scope: &[String]) -> Vec<(String, JavaVerdict)> {
|
pub fn verify_jar(jar: &Path, scope: &[String]) -> Vec<(String, JavaVerdict)> {
|
||||||
let entries = match noxis_jar::read_file(jar) {
|
let entries = match noxis_jar::read_file(jar) {
|
||||||
Ok(entries) => entries,
|
Ok(entries) => entries,
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ pub const RED_PATTERNS: &[&str] = &[
|
||||||
|
|
||||||
/// Все идентификаторы программы: классы, члены, строки кода.
|
/// Все идентификаторы программы: классы, члены, строки кода.
|
||||||
///
|
///
|
||||||
/// Члены — по простому имени (без владельца): сверка строгая и дешёвая,
|
/// Члены - по простому имени (без владельца): сверка строгая и дешёвая,
|
||||||
/// для точечной проверки есть `find_leaks` с keep-фильтром.
|
/// для точечной проверки есть `find_leaks` с keep-фильтром.
|
||||||
pub fn collect_names(program: &Program) -> BTreeSet<String> {
|
pub fn collect_names(program: &Program) -> BTreeSet<String> {
|
||||||
let mut names = BTreeSet::new();
|
let mut names = BTreeSet::new();
|
||||||
|
|
@ -42,7 +42,7 @@ pub fn collect_names(program: &Program) -> BTreeSet<String> {
|
||||||
|
|
||||||
/// Имена из эталона, пережившие обфускацию (минус keep).
|
/// Имена из эталона, пережившие обфускацию (минус keep).
|
||||||
///
|
///
|
||||||
/// Пустой результат — норма; непустой — тест падает с этим списком.
|
/// Пустой результат - норма; непустой - тест падает с этим списком.
|
||||||
pub fn find_leaks(before: &BTreeSet<String>, after: &Program, keep: &KeepRules) -> Vec<String> {
|
pub fn find_leaks(before: &BTreeSet<String>, after: &Program, keep: &KeepRules) -> Vec<String> {
|
||||||
let names = collect_names(after);
|
let names = collect_names(after);
|
||||||
before
|
before
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,9 @@
|
||||||
//!
|
//!
|
||||||
//! Проверка результата обфускации, а не процесса:
|
//! Проверка результата обфускации, а не процесса:
|
||||||
//!
|
//!
|
||||||
//! * [`leaks`] — утечки исходных имён + секреты открытым текстом;
|
//! * [`leaks`] - утечки исходных имён + секреты открытым текстом;
|
||||||
//! * [`java`] — настоящий `java -Xverify:all` по байтам на диске;
|
//! * [`java`] - настоящий `java -Xverify:all` по байтам на диске;
|
||||||
//! * [`metrics`] — счётчики программы, раздувание, стабильность маппингов.
|
//! * [`metrics`] - счётчики программы, раздувание, стабильность маппингов.
|
||||||
|
|
||||||
pub mod java;
|
pub mod java;
|
||||||
pub mod leaks;
|
pub mod leaks;
|
||||||
|
|
|
||||||
|
|
@ -1,123 +0,0 @@
|
||||||
//! Одноразовый отладочный обход байткода (scratch): ручной парсинг пула и
|
|
||||||
//! Code-атрибутов, вывод заголовков вложенных атрибутов.
|
|
||||||
#![allow(clippy::unwrap_used, clippy::expect_used)]
|
|
||||||
|
|
||||||
fn rd_u16(b: &[u8], p: &mut usize) -> u16 {
|
|
||||||
let v = u16::from_be_bytes([b[*p], b[*p + 1]]);
|
|
||||||
*p += 2;
|
|
||||||
v
|
|
||||||
}
|
|
||||||
fn rd_u32(b: &[u8], p: &mut usize) -> u32 {
|
|
||||||
let v = u32::from_be_bytes([b[*p], b[*p + 1], b[*p + 2], b[*p + 3]]);
|
|
||||||
*p += 4;
|
|
||||||
v
|
|
||||||
}
|
|
||||||
fn hex(b: &[u8]) -> String {
|
|
||||||
b.iter()
|
|
||||||
.map(|x| format!("{x:02x}"))
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join(" ")
|
|
||||||
}
|
|
||||||
fn main() {
|
|
||||||
let entries = noxis_jar::read_file(std::path::Path::new(
|
|
||||||
"/storage/project/jvm/LoReport/build/libs/lorep-1.1.0.jar",
|
|
||||||
))
|
|
||||||
.unwrap();
|
|
||||||
let program = noxis_jar::load(entries).unwrap();
|
|
||||||
let class = program
|
|
||||||
.classes
|
|
||||||
.iter()
|
|
||||||
.find(|c| c.this_class.ends_with("ReportStatsCommand"))
|
|
||||||
.unwrap();
|
|
||||||
let prog = noxis_classfile::Program {
|
|
||||||
classes: vec![class.clone()],
|
|
||||||
resources: vec![],
|
|
||||||
};
|
|
||||||
let out = noxis_jar::save(&prog).unwrap();
|
|
||||||
let entry = out.iter().find(|e| e.name.ends_with(".class")).unwrap();
|
|
||||||
let bytes = &entry.data;
|
|
||||||
// find method_execute Code attr -> sub attrs, dump each sub-attr name_idx + len + first bytes
|
|
||||||
// manual walk: pool
|
|
||||||
let mut pos = 8;
|
|
||||||
let pool_count = rd_u16(bytes, &mut pos);
|
|
||||||
// collect utf8 entries for name resolution
|
|
||||||
let mut utf8s: std::collections::BTreeMap<u16, String> = Default::default();
|
|
||||||
let mut idx = 1u16;
|
|
||||||
while idx < pool_count {
|
|
||||||
let tag = bytes[pos];
|
|
||||||
pos += 1;
|
|
||||||
match tag {
|
|
||||||
1 => {
|
|
||||||
let len = rd_u16(bytes, &mut pos) as usize;
|
|
||||||
utf8s.insert(
|
|
||||||
idx,
|
|
||||||
String::from_utf8_lossy(&bytes[pos..pos + len]).into_owned(),
|
|
||||||
);
|
|
||||||
pos += len;
|
|
||||||
}
|
|
||||||
3 | 4 => pos += 4,
|
|
||||||
5 | 6 => {
|
|
||||||
pos += 8;
|
|
||||||
idx += 1;
|
|
||||||
}
|
|
||||||
7 | 8 | 16 | 19 | 20 => pos += 2,
|
|
||||||
9 | 10 | 11 | 12 | 17 | 18 => pos += 4,
|
|
||||||
15 => pos += 3,
|
|
||||||
_ => {
|
|
||||||
eprintln!("pool tag {tag}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
idx += 1;
|
|
||||||
}
|
|
||||||
let name_of = |i: u16| utf8s.get(&i).cloned().unwrap_or(format!("#{i}"));
|
|
||||||
pos += 6;
|
|
||||||
let ifaces = rd_u16(bytes, &mut pos);
|
|
||||||
pos += 2 * ifaces as usize;
|
|
||||||
let fields = rd_u16(bytes, &mut pos);
|
|
||||||
for _ in 0..fields {
|
|
||||||
pos += 6;
|
|
||||||
let n = rd_u16(bytes, &mut pos);
|
|
||||||
for _ in 0..n {
|
|
||||||
pos += 2;
|
|
||||||
let l = rd_u32(bytes, &mut pos);
|
|
||||||
pos += l as usize;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let methods = rd_u16(bytes, &mut pos);
|
|
||||||
for _ in 0..methods {
|
|
||||||
pos += 2;
|
|
||||||
let ni = rd_u16(bytes, &mut pos);
|
|
||||||
pos += 2;
|
|
||||||
let an = rd_u16(bytes, &mut pos);
|
|
||||||
let mname = name_of(ni);
|
|
||||||
for _ in 0..an {
|
|
||||||
let aname = rd_u16(bytes, &mut pos);
|
|
||||||
let alen = rd_u32(bytes, &mut pos) as usize;
|
|
||||||
let aname_s = name_of(aname);
|
|
||||||
if aname_s == "Code" {
|
|
||||||
let start = pos;
|
|
||||||
pos += 4;
|
|
||||||
let clen = rd_u32(bytes, &mut pos) as usize;
|
|
||||||
pos += clen;
|
|
||||||
let exc = rd_u16(bytes, &mut pos) as usize;
|
|
||||||
pos += 8 * exc;
|
|
||||||
let subn = rd_u16(bytes, &mut pos);
|
|
||||||
for _ in 0..subn {
|
|
||||||
let sname = rd_u16(bytes, &mut pos);
|
|
||||||
let slen = rd_u32(bytes, &mut pos) as usize;
|
|
||||||
eprintln!(
|
|
||||||
"{mname}: sub={} len={slen} head=[{}]",
|
|
||||||
name_of(sname),
|
|
||||||
hex(&bytes[pos..pos + slen.min(24)])
|
|
||||||
);
|
|
||||||
pos += slen;
|
|
||||||
}
|
|
||||||
assert_eq!(pos, start + alen, "Code length mismatch in {mname}");
|
|
||||||
} else {
|
|
||||||
pos += alen;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
eprintln!("walk ok");
|
|
||||||
}
|
|
||||||
|
|
@ -11,7 +11,7 @@ use noxis_core::Config;
|
||||||
pub struct Args {
|
pub struct Args {
|
||||||
/// Входной jar.
|
/// Входной jar.
|
||||||
pub input: PathBuf,
|
pub input: PathBuf,
|
||||||
/// TOML-конфиг с keep-паттернами (без него — пустой keep).
|
/// TOML-конфиг с keep-паттернами (без него - пустой keep).
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
pub config: Option<PathBuf>,
|
pub config: Option<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,10 +13,10 @@ pub struct Args {
|
||||||
/// Output jar.
|
/// Output jar.
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
pub output: PathBuf,
|
pub output: PathBuf,
|
||||||
/// TOML config (without it — the Standard profile).
|
/// TOML config (without it - the Standard profile).
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
pub config: Option<PathBuf>,
|
pub config: Option<PathBuf>,
|
||||||
/// Master seed (deterministic by default — taken from the config).
|
/// Master seed (deterministic by default - taken from the config).
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pub seed: Option<u64>,
|
pub seed: Option<u64>,
|
||||||
/// Where to write the JSON report (next to the output by default).
|
/// Where to write the JSON report (next to the output by default).
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
//! Библиотека CLI: команды как вызываемые функции (тонкий `main` — в `main.rs`).
|
//! Библиотека CLI: команды как вызываемые функции (тонкий `main` - в `main.rs`).
|
||||||
|
|
||||||
pub mod cmd;
|
pub mod cmd;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# Документация noxis
|
# Документация noxis
|
||||||
|
|
||||||
Обфускатор JVM/Java-байткода на Rust. Этот файл — индекс.
|
Обфускатор JVM/Java-байткода на Rust. Этот файл - индекс.
|
||||||
|
|
||||||
## Что где
|
## Что где
|
||||||
|
|
||||||
|
|
@ -10,34 +10,34 @@
|
||||||
| [`techniques.md`](techniques.md) | как работает каждая техника + плюсы/минусы/цена |
|
| [`techniques.md`](techniques.md) | как работает каждая техника + плюсы/минусы/цена |
|
||||||
| [`stack.md`](stack.md) | почему свой IR, какой backend, `StackMapTable`, PRNG, генерация рантайма |
|
| [`stack.md`](stack.md) | почему свой IR, какой backend, `StackMapTable`, PRNG, генерация рантайма |
|
||||||
| [`learnings/lessons.md`](learnings/lessons.md) | уроки из разбора EvoVis: что защита сделала плохо и чего мы не делаем |
|
| [`learnings/lessons.md`](learnings/lessons.md) | уроки из разбора EvoVis: что защита сделала плохо и чего мы не делаем |
|
||||||
| [`learnings/reference.md`](learnings/reference.md) | разбор референсного обфускатора (Java/ASM) — что берём, что нет |
|
| [`learnings/reference.md`](learnings/reference.md) | разбор референсного обфускатора (Java/ASM) - что берём, что нет |
|
||||||
| [`learnings/priorities.md`](learnings/priorities.md) | что улучшим: стратегия и приоритеты |
|
| [`learnings/priorities.md`](learnings/priorities.md) | что улучшим: стратегия и приоритеты |
|
||||||
| [`learnings/ai-resistance.md`](learnings/ai-resistance.md) | новые техники обфускации и устойчивость к LLM-анализу |
|
| [`learnings/ai-resistance.md`](learnings/ai-resistance.md) | новые техники обфускации и устойчивость к LLM-анализу |
|
||||||
|
|
||||||
План выполнения — [`../todo.md`](../todo.md). Исходный материал разбора —
|
План выполнения - [`../todo.md`](../todo.md). Исходный материал разбора -
|
||||||
[`../info/`](../info/README.md).
|
[`../info/`](../info/README.md).
|
||||||
|
|
||||||
## Структурные правила проекта
|
## Структурные правила проекта
|
||||||
|
|
||||||
Правила — **только про Rust-код**: `crates/*/src`, `crates/*/tests`,
|
Правила - **только про Rust-код**: `crates/*/src`, `crates/*/tests`,
|
||||||
`crates/*/benches`. Документация (`docs/**`, корневые `.md`) и `info/` вне
|
`crates/*/benches`. Документация (`docs/**`, корневые `.md`) и `info/` вне
|
||||||
гейта: писать подробно там, где это документация, а не ограничивать объём
|
гейта: писать подробно там, где это документация, а не ограничивать объём
|
||||||
искусственно. Не учитываются также `Cargo.lock`, `.gitignore`, корень крейта
|
искусственно. Не учитываются также `Cargo.lock`, `.gitignore`, корень крейта
|
||||||
(в нём всегда лежат `Cargo.toml` + `build.rs`).
|
(в нём всегда лежат `Cargo.toml` + `build.rs`).
|
||||||
|
|
||||||
1. **Не больше 250 строк кода** в одном `.rs` файле. Пустые строки и
|
1. **Не больше 250 строк кода** в одном `.rs` файле. Пустые строки и
|
||||||
полно-строчные комментарии (`//`, `///`, `//!`) не считаются —
|
полно-строчные комментарии (`//`, `///`, `//!`) не считаются -
|
||||||
измеряется объём кода, а не документации.
|
измеряется объём кода, а не документации.
|
||||||
2. **Не больше 4 `.rs`-файлов** в одной директории. Подпапки не ограничены:
|
2. **Не больше 4 `.rs`-файлов** в одной директории. Подпапки не ограничены:
|
||||||
`src/` может содержать `lib.rs` и три модуля-директории, каждая из которых
|
`src/` может содержать `lib.rs` и три модуля-директории, каждая из которых
|
||||||
в свою очередь содержит не больше четырёх файлов.
|
в свою очередь содержит не больше четырёх файлов.
|
||||||
|
|
||||||
`tests/` и `benches/` не исключены намеренно — иначе правило
|
`tests/` и `benches/` не исключены намеренно - иначе правило
|
||||||
«250 строк» обходится выносом кода в тесты.
|
«250 строк» обходится выносом кода в тесты.
|
||||||
|
|
||||||
Правило 2 — это не косметика: файл из тысячи строк означает, что в нём
|
Правило 2 - это не косметика: файл из тысячи строк означает, что в нём
|
||||||
смешаны разные причины к изменению (нарушение KISS/DRY/SOLID), а команда из
|
смешаны разные причины к изменению (нарушение KISS/DRY/SOLID), а команда из
|
||||||
восьми файлов в одной папке — что модуль перестал быть модулем.
|
восьми файлов в одной папке - что модуль перестал быть модулем.
|
||||||
|
|
||||||
### Как проверяется
|
### Как проверяется
|
||||||
|
|
||||||
|
|
@ -51,7 +51,7 @@ fn main() {
|
||||||
|
|
||||||
`noxis-structure-check` обходит деревья `src/`, `tests/` и `benches/` **этого
|
`noxis-structure-check` обходит деревья `src/`, `tests/` и `benches/` **этого
|
||||||
крейта** и **падает со стопкой ошибок**, если лимит нарушен. Поэтому
|
крейта** и **падает со стопкой ошибок**, если лимит нарушен. Поэтому
|
||||||
`cargo build` не соберёт нарушение — специально запускать проверку не нужно.
|
`cargo build` не соберёт нарушение - специально запускать проверку не нужно.
|
||||||
Деревья регистрируются через `cargo:rerun-if-changed`, чтобы гейт не
|
Деревья регистрируются через `cargo:rerun-if-changed`, чтобы гейт не
|
||||||
срабатывал на правки вне проверяемых путей.
|
срабатывал на правки вне проверяемых путей.
|
||||||
|
|
||||||
|
|
@ -64,9 +64,9 @@ fn main() {
|
||||||
### Что делать при нарушении
|
### Что делать при нарушении
|
||||||
|
|
||||||
- файл разрослся → разрезать по одной ответственности на файл:
|
- файл разрослся → разрезать по одной ответственности на файл:
|
||||||
`mod.rs` = публичный контракт модуля, соседние файлы — внутренности;
|
`mod.rs` = публичный контракт модуля, соседние файлы - внутренности;
|
||||||
- папка переполнилась → завести подпапку и вынести подфункцию целиком,
|
- папка переполнилась → завести подпапку и вынести подфункцию целиком,
|
||||||
а не «чтобы влезло».
|
а не «чтобы влезло».
|
||||||
|
|
||||||
Каждый модуль получает `mod.rs` с `//!`-документацией — это точка входа,
|
Каждый модуль получает `mod.rs` с `//!`-документацией - это точка входа,
|
||||||
читающаяся вместо пяти файлов.
|
читающаяся вместо пяти файлов.
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@
|
||||||
|
|
||||||
## Собственный IR
|
## Собственный IR
|
||||||
|
|
||||||
Пассы не работают с API стороннего парсера — они работают с моделью:
|
Пассы не работают с API стороннего парсера - они работают с моделью:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
Program все классы + ресурсы архива
|
Program все классы + ресурсы архива
|
||||||
|
|
@ -63,15 +63,15 @@ Program все классы + ресурсы архива
|
||||||
└─ Resource путь + байты внутри архива
|
└─ Resource путь + байты внутри архива
|
||||||
```
|
```
|
||||||
|
|
||||||
Ключевое решение — **`Insn::Mark(Label)` вместо байтовых смещений**:
|
Ключевое решение - **`Insn::Mark(Label)` вместо байтовых смещений**:
|
||||||
|
|
||||||
- любой пасс, меняющий порядок или длину инструкций, не пересчитывает
|
- любой пасс, меняющий порядок или длину инструкций, не пересчитывает
|
||||||
jump-офсеты;
|
jump-офсеты;
|
||||||
- пересчёт делается ровно один раз — в writer'е;
|
- пересчёт делается ровно один раз - в writer'е;
|
||||||
- в IR нет состояния, в котором «смещение уехало, а метка осталась».
|
- в IR нет состояния, в котором «смещение уехало, а метка осталась».
|
||||||
|
|
||||||
Атрибуты, которые мы не понимаем, хранятся как `Attribute::Raw { name, data }`
|
Атрибуты, которые мы не понимаем, хранятся как `Attribute::Raw { name, data }`
|
||||||
и возвращаются на место без изменений — round-trip без потерь.
|
и возвращаются на место без изменений - round-trip без потерь.
|
||||||
|
|
||||||
## Контракт пасса
|
## Контракт пасса
|
||||||
|
|
||||||
|
|
@ -83,18 +83,18 @@ pub trait Pass: Send + Sync {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- `Context` — всё состояние прогона: конфиг, мастер-сид, маппинги, заметки.
|
- `Context` - всё состояние прогона: конфиг, мастер-сид, маппинги, заметки.
|
||||||
Из него берётся RNG: `ctx.rng("rename/classes")` — доменное разделение,
|
Из него берётся RNG: `ctx.rng("rename/classes")` - доменное разделение,
|
||||||
чтобы случайности разных пассов не были статистически коррелированы.
|
чтобы случайности разных пассов не были статистически коррелированы.
|
||||||
- `PassReport` — что трансформировано, что сознательно пропущено и почему.
|
- `PassReport` - что трансформировано, что сознательно пропущено и почему.
|
||||||
- `Report` — собирает отчёты всех пассов + счётчики, сериализуется в JSON.
|
- `Report` - собирает отчёты всех пассов + счётчики, сериализуется в JSON.
|
||||||
|
|
||||||
Детерминированность — свойство, а не пожелание: одинаковый seed на одинаковой
|
Детерминированность - свойство, а не пожелание: одинаковый seed на одинаковой
|
||||||
версии даёт байт-в-байт одинаковый выход. Это проверяется тестами.
|
версии даёт байт-в-байт одинаковый выход. Это проверяется тестами.
|
||||||
|
|
||||||
## Конвейер
|
## Конвейер
|
||||||
|
|
||||||
Порядок пассов — не деталь:
|
Порядок пассов - не деталь:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
Rename → Resources → Strings → Constants → Flow → Noise → DebugInfo
|
Rename → Resources → Strings → Constants → Flow → Noise → DebugInfo
|
||||||
|
|
@ -102,18 +102,18 @@ Rename → Resources → Strings → Constants → Flow → Noise → DebugInfo
|
||||||
|
|
||||||
почему именно так:
|
почему именно так:
|
||||||
|
|
||||||
1. **Rename первым** — все остальные техники порождают новые ссылки
|
1. **Rename первым** - все остальные техники порождают новые ссылки
|
||||||
(`LDC`-шум, вызовы декодера, junk-классы), и они уже должны быть в
|
(`LDC`-шум, вызовы декодера, junk-классы), и они уже должны быть в
|
||||||
обфусцированных именах.
|
обфусцированных именах.
|
||||||
2. **Resources сразу после Rename** — патч `fabric.mod.json`, `*.mixins.json`,
|
2. **Resources сразу после Rename** - патч `fabric.mod.json`, `*.mixins.json`,
|
||||||
refmap и access widener должен видеть *итоговые* имена классов.
|
refmap и access widener должен видеть *итоговые* имена классов.
|
||||||
3. **Strings и Constants** — работают по телам методов, ещё не искажённым
|
3. **Strings и Constants** - работают по телам методов, ещё не искажённым
|
||||||
control-flow'ем.
|
control-flow'ем.
|
||||||
4. **Flow** — искажает CFG. Если поставить его раньше, Strings/Constants
|
4. **Flow** - искажает CFG. Если поставить его раньше, Strings/Constants
|
||||||
перестанут находить свои шаблоны в инструкциях.
|
перестанут находить свои шаблоны в инструкциях.
|
||||||
5. **Noise** — junk-классы и аннотации добавляются в самом конце, когда
|
5. **Noise** - junk-классы и аннотации добавляются в самом конце, когда
|
||||||
целевой граф уже финален.
|
целевой граф уже финален.
|
||||||
6. **DebugInfo последним** — strip/spoof отладочной информации после всех
|
6. **DebugInfo последним** - strip/spoof отладочной информации после всех
|
||||||
трансформаций, чтобы не породить свежие `LineNumberTable`.
|
трансформаций, чтобы не породить свежие `LineNumberTable`.
|
||||||
|
|
||||||
## Статус
|
## Статус
|
||||||
|
|
@ -126,4 +126,4 @@ Rename → Resources → Strings → Constants → Flow → Noise → DebugInfo
|
||||||
| **готово** | пасс `flow` (4 схемы + ротация + мешанина, приёмка настоящим `java`) |
|
| **готово** | пасс `flow` (4 схемы + ротация + мешанина, приёмка настоящим `java`) |
|
||||||
| **план** | фикстуры и regression-тесты |
|
| **план** | фикстуры и regression-тесты |
|
||||||
|
|
||||||
Детали выбора — [`stack.md`](stack.md), план — [`../todo.md`](../todo.md).
|
Детали выбора - [`stack.md`](stack.md), план - [`../todo.md`](../todo.md).
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
|
|
||||||
## 1. Elastic Security Labs: агент с IDA Pro против Tigress (апрель 2026)
|
## 1. Elastic Security Labs: агент с IDA Pro против Tigress (апрель 2026)
|
||||||
|
|
||||||
[«The Cost of Understanding»](https://www.elastic.co/security-labs/llm-reversing-vs-llm-obfuscation) —
|
[«The Cost of Understanding»](https://www.elastic.co/security-labs/llm-reversing-vs-llm-obfuscation) -
|
||||||
Claude Opus 4.6 с доступом к IDA Pro (через MCP) реверсил crackme,
|
Claude Opus 4.6 с доступом к IDA Pro (через MCP) реверсил crackme,
|
||||||
обфусцированный [Tigress](https://tigress.wtf/), затем команда сама собрала
|
обфусцированный [Tigress](https://tigress.wtf/), затем команда сама собрала
|
||||||
("vibecode"-ила) три защиты, нацеленные именно на модель. Это ближе всего
|
("vibecode"-ила) три защиты, нацеленные именно на модель. Это ближе всего
|
||||||
|
|
@ -26,32 +26,32 @@ Claude Opus 4.6 с доступом к IDA Pro (через MCP) реверсил
|
||||||
indirect/regs и switch/stack).
|
indirect/regs и switch/stack).
|
||||||
|
|
||||||
- Opus решил ~40% из 20 валидных задач. Успех стоил в среднем **$2.39**,
|
- Opus решил ~40% из 20 валидных задач. Успех стоил в среднем **$2.39**,
|
||||||
провал — **$4.83**. Ни одна задача уровня "многослойные комбинации" не
|
провал - **$4.83**. Ни одна задача уровня "многослойные комбинации" не
|
||||||
была решена.
|
была решена.
|
||||||
- **`JIT` (XTEA + рантайм-расшифровка) оказался самой сложной одиночной
|
- **`JIT` (XTEA + рантайм-расшифровка) оказался самой сложной одиночной
|
||||||
трансформацией** — модель понимала структуру движка, но не смогла
|
трансформацией** - модель понимала структуру движка, но не смогла
|
||||||
восстановить ключевые константы. Это ровно наш подход в `Strings`
|
восстановить ключевые константы. Это ровно наш подход в `Strings`
|
||||||
(ключ выводится в рантайме, не лежит в вызове), только применённый ко
|
(ключ выводится в рантайме, не лежит в вызове), только применённый ко
|
||||||
**всему коду метода**, а не к строкам.
|
**всему коду метода**, а не к строкам.
|
||||||
- На самых жёстких комбинациях цена и время выросли в ~4.5× и ~4.3×, но
|
- На самых жёстких комбинациях цена и время выросли в ~4.5× и ~4.3×, но
|
||||||
задачи, которые Opus уже умел решать, он решил всё равно. **Flatten + MBA
|
задачи, которые Opus уже умел решать, он решил всё равно. **Flatten + MBA
|
||||||
било по модели сильнее, чем Virtualize + MBA** — авторы объясняют это тем,
|
било по модели сильнее, чем Virtualize + MBA** - авторы объясняют это тем,
|
||||||
что флаттенинг сильнее раздувает размер функции.
|
что флаттенинг сильнее раздувает размер функции.
|
||||||
|
|
||||||
### Три кастомные защиты, нацеленные на агента
|
### Три кастомные защиты, нацеленные на агента
|
||||||
|
|
||||||
| защита | идея | результат |
|
| защита | идея | результат |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Matryoshka Wall | crackme внутри 100 000 вложенных слоёв ChaCha20 (+ подделка под Salsa20) | v1 — агент сдался не расшифровав ($1.50); v2 (каждый ключ зависит от предыдущего слоя) — исчерпал бюджет $10 за 56 минут, 0 успехов |
|
| Matryoshka Wall | crackme внутри 100 000 вложенных слоёв ChaCha20 (+ подделка под Salsa20) | v1 - агент сдался не расшифровав ($1.50); v2 (каждый ключ зависит от предыдущего слоя) - исчерпал бюджет $10 за 56 минут, 0 успехов |
|
||||||
| Double Fond | патч open-source крипто-библиотеки: скрытый загрузчик через изменённую lookup-таблицу, шифрованные указатели, RWX-сегмент | v7 — 1/5 у "осведомлённого" агента (12 мин, $5.20), 0/5 у "слепого" |
|
| Double Fond | патч open-source крипто-библиотеки: скрытый загрузчик через изменённую lookup-таблицу, шифрованные указатели, RWX-сегмент | v7 - 1/5 у "осведомлённого" агента (12 мин, $5.20), 0/5 у "слепого" |
|
||||||
| Dispatch Maze | шифр раздроблен на 20 фрагментов по 3000 функций, большинство — ловушки | v1 решена за 12 минут ($2.56); v2 (разнообразные шаблоны кода + ложные переходы) — провал агента через 46 минут и $8.83 |
|
| Dispatch Maze | шифр раздроблен на 20 фрагментов по 3000 функций, большинство - ловушки | v1 решена за 12 минут ($2.56); v2 (разнообразные шаблоны кода + ложные переходы) - провал агента через 46 минут и $8.83 |
|
||||||
|
|
||||||
### Что из этого реально работает, по механизму
|
### Что из этого реально работает, по механизму
|
||||||
|
|
||||||
1. **Контекстное окно.** Длинные тупиковые пути (decoy-функции, ложные
|
1. **Контекстное окно.** Длинные тупиковые пути (decoy-функции, ложные
|
||||||
переходы) забивают контекст и портят рассуждение модели.
|
переходы) забивают контекст и портят рассуждение модели.
|
||||||
2. **Бюджет шагов/денег.** Каждый впустую потраченный цикл стоит агенту
|
2. **Бюджет шагов/денег.** Каждый впустую потраченный цикл стоит агенту
|
||||||
реальных токенов; если он упирается в лимит и сдаётся — это победа
|
реальных токенов; если он упирается в лимит и сдаётся - это победа
|
||||||
защиты, даже если код теоретически вскрываем.
|
защиты, даже если код теоретически вскрываем.
|
||||||
3. **Shortcut bias.** Вводящая в заблуждение структура (ловушки в
|
3. **Shortcut bias.** Вводящая в заблуждение структура (ловушки в
|
||||||
Dispatch Maze) заставляет модель остановиться раньше времени.
|
Dispatch Maze) заставляет модель остановиться раньше времени.
|
||||||
|
|
@ -59,14 +59,14 @@ indirect/regs и switch/stack).
|
||||||
|
|
||||||
**Ключевое экономическое наблюдение авторов:** нативное исполнение занимает
|
**Ключевое экономическое наблюдение авторов:** нативное исполнение занимает
|
||||||
~55 мс, а статический провал агента стоит **$1.50–$9.67**. Это и есть
|
~55 мс, а статический провал агента стоит **$1.50–$9.67**. Это и есть
|
||||||
реальный рычаг — не "нечитаемость навсегда", а разрыв между ценой атаки и
|
реальный рычаг - не "нечитаемость навсегда", а разрыв между ценой атаки и
|
||||||
ценой сборки для нас. Авторы прямо предупреждают: преимущество схлопнется,
|
ценой сборки для нас. Авторы прямо предупреждают: преимущество схлопнется,
|
||||||
когда у агентов появятся инструменты динамического исполнения (сейчас
|
когда у агентов появятся инструменты динамического исполнения (сейчас
|
||||||
тестировался только статический анализ).
|
тестировался только статический анализ).
|
||||||
|
|
||||||
## 2. Acoda: атака на safety-alignment и токенизацию (ICSE 2026)
|
## 2. Acoda: атака на safety-alignment и токенизацию (ICSE 2026)
|
||||||
|
|
||||||
[Acoda](https://arxiv.org/pdf/2606.11755) — другая постановка задачи: не
|
[Acoda](https://arxiv.org/pdf/2606.11755) - другая постановка задачи: не
|
||||||
агент с дизассемблером, а LLM, которой **в чат вставили исходный код** и
|
агент с дизассемблером, а LLM, которой **в чат вставили исходный код** и
|
||||||
попросили его проанализировать/деобфусцировать. Генетический алгоритм
|
попросили его проанализировать/деобфусцировать. Генетический алгоритм
|
||||||
подбирает комбинацию из 8 трансформаций, до 70% ASR (attack success rate)
|
подбирает комбинацию из 8 трансформаций, до 70% ASR (attack success rate)
|
||||||
|
|
@ -81,7 +81,7 @@ indirect/regs и switch/stack).
|
||||||
| до анализа | Vulnerability Code Injection | фрагмент с SQL-инъекцией/`pickle.loads`/command injection в заведомо мёртвой ветке (`while (сложное_но_ложное_условие)`) |
|
| до анализа | Vulnerability Code Injection | фрагмент с SQL-инъекцией/`pickle.loads`/command injection в заведомо мёртвой ветке (`while (сложное_но_ложное_условие)`) |
|
||||||
| во время анализа | Misleading Summarization | модель сама просится описать код, затем это описание (намеренно неверное) вклеивается рядом с кодом |
|
| во время анализа | Misleading Summarization | модель сама просится описать код, затем это описание (намеренно неверное) вклеивается рядом с кодом |
|
||||||
| во время анализа | String Obfuscation | `base64` + словарь, расшифровка даёт слова типа "virus"/"weapon" |
|
| во время анализа | String Obfuscation | `base64` + словарь, расшифровка даёт слова типа "virus"/"weapon" |
|
||||||
| во время анализа | Try-Except Wrapping | настоящая логика лежит в `except`-ветке, в `try` — правдоподобный, но бесполезный код |
|
| во время анализа | Try-Except Wrapping | настоящая логика лежит в `except`-ветке, в `try` - правдоподобный, но бесполезный код |
|
||||||
| после анализа | EOS Token Insertion | в текст вставлены литералы токенов-разделителей (`<|EOT|>`), из-за чего генерация обрывается раньше |
|
| после анализа | EOS Token Insertion | в текст вставлены литералы токенов-разделителей (`<|EOT|>`), из-за чего генерация обрывается раньше |
|
||||||
|
|
||||||
### Честная оценка: что из этого применимо к нам
|
### Честная оценка: что из этого применимо к нам
|
||||||
|
|
@ -93,24 +93,24 @@ indirect/regs и switch/stack).
|
||||||
- `DebugInfo` уже вычищает всё, что могло бы стать комментарием или
|
- `DebugInfo` уже вычищает всё, что могло бы стать комментарием или
|
||||||
осмысленным описанием (`LineNumberTable`, `Signature`, исходные имена);
|
осмысленным описанием (`LineNumberTable`, `Signature`, исходные имена);
|
||||||
в скомпилированном `.class` комментариев не бывает в принципе.
|
в скомпилированном `.class` комментариев не бывает в принципе.
|
||||||
- Цель Acoda — заставить модель **отказаться анализировать свой же код**,
|
- Цель Acoda - заставить модель **отказаться анализировать свой же код**,
|
||||||
вставленный в чат пользователем. У нас код анализирует **чужой** агент
|
вставленный в чат пользователем. У нас код анализирует **чужой** агент
|
||||||
чужого jar — нам не нужно, чтобы он отказался, если он не наш союзник;
|
чужого jar - нам не нужно, чтобы он отказался, если он не наш союзник;
|
||||||
и вызывать намеренно подозрительные паттерны в легитимном продукте —
|
и вызывать намеренно подозрительные паттерны в легитимном продукте -
|
||||||
прямой риск получить ложное срабатывание антивируса/EDR на свой же jar.
|
прямой риск получить ложное срабатывание антивируса/EDR на свой же jar.
|
||||||
Это противоречит `README.md` → «Ответственное использование».
|
Это противоречит `README.md` → «Ответственное использование».
|
||||||
- **EOS Token Insertion** не переносится: это свойство текстового
|
- **EOS Token Insertion** не переносится: это свойство текстового
|
||||||
стриминга токенов модели, а не байткода.
|
стриминга токенов модели, а не байткода.
|
||||||
|
|
||||||
Единственное, что частично переносится: **String Obfuscation** и
|
Единственное, что частично переносится: **String Obfuscation** и
|
||||||
**Variable Renaming** — ровно то, что `Strings` и `Rename` уже делают,
|
**Variable Renaming** - ровно то, что `Strings` и `Rename` уже делают,
|
||||||
просто дешевле (у Acoda — обратимый `base64`, у нас — ротация шифров с
|
просто дешевле (у Acoda - обратимый `base64`, у нас - ротация шифров с
|
||||||
выводом ключа в рантайме, см. [`../stack.md`](../stack.md) §4-5). Здесь мы
|
выводом ключа в рантайме, см. [`../stack.md`](../stack.md) §4-5). Здесь мы
|
||||||
уже на шаг впереди статьи, а не позади.
|
уже на шаг впереди статьи, а не позади.
|
||||||
|
|
||||||
**Vulnerability Code Injection** как идея (мёртвый код, похожий на уязвимый
|
**Vulnerability Code Injection** как идея (мёртвый код, похожий на уязвимый
|
||||||
паттерн) пересекается с нашим `Noise`, но внедрять **реально похожие на
|
паттерн) пересекается с нашим `Noise`, но внедрять **реально похожие на
|
||||||
эксплойт сигнатуры** в выдаваемый клиентам jar — плохая идея для легального
|
эксплойт сигнатуры** в выдаваемый клиентам jar - плохая идея для легального
|
||||||
продукта защиты: это именно то, от чего предостерегает
|
продукта защиты: это именно то, от чего предостерегает
|
||||||
[`info/evo-weaknesses.md`](../../info/evo-weaknesses.md) с другой стороны
|
[`info/evo-weaknesses.md`](../../info/evo-weaknesses.md) с другой стороны
|
||||||
(не подставлять защищённый продукт под чужие red-листы).
|
(не подставлять защищённый продукт под чужие red-листы).
|
||||||
|
|
@ -118,24 +118,24 @@ indirect/regs и switch/stack).
|
||||||
## 3. Что добавить в noxis практически
|
## 3. Что добавить в noxis практически
|
||||||
|
|
||||||
Экономический рычаг из Elastic (цена агента > цены нашей сборки) ложится на
|
Экономический рычаг из Elastic (цена агента > цены нашей сборки) ложится на
|
||||||
архитектуру без переделок, конкретные пункты — в [`../../todo.md`](../../todo.md)
|
архитектуру без переделок, конкретные пункты - в [`../../todo.md`](../../todo.md)
|
||||||
§7:
|
§7:
|
||||||
|
|
||||||
1. **Вложенные слои шифрования строк** (аналог Matryoshka Wall) — расширить
|
1. **Вложенные слои шифрования строк** (аналог Matryoshka Wall) - расширить
|
||||||
`StringsConfig` полем "глубина", decoder разворачивает N слоёв вместо
|
`StringsConfig` полем "глубина", decoder разворачивает N слоёв вместо
|
||||||
одного. Цена агента растёт с глубиной почти линейно, цена сборки — нет.
|
одного. Цена агента растёт с глубиной почти линейно, цена сборки - нет.
|
||||||
2. **Больше decoy-точек ветвления ключа** (аналог Dispatch Maze) — наш
|
2. **Больше decoy-точек ветвления ключа** (аналог Dispatch Maze) - наш
|
||||||
декодер уже собирает мастер-секрет из `k1^k2` (`stack.md` §4); можно
|
декодер уже собирает мастер-секрет из `k1^k2` (`stack.md` §4); можно
|
||||||
поднять число частей и раскидать их сборку по случайным junk-методам
|
поднять число частей и раскидать их сборку по случайным junk-методам
|
||||||
из `Noise`, а не держать в одном месте.
|
из `Noise`, а не держать в одном месте.
|
||||||
3. **MBA вместо простых тождеств в `Constants`** — то же решение, что ловит
|
3. **MBA вместо простых тождеств в `Constants`** - то же решение, что ловит
|
||||||
и символьный исполнитель, и SMT-решатель, и, по данным Elastic,
|
и символьный исполнитель, и SMT-решатель, и, по данным Elastic,
|
||||||
"Flatten + MBA" бьёт сильнее, чем просто Flatten.
|
"Flatten + MBA" бьёт сильнее, чем просто Flatten.
|
||||||
4. **Не делать:** имитацию вредоносных паттернов, prompt injection в
|
4. **Не делать:** имитацию вредоносных паттернов, prompt injection в
|
||||||
строках, EOS-токены. Либо не переносится на байткод, либо создаёт риск
|
строках, EOS-токены. Либо не переносится на байткод, либо создаёт риск
|
||||||
ложного срабатывания защитных систем на легитимном продукте.
|
ложного срабатывания защитных систем на легитимном продукте.
|
||||||
|
|
||||||
Критерий "сработало/не сработало" для всего этого — как и для остальных
|
Критерий "сработало/не сработало" для всего этого - как и для остальных
|
||||||
техник: прогон конкретного агента на конкретной сборке, а не вера в
|
техник: прогон конкретного агента на конкретной сборке, а не вера в
|
||||||
методичку. Эффект устаревает с каждой новой версией модели.
|
методичку. Эффект устаревает с каждой новой версией модели.
|
||||||
|
|
||||||
|
|
@ -151,17 +151,17 @@ indirect/regs и switch/stack).
|
||||||
- **Для нас это ломает собственное допущение** из
|
- **Для нас это ломает собственное допущение** из
|
||||||
[`../stack.md`](../stack.md) §3: `noxis-verify` верифицирует метод через
|
[`../stack.md`](../stack.md) §3: `noxis-verify` верифицирует метод через
|
||||||
`java -Xverify:all`, потому что это *обычный* байткод с валидным
|
`java -Xverify:all`, потому что это *обычный* байткод с валидным
|
||||||
`StackMapTable`. Виртуализированный метод — уже не обычный байткод,
|
`StackMapTable`. Виртуализированный метод - уже не обычный байткод,
|
||||||
верификатор проверит только обвязку интерпретатора, не тело. Big-ticket
|
верификатор проверит только обвязку интерпретатора, не тело. Big-ticket
|
||||||
item, не для ближайшего спринта.
|
item, не для ближайшего спринта.
|
||||||
- **MBA** укладывается без архитектурных изменений — см. выше, §3.
|
- **MBA** укладывается без архитектурных изменений - см. выше, §3.
|
||||||
|
|
||||||
## Источники
|
## Источники
|
||||||
|
|
||||||
- [Elastic Security Labs — The Cost of Understanding: LLM-Driven Reverse Engineering vs Iterative LLM Obfuscation](https://www.elastic.co/security-labs/llm-reversing-vs-llm-obfuscation)
|
- [Elastic Security Labs - The Cost of Understanding: LLM-Driven Reverse Engineering vs Iterative LLM Obfuscation](https://www.elastic.co/security-labs/llm-reversing-vs-llm-obfuscation)
|
||||||
- [Acoda: Adversarial Code Obfuscation for Defending against LLM-based Analysis (arXiv 2606.11755, ICSE-Companion '26)](https://arxiv.org/pdf/2606.11755)
|
- [Acoda: Adversarial Code Obfuscation for Defending against LLM-based Analysis (arXiv 2606.11755, ICSE-Companion '26)](https://arxiv.org/pdf/2606.11755)
|
||||||
- [A Systematic Study on Static Control Flow Obfuscation Techniques in Java (arXiv 1809.11037)](https://arxiv.org/pdf/1809.11037)
|
- [A Systematic Study on Static Control Flow Obfuscation Techniques in Java (arXiv 1809.11037)](https://arxiv.org/pdf/1809.11037)
|
||||||
- [Promon Shield: Virtual Machine obfuscation SDK protection](https://promon.io/security-news/virtual-machine-obfuscation-sdk-protection)
|
- [Promon Shield: Virtual Machine obfuscation SDK protection](https://promon.io/security-news/virtual-machine-obfuscation-sdk-protection)
|
||||||
- [CoTDeceptor: Adversarial Code Obfuscation Against CoT-Enhanced LLM Code Agents (arXiv 2512.21250)](https://arxiv.org/pdf/2512.21250)
|
- [CoTDeceptor: Adversarial Code Obfuscation Against CoT-Enhanced LLM Code Agents (arXiv 2512.21250)](https://arxiv.org/pdf/2512.21250)
|
||||||
- [Deconstructing Obfuscation: a four-dimensional framework for evaluating LLM deobfuscation (arXiv 2505.19887)](https://arxiv.org/pdf/2505.19887)
|
- [Deconstructing Obfuscation: a four-dimensional framework for evaluating LLM deobfuscation (arXiv 2505.19887)](https://arxiv.org/pdf/2505.19887)
|
||||||
- [Tigress — диверсифицирующий обфускатор для C, использованный в бенчмарке Elastic](https://tigress.wtf/)
|
- [Tigress - диверсифицирующий обфускатор для C, использованный в бенчмарке Elastic](https://tigress.wtf/)
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
# Уроки: что защита EvoVis сделала плохо
|
# Уроки: что защита EvoVis сделала плохо
|
||||||
|
|
||||||
Источник: `info/evo-weaknesses.md` — полный разбор за один проход, без
|
Источник: `info/evo-weaknesses.md` - полный разбор за один проход, без
|
||||||
рантайм-хаков. Здесь — что из этого следует для **нашей** архитектуры.
|
рантайм-хаков. Здесь - что из этого следует для **нашей** архитектуры.
|
||||||
|
|
||||||
Правило, которое стоит держать головой: **защита ломается не в рантайме,
|
Правило, которое стоит держать головой: **защита ломается не в рантайме,
|
||||||
а на этапе, когда атакующий смотрит на статику.** Всё, что видно в `javap -p`
|
а на этапе, когда атакующий смотрит на статику.** Всё, что видно в `javap -p`
|
||||||
или в содержимом jar, — это утечка.
|
или в содержимом jar, - это утечка.
|
||||||
|
|
||||||
## Сводная таблица: провал → наше решение
|
## Сводная таблица: провал → наше решение
|
||||||
|
|
||||||
|
|
@ -13,21 +13,21 @@
|
||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| 1 | ключ лежит рядом с шифротекстом | вывод мастер-секрета в декодере, ключ из `master‖salt` | Strings | **готово** |
|
| 1 | ключ лежит рядом с шифротекстом | вывод мастер-секрета в декодере, ключ из `master‖salt` | Strings | **готово** |
|
||||||
| 2 | переименованы типы, но не члены | rename полей/методов + семейства virtual | Rename | **готово** |
|
| 2 | переименованы типы, но не члены | rename полей/методов + семейства virtual | Rename | **готово** |
|
||||||
| 3 | отладочная информация нетронута | strip + ложный `SourceFile`/номера строк | DebugInfo | **готово** (`SourceFile`; номера строк — план) |
|
| 3 | отладочная информация нетронута | strip + ложный `SourceFile`/номера строк | DebugInfo | **готово** (`SourceFile`; номера строк - план) |
|
||||||
| 4 | шаблонный flattening | 4 схемы CFG + мешанина внутри метода | Flow | **готово** (4 схемы, приёмка `java`) |
|
| 4 | шаблонный flattening | 4 схемы CFG + мешанина внутри метода | Flow | **готово** (4 схемы, приёмка `java`) |
|
||||||
| 5 | opaque-предикаты = `A+B` | ротация тождеств + предикаты от состояния | Constants | **готово** (int/long) |
|
| 5 | opaque-предикаты = `A+B` | ротация тождеств + предикаты от состояния | Constants | **готово** (int/long) |
|
||||||
| 6 | junk-классы вне графа reachability | `weave_reachable` — обязательная вплетённость | Noise | **готово** (без аннотаций) |
|
| 6 | junk-классы вне графа reachability | `weave_reachable` - обязательная вплетённость | Noise | **готово** (без аннотаций) |
|
||||||
| 7 | имена говорят «я защита» | все артефакты получают данные псевдонимы | Rename | **готово** |
|
| 7 | имена говорят «я защита» | все артефакты получают данные псевдонимы | Rename | **готово** |
|
||||||
| 8 | вторичные компоненты без защиты | весь jar, а не только основной класс | Resources | **готово** |
|
| 8 | вторичные компоненты без защиты | весь jar, а не только основной класс | Resources | **готово** |
|
||||||
| 9 | шифр без MAC и без per-string salt | salt на строку, уникальный nonce, MAC | Strings | **готово** |
|
| 9 | шифр без MAC и без per-string salt | salt на строку, уникальный nonce, MAC | Strings | **готово** |
|
||||||
|
|
||||||
## 1. Ключ рядом с шифротекстом
|
## 1. Ключ рядом с шифротекстом
|
||||||
|
|
||||||
`awc.d("<cipher>", <opaque-выражение>)` — ключ был **константным выражением
|
`awc.d("<cipher>", <opaque-выражение>)` - ключ был **константным выражением
|
||||||
в вызове**. Свёртка выражения — ~200 строк любого парсера. Итог: 8993 вызова
|
в вызове**. Свёртка выражения - ~200 строк любого парсера. Итог: 8993 вызова
|
||||||
расшифрованы детерминированно, **0 обращений к JVM**.
|
расшифрованы детерминированно, **0 обращений к JVM**.
|
||||||
|
|
||||||
Хуже: один шифр на весь продукт (LCG с фиксированными `a/c`) — зная одну
|
Хуже: один шифр на весь продукт (LCG с фиксированными `a/c`) - зная одну
|
||||||
пару «шифротекст → текст», остальные раскрываются по обучению.
|
пару «шифротекст → текст», остальные раскрываются по обучению.
|
||||||
|
|
||||||
**Что делаем иначе.**
|
**Что делаем иначе.**
|
||||||
|
|
@ -41,56 +41,56 @@
|
||||||
- MAC: подмена байта не «тихо» портит текст.
|
- MAC: подмена байта не «тихо» портит текст.
|
||||||
|
|
||||||
Остаточный риск честно признан: целевой анализ декодера раскрывает текст.
|
Остаточный риск честно признан: целевой анализ декодера раскрывает текст.
|
||||||
Задача — поднять цену, а не сделать невозможным.
|
Задача - поднять цену, а не сделать невозможным.
|
||||||
|
|
||||||
## 2. Члены не переименованы
|
## 2. Члены не переименованы
|
||||||
|
|
||||||
`onDraw`, `getProjectiles`, `isAuthorized()` — открытые имена. Класс `wb`
|
`onDraw`, `getProjectiles`, `isAuthorized()` - открытые имена. Класс `wb`
|
||||||
опознаётся за секунду по трём понятным методам.
|
опознаётся за секунду по трём понятным методам.
|
||||||
|
|
||||||
**Что делаем иначе:** переименование полей и методов входит в базовый
|
**Что делаем иначе:** переименование полей и методов входит в базовый
|
||||||
уровень, а не в «продвинутый». Уже готово, включая:
|
уровень, а не в «продвинутый». Уже готово, включая:
|
||||||
|
|
||||||
- **семейства virtual** — переименование на всю иерархию, чтобы не
|
- **семейства virtual** - переименование на всю иерархию, чтобы не
|
||||||
разъединить переопределения и не сломать `invokevirtual`;
|
разъединить переопределения и не сломать `invokevirtual`;
|
||||||
- **keep-список** — private/static/final можно сразу, `rename_public_members`
|
- **keep-список** - private/static/final можно сразу, `rename_public_members`
|
||||||
по умолчанию выключен для библиотек, чтобы не сломать API;
|
по умолчанию выключен для библиотек, чтобы не сломать API;
|
||||||
- **автодетект рефлексии по имени** — `Class.forName`, `getMethod`,
|
- **автодетект рефлексии по имени** - `Class.forName`, `getMethod`,
|
||||||
`getField` с литералами попадают в keep автоматически.
|
`getField` с литералами попадают в keep автоматически.
|
||||||
|
|
||||||
## 3. Отладочная информация
|
## 3. Отладочная информация
|
||||||
|
|
||||||
`SourceFile` оригинальные у 160 классов, `LineNumberTable` — 10 218,
|
`SourceFile` оригинальные у 160 классов, `LineNumberTable` - 10 218,
|
||||||
`LocalVariableTable` — имена локальных переменных, `@rn(name, description)` —
|
`LocalVariableTable` - имена локальных переменных, `@rn(name, description)` -
|
||||||
каталог всех модулей с описаниями.
|
каталог всех модулей с описаниями.
|
||||||
|
|
||||||
**Что делаем иначе:** пасс `DebugInfo` — strip `SourceFile`,
|
**Что делаем иначе:** пасс `DebugInfo` - strip `SourceFile`,
|
||||||
`LineNumberTable`, `LocalVariableTable`, `Signature`, `InnerClasses` +
|
`LineNumberTable`, `LocalVariableTable`, `Signature`, `InnerClasses` +
|
||||||
**ложная** отладочная информация (чужие имена и номера строк, чтобы
|
**ложная** отладочная информация (чужие имена и номера строк, чтобы
|
||||||
деобфускатор выдавал правдоподобный, но чужой код).
|
деобфускатор выдавал правдоподобный, но чужой код).
|
||||||
|
|
||||||
Отсюда обязательное требование: **маппинги в отчёте** — иначе мы сами не
|
Отсюда обязательное требование: **маппинги в отчёте** - иначе мы сами не
|
||||||
читаем свои stack trace'и.
|
читаем свои stack trace'и.
|
||||||
|
|
||||||
## 4. Шаблонный flattening
|
## 4. Шаблонный flattening
|
||||||
|
|
||||||
Один regex нашёл 3530 автоматов: `while(true) { switch(i + i2) }`, метки —
|
Один regex нашёл 3530 автоматов: `while(true) { switch(i + i2) }`, метки -
|
||||||
константы, переходы — присваивания в одном basic block, тела блоков не
|
константы, переходы - присваивания в одном basic block, тела блоков не
|
||||||
тронуты.
|
тронуты.
|
||||||
|
|
||||||
**Что делаем иначе:** не одна схема, а **четыре** (`state-spread`,
|
**Что делаем иначе:** не одна схема, а **четыре** (`state-spread`,
|
||||||
`computed-switch`, `butterfly`, `exception-goto`), с ротацией между методами
|
`computed-switch`, `butterfly`, `exception-goto`), с ротацией между методами
|
||||||
и с перемешиванием внутри метода. Плюс `butterfly` и `exception-goto`
|
и с перемешиванием внутри метода. Плюс `butterfly` и `exception-goto`
|
||||||
ломают «все переходы — присваивания».
|
ломают «все переходы - присваивания».
|
||||||
|
|
||||||
Это единственная техника с **высоким риском**: любая ошибка в CFG =
|
Это единственная техника с **высоким риском**: любая ошибка в CFG =
|
||||||
`VerifyError`. Поэтому блокером Flow объявлен не «напишем потом», а
|
`VerifyError`. Поэтому блокером Flow объявлен не «напишем потом», а
|
||||||
`StackMapTable` — см. [`../stack.md`](../stack.md) §3. Без верификатора
|
`StackMapTable` - см. [`../stack.md`](../stack.md) §3. Без верификатора
|
||||||
Flow не включается.
|
Flow не включается.
|
||||||
|
|
||||||
## 5. Opaque-предикаты
|
## 5. Opaque-предикаты
|
||||||
|
|
||||||
`(A^B)+((A&B)<<1)` — школьное тождество, узнаётся регуляркой и даёт
|
`(A^B)+((A&B)<<1)` - школьное тождество, узнаётся регуляркой и даёт
|
||||||
бесплатный фолдинг остальных констант. Настоящих предикатов (зависящих
|
бесплатный фолдинг остальных констант. Настоящих предикатов (зависящих
|
||||||
от состояния и всегда истинных) нет.
|
от состояния и всегда истинных) нет.
|
||||||
|
|
||||||
|
|
@ -103,26 +103,26 @@ Flow не включается.
|
||||||
42 файла, ни одного входящего ссылока, `dangling=0` → BFS от 4
|
42 файла, ни одного входящего ссылока, `dangling=0` → BFS от 4
|
||||||
entrypoint'ов, которые открыто лежат в `fabric.mod.json`.
|
entrypoint'ов, которые открыто лежат в `fabric.mod.json`.
|
||||||
|
|
||||||
**Что делаем иначе:** `weave_reachable` — junk попадает в граф
|
**Что делаем иначе:** `weave_reachable` - junk попадает в граф
|
||||||
достижимости, создаёт ложные рёбра и участвует в вызовах. Это свойство
|
достижимости, создаёт ложные рёбра и участвует в вызовах. Это свойство
|
||||||
пасса, а не настройка: без вплетённости junk не включается вовсе.
|
пасса, а не настройка: без вплетённости junk не включается вовсе.
|
||||||
|
|
||||||
## 7. Самоописание защиты
|
## 7. Самоописание защиты
|
||||||
|
|
||||||
`StringRuntime`, `KeyAssembler`, `AttestClient`, `RuntimeGuard`,
|
`StringRuntime`, `KeyAssembler`, `AttestClient`, `RuntimeGuard`,
|
||||||
`SealedBoot`, `NativeGuard` — названия указывают атакующему, куда смотреть.
|
`SealedBoot`, `NativeGuard` - названия указывают атакующему, куда смотреть.
|
||||||
|
|
||||||
**Что делаем иначе:** псевдонимы генерируются одним `NameGenerator` для
|
**Что делаем иначе:** псевдонимы генерируются одним `NameGenerator` для
|
||||||
всех сущностей — классы, пакеты, поля, методы, junk и даже имена атрибутов
|
всех сущностей - классы, пакеты, поля, методы, junk и даже имена атрибутов
|
||||||
и схем выглядят как обычные данные. Отдельного «защитного» стиля не
|
и схем выглядят как обычные данные. Отдельного «защитного» стиля не
|
||||||
существует, поэтому отличить его от остального кода не по чему.
|
существует, поэтому отличить его от остального кода не по чему.
|
||||||
|
|
||||||
## 8. Вторичные компоненты
|
## 8. Вторичные компоненты
|
||||||
|
|
||||||
`evo-agent.jar` вообще без обфускации, .NET-лаунчер — статическая XOR-таблица.
|
`evo-agent.jar` вообще без обфускации, .NET-лаунчер - статическая XOR-таблица.
|
||||||
|
|
||||||
**Что делаем иначе:** обфускация применяется ко **всему архиву**, включая
|
**Что делаем иначе:** обфускация применяется ко **всему архиву**, включая
|
||||||
ресурсы (`fabric.mod.json`, mixins, refmap, access widener) — см. пасс
|
ресурсы (`fabric.mod.json`, mixins, refmap, access widener) - см. пасс
|
||||||
`Resources`. Отдельного «ядро защищено, остальное нет» не бывает.
|
`Resources`. Отдельного «ядро защищено, остальное нет» не бывает.
|
||||||
|
|
||||||
## 9. Шифр без аутентификации
|
## 9. Шифр без аутентификации
|
||||||
|
|
@ -131,16 +131,16 @@ entrypoint'ов, которые открыто лежат в `fabric.mod.json`.
|
||||||
работает.
|
работает.
|
||||||
|
|
||||||
**Уже сделано:** `salt` на строку, уникальный `nonce`, FNV-1a MAC в
|
**Уже сделано:** `salt` на строку, уникальный `nonce`, FNV-1a MAC в
|
||||||
`noxis-runtime::scheme`. `cipher:tag` лежат рядом — порча обнаруживается.
|
`noxis-runtime::scheme`. `cipher:tag` лежат рядом - порча обнаруживается.
|
||||||
|
|
||||||
## Что подтвердилось и не нужно повторять в выводах
|
## Что подтвердилось и не нужно повторять в выводах
|
||||||
|
|
||||||
Разбор дал и **опровержения** — важно не строить защиту на мифах:
|
Разбор дал и **опровержения** - важно не строить защиту на мифах:
|
||||||
|
|
||||||
- «сдвиги в байткоде бывают отрицательными» — по 1277 классам **0** случаев
|
- «сдвиги в байткоде бывают отрицательными» - по 1277 классам **0** случаев
|
||||||
(32 559 сдвигов, константные всегда `1`);
|
(32 559 сдвигов, константные всегда `1`);
|
||||||
- `awc.g(...)` (session-gate) в билде **не вызывался ни разу**.
|
- `awc.g(...)` (session-gate) в билде **не вызывался ни разу**.
|
||||||
|
|
||||||
Вывод: проверять гипотезу на статистике **до** того, как вкладываться
|
Вывод: проверять гипотезу на статистике **до** того, как вкладываться
|
||||||
в технику против неё. Отсюда требование к `noxis-verify` — не «красиво
|
в технику против неё. Отсюда требование к `noxis-verify` - не «красиво
|
||||||
обфусцировано», а измеримые метрики ([`priorities.md`](priorities.md)).
|
обфусцировано», а измеримые метрики ([`priorities.md`](priorities.md)).
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,18 @@
|
||||||
# Что улучшим: стратегия и приоритеты
|
# Что улучшим: стратегия и приоритеты
|
||||||
|
|
||||||
Материал собран из `info/evo-improvements.md` (план после разбора EvoVis) и
|
Материал собран из `info/evo-improvements.md` (план после разбора EvoVis) и
|
||||||
`info/evo-weaknesses.md` (провалы). Здесь — что мы берём, в каком порядке
|
`info/evo-weaknesses.md` (провалы). Здесь - что мы берём, в каком порядке
|
||||||
и **что осознанно не делаем**.
|
и **что осознанно не делаем**.
|
||||||
|
|
||||||
## Стратегия в одном абзаце
|
## Стратегия в одном абзаце
|
||||||
|
|
||||||
Максимум защиты при минимуме риска сломать рантайм. Дешёвые техники,
|
Максимум защиты при минимуме риска сломать рантайм. Дешёвые техники,
|
||||||
дающие 80% результата, включаются по умолчанию; дорогие и опасные —
|
дающие 80% результата, включаются по умолчанию; дорогие и опасные -
|
||||||
включаются **только** после того, как для них есть верификатор. Каждая
|
включаются **только** после того, как для них есть верификатор. Каждая
|
||||||
техника имеет измеримый критерий «сработала/не сработала», иначе это
|
техника имеет измеримый критерий «сработала/не сработала», иначе это
|
||||||
не защита, а мнение.
|
не защита, а мнение.
|
||||||
|
|
||||||
## P0 — базовый уровень (дёшево, снимает главные утечки)
|
## P0 - базовый уровень (дёшево, снимает главные утечки)
|
||||||
|
|
||||||
| пункт | что это | пасс | статус |
|
| пункт | что это | пасс | статус |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
|
|
@ -20,63 +20,63 @@
|
||||||
| A2 | вычистить `LineNumberTable`/`LocalVariableTable`/`SourceFile`/`InnerClasses`/`Signature`/`MethodParameters` | DebugInfo | ✅ готово |
|
| A2 | вычистить `LineNumberTable`/`LocalVariableTable`/`SourceFile`/`InnerClasses`/`Signature`/`MethodParameters` | DebugInfo | ✅ готово |
|
||||||
| A3 | никаких сигнатурных имён у защитного кода, защитный код смешан с обычным | Rename + Noise | ✅ готово |
|
| A3 | никаких сигнатурных имён у защитного кода, защитный код смешан с обычным | Rename + Noise | ✅ готово |
|
||||||
| A4 | обфусцировать **весь** архив, включая ресурсы, mixin-JSON, access widener, service loader'ы | Resources | ✅ готово |
|
| A4 | обфусцировать **весь** архив, включая ресурсы, mixin-JSON, access widener, service loader'ы | Resources | ✅ готово |
|
||||||
| — | маппинг в отчёт (иначе не читаем свои же stack trace) | Rename | ✅ готово |
|
| - | маппинг в отчёт (иначе не читаем свои же stack trace) | Rename | ✅ готово |
|
||||||
|
|
||||||
Это самый дешёвый блок и одновременно **самый большой утечка** в разобранном
|
Это самый дешёвый блок и одновременно **самый большой утечка** в разобранном
|
||||||
образце: оригинальные `SourceFile` у 160 классов, имена полей и методов,
|
образце: оригинальные `SourceFile` у 160 классов, имена полей и методов,
|
||||||
каталог модулей в атрибутах.
|
каталог модулей в атрибутах.
|
||||||
|
|
||||||
## P1 — строки и константы
|
## P1 - строки и константы
|
||||||
|
|
||||||
| пункт | что это | пасс | статус |
|
| пункт | что это | пасс | статус |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| B1 | ключ на строку + ротация 3–5 схем + MAC + уникальный nonce | Strings | ✅ готово |
|
| B1 | ключ на строку + ротация 3–5 схем + MAC + уникальный nonce | Strings | ✅ готово |
|
||||||
| B2 | **вывод** ключа в рантайме, а не константа в вызове | Strings + генератор декодера | ✅ готово |
|
| B2 | **вывод** ключа в рантайме, а не константа в вызове | Strings + генератор декодера | ✅ готово |
|
||||||
| B3 | константы — не тождества, а вычисления, разнесённые по блокам | Constants | ✅ готово (int/long) |
|
| B3 | константы - не тождества, а вычисления, разнесённые по блокам | Constants | ✅ готово (int/long) |
|
||||||
| B4 | **ложная** отладочная информация: чужие имена переменных и строк | DebugInfo | ✅ `SourceFile` |
|
| B4 | **ложная** отладочная информация: чужие имена переменных и строк | DebugInfo | ✅ `SourceFile` |
|
||||||
|
|
||||||
B2 — главное отличие от разобранного образца: у него ключ был константным
|
B2 - главное отличие от разобранного образца: у него ключ был константным
|
||||||
выражением в вызове и расшифровывался статически, без JVM. Мастер-секрет
|
выражением в вызове и расшифровывался статически, без JVM. Мастер-секрет
|
||||||
теперь собирается в рантайме из двух частей (`k1 ^ k2`) декодером класса —
|
теперь собирается в рантайме из двух частей (`k1 ^ k2`) декодером класса -
|
||||||
см. [`../stack.md`](../stack.md) §4. B4 пока покрывает только `SourceFile`;
|
см. [`../stack.md`](../stack.md) §4. B4 пока покрывает только `SourceFile`;
|
||||||
ложные номера строк (`fake_line_numbers`) — см. [`../../todo.md`](../../todo.md) §3.6.
|
ложные номера строк (`fake_line_numbers`) - см. [`../../todo.md`](../../todo.md) §3.6.
|
||||||
|
|
||||||
## P2 — контрольный поток
|
## P2 - контрольный поток
|
||||||
|
|
||||||
| пункт | что это | статус |
|
| пункт | что это | статус |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| C1 | **несколько схем** флаттенинга вместо одной, с мешаниной внутри метода | план |
|
| C1 | **несколько схем** флаттенинга вместо одной, с мешаниной внутри метода | план |
|
||||||
| C2 | верификатор обязателен: ошибка CFG = `VerifyError` | ✅ готово (кадры + `java -Xverify:all`) |
|
| C2 | верификатор обязателен: ошибка CFG = `VerifyError` | ✅ готово (кадры + `java -Xverify:all`) |
|
||||||
| C3 | реальный объём ≠ раздувание: бюджет на размер и время | ✅ размер; время — план |
|
| C3 | реальный объём ≠ раздувание: бюджет на размер и время | ✅ размер; время - план |
|
||||||
|
|
||||||
Приоритет здесь строго подчинён: **C2 раньше C1**. Кадры `StackMapTable`
|
Приоритет здесь строго подчинён: **C2 раньше C1**. Кадры `StackMapTable`
|
||||||
и приёмка `java -Xverify:all` уже готовы (см. [`../stack.md`](../stack.md)
|
и приёмка `java -Xverify:all` уже готовы (см. [`../stack.md`](../stack.md)
|
||||||
§3) — единственное, чего не хватает, это сами схемы флаттенинга (C1).
|
§3) - единственное, чего не хватает, это сами схемы флаттенинга (C1).
|
||||||
|
|
||||||
## P3 — измеримость (метрики, без которых ничего не доказано)
|
## P3 - измеримость (метрики, без которых ничего не доказано)
|
||||||
|
|
||||||
Обязательный блок, иначе фраза «обфускация есть» ничего не означает.
|
Обязательный блок, иначе фраза «обфускация есть» ничего не означает.
|
||||||
Крейт `noxis-verify` реализован — вот что из списка уже цифра, а что ещё мнение:
|
Крейт `noxis-verify` реализован - вот что из списка уже цифра, а что ещё мнение:
|
||||||
|
|
||||||
1. **Утечки имён.** ✅ `collect_names`/`find_leaks` — сверка идентификаторов
|
1. **Утечки имён.** ✅ `collect_names`/`find_leaks` - сверка идентификаторов
|
||||||
с учётом keep-списка.
|
с учётом keep-списка.
|
||||||
2. **Читаемость декомпилята.** ⬜ Прогон JADX/CFR → доля распознанных
|
2. **Читаемость декомпилята.** ⬜ Прогон JADX/CFR → доля распознанных
|
||||||
структур — не начато.
|
структур - не начато.
|
||||||
3. **Противник.** ✅ `scan_secrets` + `RED_PATTERNS` держим в репозитории;
|
3. **Противник.** ✅ `scan_secrets` + `RED_PATTERNS` держим в репозитории;
|
||||||
⬜ полноценный скрипт-деобфускатор (свёртка констант) как тест — план.
|
⬜ полноценный скрипт-деобфускатор (свёртка констант) как тест - план.
|
||||||
4. **Семантика.** ⬜ Эталонные программы до/после — не начато
|
4. **Семантика.** ⬜ Эталонные программы до/после - не начато
|
||||||
(сейчас семантику подтверждает только `java -Xverify:all`).
|
(сейчас семантику подтверждает только `java -Xverify:all`).
|
||||||
5. **Бюджеты.** ✅ размер (`inflation_ratio`/`check_inflation`);
|
5. **Бюджеты.** ✅ размер (`inflation_ratio`/`check_inflation`);
|
||||||
⬜ время исполнения — не начато.
|
⬜ время исполнения - не начато.
|
||||||
6. **Стабильность между сборками.** ✅ `mappings_changed` — маппинги
|
6. **Стабильность между сборками.** ✅ `mappings_changed` - маппинги
|
||||||
обязаны отличаться между прогонами с разным seed.
|
обязаны отличаться между прогонами с разным seed.
|
||||||
|
|
||||||
Итог: `noxis-verify` закрывает красные паттерны, утечки имён,
|
Итог: `noxis-verify` закрывает красные паттерны, утечки имён,
|
||||||
`java -Xverify:all`, бюджет размера и стабильность маппингов. Остаются
|
`java -Xverify:all`, бюджет размера и стабильность маппингов. Остаются
|
||||||
декомпилят-метрика, бюджет времени и эталонная семантика — три пункта,
|
декомпилят-метрика, бюджет времени и эталонная семантика - три пункта,
|
||||||
где «нам кажется защищённым» всё ещё мнение, а не цифра.
|
где «нам кажется защищённым» всё ещё мнение, а не цифра.
|
||||||
|
|
||||||
## P4 — долгий горизонт (пока не делаем)
|
## P4 - долгий горизонт (пока не делаем)
|
||||||
|
|
||||||
| пункт | что это | почему не сразу |
|
| пункт | что это | почему не сразу |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
|
|
@ -86,35 +86,35 @@ B2 — главное отличие от разобранного образц
|
||||||
| D4 | честный anti-tamper, а не «зашейрь ключ» | целая инженерная задача, не пасс |
|
| D4 | честный anti-tamper, а не «зашейрь ключ» | целая инженерная задача, не пасс |
|
||||||
| E | собственный нативный/Rust-цель вместо JVM | другой продукт, не наша цель |
|
| E | собственный нативный/Rust-цель вместо JVM | другой продукт, не наша цель |
|
||||||
|
|
||||||
D1 — явно ограничен: включать только для действительно ценных функций,
|
D1 - явно ограничен: включать только для действительно ценных функций,
|
||||||
остальное оправдания не имеет. E — отдельный продукт; если он когда-нибудь
|
остальное оправдания не имеет. E - отдельный продукт; если он когда-нибудь
|
||||||
понадобится, наш IR как раз делает его возможным (те же пассы, другой
|
понадобится, наш IR как раз делает его возможным (те же пассы, другой
|
||||||
writer).
|
writer).
|
||||||
|
|
||||||
## Что мы добавили сверх исходного плана
|
## Что мы добавили сверх исходного плана
|
||||||
|
|
||||||
1. **Структурный гейт при компиляции.** Лимиты «250 строк / 4 файла»
|
1. **Структурный гейт при компиляции.** Лимиты «250 строк / 4 файла»
|
||||||
гоняются в `build.rs` — см. [`../README.md`](../README.md). Без этого
|
гоняются в `build.rs` - см. [`../README.md`](../README.md). Без этого
|
||||||
план P0–P3 превращается в один гигантский файл через месяц.
|
план P0–P3 превращается в один гигантский файл через месяц.
|
||||||
2. **Собственный IR с метками.** Исходный план предполагал работу поверх
|
2. **Собственный IR с метками.** Исходный план предполагал работу поверх
|
||||||
готового парсера. `Insn::Mark(Label)` убирает пересчёт смещений из
|
готового парсера. `Insn::Mark(Label)` убирает пересчёт смещений из
|
||||||
всех пассов — это меняет цену каждой техники, меняющей CFG.
|
всех пассов - это меняет цену каждой техники, меняющей CFG.
|
||||||
3. **Детерминированный PRNG с доменным разделением.** Без этого
|
3. **Детерминированный PRNG с доменным разделением.** Без этого
|
||||||
пункт «стабильность между сборками» невозможно проверить тестом.
|
пункт «стабильность между сборками» невозможно проверить тестом.
|
||||||
4. **Keep как отдельный крейт (`noxis-analysis`), а не флаг CLI.**
|
4. **Keep как отдельный крейт (`noxis-analysis`), а не флаг CLI.**
|
||||||
Рефлексия, JNI, сериализация и entrypoint'ы — это анализ иерархии,
|
Рефлексия, JNI, сериализация и entrypoint'ы - это анализ иерархии,
|
||||||
а не настройка, которую надо помнить руками.
|
а не настройка, которую надо помнить руками.
|
||||||
|
|
||||||
## Порядок выполнения
|
## Порядок выполнения
|
||||||
|
|
||||||
Смотрите [`../../todo.md`](../../todo.md). P0–P3 и P3-измеримость уже
|
Смотрите [`../../todo.md`](../../todo.md). P0–P3 и P3-измеримость уже
|
||||||
готовы; единственный крупный пункт, который остался, — схемы `Flow` (C1),
|
готовы; единственный крупный пункт, который остался, - схемы `Flow` (C1),
|
||||||
потому что верификатор (C2) для них уже есть:
|
потому что верификатор (C2) для них уже есть:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
✅ P0 DebugInfo + Resources → главные утечки сняты
|
✅ P0 DebugInfo + Resources → главные утечки сняты
|
||||||
✅ P1 Strings (B2) + Constants → ключ выводится, схемы ротируются
|
✅ P1 Strings (B2) + Constants → ключ выводится, схемы ротируются
|
||||||
✅ P1 backend (ristretto) + frames
|
✅ P1 backend (ristretto) + frames
|
||||||
✅ P2 Flow (C1) — готов (09.10.2026): 4 схемы + ротация + мешанина, приёмка настоящим `java`
|
✅ P2 Flow (C1) - готов (09.10.2026): 4 схемы + ротация + мешанина, приёмка настоящим `java`
|
||||||
✅ P3 метрики, красные паттерны, CLI
|
✅ P3 метрики, красные паттерны, CLI
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@
|
||||||
| `DebugInfoStripper` | 69 | strip отладки | пасс `debug` |
|
| `DebugInfoStripper` | 69 | strip отладки | пасс `debug` |
|
||||||
| `StringFilter` | 35 | что считать строкой | функция внутри `strings` |
|
| `StringFilter` | 35 | что считать строкой | функция внутри `strings` |
|
||||||
|
|
||||||
**Вывод:** структура совпадает с нашей почти один в один — это подтверждает
|
**Вывод:** структура совпадает с нашей почти один в один - это подтверждает
|
||||||
раскладку крейтов. Ключевое отличие: у них всё живёт в одном jar'е и
|
раскладку крейтов. Ключевое отличие: у них всё живёт в одном jar'е и
|
||||||
одном проекте; у нас конвейер отделён от I/O и от CLI.
|
одном проекте; у нас конвейер отделён от I/O и от CLI.
|
||||||
|
|
||||||
|
|
@ -47,11 +47,11 @@
|
||||||
|
|
||||||
У них: remap → strings → numbers → flow → junk → debug. Тот же инвариант,
|
У них: remap → strings → numbers → flow → junk → debug. Тот же инвариант,
|
||||||
что и у нас: **переименование раньше всех техник, порождающих новые ссылки**,
|
что и у нас: **переименование раньше всех техник, порождающих новые ссылки**,
|
||||||
а `debug` — последним.
|
а `debug` - последним.
|
||||||
|
|
||||||
### 3.2 Наследование как условие безопасности
|
### 3.2 Наследование как условие безопасности
|
||||||
|
|
||||||
`InheritanceAnalyzer` — самый большой после remap'ера модуль. Совпадает с
|
`InheritanceAnalyzer` - самый большой после remap'ера модуль. Совпадает с
|
||||||
нашим выводом: переименование virtual-методов **без** иерархии ломает
|
нашим выводом: переименование virtual-методов **без** иерархии ломает
|
||||||
`invokevirtual`. У нас это `hierarchy` + семейства в `rename`.
|
`invokevirtual`. У нас это `hierarchy` + семейства в `rename`.
|
||||||
|
|
||||||
|
|
@ -63,13 +63,13 @@
|
||||||
|
|
||||||
### 3.4 Подпись срезается
|
### 3.4 Подпись срезается
|
||||||
|
|
||||||
`*.SF/*.RSA/*.DSA/*.EC` удаляются при перепаковке — после записи подпись
|
`*.SF/*.RSA/*.DSA/*.EC` удаляются при перепаковке - после записи подпись
|
||||||
всё равно невалидна, а её наличие заставляет JVM падать. Это входит в
|
всё равно невалидна, а её наличие заставляет JVM падать. Это входит в
|
||||||
`noxis-jar`.
|
`noxis-jar`.
|
||||||
|
|
||||||
### 3.5 Маппинг в файл + ложные ограничения
|
### 3.5 Маппинг в файл + ложные ограничения
|
||||||
|
|
||||||
`--mappings` пишет соответствие «новое → старое» — без него автор сам не
|
`--mappings` пишет соответствие «новое → старое» - без него автор сам не
|
||||||
читает stack trace. Плюс честные off-ключи: `--no-strings`, `--no-flow`,
|
читает stack trace. Плюс честные off-ключи: `--no-strings`, `--no-flow`,
|
||||||
`--no-junk`, `--keep-lines`, `--keep-source`, `--keep-vars`, `--no-widen`.
|
`--no-junk`, `--keep-lines`, `--keep-source`, `--keep-vars`, `--no-widen`.
|
||||||
|
|
||||||
|
|
@ -78,7 +78,7 @@
|
||||||
|
|
||||||
### 3.6 Seed
|
### 3.6 Seed
|
||||||
|
|
||||||
`--seed` — детерминированный прогон. У нас это базовое свойство
|
`--seed` - детерминированный прогон. У нас это базовое свойство
|
||||||
`noxis-core`, без отдельного флага.
|
`noxis-core`, без отдельного флага.
|
||||||
|
|
||||||
## 4. Что не берём
|
## 4. Что не берём
|
||||||
|
|
@ -86,24 +86,24 @@
|
||||||
### 4.1 Java + ASM
|
### 4.1 Java + ASM
|
||||||
|
|
||||||
Главное отличие стека. Пара Java+ASM даёт родную JVM, готовые visitor'ы и
|
Главное отличие стека. Пара Java+ASM даёт родную JVM, готовые visitor'ы и
|
||||||
`CheckClassAdapter` — но и:
|
`CheckClassAdapter` - но и:
|
||||||
|
|
||||||
- нужно раскатывать JDK для сборки и запуска тестов;
|
- нужно раскатывать JDK для сборки и запуска тестов;
|
||||||
- наш проект — Rust (`deny(unsafe_code)`, свои лимиты, свои гейты);
|
- наш проект - Rust (`deny(unsafe_code)`, свои лимиты, свои гейты);
|
||||||
- отсутствует возможность отдавать один бинарь.
|
- отсутствует возможность отдавать один бинарь.
|
||||||
|
|
||||||
Цена, которую мы платим: `StackMapTable` придётся считать самим (через
|
Цена, которую мы платим: `StackMapTable` придётся считать самим (через
|
||||||
inference-верификатор — [`../stack.md`](../stack.md) §3). Это осознанно.
|
inference-верификатор - [`../stack.md`](../stack.md) §3). Это осознанно.
|
||||||
|
|
||||||
### 4.2 Swing GUI
|
### 4.2 Swing GUI
|
||||||
|
|
||||||
Лишняя точка входа: GUI дублирует CLI и удваивает площадь тестирования.
|
Лишняя точка входа: GUI дублирует CLI и удваивает площадь тестирования.
|
||||||
У нас — только clap.
|
У нас - только clap.
|
||||||
|
|
||||||
### 4.3 `StringDecoderGenerator` как «генератор текста»
|
### 4.3 `StringDecoderGenerator` как «генератор текста»
|
||||||
|
|
||||||
Они генерируют Java-исходник декодера и компилируют. Для нас это означало бы
|
Они генерируют Java-исходник декодера и компилируют. Для нас это означало бы
|
||||||
зависимость от `javac` на этапе обфускации. Наш путь — генерировать
|
зависимость от `javac` на этапе обфускации. Наш путь - генерировать
|
||||||
**байткод напрямую** через наш IR (`noxis-runtime` держит схемы, генератор
|
**байткод напрямую** через наш IR (`noxis-runtime` держит схемы, генератор
|
||||||
собирает `ClassFile`).
|
собирает `ClassFile`).
|
||||||
|
|
||||||
|
|
@ -120,14 +120,14 @@ inference-верификатор — [`../stack.md`](../stack.md) §3). Это
|
||||||
|
|
||||||
## 5. Что проверяем на их smoke-тестах
|
## 5. Что проверяем на их smoke-тестах
|
||||||
|
|
||||||
В `target/` референса лежат готовые проверочные кейсы — их стоит
|
В `target/` референса лежат готовые проверочные кейсы - их стоит
|
||||||
переиспользовать как наш regression-набор:
|
переиспользовать как наш regression-набор:
|
||||||
|
|
||||||
| кейс | что ломает |
|
| кейс | что ломает |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `smoke-safe` | базовый round-trip, `Sample`/`MyMixin` |
|
| `smoke-safe` | базовый round-trip, `Sample`/`MyMixin` |
|
||||||
| `smoke-inheritance` | `Base`/`Child`/`InheritedChild`/`ExternalChild` — семейства virtual и мосты |
|
| `smoke-inheritance` | `Base`/`Child`/`InheritedChild`/`ExternalChild` - семейства virtual и мосты |
|
||||||
| `smoke-flow` | `LoadAll` — загрузка всех классов, ловит `VerifyError` |
|
| `smoke-flow` | `LoadAll` - загрузка всех классов, ловит `VerifyError` |
|
||||||
| `smoke-fields` | переименование полей и статических полей |
|
| `smoke-fields` | переименование полей и статических полей |
|
||||||
| `smoke-aes` | шифрование строк, `RunStrings` сверяет расшифровку |
|
| `smoke-aes` | шифрование строк, `RunStrings` сверяет расшифровку |
|
||||||
| `Probe.java` | точечная проверка одной гипотезы |
|
| `Probe.java` | точечная проверка одной гипотезы |
|
||||||
|
|
@ -144,7 +144,7 @@ inference-верификатор — [`../stack.md`](../stack.md) §3). Это
|
||||||
| keep-сканер | генератор Java-исходника |
|
| keep-сканер | генератор Java-исходника |
|
||||||
| срез подписей, патч ресурсов | единственная схема шифра |
|
| срез подписей, патч ресурсов | единственная схема шифра |
|
||||||
| маппинг в отчёт, честные off-ключи | эмпирическая верификация flow |
|
| маппинг в отчёт, честные off-ключи | эмпирическая верификация flow |
|
||||||
| их smoke-кейсы как regression | — |
|
| их smoke-кейсы как regression | - |
|
||||||
|
|
||||||
Полный перечень провалов защиты, которую они **не** закрывают, — в
|
Полный перечень провалов защиты, которую они **не** закрывают, - в
|
||||||
[`lessons.md`](lessons.md).
|
[`lessons.md`](lessons.md).
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# Стек: почему именно так
|
# Стек: почему именно так
|
||||||
|
|
||||||
Документ фиксирует решения, которые трудно отменить. Для каждого —
|
Документ фиксирует решения, которые трудно отменить. Для каждого -
|
||||||
альтернативы, критерии, цена и что меняет решение.
|
альтернативы, критерии, цена и что меняет решение.
|
||||||
|
|
||||||
## 1. Собственный IR, а не API чужой библиотеки
|
## 1. Собственный IR, а не API чужой библиотеки
|
||||||
|
|
@ -10,12 +10,12 @@ Code, Insn}`, а не с типами парсера.
|
||||||
|
|
||||||
**Почему не «просто использовать готовую библиотеку целиком»:**
|
**Почему не «просто использовать готовую библиотеку целиком»:**
|
||||||
|
|
||||||
- библиотека диктует API — каждый пасс привязывается к чужим миграциям
|
- библиотека диктует API - каждый пасс привязывается к чужим миграциям
|
||||||
мажорной версии (наш лимит «≤4 файлов» легко разрывается «транзитивно»);
|
мажорной версии (наш лимит «≤4 файлов» легко разрывается «транзитивно»);
|
||||||
- у нас есть конкретное требование, которого нет ни в одном парсере:
|
- у нас есть конкретное требование, которого нет ни в одном парсере:
|
||||||
`Insn::Mark(Label)` вместо смещений;
|
`Insn::Mark(Label)` вместо смещений;
|
||||||
- мы не знаём заранее, захочет ли пользователь backend'а ещё одну
|
- мы не знаём заранее, захочет ли пользователь backend'а ещё одну
|
||||||
библиотеку (например, для `StackMapTable`) — изоляция через IR даёт
|
библиотеку (например, для `StackMapTable`) - изоляция через IR даёт
|
||||||
смену backend'а без переписывания пассов.
|
смену backend'а без переписывания пассов.
|
||||||
|
|
||||||
**Что IR даёт на практике:**
|
**Что IR даёт на практике:**
|
||||||
|
|
@ -25,26 +25,26 @@ Code, Insn}`, а не с типами парсера.
|
||||||
| метки вместо смещений | пассы никогда не пересчитывают jump-офсеты |
|
| метки вместо смещений | пассы никогда не пересчитывают jump-офсеты |
|
||||||
| `Attribute::Raw` | round-trip без потерь: неизвестный атрибут возвращается как есть |
|
| `Attribute::Raw` | round-trip без потерь: неизвестный атрибут возвращается как есть |
|
||||||
| `Program` включает ресурсы архива | переименование и патч ресурсов в одном прогоне |
|
| `Program` включает ресурсы архива | переименование и патч ресурсов в одном прогоне |
|
||||||
| одна точка записи | пересчёт смещений, `max_stack` и `StackMapTable` — ровно один раз |
|
| одна точка записи | пересчёт смещений, `max_stack` и `StackMapTable` - ровно один раз |
|
||||||
|
|
||||||
**Цена:** конвертация `model ↔ backend` на границах (чтение и запись) —
|
**Цена:** конвертация `model ↔ backend` на границах (чтение и запись) -
|
||||||
примерно один файл на направление. Дёшево.
|
примерно один файл на направление. Дёшево.
|
||||||
|
|
||||||
## 2. Backend для чтения/записи — `ristretto_classfile`
|
## 2. Backend для чтения/записи - `ristretto_classfile`
|
||||||
|
|
||||||
**Решение:** `format::parse` / `format::write` реализуются поверх
|
**Решение:** `format::parse` / `format::write` реализуются поверх
|
||||||
`ristretto_classfile 0.34`, наш IR остаётся единим API для пассов.
|
`ristretto_classfile 0.34`, наш IR остаётся единим API для пассов.
|
||||||
Реализация — этап P1, до него обе функции возвращают `Unimplemented`.
|
Реализация - этап P1, до него обе функции возвращают `Unimplemented`.
|
||||||
|
|
||||||
### Кандидаты
|
### Кандидаты
|
||||||
|
|
||||||
| крейт | чтение | запись | верификация | активность | загрузки | MSRV |
|
| крейт | чтение | запись | верификация | активность | загрузки | MSRV |
|
||||||
|---|---|---|---|---|---|---|
|
|---|---|---|---|---|---|---|
|
||||||
| **`ristretto_classfile` 0.34** | ✓ | ✓ | ✓ (JVMS §4.10.2) | 2026-09 | 41k | **1.97.1** |
|
| **`ristretto_classfile` 0.34** | ✓ | ✓ | ✓ (JVMS §4.10.2) | 2026-09 | 41k | **1.97.1** |
|
||||||
| `jclassfile` | ✓ | — (parser) | — | 2026-08 | 28k | — |
|
| `jclassfile` | ✓ | - (parser) | - | 2026-08 | 28k | - |
|
||||||
| `classfile-parser` | ✓ | — (parser) | — | 2024-10 (устарел) | 31k | — |
|
| `classfile-parser` | ✓ | - (parser) | - | 2024-10 (устарел) | 31k | - |
|
||||||
| `java-bytecode` | ✓ | ✓ | ? | 2026-10 | 41 | — |
|
| `java-bytecode` | ✓ | ✓ | ? | 2026-10 | 41 | - |
|
||||||
| `jvm-assembler` | — | — | — | **2015, мёртв** | 3k | — |
|
| `jvm-assembler` | - | - | - | **2015, мёртв** | 3k | - |
|
||||||
|
|
||||||
### Критерии и что подтвердил spike
|
### Критерии и что подтвердил spike
|
||||||
|
|
||||||
|
|
@ -56,14 +56,14 @@ MSRV ≤ 1.97.1 (наш тулчейн), лицензия `Apache-2.0 OR MIT` (
|
||||||
|
|
||||||
### Что отвергнуто
|
### Что отвергнуто
|
||||||
|
|
||||||
- **Писать парсер с нуля** — спецификация JVMS §4 по формату классов
|
- **Писать парсер с нуля** - спецификация JVMS §4 по формату классов
|
||||||
плюс MUTF-8, constant pool, модули, records, переполнения — это месяцы
|
плюс MUTF-8, constant pool, модули, records, переполнения - это месяцы
|
||||||
работы ради того, что уже написано и покрыто тестами. Это нарушает KISS
|
работы ради того, что уже написано и покрыто тестами. Это нарушает KISS
|
||||||
и не добавляет ни одной защиты.
|
и не добавляет ни одной защиты.
|
||||||
- **`classfile-parser`, `jclassfile`** — только чтение, значит writer всё
|
- **`classfile-parser`, `jclassfile`** - только чтение, значит writer всё
|
||||||
равно наш; `classfile-parser` не обновлялся с 2024-10.
|
равно наш; `classfile-parser` не обновлялся с 2024-10.
|
||||||
- **`java-bytecode`** — 41 загрузка: у проекта нет инерции и нет экосистемы.
|
- **`java-bytecode`** - 41 загрузка: у проекта нет инерции и нет экосистемы.
|
||||||
- **`jvm-assembler`** — последний коммит в 2015.
|
- **`jvm-assembler`** - последний коммит в 2015.
|
||||||
|
|
||||||
## 3. `StackMapTable`: генерируем сами, пользуемся верификатором
|
## 3. `StackMapTable`: генерируем сами, пользуемся верификатором
|
||||||
|
|
||||||
|
|
@ -80,36 +80,36 @@ MSRV ≤ 1.97.1 (наш тулчейн), лицензия `Apache-2.0 OR MIT` (
|
||||||
→ StackMapTable → запись
|
→ StackMapTable → запись
|
||||||
```
|
```
|
||||||
|
|
||||||
`ristretto_classfile` содержит всё нужное **в исходном виде** — но
|
`ristretto_classfile` содержит всё нужное **в исходном виде** - но
|
||||||
снаружи, к сожалению, отдаёт не всё (см. результат спайка ниже):
|
снаружи, к сожалению, отдаёт не всё (см. результат спайка ниже):
|
||||||
|
|
||||||
- `verifiers/bytecode/inference.rs` — итеративный dataflow, LUB, фикспоинт;
|
- `verifiers/bytecode/inference.rs` - итеративный dataflow, LUB, фикспоинт;
|
||||||
- `verifiers/bytecode/control_flow.rs` — `compute_successors`, worklist;
|
- `verifiers/bytecode/control_flow.rs` - `compute_successors`, worklist;
|
||||||
- `verifiers/bytecode/stackmap.rs` — декодер/кэш `StackMapTable`;
|
- `verifiers/bytecode/stackmap.rs` - декодер/кэш `StackMapTable`;
|
||||||
- `attributes/{max_stack,max_locals,instruction/stack_effect}.rs` —
|
- `attributes/{max_stack,max_locals,instruction/stack_effect}.rs` -
|
||||||
пересчёт `max_stack`/`max_locals` из инструкций;
|
пересчёт `max_stack`/`max_locals` из инструкций;
|
||||||
- `attributes/{stack_frame,verification_type}.rs` — типы для записи.
|
- `attributes/{stack_frame,verification_type}.rs` - типы для записи.
|
||||||
|
|
||||||
**Почему это лучше, чем свои кадры:** это ровно та часть JVMS, в которой
|
**Почему это лучше, чем свои кадры:** это ровно та часть JVMS, в которой
|
||||||
ошибка невидима в исходниках и всплывает только на `VerifyError` в чужом
|
ошибка невидима в исходниках и всплывает только на `VerifyError` в чужом
|
||||||
рантайме. Писать такой верификатор — самая дорогая и самая опасная часть
|
рантайме. Писать такой верификатор - самая дорогая и самая опасная часть
|
||||||
всего проекта.
|
всего проекта.
|
||||||
|
|
||||||
**Альтернативы, которые отвергнуты:**
|
**Альтернативы, которые отвергнуты:**
|
||||||
|
|
||||||
- **Понижать версию класса до 49 и выкинуть `StackMapTable`** — ломает
|
- **Понижать версию класса до 49 и выкинуть `StackMapTable`** - ломает
|
||||||
`invokedynamic`/лямбды и включает дорогой fallback-верификатор; на Java 9+
|
`invokedynamic`/лямбды и включает дорогой fallback-верификатор; на Java 9+
|
||||||
это заметно и противоречит цели.
|
это заметно и противоречит цели.
|
||||||
- **Не делать Flow вообще** — лишает технику, которая даёт ★★★★ к
|
- **Не делать Flow вообще** - лишает технику, которая даёт ★★★★ к
|
||||||
читаемости. Не вариант: вычислим кадры.
|
читаемости. Не вариант: вычислим кадры.
|
||||||
|
|
||||||
**Результат spike'а (`tests/spike_frames.rs`) — главный вывод раздела:**
|
**Результат spike'а (`tests/spike_frames.rs`) - главный вывод раздела:**
|
||||||
|
|
||||||
| что проверяли | что получили |
|
| что проверяли | что получили |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `verify_class` + `FallbackStrategy::AlwaysInference` на классе с ветвлением и без `StackMapTable` | ✅ `success`, `path_used == Inference` — движок вывода типов работает |
|
| `verify_class` + `FallbackStrategy::AlwaysInference` на классе с ветвлением и без `StackMapTable` | ✅ `success`, `path_used == Inference` - движок вывода типов работает |
|
||||||
| `FallbackStrategy::Strict` на том же классе | ✅ `Err("StackMapTable required ... with control flow")` — требование v50+ реально проверяется |
|
| `FallbackStrategy::Strict` на том же классе | ✅ `Err("StackMapTable required ... with control flow")` - требование v50+ реально проверяется |
|
||||||
| `StackMapTable` с кадром **не на том** сдвиге | ✅ `Err("Merge point at offset N requires StackMapTable frame")` — ошибка локализована точно |
|
| `StackMapTable` с кадром **не на том** сдвиге | ✅ `Err("Merge point at offset N requires StackMapTable frame")` - ошибка локализована точно |
|
||||||
| получить сам кадр из крейта | ❌ **нельзя** |
|
| получить сам кадр из крейта | ❌ **нельзя** |
|
||||||
|
|
||||||
Причина последней строки: в `ristretto_classfile/src/verifiers/bytecode.rs`
|
Причина последней строки: в `ristretto_classfile/src/verifiers/bytecode.rs`
|
||||||
|
|
@ -118,7 +118,7 @@ MSRV ≤ 1.97.1 (наш тулчейн), лицензия `Apache-2.0 OR MIT` (
|
||||||
`verify_method`, `VerificationResult`, `VerifierConfig` и
|
`verify_method`, `VerificationResult`, `VerifierConfig` и
|
||||||
`VerificationContext`. Диагностика в `VerificationResult.diagnostic`
|
`VerificationContext`. Диагностика в `VerificationResult.diagnostic`
|
||||||
заполняется лишь на отдельных путях отказа, а в строгом режиме крейт
|
заполняется лишь на отдельных путях отказа, а в строгом режиме крейт
|
||||||
возвращает `Err(VerifyError)` целиком — то есть **ожидаемый кадр до нас
|
возвращает `Err(VerifyError)` целиком - то есть **ожидаемый кадр до нас
|
||||||
не доходит**, только его смещение.
|
не доходит**, только его смещение.
|
||||||
|
|
||||||
**Что это меняет для P1.** Кадры считаем сами, а крейт используем как
|
**Что это меняет для P1.** Кадры считаем сами, а крейт используем как
|
||||||
|
|
@ -132,7 +132,7 @@ MSRV ≤ 1.97.1 (наш тулчейн), лицензия `Apache-2.0 OR MIT` (
|
||||||
|
|
||||||
Три способа получить сами кадры, в порядке предпочтения:
|
Три способа получить сами кадры, в порядке предпочтения:
|
||||||
|
|
||||||
1. ~~**Вендор `verifiers::bytecode::inference`.**~~ — **отвергнут после
|
1. ~~**Вендор `verifiers::bytecode::inference`.**~~ - **отвергнут после
|
||||||
оценки объёма.** Один модуль не оторвать: в `inference.rs` 14 ссылок
|
оценки объёма.** Один модуль не оторвать: в `inference.rs` 14 ссылок
|
||||||
`crate::`, включая private-модули `frame`, `type_system`, `handlers`,
|
`crate::`, включая private-модули `frame`, `type_system`, `handlers`,
|
||||||
`control_flow`, `diagnostics`. Вендорить пришлось бы весь
|
`control_flow`, `diagnostics`. Вендорить пришлось бы весь
|
||||||
|
|
@ -140,19 +140,19 @@ MSRV ≤ 1.97.1 (наш тулчейн), лицензия `Apache-2.0 OR MIT` (
|
||||||
`src/`**, 2.3 MB) ради одной патчи `pub(crate)` → `pub`. Чужой код
|
`src/`**, 2.3 MB) ради одной патчи `pub(crate)` → `pub`. Чужой код
|
||||||
становится третьей по объёму частью репозитория, отстаёт от апстрима и
|
становится третьей по объёму частью репозитория, отстаёт от апстрима и
|
||||||
не проходит наши правила (250 строк / 4 файла).
|
не проходит наши правила (250 строк / 4 файла).
|
||||||
2. **Свой вывод на своём IR — принято.** Кадры считаются по `noxis::model`,
|
2. **Свой вывод на своём IR - принято.** Кадры считаются по `noxis::model`,
|
||||||
а не по ристретто: `format::parse` → IR → пассы → `compute_frames(IR)` →
|
а не по ристретто: `format::parse` → IR → пассы → `compute_frames(IR)` →
|
||||||
`Attribute::StackMapTable` → `format::write`. Ристретто остаётся
|
`Attribute::StackMapTable` → `format::write`. Ристретто остаётся
|
||||||
**парсером, писателем и приёмочным тестом** из crates.io, без форка.
|
**парсером, писателем и приёмочным тестом** из crates.io, без форка.
|
||||||
3. ~~Итеративная починка через `diagnostic`~~ — **отвергнута спайком**: поля
|
3. ~~Итеративная починка через `diagnostic`~~ - **отвергнута спайком**: поля
|
||||||
`expected`/`pre_frame` в этом пути недостижимы.
|
`expected`/`pre_frame` в этом пути недостижимы.
|
||||||
|
|
||||||
**Почему 2, а не 1.** Наш dataflow живёт на типах, которые и так нужны
|
**Почему 2, а не 1.** Наш dataflow живёт на типах, которые и так нужны
|
||||||
пассам: `Insn`, метки вместо смещений, иерархия классов из
|
пассам: `Insn`, метки вместо смещений, иерархия классов из
|
||||||
`noxis-analysis::hierarchy` для LUB на merge-точках. Кадры — это ровно
|
`noxis-analysis::hierarchy` для LUB на merge-точках. Кадры - это ровно
|
||||||
те же инструкции, что уже переписывает `rename::rewrite`, поэтому
|
те же инструкции, что уже переписывает `rename::rewrite`, поэтому
|
||||||
промежуточная конвертация `model → Instruction[] → model` (требование
|
промежуточная конвертация `model → Instruction[] → model` (требование
|
||||||
вендора) добавляет ещё один источник расхождений. Цена — своя реализация
|
вендора) добавляет ещё один источник расхождений. Цена - своя реализация
|
||||||
(~1 000–1 500 строк: границы блоков, worklist, merge, LUB, эмиссия
|
(~1 000–1 500 строк: границы блоков, worklist, merge, LUB, эмиссия
|
||||||
`StackFrame`), но она покрывается двумя независимыми судьями:
|
`StackFrame`), но она покрывается двумя независимыми судьями:
|
||||||
|
|
||||||
|
|
@ -164,7 +164,7 @@ MSRV ≤ 1.97.1 (наш тулчейн), лицензия `Apache-2.0 OR MIT` (
|
||||||
java -Xverify:all jar
|
java -Xverify:all jar
|
||||||
```
|
```
|
||||||
|
|
||||||
Второй судья — ристретто `verify_class` со стратегией `Strict` — ловит
|
Второй судья - ристретто `verify_class` со стратегией `Strict` - ловит
|
||||||
ровно тот класс ошибок, ради которого вендор и рассматривался: неверный
|
ровно тот класс ошибок, ради которого вендор и рассматривался: неверный
|
||||||
сдвиг кадра, пропущенная merge-точка, неверный тип.
|
сдвиг кадра, пропущенная merge-точка, неверный тип.
|
||||||
|
|
||||||
|
|
@ -173,12 +173,12 @@ MSRV ≤ 1.97.1 (наш тулчейн), лицензия `Apache-2.0 OR MIT` (
|
||||||
|
|
||||||
## 4. Генерация рантайма, а не вшитая таблица
|
## 4. Генерация рантайма, а не вшитая таблица
|
||||||
|
|
||||||
**Решение:** `noxis-runtime` — это **заготовки Rust-кода**, из которых
|
**Решение:** `noxis-runtime` - это **заготовки Rust-кода**, из которых
|
||||||
собирается класс-декодер (P2), а не готовый бинарь.
|
собирается класс-декодер (P2), а не готовый бинарь.
|
||||||
|
|
||||||
**Почему:** ABI декодера — `d(Ljava/lang/String;I)Ljava/lang/String;`,
|
**Почему:** ABI декодера - `d(Ljava/lang/String;I)Ljava/lang/String;`,
|
||||||
но схем шифрования несколько (`xor-splitmix`, `xtea-ctr`) и их число
|
но схем шифрования несколько (`xor-splitmix`, `xtea-ctr`) и их число
|
||||||
растёт. Константная таблица селекторов рядом с текстом — ровно та ошибка,
|
растёт. Константная таблица селекторов рядом с текстом - ровно та ошибка,
|
||||||
которую мы разбирали (`info/evo-weaknesses.md` §1): **ключ рядом с
|
которую мы разбирали (`info/evo-weaknesses.md` §1): **ключ рядом с
|
||||||
шифротекстом**. Генератор выбирает схемы на этапе сборки артефакта и
|
шифротекстом**. Генератор выбирает схемы на этапе сборки артефакта и
|
||||||
кодирует выбор в самих вызовах, а мастер-секрет выводится в рантайме.
|
кодирует выбор в самих вызовах, а мастер-секрет выводится в рантайме.
|
||||||
|
|
@ -193,7 +193,7 @@ MSRV ≤ 1.97.1 (наш тулчейн), лицензия `Apache-2.0 OR MIT` (
|
||||||
байт-в-байт одинаковый jar. Это свойство, а не удобство: без него
|
байт-в-байт одинаковый jar. Это свойство, а не удобство: без него
|
||||||
воспроизводимая сборка, откат и сравнение версий невозможны.
|
воспроизводимая сборка, откат и сравнение версий невозможны.
|
||||||
- **Доменное разделение:** `ctx.rng("rename/classes")`,
|
- **Доменное разделение:** `ctx.rng("rename/classes")`,
|
||||||
`ctx.rng("noise/junk")` — сид деривается из имени домена, поэтому
|
`ctx.rng("noise/junk")` - сид деривается из имени домена, поэтому
|
||||||
случайности разных пассов не статистически коррелированы.
|
случайности разных пассов не статистически коррелированы.
|
||||||
- **Зависимость `rand` тянет** OS-энтропию и меняющееся поведение между
|
- **Зависимость `rand` тянет** OS-энтропию и меняющееся поведение между
|
||||||
версиями. Нам нужен ровно обратный эффект.
|
версиями. Нам нужен ровно обратный эффект.
|
||||||
|
|
@ -201,17 +201,17 @@ MSRV ≤ 1.97.1 (наш тулчейн), лицензия `Apache-2.0 OR MIT` (
|
||||||
## 6. ZIP: `zip`
|
## 6. ZIP: `zip`
|
||||||
|
|
||||||
Критерии: чтение, запись, **сохранение порядка и сжатия**, контроль
|
Критерии: чтение, запись, **сохранение порядка и сжатия**, контроль
|
||||||
дубликатов. Берём `zip` (стабильный релиз, без `time`/`aes`/`zstd` —
|
дубликатов. Берём `zip` (стабильный релиз, без `time`/`aes`/`zstd` -
|
||||||
лишние фичи только раздувают бинарь; `default-features = false,
|
лишние фичи только раздувают бинарь; `default-features = false,
|
||||||
features = ["deflate"]`). Подпись (`*.SF/*.RSA/*.DSA/*.EC`) вырезаем на
|
features = ["deflate"]`). Подпись (`*.SF/*.RSA/*.DSA/*.EC`) вырезаем на
|
||||||
уровне записей архива — после перепаковки она всё равно невалидна.
|
уровне записей архива - после перепаковки она всё равно невалидна.
|
||||||
|
|
||||||
Альтернатива `zip6` — форк того же кода; при равном API он даёт меньше
|
Альтернатива `zip6` - форк того же кода; при равном API он даёт меньше
|
||||||
тестов и меньшую инерцию. Нет причины платить за это.
|
тестов и меньшую инерцию. Нет причины платить за это.
|
||||||
|
|
||||||
**MSRV воркспейса поднят до 1.97** — это ровно наш тулчейн (rustc 1.97.1)
|
**MSRV воркспейса поднят до 1.97** - это ровно наш тулчейн (rustc 1.97.1)
|
||||||
и минимум `ristretto_classfile 0.34`. Заявлять 1.85, когда зависимость
|
и минимум `ristretto_classfile 0.34`. Заявлять 1.85, когда зависимость
|
||||||
требует 1.97, — враньё в `Cargo.toml`.
|
требует 1.97, - враньё в `Cargo.toml`.
|
||||||
|
|
||||||
## 7. CLI: `clap` (derive)
|
## 7. CLI: `clap` (derive)
|
||||||
|
|
||||||
|
|
@ -221,11 +221,11 @@ features = ["deflate"]`). Подпись (`*.SF/*.RSA/*.DSA/*.EC`) выреза
|
||||||
|
|
||||||
## 8. Верификация: `java -Xverify:all` первым делом
|
## 8. Верификация: `java -Xverify:all` первым делом
|
||||||
|
|
||||||
Единственная честная проверка «я ничего не сломал» — запустить настоящую
|
Единственная честная проверка «я ничего не сломал» - запустить настоящую
|
||||||
JVM. `noxis-verify` гоняет `java -Xverify:all` на обфусцированном jar
|
JVM. `noxis-verify` гоняет `java -Xverify:all` на обфусцированном jar
|
||||||
и по red-паттернам ищет дыры в защите.
|
и по red-паттернам ищет дыры в защите.
|
||||||
|
|
||||||
**Запасной вариант, если JDK не гарантирован:** `ristretto_vm` —
|
**Запасной вариант, если JDK не гарантирован:** `ristretto_vm` -
|
||||||
встраиваемый JVM на Rust из того же репозитория. Для нас это означает
|
встраиваемый JVM на Rust из того же репозитория. Для нас это означает
|
||||||
`noxis-verify` без внешних зависимостей. Стоит проверить в P3, пока
|
`noxis-verify` без внешних зависимостей. Стоит проверить в P3, пока
|
||||||
используем системный `java` (в окружении есть OpenJDK 21).
|
используем системный `java` (в окружении есть OpenJDK 21).
|
||||||
|
|
|
||||||
|
|
@ -1,43 +1,43 @@
|
||||||
# Как работает: техники, плюсы, минусы
|
# Как работает: техники, плюсы, минусы
|
||||||
|
|
||||||
Каждая техника описана одинаково: **как работает → что даёт → что стоит /
|
Каждая техника описана одинаково: **как работает → что даёт → что стоит /
|
||||||
где ломается**. Цена указана честно: обфускация — это всегда обмен
|
где ломается**. Цена указана честно: обфускация - это всегда обмен
|
||||||
читаемости и времени против аналитика.
|
читаемости и времени против аналитика.
|
||||||
|
|
||||||
Итоговая таблица — в конце.
|
Итоговая таблица - в конце.
|
||||||
|
|
||||||
## 1. Переименование (`Rename`)
|
## 1. Переименование (`Rename`)
|
||||||
|
|
||||||
**Как работает.** Классы получают случайные имена в случайных пакетах
|
**Как работает.** Классы получают случайные имена в случайных пакетах
|
||||||
(или в исходном пакете, если `spread_packages = false`). Поля и методы
|
(или в исходном пакете, если `spread_packages = false`). Поля и методы
|
||||||
переименовываются, если `method_rename_safety` считает их безопасными
|
переименовываются, если `method_rename_safety` считает их безопасными
|
||||||
(private/static/final — сразу; virtual — только «семейством» на всю
|
(private/static/final - сразу; virtual - только «семейством» на всю
|
||||||
иерархию, чтобы не разъединить переопределения и мостовые методы).
|
иерархию, чтобы не разъединить переопределения и мостовые методы).
|
||||||
Затем переписываются дескрипторы, `LDC`-классы, вызовы, поля,
|
Затем переписываются дескрипторы, `LDC`-классы, вызовы, поля,
|
||||||
исключительная таблица, `Signature`/`Exceptions`/`InnerClasses`.
|
исключительная таблица, `Signature`/`Exceptions`/`InnerClasses`.
|
||||||
|
|
||||||
**Плюсы.**
|
**Плюсы.**
|
||||||
- Самая дешёвая и самая отдающая техника: читает `javap -p` любой анализатор.
|
- Самая дешёвая и самая отдающая техника: читает `javap -p` любой анализатор.
|
||||||
- Не меняет семантику — риск сломать код минимален.
|
- Не меняет семантику - риск сломать код минимален.
|
||||||
- Маппиинг пишется в отчёт → stack trace'и переводятся обратно.
|
- Маппиинг пишется в отчёт → stack trace'и переводятся обратно.
|
||||||
|
|
||||||
**Минусы / цена.**
|
**Минусы / цена.**
|
||||||
- Keep-список обязателен: рефлексия по имени (`Class.forName("pkg.C")`),
|
- Keep-список обязателен: рефлексия по имени (`Class.forName("pkg.C")`),
|
||||||
JNI, `serialVersionUID`, сериализация, entrypoint'ы. Пропустил — получил
|
JNI, `serialVersionUID`, сериализация, entrypoint'ы. Пропустил - получил
|
||||||
`ClassNotFoundException` в рантайме.
|
`ClassNotFoundException` в рантайме.
|
||||||
- Рефлексия через строки мы ловим автоматически (анализ `Class.forName`,
|
- Рефлексия через строки мы ловим автоматически (анализ `Class.forName`,
|
||||||
`getMethod`/`getField` и т.п.), но динамическую конкатенацию — нет.
|
`getMethod`/`getField` и т.п.), но динамическую конкатенацию - нет.
|
||||||
- JSON-(де)сериализация по именам полей (Gson/Jackson/`Codec`) не ловится:
|
- JSON-(де)сериализация по именам полей (Gson/Jackson/`Codec`) не ловится:
|
||||||
биндинг идёт через `X.class`-литерал, не строку — см. `todo.md` §2.
|
биндинг идёт через `X.class`-литерал, не строку - см. `todo.md` §2.
|
||||||
- Для библиотек `rename_public_members = false` по умолчанию: менять
|
- Для библиотек `rename_public_members = false` по умолчанию: менять
|
||||||
публичный API библиотеки — сломать всех потребителей.
|
публичный API библиотеки - сломать всех потребителей.
|
||||||
|
|
||||||
Шесть стилей имён (`ShortLatin`, `IlO0`, `Unicode`, `MixedLatin`,
|
Шесть стилей имён (`ShortLatin`, `IlO0`, `Unicode`, `MixedLatin`,
|
||||||
`DecoyWords`, `Plausible`) — компромисс по-разному: `IlO0`/`Unicode` явно
|
`DecoyWords`, `Plausible`) - компромисс по-разному: `IlO0`/`Unicode` явно
|
||||||
сигнализируют «это обфускация», `Plausible` маскируется под обычный
|
сигнализируют «это обфускация», `Plausible` маскируется под обычный
|
||||||
прикладной код (`TextureCache`, `resolveToken`), ценой того, что при
|
прикладной код (`TextureCache`, `resolveToken`), ценой того, что при
|
||||||
ручном чтении он менее заметно отличается от настоящей логики, чем
|
ручном чтении он менее заметно отличается от настоящей логики, чем
|
||||||
намеренный мусор — скрыть обфускацию и скрыть её цену ценой чуть большей
|
намеренный мусор - скрыть обфускацию и скрыть её цену ценой чуть большей
|
||||||
путаницы для собственной команды при отладке маппингов.
|
путаницы для собственной команды при отладке маппингов.
|
||||||
|
|
||||||
**Статус: готово.**
|
**Статус: готово.**
|
||||||
|
|
@ -51,13 +51,13 @@
|
||||||
уникален на строку, MAC ловит порчу.
|
уникален на строку, MAC ловит порчу.
|
||||||
|
|
||||||
**Плюсы.**
|
**Плюсы.**
|
||||||
- Строки — главный источник логики (`"admin"`, URL, SQL, regex).
|
- Строки - главный источник логики (`"admin"`, URL, SQL, regex).
|
||||||
- Ротация схем: один скрипт деобфускации не покрывает весь jar.
|
- Ротация схем: один скрипт деобфускации не покрывает весь jar.
|
||||||
- MAC: подмена шифротекста не «молча» портит текст, а падает.
|
- MAC: подмена шифротекста не «молча» портит текст, а падает.
|
||||||
|
|
||||||
**Минусы / цена.**
|
**Минусы / цена.**
|
||||||
- Декодер добавляет вызов на каждый доступ к строке → оверхед в рантайме.
|
- Декодер добавляет вызов на каждый доступ к строке → оверхед в рантайме.
|
||||||
- Целевой анализ декодера всё равно раскроет текст; задача — поднять цену,
|
- Целевой анализ декодера всё равно раскроет текст; задача - поднять цену,
|
||||||
а не сделать невозможным.
|
а не сделать невозможным.
|
||||||
- Мастер-секрет собирается в декодере из двух частей (`k1 ^ k2`).
|
- Мастер-секрет собирается в декодере из двух частей (`k1 ^ k2`).
|
||||||
|
|
||||||
|
|
@ -68,7 +68,7 @@
|
||||||
|
|
||||||
**Как работает.** Числовая константа не хранится как есть, а собирается из
|
**Как работает.** Числовая константа не хранится как есть, а собирается из
|
||||||
разных выражений: `x ^ k`, `a + b`, `(a << 1) + c`. Плюс настоящие
|
разных выражений: `x ^ k`, `a + b`, `(a << 1) + c`. Плюс настоящие
|
||||||
opaque-предикаты — условия, истинность которых зависит от инварианта
|
opaque-предикаты - условия, истинность которых зависит от инварианта
|
||||||
(например, `i*(i+1) % 2 == 0`), а не от константы.
|
(например, `i*(i+1) % 2 == 0`), а не от константы.
|
||||||
|
|
||||||
**Плюсы.**
|
**Плюсы.**
|
||||||
|
|
@ -80,7 +80,7 @@ opaque-предикаты — условия, истинность которы
|
||||||
(`executor` для `LCONST/ICONST`-цепочек).
|
(`executor` для `LCONST/ICONST`-цепочек).
|
||||||
- Развдувание кода растёт по квадрату от числа вставок.
|
- Развдувание кода растёт по квадрату от числа вставок.
|
||||||
- Шаблонный opaque-предикат (всегда один и тот же `A+B`) узнаётся
|
- Шаблонный opaque-предикат (всегда один и тот же `A+B`) узнаётся
|
||||||
паттерном — отсюда требование нескольких схем.
|
паттерном - отсюда требование нескольких схем.
|
||||||
|
|
||||||
**Статус: готово** для `int`/`long` (`xor`/`add`/`shift` + opaque-предикаты);
|
**Статус: готово** для `int`/`long` (`xor`/`add`/`shift` + opaque-предикаты);
|
||||||
`float`/`double` не трогаются.
|
`float`/`double` не трогаются.
|
||||||
|
|
@ -98,22 +98,22 @@ opaque-предикаты — условия, истинность которы
|
||||||
|
|
||||||
**Минусы / цена.**
|
**Минусы / цена.**
|
||||||
- **Самая дорогая и самая опасная техника.** Требует пересчёта
|
- **Самая дорогая и самая опасная техника.** Требует пересчёта
|
||||||
`StackMapTable` для классов версии ≥ 50 — иначе JVM не грузит класс.
|
`StackMapTable` для классов версии ≥ 50 - иначе JVM не грузит класс.
|
||||||
- Ломает профилировщики, coverage-инструменты и отладку.
|
- Ломает профилировщики, coverage-инструменты и отладку.
|
||||||
- Нужен верификатор: любая ошибка в CFG — это `VerifyError` в рантайме.
|
- Нужен верификатор: любая ошибка в CFG - это `VerifyError` в рантайме.
|
||||||
- Шаблонная схема (все методы перестроены одинаково) детектируется
|
- Шаблонная схема (все методы перестроены одинаково) детектируется
|
||||||
гистограммой длины блоков.
|
гистограммой длины блоков.
|
||||||
|
|
||||||
**Статус: готов** (`noxis-passes/src/flow/`). Одна схема на метод (ротация
|
**Статус: готов** (`noxis-passes/src/flow/`). Одна схема на метод (ротация
|
||||||
через PRNG) плюс нарезка тела `goto`-сплайсами во всех схемах; кадр
|
через PRNG) плюс нарезка тела `goto`-сплайсами во всех схемах; кадр
|
||||||
`StackMapTable` пересчитывается после каждой перестройки. Приёмка —
|
`StackMapTable` пересчитывается после каждой перестройки. Приёмка -
|
||||||
настоящий `java -Xverify:all` и исполнение под JVM (`calc` печатает 45
|
настоящий `java -Xverify:all` и исполнение под JVM (`calc` печатает 45
|
||||||
после всех схем). Ограничения: `<init>` пропускается (форма
|
после всех схем). Ограничения: `<init>` пропускается (форма
|
||||||
super-call-first); методы с `jsr`/`ret`, короче 3 инструкций и длиннее
|
super-call-first); методы с `jsr`/`ret`, короче 3 инструкций и длиннее
|
||||||
`max_insns` пропускаются; `encrypt_block_bodies` добавляет opaque-guard
|
`max_insns` пропускаются; `encrypt_block_bodies` добавляет opaque-guard
|
||||||
на входе. Два правила, выученных на крови (см. `StackMapTable`):
|
на входе. Два правила, выученных на крови (см. `StackMapTable`):
|
||||||
вставленная метка не должна делить байт-смещение с чужой (иначе кадры
|
вставленная метка не должна делить байт-смещение с чужой (иначе кадры
|
||||||
мерджатся из несовместимых состояний — после каждой такой метки `nop`),
|
мерджатся из несовместимых состояний - после каждой такой метки `nop`),
|
||||||
а инструкция после терминальной обязана иметь свой кадр (поэтому dead-блоки
|
а инструкция после терминальной обязана иметь свой кадр (поэтому dead-блоки
|
||||||
входят через opaque-ветвь и кончаются `athrow`, а не висят недостижимыми).
|
входят через opaque-ветвь и кончаются `athrow`, а не висят недостижимыми).
|
||||||
|
|
||||||
|
|
@ -129,32 +129,32 @@ super-call-first); методы с `jsr`/`ret`, короче 3 инструкц
|
||||||
|
|
||||||
**Минусы / цена.**
|
**Минусы / цена.**
|
||||||
- Reachability-анализ отсеивает junk мгновенно: класс, на который никто
|
- Reachability-анализ отсеивает junk мгновенно: класс, на который никто
|
||||||
не ссылается, — мёртвый груз. Отсюда обязательное `weave_reachable`.
|
не ссылается, - мёртвый груз. Отсюда обязательное `weave_reachable`.
|
||||||
- Пустые методы/тела — признак, который ищут первым.
|
- Пустые методы/тела - признак, который ищут первым.
|
||||||
- Раздувание jar.
|
- Раздувание jar.
|
||||||
|
|
||||||
**Статус: готово** (junk-классы + `weave_reachable`; junk-аннотации
|
**Статус: готово** (junk-классы + `weave_reachable`; junk-аннотации
|
||||||
пропускаются — сырые атрибуты не несут свежие индексы пула).
|
пропускаются - сырые атрибуты не несут свежие индексы пула).
|
||||||
|
|
||||||
## 6. Отладочная информация (`DebugInfo`)
|
## 6. Отладочная информация (`DebugInfo`)
|
||||||
|
|
||||||
**Как работает.** Strip `LineNumberTable`, `LocalVariableTable`; подмена
|
**Как работает.** Strip `LineNumberTable`, `LocalVariableTable`; подмена
|
||||||
`SourceFile` на ложное имя; опционально strip `Signature`/`InnerClasses`.
|
`SourceFile` на ложное имя; опционально strip `Signature`/`InnerClasses`.
|
||||||
Плюс *ложная* отладочная информация — чужие имена переменных и номера строк.
|
Плюс *ложная* отладочная информация - чужие имена переменных и номера строк.
|
||||||
|
|
||||||
**Плюсы.**
|
**Плюсы.**
|
||||||
- Практически бесплатно.
|
- Практически бесплатно.
|
||||||
- Восстанавливает исходные имена переменных и файлов — прямая утечка.
|
- Восстанавливает исходные имена переменных и файлов - прямая утечка.
|
||||||
|
|
||||||
**Минусы / цена.**
|
**Минусы / цена.**
|
||||||
- Убивает нормальные stack trace'и (поэтому маппинги в отчёте обязательны).
|
- Убивает нормальные stack trace'и (поэтому маппинги в отчёте обязательны).
|
||||||
- Слишком «чистый» класс (`LineNumberTable` есть, а `SourceFile` = `X.java`,
|
- Слишком «чистый» класс (`LineNumberTable` есть, а `SourceFile` = `X.java`,
|
||||||
которого нет) — тоже признак.
|
которого нет) - тоже признак.
|
||||||
|
|
||||||
**Статус: готово** (strip + `spoof_source_file`, детерминированная подмена
|
**Статус: готово** (strip + `spoof_source_file`, детерминированная подмена
|
||||||
от мастер-сида). Не реализовано: `fake_line_numbers` (ложные номера строк,
|
от мастер-сида). Не реализовано: `fake_line_numbers` (ложные номера строк,
|
||||||
а не только `SourceFile`) и `strip_source_file` (полное удаление вместо
|
а не только `SourceFile`) и `strip_source_file` (полное удаление вместо
|
||||||
подмены) — см. [`todo.md`](../todo.md) §3.6.
|
подмены) - см. [`todo.md`](../todo.md) §3.6.
|
||||||
|
|
||||||
## 7. Ресурсы (`Resources`)
|
## 7. Ресурсы (`Resources`)
|
||||||
|
|
||||||
|
|
@ -164,7 +164,7 @@ super-call-first); методы с `jsr`/`ret`, короче 3 инструкц
|
||||||
|
|
||||||
**Плюсы.**
|
**Плюсы.**
|
||||||
- Без этого обфускация Fabric-мода просто не стартует.
|
- Без этого обфускация Fabric-мода просто не стартует.
|
||||||
- Убранные подписи — иначе jar считается подделанным.
|
- Убранные подписи - иначе jar считается подделанным.
|
||||||
|
|
||||||
**Минусы / цена.**
|
**Минусы / цена.**
|
||||||
- Патч только текстовых ресурсов: бинарные (картинки, классы в `versions/`)
|
- Патч только текстовых ресурсов: бинарные (картинки, классы в `versions/`)
|
||||||
|
|
@ -189,6 +189,6 @@ super-call-first); методы с `jsr`/`ret`, короче 3 инструкц
|
||||||
| Flow | ★★★★ | **высокий** | высокая | план, кадры готовы |
|
| Flow | ★★★★ | **высокий** | высокая | план, кадры готовы |
|
||||||
|
|
||||||
Вывод, который стоит держать в голове: **Rename + Resources + DebugInfo**
|
Вывод, который стоит держать в голове: **Rename + Resources + DebugInfo**
|
||||||
дают большую часть защиты за небольшую цену. Flow — единственная техника,
|
дают большую часть защиты за небольшую цену. Flow - единственная техника,
|
||||||
которая может сломать рантайм, и она же единственная, чья отдача
|
которая может сломать рантайм, и она же единственная, чья отдача
|
||||||
не покрывает риск без верификатора.
|
не покрывает риск без верификатора.
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
# info/ — материал по обфускации EvoVis (для нашего обфускатора)
|
# info/ - материал по обфускации EvoVis (для нашего обфускатора)
|
||||||
|
|
||||||
Источник: `LoVisual/ref/evo/` (реверс EvoVis / EvolutionDLC, Minecraft-клиент).
|
Источник: `LoVisual/ref/evo/` (реверс EvoVis / EvolutionDLC, Minecraft-клиент).
|
||||||
Сюда перенесено только то, что касается защиты/обфускации кода.
|
Сюда перенесено только то, что касается защиты/обфускации кода.
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
|---|---|
|
|---|---|
|
||||||
| `evo-obfuscation.md` | базовое описание трёх уровней обфускации (перенесено as-is из `ref/evo/obfuscation.md`) |
|
| `evo-obfuscation.md` | базовое описание трёх уровней обфускации (перенесено as-is из `ref/evo/obfuscation.md`) |
|
||||||
| `evo-obfuscation-full.md` | **сводный разбор**: все уровни + .NET-лаунчер + натив/антиотладка, с кодом и проверенными цифрами |
|
| `evo-obfuscation-full.md` | **сводный разбор**: все уровни + .NET-лаунчер + натив/антиотладка, с кодом и проверенными цифрами |
|
||||||
| `evo-weaknesses.md` | где защита провалилась при разборе — чего нам делать нельзя (+ опровержения) |
|
| `evo-weaknesses.md` | где защита провалилась при разборе - чего нам делать нельзя (+ опровержения) |
|
||||||
| `evo-improvements.md` | что сделать в своём обфускаторе, чтобы этих дыр не было |
|
| `evo-improvements.md` | что сделать в своём обфускаторе, чтобы этих дыр не было |
|
||||||
| `evo-payload-pipeline.md` | доставка: AES-256-GCM payload + java-agent + подменённый JVM |
|
| `evo-payload-pipeline.md` | доставка: AES-256-GCM payload + java-agent + подменённый JVM |
|
||||||
| `evo-native-reverse.md` | реверс `evoguard.dll` / `java.dll`: HMAC-шейры, антиотладка, порча ключа |
|
| `evo-native-reverse.md` | реверс `evoguard.dll` / `java.dll`: HMAC-шейры, антиотладка, порча ключа |
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
# Как сделать лучше: план улучшений для нашего обфускатора
|
# Как сделать лучше: план улучшений для нашего обфускатора
|
||||||
|
|
||||||
Привязка: референс — EvoVis (`evo-obfuscation-full.md`), провалы — `evo-weaknesses.md`.
|
Привязка: референс - EvoVis (`evo-obfuscation-full.md`), провалы - `evo-weaknesses.md`.
|
||||||
Ниже — что делаем иначе, в порядке отдачи/цены. Для каждой техники — уровень угрозы,
|
Ниже - что делаем иначе, в порядке отдачи/цены. Для каждой техники - уровень угрозы,
|
||||||
которую она закрывает (скрипт-деобфускатор / статический анализ / ручной реверс).
|
которую она закрывает (скрипт-деобфускатор / статический анализ / ручной реверс).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -14,20 +14,20 @@
|
||||||
- Индивидуальный алфавит на каждый символ (см. B1), запрет ключевых слов и
|
- Индивидуальный алфавит на каждый символ (см. B1), запрет ключевых слов и
|
||||||
одинаковых имён в одном scope.
|
одинаковых имён в одном scope.
|
||||||
- Никаких «readable» ярлыков: у EvoVis `@rn(name=…)` и `description` отдали каталог
|
- Никаких «readable» ярлыков: у EvoVis `@rn(name=…)` и `description` отдали каталог
|
||||||
модулей целиком. Если метаданные нужны рантайму — шифруем их (C2).
|
модулей целиком. Если метаданные нужны рантайму - шифруем их (C2).
|
||||||
|
|
||||||
### A2. Вычистить отладочную информацию
|
### A2. Вычистить отладочную информацию
|
||||||
- `-g` → `LineNumberTable` / `LocalVariableTable` / `SourceFile` / `InnerClasses`
|
- `-g` → `LineNumberTable` / `LocalVariableTable` / `SourceFile` / `InnerClasses`
|
||||||
/ `EnclosingMethod` / `MethodParameters` / `Signature` — по policy: либо полностью
|
/ `EnclosingMethod` / `MethodParameters` / `Signature` - по policy: либо полностью
|
||||||
выкинуть, либо заменить на шум (см. B4).
|
выкинуть, либо заменить на шум (см. B4).
|
||||||
- Ресурсы тоже часть кода: mixin-JSON, `fabric.mod.json`, access widener, service
|
- Ресурсы тоже часть кода: mixin-JSON, `fabric.mod.json`, access widener, service
|
||||||
loaders, SPI-файлы — шифруем/шифруем имена внутри, раскрываем в рантайме.
|
loaders, SPI-файлы - шифруем/шифруем имена внутри, раскрываем в рантайме.
|
||||||
|
|
||||||
### A3. Никаких сигнатурных имён у защитного кода
|
### A3. Никаких сигнатурных имён у защитного кода
|
||||||
`KeyAssembler`, `RuntimeGuard`, `NativeGuard` → рандомные имена, а сам защитный код
|
`KeyAssembler`, `RuntimeGuard`, `NativeGuard` → рандомные имена, а сам защитный код
|
||||||
**смешиваем с обычным** (секции, которые выглядят как мусорные утилиты).
|
**смешиваем с обычным** (секции, которые выглядят как мусорные утилиты).
|
||||||
Рефлексия по именам в открытом виде (`Class.forName("aln")`) — запрещена:
|
Рефлексия по именам в открытом виде (`Class.forName("aln")`) - запрещена:
|
||||||
имена отдаются только из зашифрованной строки (C2), лучше — через indirection-таблицу.
|
имена отдаются только из зашифрованной строки (C2), лучше - через indirection-таблицу.
|
||||||
|
|
||||||
### A4. Обфусцировать весь артефакт, а не только ядро
|
### A4. Обфусцировать весь артефакт, а не только ядро
|
||||||
Правило: **нет незащищённого компонента**. У EvoVis лаунчер и java-agent были
|
Правило: **нет незащищённого компонента**. У EvoVis лаунчер и java-agent были
|
||||||
|
|
@ -42,9 +42,9 @@
|
||||||
Вместо общего LCG:
|
Вместо общего LCG:
|
||||||
- seed = `random_u64` на строку (храним неявно: derive из позиции/ид + master-seed);
|
- seed = `random_u64` на строку (храним неявно: derive из позиции/ид + master-seed);
|
||||||
- алгоритм выбирается из набора (3–5 схем: RC4-like, chacha-с fragments, XTEA-блочный,
|
- алгоритм выбирается из набора (3–5 схем: RC4-like, chacha-с fragments, XTEA-блочный,
|
||||||
plain XOR с keystream от BLAKE3, base85+flate перед шифром) — **перемешивание схем
|
plain XOR с keystream от BLAKE3, base85+flate перед шифром) - **перемешивание схем
|
||||||
между строками**, чтобы один скрипт не покрыл всё;
|
между строками**, чтобы один скрипт не покрыл всё;
|
||||||
- AAD/MAC: `HMAC(fragment, plaintext_hash)` — повреждение ловится, подмена — тоже;
|
- AAD/MAC: `HMAC(fragment, plaintext_hash)` - повреждение ловится, подмена - тоже;
|
||||||
- keystream не повторяется: nonce включает индекс строки.
|
- keystream не повторяется: nonce включает индекс строки.
|
||||||
- **против обучения по частотам**: словарь/частотный анализ не работает, если схема
|
- **против обучения по частотам**: словарь/частотный анализ не работает, если схема
|
||||||
и nonce разные.
|
и nonce разные.
|
||||||
|
|
@ -53,18 +53,18 @@
|
||||||
Никаких `d(cipher, literal_key)`:
|
Никаких `d(cipher, literal_key)`:
|
||||||
- ключ = `KDF(master_key, class_id, method_id, string_index)` либо приходит из
|
- ключ = `KDF(master_key, class_id, method_id, string_index)` либо приходит из
|
||||||
защищённого контекста (см. D3);
|
защищённого контекста (см. D3);
|
||||||
- вычисление — через серию операций, **растворённых в теле вызывающего метода**
|
- вычисление - через серию операций, **растворённых в теле вызывающего метода**
|
||||||
(не выделенная функция `decrypt(s)` — иначе её найдут и развернут);
|
(не выделенная функция `decrypt(s)` - иначе её найдут и развернут);
|
||||||
- мастер-секрет не в константах: собирается из нескольких мест + env/хост-фактора.
|
- мастер-секрет не в константах: собирается из нескольких мест + env/хост-фактора.
|
||||||
|
|
||||||
### B3. Константы — не тождества, а вычисления
|
### B3. Константы - не тождества, а вычисления
|
||||||
Отказ от `(A^B)+((A&B)<<1)` (опознаётся regex'ом). Вместо этого:
|
Отказ от `(A^B)+((A&B)<<1)` (опознаётся regex'ом). Вместо этого:
|
||||||
- константа собирается из **нескольких узлов**, разнесённых по разным basic block'ам
|
- константа собирается из **нескольких узлов**, разнесённых по разным basic block'ам
|
||||||
и разным методам (возврат частей, сложение на месте использования);
|
и разным методам (возврат частей, сложение на месте использования);
|
||||||
- включаем в цепочку **зависимые от окружения** величины, которые статически вычислимы
|
- включаем в цепочку **зависимые от окружения** величины, которые статически вычислимы
|
||||||
не полностью: `hashCode()` строки-литерала, `System.identityHashCode` статической
|
не полностью: `hashCode()` строки-литерала, `System.identityHashCode` статической
|
||||||
константы, значение поля, инициализируемого в `<clinit>`, размер массива;
|
константы, значение поля, инициализируемого в `<clinit>`, размер массива;
|
||||||
- для `int`/`long`/`float`/`double` — отдельные схемы (bit-cast, разложение по
|
- для `int`/`long`/`float`/`double` - отдельные схемы (bit-cast, разложение по
|
||||||
мантиссе), потому что `double`-константы особенно заметны.
|
мантиссе), потому что `double`-константы особенно заметны.
|
||||||
- opaque-предикаты делать **настоящими**: всегда истинные, но зависящие от состояния,
|
- opaque-предикаты делать **настоящими**: всегда истинные, но зависящие от состояния,
|
||||||
которое статически не выводится (напр. инвариант «массив отсортирован по заполнению»,
|
которое статически не выводится (напр. инвариант «массив отсортирован по заполнению»,
|
||||||
|
|
@ -85,10 +85,10 @@
|
||||||
Минимум набор (применять **случайно и по-разному в разных методах**):
|
Минимум набор (применять **случайно и по-разному в разных методах**):
|
||||||
1. **Switch по вычислимому индексу**: `table[ (x*31 ^ y) & mask ]` → адрес возврата;
|
1. **Switch по вычислимому индексу**: `table[ (x*31 ^ y) & mask ]` → адрес возврата;
|
||||||
переходы через индексы, а не метки.
|
переходы через индексы, а не метки.
|
||||||
2. **Распределённое состояние**: как у EvoVis (`i + i2`), но одно слагаемое —
|
2. **Распределённое состояние**: как у EvoVis (`i + i2`), но одно слагаемое -
|
||||||
из поля объекта/статики, второе — из параметра, обновление — через вызов метода;
|
из поля объекта/статики, второе - из параметра, обновление - через вызов метода;
|
||||||
**метки case не константные**, а пересчитанные (opaque).
|
**метки case не константные**, а пересчитанные (opaque).
|
||||||
3. **Бинарное дерево/бабочка**: вместо линейного списка состояний — рандомизированный
|
3. **Бинарное дерево/бабочка**: вместо линейного списка состояний - рандомизированный
|
||||||
граф переходов, эквивалентный исходному CFG (проверяется верификатором).
|
граф переходов, эквивалентный исходному CFG (проверяется верификатором).
|
||||||
4. **Вызов через стек/ретурн-адрес**: эмуляция `goto` через `athrow`-как-прыжок
|
4. **Вызов через стек/ретурн-адрес**: эмуляция `goto` через `athrow`-как-прыжок
|
||||||
(exception-as-goto), `finally`-цепочки, или virtual-dispatch по фейковому интерфейсу.
|
(exception-as-goto), `finally`-цепочки, или virtual-dispatch по фейковому интерфейсу.
|
||||||
|
|
@ -101,9 +101,9 @@
|
||||||
Плюс автоматические метрики (см. F).
|
Плюс автоматические метрики (см. F).
|
||||||
|
|
||||||
### C3. Реальный объём ≠ раздувание
|
### C3. Реальный объём ≠ раздувание
|
||||||
У EvoVis выигрыш — только в размере декомпилята, но и цена (читаемость для нас же).
|
У EvoVis выигрыш - только в размере декомпилята, но и цена (читаемость для нас же).
|
||||||
Оптимальнее: комбинировать обфускацию с **компиляцией в другой целевой код**
|
Оптимальнее: комбинировать обфускацию с **компиляцией в другой целевой код**
|
||||||
(см. E), где раздувание не надо — там и так нечем читать.
|
(см. E), где раздувание не надо - там и так нечем читать.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -111,7 +111,7 @@
|
||||||
|
|
||||||
### D1. Маленький VM / interpreter для критичных кусков
|
### D1. Маленький VM / interpreter для критичных кусков
|
||||||
Свой bytecode (10–20 опкодов: арифметика, ветвление, вызов, доступ к полю),
|
Свой bytecode (10–20 опкодов: арифметика, ветвление, вызов, доступ к полю),
|
||||||
инструкции в зашифрованном массиве, диспетчер — обычный код с обфусцированным CFG.
|
инструкции в зашифрованном массиве, диспетчер - обычный код с обфусцированным CFG.
|
||||||
Экспортируем в VM только чувствительное: лицензирование, ключи, критическую логику.
|
Экспортируем в VM только чувствительное: лицензирование, ключи, критическую логику.
|
||||||
Это единственный уровень, который реально останавливает ручной реверс.
|
Это единственный уровень, который реально останавливает ручной реверс.
|
||||||
|
|
||||||
|
|
@ -122,38 +122,38 @@
|
||||||
|
|
||||||
### D3. Анти-анализ в рантайме (мягкий)
|
### D3. Анти-анализ в рантайме (мягкий)
|
||||||
- self-check целостности: `SHA-256` своих классов/секций → при расхождении
|
- self-check целостности: `SHA-256` своих классов/секций → при расхождении
|
||||||
**меняем поведение**, а не падаем (у EvoVis падение выдавало мусорный ключ —
|
**меняем поведение**, а не падаем (у EvoVis падение выдавало мусорный ключ -
|
||||||
правильно: не сообщать атакующему, что он пойман);
|
правильно: не сообщать атакующему, что он пойман);
|
||||||
- детект отладчика/трейсера/инструментов эмуляции — реакция: ложные данные,
|
- детект отладчика/трейсера/инструментов эмуляции - реакция: ложные данные,
|
||||||
замедление, отключение функций;
|
замедление, отключение функций;
|
||||||
- **не детектить и не ломать легитимные сценарии** (обновления, IDE, символы) —
|
- **не детектить и не ломать легитимные сценарии** (обновления, IDE, символы) -
|
||||||
ложные срабатывания дороже, чем польза защиты.
|
ложные срабатывания дороже, чем польза защиты.
|
||||||
|
|
||||||
### D4. Честный anti-tamper, а не «зашейрь ключ»
|
### D4. Честный anti-tamper, а не «зашейрь ключ»
|
||||||
У EvoVis сильная идея: ключ = изолированная доля в подменённом JVM + HMAC от hwid +
|
У EvoVis сильная идея: ключ = изолированная доля в подменённом JVM + HMAC от hwid +
|
||||||
порча при детекте. Минус — вся схема работает только на своём окружении и падает
|
порча при детекте. Минус - вся схема работает только на своём окружении и падает
|
||||||
на другой ОС/сборке. Для нас: секрет должен переживать **наши** легитимные платформы,
|
на другой ОС/сборке. Для нас: секрет должен переживать **наши** легитимные платформы,
|
||||||
а ломаться только при модификации кода/подмене целостности.
|
а ломаться только при модификации кода/подмене целостности.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## E. Если цель — не JVM, а свой натив/Rust-бинарник
|
## E. Если цель - не JVM, а свой натив/Rust-бинарник
|
||||||
|
|
||||||
Референс JVM-слабости здесь не работают автоматически, но появляются свои:
|
Референс JVM-слабости здесь не работают автоматически, но появляются свои:
|
||||||
- **строки**: все plaintext-строки в `.rodata` — шифровать, раскрывать лениво;
|
- **строки**: все plaintext-строки в `.rodata` - шифровать, раскрывать лениво;
|
||||||
не держать AES-ключ рядом с шифротекстом (та же ошибка, что у `awc.d`);
|
не держать AES-ключ рядом с шифротекстом (та же ошибка, что у `awc.d`);
|
||||||
- **имена**: `stripped` бинарник + рандомизация символов; в Rust — макрос, который
|
- **имена**: `stripped` бинарник + рандомизация символов; в Rust - макрос, который
|
||||||
переименовывает `pub`-символы и убирает `#[doc]`/Debug-имена;
|
переименовывает `pub`-символы и убирает `#[doc]`/Debug-имена;
|
||||||
- **panic/Debug-вывод**: `{:?}` отдаёт имена типов и полей — для чувствительных типов
|
- **panic/Debug-вывод**: `{:?}` отдаёт имена типов и полей - для чувствительных типов
|
||||||
свой `Debug`, либо не выводить;
|
свой `Debug`, либо не выводить;
|
||||||
- **CFG**: обфускация на уровне MIR/LLVM-IR (passes): bogus control flow, flattening
|
- **CFG**: обфускация на уровне MIR/LLVM-IR (passes): bogus control flow, flattening
|
||||||
на `switch` по вычисленному индексу, вставка недостижимых блоков, размытие
|
на `switch` по вычисленному индексу, вставка недостижимых блоков, размытие
|
||||||
границ функций (inlining + outlined);
|
границ функций (inlining + outlined);
|
||||||
- **константы**: как B3, плюс вынос в вычисляемые таблицы;
|
- **константы**: как B3, плюс вынос в вычисляемые таблицы;
|
||||||
- **анти-анализ**: `ptrace`-selfcheck, проверка `/proc/self/maps` на свои секции,
|
- **анти-анализ**: `ptrace`-selfcheck, проверка `/proc/self/maps` на свои секции,
|
||||||
timing-checks; реакция — молчаливая порча данных;
|
timing-checks; реакция - молчаливая порча данных;
|
||||||
- **обфускация метаданных Rust**: `TypeId`/`Any`, `std::any::type_name`, логи
|
- **обфускация метаданных Rust**: `TypeId`/`Any`, `std::any::type_name`, логи
|
||||||
`tracing` — выключать в release.
|
`tracing` - выключать в release.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -161,25 +161,25 @@
|
||||||
|
|
||||||
Автотесты на каждую технику, гоняются в CI:
|
Автотесты на каждую технику, гоняются в CI:
|
||||||
|
|
||||||
1. **Утечки имён**: `strings`/анализ констант — не должно остаться ни одного
|
1. **Утечки имён**: `strings`/анализ констант - не должно остаться ни одного
|
||||||
осмысленного идентификатора из списка-эталона (ошибка = фейл).
|
осмысленного идентификатора из списка-эталона (ошибка = фейл).
|
||||||
2. **Читаемость декомпилята**: прогнать JADX/CFR на собранный артефакт →
|
2. **Читаемость декомпилята**: прогнать JADX/CFR на собранный артефакт →
|
||||||
метрики: доля распознанных структур (try/catch, switch, циклы), доля классов
|
метрики: доля распознанных структур (try/catch, switch, циклы), доля классов
|
||||||
с восстановленными именами, число строк «смыслового» кода.
|
с восстановленными именами, число строк «смыслового» кода.
|
||||||
3. **Автоматический деобфускатор-противник**: минимальный скрипт-анализатор
|
3. **Автоматический деобфускатор-противник**: минимальный скрипт-анализатор
|
||||||
(тот же `expr`-свёртка, regex на паттерны) — если он снимает >X% строк/констант,
|
(тот же `expr`-свёртка, regex на паттерны) - если он снимает >X% строк/констант,
|
||||||
техника провалена. Держим этот «красный список паттернов» в репозитории.
|
техника провалена. Держим этот «красный список паттернов» в репозитории.
|
||||||
4. **Семантика**: тесты/эталонные программы до и после — 100% совпадение.
|
4. **Семантика**: тесты/эталонные программы до и после - 100% совпадение.
|
||||||
5. **Размер и скорость**: бюджет (≤ N% размера, ≤ M% времени исполнения),
|
5. **Размер и скорость**: бюджет (≤ N% размера, ≤ M% времени исполнения),
|
||||||
отдельно — бюджет на рантайм-раскрытие строк (кэш, чтобы не платить на каждый вызов).
|
отдельно - бюджет на рантайм-раскрытие строк (кэш, чтобы не платить на каждый вызов).
|
||||||
6. **Стабильность между сборками**: имена/схемы меняются от билда к билду
|
6. **Стабильность между сборками**: имена/схемы меняются от билда к билду
|
||||||
(у EvoVis шифр один на все версии — окно для обучения).
|
(у EvoVis шифр один на все версии - окно для обучения).
|
||||||
|
|
||||||
## Приоритет
|
## Приоритет
|
||||||
|
|
||||||
1. A1+A2+A4 (символы, отладка, всё артефакты) — дёшево, biggest leak у EvoVis.
|
1. A1+A2+A4 (символы, отладка, всё артефакты) - дёшево, biggest leak у EvoVis.
|
||||||
2. B1+B2 (пер-строчные ключи, вывод ключа) — снимает главный провал `awc.d`.
|
2. B1+B2 (пер-строчные ключи, вывод ключа) - снимает главный провал `awc.d`.
|
||||||
3. C1 несколько схем флаттенинга + C2 верификатор.
|
3. C1 несколько схем флаттенинга + C2 верификатор.
|
||||||
4. F метрики — чтобы не спорить на глаз.
|
4. F метрики - чтобы не спорить на глаз.
|
||||||
5. C3/E (другой целевой код) — как долгосрочный путь.
|
5. C3/E (другой целевой код) - как долгосрочный путь.
|
||||||
6. D1 VM — только для действительно ценных кусков, остальное не оправдывает цену.
|
6. D1 VM - только для действительно ценных кусков, остальное не оправдывает цену.
|
||||||
|
|
|
||||||
|
|
@ -25,18 +25,18 @@ cd ref/evo/reverse
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `mod/jar/META-INF/evo/native/win-x64/evoguard.dll` | 51 321 | 18 | своя маленькая JNI-библиотека (MinGW, PE32+) |
|
| `mod/jar/META-INF/evo/native/win-x64/evoguard.dll` | 51 321 | 18 | своя маленькая JNI-библиотека (MinGW, PE32+) |
|
||||||
| `mod/evojdk_x/bin/java.dll` | 111 104 | 6 | стандартный `java.dll` из JDK **+ новая функция** |
|
| `mod/evojdk_x/bin/java.dll` | 111 104 | 6 | стандартный `java.dll` из JDK **+ новая функция** |
|
||||||
| `mod/evojdk_x/bin/server/jvm.dll` | 15 463 936 | — | **патченный HotSpot** + новый экспорт |
|
| `mod/evojdk_x/bin/server/jvm.dll` | 15 463 936 | - | **патченный HotSpot** + новый экспорт |
|
||||||
|
|
||||||
Отдельно (Java, не натив): `mod/evojdk_x/classes/java/lang/EvoShare.class`,
|
Отдельно (Java, не натив): `mod/evojdk_x/classes/java/lang/EvoShare.class`,
|
||||||
исходник — `mod/evoshare/sources/java/lang/EvoShare.java` (JADX), вызывающий код —
|
исходник - `mod/evoshare/sources/java/lang/EvoShare.java` (JADX), вызывающий код -
|
||||||
`mod/agent-src/sources/evo/agent/key/NativeJvmShare.java`.
|
`mod/agent-src/sources/evo/agent/key/NativeJvmShare.java`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. evoguard.dll — JNI-«нативный шейр»
|
## 1. evoguard.dll - JNI-«нативный шейр»
|
||||||
|
|
||||||
Экспорт ровно один (плюс mingw-рантайм `_CRT_INIT`, `DllMainCRTStartup`, `atexit`,
|
Экспорт ровно один (плюс mingw-рантайм `_CRT_INIT`, `DllMainCRTStartup`, `atexit`,
|
||||||
`sha256_block/update/final`, `env_flags_debug`, `hmac_sha256.constprop.0`) — **`JNI_OnLoad` нет**,
|
`sha256_block/update/final`, `env_flags_debug`, `hmac_sha256.constprop.0`) - **`JNI_OnLoad` нет**,
|
||||||
`nm`-имена не вычищены (COFF-символы остались).
|
`nm`-имена не вычищены (COFF-символы остались).
|
||||||
|
|
||||||
### `Java_evo_systems_security_natives_NativeGuard_nativeShare(JNIEnv*, jclass, jstring hwid)` → `byte[32]`
|
### `Java_evo_systems_security_natives_NativeGuard_nativeShare(JNIEnv*, jclass, jstring hwid)` → `byte[32]`
|
||||||
|
|
@ -67,7 +67,7 @@ return res;
|
||||||
d42394b1dbe62f2d466fe6f8c4ee3cf5ce46949a032627a9ba00e8c5ef533bd2
|
d42394b1dbe62f2d466fe6f8c4ee3cf5ce46949a032627a9ba00e8c5ef533bd2
|
||||||
```
|
```
|
||||||
|
|
||||||
Итог: `NativeGuard.share(hwid) = HMAC-SHA256(key_eg, hwid_utf8)`, при детекте отладки —
|
Итог: `NativeGuard.share(hwid) = HMAC-SHA256(key_eg, hwid_utf8)`, при детекте отладки -
|
||||||
`XOR 0xA5` (не ошибка, а **молчаливо неверный шейр**).
|
`XOR 0xA5` (не ошибка, а **молчаливо неверный шейр**).
|
||||||
|
|
||||||
### Платформенный паритет (`evoguard`)
|
### Платформенный паритет (`evoguard`)
|
||||||
|
|
@ -83,9 +83,9 @@ d42394b1dbe62f2d466fe6f8c4ee3cf5ce46949a032627a9ba00e8c5ef533bd2
|
||||||
ровно `d42394b1dbe62f2d466fe6f8c4ee3cf5ce46949a032627a9ba00e8c5ef533bd2`; следующие 32
|
ровно `d42394b1dbe62f2d466fe6f8c4ee3cf5ce46949a032627a9ba00e8c5ef533bd2`; следующие 32
|
||||||
байта = тот же ключ с XOR `0x5c`. SHA-IV рядом (`0x180003090` = `67e6096a…`).
|
байта = тот же ключ с XOR `0x5c`. SHA-IV рядом (`0x180003090` = `67e6096a…`).
|
||||||
Во всех сборках есть строки `IsDebuggerPresent`, `jdwp`, `agentlib`, `JAVA_TOOL_OPTIONS`,
|
Во всех сборках есть строки `IsDebuggerPresent`, `jdwp`, `agentlib`, `JAVA_TOOL_OPTIONS`,
|
||||||
`_JAVA_OPTIONS` — логика антиотладки одинаковая. **Linux-сборки натива нет вообще**.
|
`_JAVA_OPTIONS` - логика антиотладки одинаковая. **Linux-сборки натива нет вообще**.
|
||||||
|
|
||||||
Потребитель: `alm.java` (KeyAssembler) — шейр идёт в 4-of-4 сборку Шамира:
|
Потребитель: `alm.java` (KeyAssembler) - шейр идёт в 4-of-4 сборку Шамира:
|
||||||
|
|
||||||
```java
|
```java
|
||||||
byte[] nativeShare = NativeGuard.share(hwid);
|
byte[] nativeShare = NativeGuard.share(hwid);
|
||||||
|
|
@ -97,7 +97,7 @@ ama.combine(new int[]{share.x(), share.xIntegrity(), share.xNative(), share.xHwi
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. java.dll — `java.lang.EvoShare.computeRaw` (патченный JDK)
|
## 2. java.dll - `java.lang.EvoShare.computeRaw` (патченный JDK)
|
||||||
|
|
||||||
Экспорт: `Java_java_lang_EvoShare_computeRaw` (@ `0x180001ca4`, 286 байт) +
|
Экспорт: `Java_java_lang_EvoShare_computeRaw` (@ `0x180001ca4`, 286 байт) +
|
||||||
импорт `JVM_EvoForeignAgentPresent` **из `jvm.dll`** (см. §3).
|
импорт `JVM_EvoForeignAgentPresent` **из `jvm.dll`** (см. §3).
|
||||||
|
|
@ -121,9 +121,9 @@ longlong Java_java_lang_EvoShare_computeRaw(JNIEnv* env, jclass cls, jbyteArray
|
||||||
|
|
||||||
`FUN_180001e4c(input, len, out32)`:
|
`FUN_180001e4c(input, len, out32)`:
|
||||||
|
|
||||||
1. `FUN_180001f1c(&DAT_180010e90, 0x20, input, len, out32)` — **стандартный HMAC-SHA256**
|
1. `FUN_180001f1c(&DAT_180010e90, 0x20, input, len, out32)` - **стандартный HMAC-SHA256**
|
||||||
(внутри явные SHA-IV `0x6a09e667…`, копирование ключа в ipad/opad);
|
(внутри явные SHA-IV `0x6a09e667…`, копирование ключа в ipad/opad);
|
||||||
2. затем антиотладка, при срабатывании — `out32 ^= 0xA5…`:
|
2. затем антиотладка, при срабатывании - `out32 ^= 0xA5…`:
|
||||||
- `IsDebuggerPresent()`;
|
- `IsDebuggerPresent()`;
|
||||||
- `CheckRemoteDebuggerPresent()`;
|
- `CheckRemoteDebuggerPresent()`;
|
||||||
- загружены модули `jdwp.dll`, `dt_socket.dll`, `dt_shmem.dll` (транспорты JDWP);
|
- загружены модули `jdwp.dll`, `dt_socket.dll`, `dt_shmem.dll` (транспорты JDWP);
|
||||||
|
|
@ -135,16 +135,16 @@ longlong Java_java_lang_EvoShare_computeRaw(JNIEnv* env, jclass cls, jbyteArray
|
||||||
70ea2f4857869bdc8606a29a6c8cdcedbe92c18bef1aad106024db104a8f25d1
|
70ea2f4857869bdc8606a29a6c8cdcedbe92c18bef1aad106024db104a8f25d1
|
||||||
```
|
```
|
||||||
|
|
||||||
Ключи `evoguard.dll` и `java.dll` **разные** — это два независимых шейра.
|
Ключи `evoguard.dll` и `java.dll` **разные** - это два независимых шейра.
|
||||||
|
|
||||||
Константы-маски: `0xA5` — антиотладка (натив), `0x3C` — «чужой агент» из HotSpot,
|
Константы-маски: `0xA5` - антиотладка (натив), `0x3C` - «чужой агент» из HotSpot,
|
||||||
`0x5A` — Java-уровень (см. §4).
|
`0x5A` - Java-уровень (см. §4).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. jvm.dll — экспорт `JVM_EvoForeignAgentPresent`
|
## 3. jvm.dll - экспорт `JVM_EvoForeignAgentPresent`
|
||||||
|
|
||||||
`mod/evojdk_x/bin/server/jvm.dll` (15,4 МБ) — тот же HotSpot, что и в `java.dll`, но с
|
`mod/evojdk_x/bin/server/jvm.dll` (15,4 МБ) - тот же HotSpot, что и в `java.dll`, но с
|
||||||
добавленным экспортом (в таблице экспортов: `[4281] +base[4282] 10b9 JVM_EvoForeignAgentPresent`).
|
добавленным экспортом (в таблице экспортов: `[4281] +base[4282] 10b9 JVM_EvoForeignAgentPresent`).
|
||||||
`java.dll` импортирует его и вызывает в `computeRaw`.
|
`java.dll` импортирует его и вызывает в `computeRaw`.
|
||||||
|
|
||||||
|
|
@ -171,7 +171,7 @@ undefined1 JVM_EvoForeignAgentPresent(void) {
|
||||||
|
|
||||||
### Откуда берётся список и что такое `+0x53`
|
### Откуда берётся список и что такое `+0x53`
|
||||||
|
|
||||||
- `DAT_180e2c160` — **глава глобального связного списка JVMTI-агентов** (stock-механизм HotSpot,
|
- `DAT_180e2c160` - **глава глобального связного списка JVMTI-агентов** (stock-механизм HotSpot,
|
||||||
`+0x20` = `next`, `+0x58` размер элемента). Доказательство: рядом лежит функция с литералом
|
`+0x20` = `next`, `+0x58` размер элемента). Доказательство: рядом лежит функция с литералом
|
||||||
`"Disabled all JVMTI agents during -XX:AOTMode=create"`, которая делает `DAT_180e2c160 = 0`;
|
`"Disabled all JVMTI agents during -XX:AOTMode=create"`, которая делает `DAT_180e2c160 = 0`;
|
||||||
push-функция `FUN_1805f40e0(name, path, is_absolute)` аллоцирует `0x58` байт, инициализирует
|
push-функция `FUN_1805f40e0(name, path, is_absolute)` аллоцирует `0x58` байт, инициализирует
|
||||||
|
|
@ -196,8 +196,8 @@ JVMTI-списке есть **хотя бы один не-instrument агент*
|
||||||
|
|
||||||
Вызов из `EvoShare.computeRaw` → `XOR 0x3C` на HMAC-шейре (см. §2, §5).
|
Вызов из `EvoShare.computeRaw` → `XOR 0x3C` на HMAC-шейре (см. §2, §5).
|
||||||
|
|
||||||
Также в `jvm_focus.c` — две функции, содержащие доступы к полю `0x53`
|
Также в `jvm_focus.c` - две функции, содержащие доступы к полю `0x53`
|
||||||
(`FUN_180268640`, `FUN_1804a7920`), — это чтения в фильтрах классов/загрузчика, к
|
(`FUN_180268640`, `FUN_1804a7920`), - это чтения в фильтрах классов/загрузчика, к
|
||||||
логике агентов отношения не имеют. Прямой записи `mov byte [reg+0x53]` в программе нет:
|
логике агентов отношения не имеют. Прямой записи `mov byte [reg+0x53]` в программе нет:
|
||||||
поле инициализируется в конструкторе широкой записью через `strcmp`-результат.
|
поле инициализируется в конструкторе широкой записью через `strcmp`-результат.
|
||||||
|
|
||||||
|
|
@ -210,12 +210,12 @@ public static byte[] compute(byte[] input) {
|
||||||
byte[] out = computeRaw(input); // натив, §2
|
byte[] out = computeRaw(input); // натив, §2
|
||||||
if (out != null && out.length == 32 && agentMismatch())
|
if (out != null && out.length == 32 && agentMismatch())
|
||||||
for (int i = 0; i < out.length; i++)
|
for (int i = 0; i < out.length; i++)
|
||||||
out[i] = (byte) (out[i] ^ 90); // 0x5A — третий уровень порчи
|
out[i] = (byte) (out[i] ^ 90); // 0x5A - третий уровень порчи
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
`agentMismatch()` — сканирует `VM.getRuntimeArguments()`:
|
`agentMismatch()` - сканирует `VM.getRuntimeArguments()`:
|
||||||
|
|
||||||
- аргумент `-agentpath:` или `-agentlib:` (любой нативный агент, в т.ч. `-agentlib:jdwp`) → `true`;
|
- аргумент `-agentpath:` или `-agentlib:` (любой нативный агент, в т.ч. `-agentlib:jdwp`) → `true`;
|
||||||
- `-javaagent:<jar>[=opts]`: берёт путь jar, читает файл, считает SHA-256 и сравнивает с
|
- `-javaagent:<jar>[=opts]`: берёт путь jar, читает файл, считает SHA-256 и сравнивает с
|
||||||
|
|
@ -230,11 +230,11 @@ AGENT_CORRUPT_MASK = 90 // 0x5A
|
||||||
EXPECTED_AGENT_SHA256 = 374ace507bd0cc2d0ce6dbbe07e50ddb3173ff819e92470271e2bf0fccff2038
|
EXPECTED_AGENT_SHA256 = 374ace507bd0cc2d0ce6dbbe07e50ddb3173ff819e92470271e2bf0fccff2038
|
||||||
```
|
```
|
||||||
|
|
||||||
Проверено: `sha256sum mod/evo-agent.jar` = `374ace50…` — **пиннинг на свой агент
|
Проверено: `sha256sum mod/evo-agent.jar` = `374ace50…` - **пиннинг на свой агент
|
||||||
`evo-agent.jar`**. То есть JVM должен быть запущен либо вообще без `-javaagent`,
|
`evo-agent.jar`**. То есть JVM должен быть запущен либо вообще без `-javaagent`,
|
||||||
либо именно с их агентом; чужой агент тихо ломает шейр.
|
либо именно с их агентом; чужой агент тихо ломает шейр.
|
||||||
|
|
||||||
Потребитель — `evo.agent.key.NativeJvmShare` (в `evo-agent.jar`):
|
Потребитель - `evo.agent.key.NativeJvmShare` (в `evo-agent.jar`):
|
||||||
|
|
||||||
```java
|
```java
|
||||||
Method compute = Class.forName("java.lang.EvoShare").getMethod("compute", byte[].class);
|
Method compute = Class.forName("java.lang.EvoShare").getMethod("compute", byte[].class);
|
||||||
|
|
@ -242,7 +242,7 @@ byte[] share = (byte[]) compute.invoke(null, hwid.getBytes(UTF_8));
|
||||||
// ClassNotFoundException → KeyProviderException("not running on the EvoVis custom JVM")
|
// ClassNotFoundException → KeyProviderException("not running on the EvoVis custom JVM")
|
||||||
```
|
```
|
||||||
|
|
||||||
То есть **payload-ключ требует их кастомного JDK** — на стоковом `java` шейр получить нельзя.
|
То есть **payload-ключ требует их кастомного JDK** - на стоковом `java` шейр получить нельзя.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -255,19 +255,19 @@ byte[] share = (byte[]) compute.invoke(null, hwid.getBytes(UTF_8));
|
||||||
| натив JDK | `EvoShare.computeRaw` | `JVM_EvoForeignAgentPresent() == 1`: есть `-agentlib:`/`-agentpath:` агент либо ≥2 `-javaagent:` | `XOR 0x3C` |
|
| натив JDK | `EvoShare.computeRaw` | `JVM_EvoForeignAgentPresent() == 1`: есть `-agentlib:`/`-agentpath:` агент либо ≥2 `-javaagent:` | `XOR 0x3C` |
|
||||||
| Java JDK | `EvoShare.compute` | `-agentpath:`/`-agentlib:` или `-javaagent:` с чужим sha256 | `XOR 0x5A` |
|
| Java JDK | `EvoShare.compute` | `-agentpath:`/`-agentlib:` или `-javaagent:` с чужим sha256 | `XOR 0x5A` |
|
||||||
|
|
||||||
Во всех случаях результат остаётся валидным `byte[32]` — ошибка не сигнализируется,
|
Во всех случаях результат остаётся валидным `byte[32]` - ошибка не сигнализируется,
|
||||||
ключ просто собирается неверно (в `alm.java` дальше идёт `combine` Шамира и `alz.wipe`).
|
ключ просто собирается неверно (в `alm.java` дальше идёт `combine` Шамира и `alz.wipe`).
|
||||||
|
|
||||||
Две разные схемы HMAC (разные ключи) дают два разных шейра:
|
Две разные схемы HMAC (разные ключи) дают два разных шейра:
|
||||||
|
|
||||||
- `NativeGuard.share(hwid)` — в сборку ключа мода (`alm.java`, 4-of-4 Шамир);
|
- `NativeGuard.share(hwid)` - в сборку ключа мода (`alm.java`, 4-of-4 Шамир);
|
||||||
- `EvoShare.compute(hwid)` — в цепочку ключа payload'а агента (`NativeJvmShare`, `EvoAgent`).
|
- `EvoShare.compute(hwid)` - в цепочку ключа payload'а агента (`NativeJvmShare`, `EvoAgent`).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 6. `evovis-audiotap` (macOS, Swift)
|
## 6. `evovis-audiotap` (macOS, Swift)
|
||||||
|
|
||||||
`mod/jar/assets/evolution/native/macos/evovis-audiotap` — **Mach-O universal (x86_64 + arm64),
|
`mod/jar/assets/evolution/native/macos/evovis-audiotap` - **Mach-O universal (x86_64 + arm64),
|
||||||
174 848 байт, исполняемый файл** (`_main` + `__mh_execute_header`, не dylib).
|
174 848 байт, исполняемый файл** (`_main` + `__mh_execute_header`, не dylib).
|
||||||
|
|
||||||
Факты по `strings` / `r2`:
|
Факты по `strings` / `r2`:
|
||||||
|
|
@ -275,17 +275,17 @@ byte[] share = (byte[]) compute.invoke(null, hwid.getBytes(UTF_8));
|
||||||
- написан на **Swift** (секции `__swift5_typeref/capture/entry/reflstr/fieldmd/types`),
|
- написан на **Swift** (секции `__swift5_typeref/capture/entry/reflstr/fieldmd/types`),
|
||||||
класс `main.AudioTap` (`_TtC4main8AudioTap`);
|
класс `main.AudioTap` (`_TtC4main8AudioTap`);
|
||||||
- зависимости: **ScreenCaptureKit**, CoreMedia, AVFoundation, CoreAudio (swiftCoreAudio),
|
- зависимости: **ScreenCaptureKit**, CoreMedia, AVFoundation, CoreAudio (swiftCoreAudio),
|
||||||
Foundation, IOKit — то есть захват системного звука/видеопотока через ScreenCaptureKit;
|
Foundation, IOKit - то есть захват системного звука/видеопотока через ScreenCaptureKit;
|
||||||
- делегат `stream(_:didOutputSampleBuffer:of:)` (`SCStream` + `CMSampleBuffer`) и
|
- делегат `stream(_:didOutputSampleBuffer:of:)` (`SCStream` + `CMSampleBuffer`) и
|
||||||
`stream(_:didStopWithError:)`; поля `output: NSFileHandle`, `mono: [Float]`;
|
`stream(_:didStopWithError:)`; поля `output: NSFileHandle`, `mono: [Float]`;
|
||||||
- логи/метки `evovis.audiotap.audio`, `evovis.audiotap.screen`;
|
- логи/метки `evovis.audiotap.audio`, `evovis.audiotap.screen`;
|
||||||
- **сетевых строк нет** (ни `http`, ни сокетов, ни доменов) — данные пишутся в
|
- **сетевых строк нет** (ни `http`, ни сокетов, ни доменов) - данные пишутся в
|
||||||
`NSFileHandle` (pipe/stdout), то есть процесс-потребитель мод запускает локально.
|
`NSFileHandle` (pipe/stdout), то есть процесс-потребитель мод запускает локально.
|
||||||
|
|
||||||
Вывод: это локальный helper для тапа аудио (и экрана) на macOS, результат уходит пайпом
|
Вывод: это локальный helper для тапа аудио (и экрана) на macOS, результат уходит пайпом
|
||||||
в родительский процесс; в аудио/сеть наружу ничего не отдаётся.
|
в родительский процесс; в аудио/сеть наружу ничего не отдаётся.
|
||||||
|
|
||||||
Декомпиляция Ghidra для него не запускалась (предмет — Swift + macOS-фреймворки, на
|
Декомпиляция Ghidra для него не запускалась (предмет - Swift + macOS-фреймворки, на
|
||||||
уровне строк/символов картина уже ясна); запускается по необходимости:
|
уровне строк/символов картина уже ясна); запускается по необходимости:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
|
||||||
|
|
@ -22,25 +22,25 @@
|
||||||
|
|
||||||
- **jadx переименовал их класс `if` → `Cif`** (keyword): класс `Cif.class` в джаре не существует,
|
- **jadx переименовал их класс `if` → `Cif`** (keyword): класс `Cif.class` в джаре не существует,
|
||||||
поэтому весь мёртвый код с ссылками на `Cif` вырезан, а не пересобран.
|
поэтому весь мёртвый код с ссылками на `Cif` вырезан, а не пересобран.
|
||||||
- `?? r1;` (недообфусцированный фрагмент) и `throw th;` вне catch — в вырезанных методах.
|
- `?? r1;` (недообфусцированный фрагмент) и `throw th;` вне catch - в вырезанных методах.
|
||||||
- Обфускация `setDaemon((A^B)+((A&B)<<1))` — это int-эквивалент `true` (A+B=1), javac не берёт.
|
- Обфускация `setDaemon((A^B)+((A&B)<<1))` - это int-эквивалент `true` (A+B=1), javac не берёт.
|
||||||
|
|
||||||
## Что заменено
|
## Что заменено
|
||||||
|
|
||||||
### `cp` = `Avatar2` (аккаунт)
|
### `cp` = `Avatar2` (аккаунт)
|
||||||
- `initialize()` → `setUser(new cr("Player", null, "user", 0, null))`:
|
- `initialize()` → `setUser(new cr("Player", null, "user", 0, null))`:
|
||||||
**нет** `POST /api/auth/me`, нет чтения `EVO_TOKEN`, нет лаунчер-сессии, нет `refreshMe` (HTTP).
|
**нет** `POST /api/auth/me`, нет чтения `EVO_TOKEN`, нет лаунчер-сессии, нет `refreshMe` (HTTP).
|
||||||
Профиль — локальный dummy.
|
Профиль - локальный dummy.
|
||||||
- `isAuthorized()` → `return true` — открываются все их шлюзы
|
- `isAuthorized()` → `return true` - открываются все их шлюзы
|
||||||
(`Panel`, `Camera2`, `First`, `Friends`, `Crosshair2`, `KeyAssembler`, `SessionHeartbeat`).
|
(`Panel`, `Camera2`, `First`, `Friends`, `Crosshair2`, `KeyAssembler`, `SessionHeartbeat`).
|
||||||
- `user()` → lazy-dummy: если профиль не задан, создаёт тот же dummy.
|
- `user()` → lazy-dummy: если профиль не задан, создаёт тот же dummy.
|
||||||
**`token == null`** — принципиально: `KeyAssembler.token()` и `SessionHeartbeat.token()`
|
**`token == null`** - принципиально: `KeyAssembler.token()` и `SessionHeartbeat.token()`
|
||||||
возвращают null → `AttestClient4` (`evovis-protect.vercel.app`: challenge/attest/heartbeat)
|
возвращают null → `AttestClient4` (`evovis-protect.vercel.app`: challenge/attest/heartbeat)
|
||||||
**недостижим**, поток сборки Shamir-ключа крутится вхолостую (sleep 500 мс).
|
**недостижим**, поток сборки Shamir-ключа крутится вхолостую (sleep 500 мс).
|
||||||
- Удалён приватный `refreshMe()` (единственный источник HTTP на auth/me).
|
- Удалён приватный `refreshMe()` (единственный источник HTTP на auth/me).
|
||||||
|
|
||||||
### `akj` = `Report` (телеметрия)
|
### `akj` = `Report` (телеметрия)
|
||||||
- `submit(String)` → no-op (единственный внешний вызов — `dn`/`Message2`, их команда отчёта).
|
- `submit(String)` → no-op (единственный внешний вызов - `dn`/`Message2`, их команда отчёта).
|
||||||
- Удалены `feedback`, `buildBody`, `run` (достигались только из `submit`).
|
- Удалены `feedback`, `buildBody`, `run` (достигались только из `submit`).
|
||||||
|
|
||||||
### `alr` = `SessionHeartbeat` (сессия/отключение модулей)
|
### `alr` = `SessionHeartbeat` (сессия/отключение модулей)
|
||||||
|
|
@ -50,12 +50,12 @@
|
||||||
|
|
||||||
## Почему остальное работает без их сервера
|
## Почему остальное работает без их сервера
|
||||||
|
|
||||||
- **Обычные строки мода** — `awc.d(String,int)`: статический LCG+XOR, ключа не требует.
|
- **Обычные строки мода** - `awc.d(String,int)`: статический LCG+XOR, ключа не требует.
|
||||||
- **`SecureStrings`** (5 API-блобов: probe/friends/messages/cosmetic_sync/config_share) —
|
- **`SecureStrings`** (5 API-блобов: probe/friends/messages/cosmetic_sync/config_share) -
|
||||||
`KeyAssembler.key()` всегда null → `get()` возвращает null → их серверные фичи
|
`KeyAssembler.key()` всегда null → `get()` возвращает null → их серверные фичи
|
||||||
(синк друзей, сообщения, косметика, конфиги) **выключаются сами**, без крашей на старте
|
(синк друзей, сообщения, косметика, конфиги) **выключаются сами**, без крашей на старте
|
||||||
(null проверяется в самом `SecureStrings.get`).
|
(null проверяется в самом `SecureStrings.get`).
|
||||||
- `KeyAssembler`/`SealedBoot`/`RuntimeGuard`/JNI `NativeGuard` — живут только после сборки
|
- `KeyAssembler`/`SealedBoot`/`RuntimeGuard`/JNI `NativeGuard` - живут только после сборки
|
||||||
ключа, то есть недостижимы при `token == null`.
|
ключа, то есть недостижимы при `token == null`.
|
||||||
|
|
||||||
## Оставлено (осознанно)
|
## Оставлено (осознанно)
|
||||||
|
|
@ -82,7 +82,7 @@ OK: auth=local, report=no-op, heartbeat=off
|
||||||
|
|
||||||
## Ограничение
|
## Ограничение
|
||||||
|
|
||||||
В игре не прогоняли: здесь Linux, а их `evojdk` только win-x64. Запускать патченный джар —
|
В игре не прогоняли: здесь Linux, а их `evojdk` только win-x64. Запускать патченный джар -
|
||||||
на Windows (их лаунчер или свой fabric-лодер); при проблемах — смотреть лог на загрузке
|
на Windows (их лаунчер или свой fabric-лодер); при проблемах - смотреть лог на загрузке
|
||||||
модулей. Ожидаемо «пустым» (серверное) останется: список друзей, сообщения, косметика синк,
|
модулей. Ожидаемо «пустым» (серверное) останется: список друзей, сообщения, косметика синк,
|
||||||
аватарки, тикеты.
|
аватарки, тикеты.
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
# Обфускация EvoVis / EvolutionDLC — полный сводный разбор
|
# Обфускация EvoVis / EvolutionDLC - полный сводный разбор
|
||||||
|
|
||||||
Всё проверено по артефактам в `LoVisual/ref/evo/` (jadx-декомпиляция `evovis-dec.jar`,
|
Всё проверено по артефактам в `LoVisual/ref/evo/` (jadx-декомпиляция `evovis-dec.jar`,
|
||||||
`javap -v` по 1277 классам корня, Rust-скрипты `scripts/`). Цифры, которые пересчитаны
|
`javap -v` по 1277 классам корня, Rust-скрипты `scripts/`). Цифры, которые пересчитаны
|
||||||
вручную, помечены ✓; расхождения с исходным README — в конце.
|
вручную, помечены ✓; расхождения с исходным README - в конце.
|
||||||
|
|
||||||
Пакеты: 1405 `.class` всего = **1277 в default package** (корень jar) + **16 в `a/`**
|
Пакеты: 1405 `.class` всего = **1277 в default package** (корень jar) + **16 в `a/`**
|
||||||
+ **111 в `b/`** + 128 вложенных (`evo/**`, зависимости). jadx даёт 6991 `.java`
|
+ **111 в `b/`** + 128 вложенных (`evo/**`, зависимости). jadx даёт 6991 `.java`
|
||||||
|
|
@ -10,32 +10,32 @@
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Уровень 1 — имена
|
## Уровень 1 - имена
|
||||||
|
|
||||||
- Все классы мода вынесены в default package (`defpackage/` в jadx) с именами
|
- Все классы мода вынесены в default package (`defpackage/` в jadx) с именами
|
||||||
`a`…`zz` (1277 шт.); миксины разложены по `a/` (16) и `b/` (111).
|
`a`…`zz` (1277 шт.); миксины разложены по `a/` (16) и `b/` (111).
|
||||||
- Миксины в `evolutiondlc.mixins.json` / `holdmyitems.mixins.json` тоже закодированы:
|
- Миксины в `evolutiondlc.mixins.json` / `holdmyitems.mixins.json` тоже закодированы:
|
||||||
`"package": "b"`, список `"y"`, `"z"`, `"al"`, … — смысловых имён в манифесте нет.
|
`"package": "b"`, список `"y"`, `"z"`, `"al"`, … - смысловых имён в манифесте нет.
|
||||||
- Часть имён — **ключевые слова Java** (`if`, `do`…): декомпилятор вынужден их
|
- Часть имён - **ключевые слова Java** (`if`, `do`…): декомпилятор вынужден их
|
||||||
переименовывать (`Cif`, `Cdo`), в самом jar классов с такими именами нет.
|
переименовывать (`Cif`, `Cdo`), в самом jar классов с такими именами нет.
|
||||||
- **Члены (поля/методы) НЕ переименовываются** ✓: `wb.onDraw`, `getProjectiles`,
|
- **Члены (поля/методы) НЕ переименовываются** ✓: `wb.onDraw`, `getProjectiles`,
|
||||||
`BreakingBad`, `aln.key()`, `setDaemon`, `REFRESH_THREAD` и т. д. читаются как есть.
|
`BreakingBad`, `aln.key()`, `setDaemon`, `REFRESH_THREAD` и т. д. читаются как есть.
|
||||||
Обфусцируются только типы и пакеты.
|
Обфусцируются только типы и пакеты.
|
||||||
- **`SourceFile` частично сохранён** ✓ (27 классов в корне + все 127 миксинов):
|
- **`SourceFile` частично сохранён** ✓ (27 классов в корне + все 127 миксинов):
|
||||||
`javap` даёт `Compiled from "StringRuntime.java"`, `"KeyAssembler.java"`,
|
`javap` даёт `Compiled from "StringRuntime.java"`, `"KeyAssembler.java"`,
|
||||||
`"AttestClient.java"`, `"PayloadCrypto.java"`, … у миксинов — `"GameRendererMixin.java"`.
|
`"AttestClient.java"`, `"PayloadCrypto.java"`, … у миксинов - `"GameRendererMixin.java"`.
|
||||||
160 файлов содержат `compiled from:` — по ним восстанавливаются оригинальные имена.
|
160 файлов содержат `compiled from:` - по ним восстанавливаются оригинальные имена.
|
||||||
- **Отладочная информация осталась**: 10 218 `LineNumberTable` и 151
|
- **Отладочная информация осталась**: 10 218 `LineNumberTable` и 151
|
||||||
`LocalVariableTable` ✓ (локальные имены переменных живут у security-классов:
|
`LocalVariableTable` ✓ (локальные имены переменных живут у security-классов:
|
||||||
`awc` — 5, `aln` — 11).
|
`awc` - 5, `aln` - 11).
|
||||||
- Аннотации модулей не тронуты: `@rn(name = "HitFix", category = rl.COMBAT,
|
- Аннотации модулей не тронуты: `@rn(name = "HitFix", category = rl.COMBAT,
|
||||||
description = "Makes attacks land on the hitbox you actually see…")` — 57 классов ✓,
|
description = "Makes attacks land on the hitbox you actually see…")` - 57 классов ✓,
|
||||||
то есть каталог модулей читается без всякой деобфускации.
|
то есть каталог модулей читается без всякой деобфускации.
|
||||||
- **Enum-константы не переименованы**: `SCALE`, `OPACITY`, `SPAWN`, `KEYFRAME`.
|
- **Enum-константы не переименованы**: `SCALE`, `OPACITY`, `SPAWN`, `KEYFRAME`.
|
||||||
При этом 102 enum jadx не восстановил (`Failed to restore enum class`) — обфускатор
|
При этом 102 enum jadx не восстановил (`Failed to restore enum class`) - обфускатор
|
||||||
сломал декомпиляторский паттерн enum, но имена констант в байткоде остались.
|
сломал декомпиляторский паттерн enum, но имена констант в байткоде остались.
|
||||||
|
|
||||||
## Уровень 2 — строки
|
## Уровень 2 - строки
|
||||||
|
|
||||||
Класс `awc` = `evovis.security.StringRuntime` (раскрыт по `SourceFile`):
|
Класс `awc` = `evovis.security.StringRuntime` (раскрыт по `SourceFile`):
|
||||||
|
|
||||||
|
|
@ -48,16 +48,16 @@ for (i = 0; i < cipher.length(); i++) {
|
||||||
return new String(bytes, UTF_8).intern(); // + ConcurrentHashMap CACHE
|
return new String(bytes, UTF_8).intern(); // + ConcurrentHashMap CACHE
|
||||||
```
|
```
|
||||||
|
|
||||||
- `awc.d(cipher, key)` — обычная строка. **Ключ передаётся как opaque-выражение прямо
|
- `awc.d(cipher, key)` - обычная строка. **Ключ передаётся как opaque-выражение прямо
|
||||||
в вызове**, ни одного «настоящего» ключа в рантайме нет.
|
в вызове**, ни одного «настоящего» ключа в рантайме нет.
|
||||||
- `awc.g(cipher, key)` — то же, но session-gate: сначала
|
- `awc.g(cipher, key)` - то же, но session-gate: сначала
|
||||||
`Class.forName("aln").getMethod("key").invoke(null)` (рефлексией, имя класса открытым
|
`Class.forName("aln").getMethod("key").invoke(null)` (рефлексией, имя класса открытым
|
||||||
текстом), если ключа сессии нет — возвращает `null`.
|
текстом), если ключа сессии нет - возвращает `null`.
|
||||||
- `SecureStrings` (5 API-блобов) — гейт по `KeyAssembler.key()`, при отсутствии ключа
|
- `SecureStrings` (5 API-блобов) - гейт по `KeyAssembler.key()`, при отсутствии ключа
|
||||||
`get()` возвращает `null` и серверные фичи просто выключаются.
|
`get()` возвращает `null` и серверные фичи просто выключаются.
|
||||||
- Шифр одинаков для всех строк, кэш по шифротексту, `.intern()`.
|
- Шифр одинаков для всех строк, кэш по шифротексту, `.intern()`.
|
||||||
|
|
||||||
## Уровень 3 — контрольный поток
|
## Уровень 3 - контрольный поток
|
||||||
|
|
||||||
Тело метода разложено в конечный автомат **с раздвоенным состоянием**:
|
Тело метода разложено в конечный автомат **с раздвоенным состоянием**:
|
||||||
|
|
||||||
|
|
@ -75,11 +75,11 @@ while (true) {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- Переход — присваивание **двум** переменным (`i`, `i2`) и обратный `goto` в начало
|
- Переход - присваивание **двум** переменным (`i`, `i2`) и обратный `goto` в начало
|
||||||
цикла. Состояние хранится не одной константой, а суммой двух opaque-полей:
|
цикла. Состояние хранится не одной константой, а суммой двух opaque-полей:
|
||||||
константная пропаганда обязана проследить пару, а не одно присваивание.
|
константная пропаганда обязана проследить пару, а не одно присваивание.
|
||||||
- Формы встреч ✓: `switch (i + i2)` — 1826, `switch (i2 + i3)` — 1067,
|
- Формы встреч ✓: `switch (i + i2)` - 1826, `switch (i2 + i3)` - 1067,
|
||||||
`switch (i3 + i4)` — 283, `switch (i4 + i5)` — 161, дальше хвост до `i15 + i16`.
|
`switch (i3 + i4)` - 283, `switch (i4 + i5)` - 161, дальше хвост до `i15 + i16`.
|
||||||
В сумме ≈ **3530 автоматов в 505 файлах**.
|
В сумме ≈ **3530 автоматов в 505 файлах**.
|
||||||
- `while (true)` + `switch` вместе встречаются в 726 файлах из 6991 ✓.
|
- `while (true)` + `switch` вместе встречаются в 726 файлах из 6991 ✓.
|
||||||
- Из-за этого `defpackage/` раздувается до ~319k строк на ~1242 файла (реальный объём
|
- Из-за этого `defpackage/` раздувается до ~319k строк на ~1242 файла (реальный объём
|
||||||
|
|
@ -87,7 +87,7 @@ while (true) {
|
||||||
|
|
||||||
## Кодирование констант (opaque predicates)
|
## Кодирование констант (opaque predicates)
|
||||||
|
|
||||||
Все целочисленные литералы — тождества по модулю 2³² (и 2⁶⁴):
|
Все целочисленные литералы - тождества по модулю 2³² (и 2⁶⁴):
|
||||||
|
|
||||||
| паттерн | равен |
|
| паттерн | равен |
|
||||||
|---|---|
|
|---|---|
|
||||||
|
|
@ -106,7 +106,7 @@ strArr[((-702410340) | 702410340) + ((-702410340) & 702410340)] = ... // ин
|
||||||
// long-вариант (Duration.ofSeconds(10))
|
// long-вариант (Duration.ofSeconds(10))
|
||||||
(703617214384230201L ^ (-703617214384230191L)) + ((703617214384230201L & (-703617214384230191L)) << 1)
|
(703617214384230201L ^ (-703617214384230191L)) + ((703617214384230201L & (-703617214384230191L)) << 1)
|
||||||
|
|
||||||
// сдвиговые константы тоже закодированы — см. «опровержение» ниже
|
// сдвиговые константы тоже закодированы - см. «опровержение» ниже
|
||||||
```
|
```
|
||||||
|
|
||||||
## Мёртвый код / декой-классы
|
## Мёртвый код / декой-классы
|
||||||
|
|
@ -114,33 +114,33 @@ strArr[((-702410340) | 702410340) + ((-702410340) & 702410340)] = ... // ин
|
||||||
Reachability-BFS от bootstrap/`@rn`/`a`+`b`/security: **1370 классов → 1316 живых,
|
Reachability-BFS от bootstrap/`@rn`/`a`+`b`/security: **1370 классов → 1316 живых,
|
||||||
42 unreachable** ✓ (`analysis/structure.md`). Среди них `apc` (299 LOC, `@Deprecated`,
|
42 unreachable** ✓ (`analysis/structure.md`). Среди них `apc` (299 LOC, `@Deprecated`,
|
||||||
`implements aqg`), интерфейсы (`aa`), enum'ы (`bc`, `bd`, `be`). Ни один живой файл не
|
`implements aqg`), интерфейсы (`aa`), enum'ы (`bc`, `bd`, `be`). Ни один живой файл не
|
||||||
ссылается на отброшенный (`dangling=0`) — то есть это балласт, выключенный из графа,
|
ссылается на отброшенный (`dangling=0`) - то есть это балласт, выключенный из графа,
|
||||||
а не случайные остатки.
|
а не случайные остатки.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Лаунчер (.NET) — отдельная, очень слабая схема
|
## Лаунчер (.NET) - отдельная, очень слабая схема
|
||||||
|
|
||||||
- Один байтовый блоб `byte[15927] _4` в `PrivateImplementationDetails`,
|
- Один байтовый блоб `byte[15927] _4` в `PrivateImplementationDetails`,
|
||||||
в статическом конструкторе `_4[i] = (byte)(_4[i] ^ i ^ 0xAA)`;
|
в статическом конструкторе `_4[i] = (byte)(_4[i] ^ i ^ 0xAA)`;
|
||||||
чтение через аксессоры `return _5[N] ?? _6(off, len)` → `Encoding.UTF8`.
|
чтение через аксессоры `return _5[N] ?? _6(off, len)` → `Encoding.UTF8`.
|
||||||
- **911 аксессоров** ✓ (все в одном файле), вызовы по всему коду — итог 1226 замен
|
- **911 аксессоров** ✓ (все в одном файле), вызовы по всему коду - итог 1226 замен
|
||||||
(по `scripts/deobfuscate_launcher`).
|
(по `scripts/deobfuscate_launcher`).
|
||||||
- Имена сервисов не тронуты: `ModProvisioner`, `PayloadKeyClient`, `EvoVisAuth`,
|
- Имена сервисов не тронуты: `ModProvisioner`, `PayloadKeyClient`, `EvoVisAuth`,
|
||||||
`HardwareId`, `DeliveryMirror`. Никакой обфускации имён, никакой обфускации потока —
|
`HardwareId`, `DeliveryMirror`. Никакой обфускации имён, никакой обфускации потока -
|
||||||
одна статическая XOR-таблица, восстанавливается одним проходом.
|
одна статическая XOR-таблица, восстанавливается одним проходом.
|
||||||
- `strings.txt` — 929 расшифрованных записей.
|
- `strings.txt` - 929 расшифрованных записей.
|
||||||
|
|
||||||
## Смежные слои защиты (не «обфускация кода», но часть контура)
|
## Смежные слои защиты (не «обфускация кода», но часть контура)
|
||||||
|
|
||||||
1. **Payload**: `12B IV || AES-256-GCM(ct||tag)`, ключ из сервера, раскрывается цепочкой
|
1. **Payload**: `12B IV || AES-256-GCM(ct||tag)`, ключ из сервера, раскрывается цепочкой
|
||||||
HMAC/HKDF от `hwid` + `token` + доли из `java.dll` — см. `evo-payload-pipeline.md`.
|
HMAC/HKDF от `hwid` + `token` + доли из `java.dll` - см. `evo-payload-pipeline.md`.
|
||||||
2. **Антиотладка и порча ключа** — 3 независимых уровня XOR-масок (`0xA5`, `0x3C`, `0x5A`),
|
2. **Антиотладка и порча ключа** - 3 независимых уровня XOR-масок (`0xA5`, `0x3C`, `0x5A`),
|
||||||
`IsDebuggerPresent`, `JAVA_TOOL_OPTIONS`, `JVM_EvoForeignAgentPresent` —
|
`IsDebuggerPresent`, `JAVA_TOOL_OPTIONS`, `JVM_EvoForeignAgentPresent` -
|
||||||
см. `evo-native-reverse.md`.
|
см. `evo-native-reverse.md`.
|
||||||
3. **HWID** = `sha256("evovis|" + MachineGuid)` — привязка к машине, Shamir-фрагменты
|
3. **HWID** = `sha256("evovis|" + MachineGuid)` - привязка к машине, Shamir-фрагменты
|
||||||
ключа, аттестация (`evovis-protect.vercel.app`).
|
ключа, аттестация (`evovis-protect.vercel.app`).
|
||||||
4. **`evo-agent.jar` вообще не обфусцирован** ✓ — читаемые строки, `SourceFile:
|
4. **`evo-agent.jar` вообще не обфусцирован** ✓ - читаемые строки, `SourceFile:
|
||||||
"EvoAgent.java"`, т. е. расшифровщик payload открыт.
|
"EvoAgent.java"`, т. е. расшифровщик payload открыт.
|
||||||
5. Скрипт-логирование с детектом `$ExecutionContext.SessionState.LanguageMode`
|
5. Скрипт-логирование с детектом `$ExecutionContext.SessionState.LanguageMode`
|
||||||
(защита от поломки PowerShell-обфускации у себя).
|
(защита от поломки PowerShell-обфускации у себя).
|
||||||
|
|
@ -153,10 +153,10 @@ Reachability-BFS от bootstrap/`@rn`/`a`+`b`/security: **1370 классов
|
||||||
|---|---|
|
|---|---|
|
||||||
| 1277 классов в default package, 16 в `a/`, 111 в `b/` | ✓ подтверждено |
|
| 1277 классов в default package, 16 в `a/`, 111 в `b/` | ✓ подтверждено |
|
||||||
| LCG `1103515245` / `12345`, XOR со старшим байтом | ✓ подтверждено (`awc.java`) |
|
| LCG `1103515245` / `12345`, XOR со старшим байтом | ✓ подтверждено (`awc.java`) |
|
||||||
| «все строковые литералы зашифрованы `awc.d`» | частично: 8993 вызова `awc.d(` в `jar-src` ✓, но `awc.g(` — **0** вызовов в этом билде (описанная ветка не используется) |
|
| «все строковые литералы зашифрованы `awc.d`» | частично: 8993 вызова `awc.d(` в `jar-src` ✓, но `awc.g(` - **0** вызовов в этом билде (описанная ветка не используется) |
|
||||||
| 8924 вызова / 5367 уникальных строк | расхождение: в файлах 8993 вызовов, `mod_strings.txt` — 5665 строк, `tsv` — 9248 пар. README, видимо, от усечённого прогона |
|
| 8924 вызова / 5367 уникальных строк | расхождение: в файлах 8993 вызовов, `mod_strings.txt` - 5665 строк, `tsv` - 9248 пар. README, видимо, от усечённого прогона |
|
||||||
| opaque-тождества `(A^B)+((A&B)<<1)` и `(A\|B)+(A&B)` | ✓ подтверждено, есть и long-вариант |
|
| opaque-тождества `(A^B)+((A&B)<<1)` и `(A\|B)+(A&B)` | ✓ подтверждено, есть и long-вариант |
|
||||||
| «сдвиговые константы в байткоде отрицательные (`>> -8` == `>> 24`)» | ✗ **не подтверждено**: по `javap` всех 1277 классов — 32 559 сдвигов, **ни одного** отрицательного непосредственного операнда (все константные сдвиги — это `iconst_1` из opaque-паттерна `<< 1`). В исходниках тоже 0 вхождений `>> -`. Скорее всего, наблюдение из другого билда/декомпилятора |
|
| «сдвиговые константы в байткоде отрицательные (`>> -8` == `>> 24`)» | ✗ **не подтверждено**: по `javap` всех 1277 классов - 32 559 сдвигов, **ни одного** отрицательного непосредственного операнда (все константные сдвиги - это `iconst_1` из opaque-паттерна `<< 1`). В исходниках тоже 0 вхождений `>> -`. Скорее всего, наблюдение из другого билда/декомпилятора |
|
||||||
| «декой-классы — пустышки с бессмысленными членами» | ⚠ reachability (42 файла) подтверждается, но часть из них — осмысленный код (`apc`, 299 LOC, `@Deprecated`), часть — пустые интерфейсы/enum. «Пустышки» — интерпретация |
|
| «декой-классы - пустышки с бессмысленными членами» | ⚠ reachability (42 файла) подтверждается, но часть из них - осмысленный код (`apc`, 299 LOC, `@Deprecated`), часть - пустые интерфейсы/enum. «Пустышки» - интерпретация |
|
||||||
| «объём декомпилированного кода раздут ~25 раз» | ⚠ порядок величины верен (319k строк против, возможно, ~15k реальных), точный коэффициент не измерен |
|
| «объём декомпилированного кода раздут ~25 раз» | ⚠ порядок величины верен (319k строк против, возможно, ~15k реальных), точный коэффициент не измерен |
|
||||||
| «911 accessor'ов, 1226 вызовов» в лаунчере | ✓ 911 аксессоров подтверждено |
|
| «911 accessor'ов, 1226 вызовов» в лаунчере | ✓ 911 аксессоров подтверждено |
|
||||||
|
|
|
||||||
|
|
@ -2,20 +2,20 @@
|
||||||
|
|
||||||
## Три уровня
|
## Три уровня
|
||||||
|
|
||||||
1. **Имена** — 1277 классов вынесены в default package с именами вида `a…aa…`;
|
1. **Имена** - 1277 классов вынесены в default package с именами вида `a…aa…`;
|
||||||
миксины разбросаны по пакетам `a/` и `b/`. Смысловых имён классов в коде нет.
|
миксины разбросаны по пакетам `a/` и `b/`. Смысловых имён классов в коде нет.
|
||||||
|
|
||||||
2. **Строки** — все строковые литералы зашифрованы вызовом `awc.d(cipher, key)`:
|
2. **Строки** - все строковые литералы зашифрованы вызовом `awc.d(cipher, key)`:
|
||||||
- LCG: `state = state * 1103515245 + 12345` (32-бит переполнение);
|
- LCG: `state = state * 1103515245 + 12345` (32-бит переполнение);
|
||||||
- байт: `cipherChar ^ (state >>> 24)`;
|
- байт: `cipherChar ^ (state >>> 24)`;
|
||||||
- результат — UTF-8 строка.
|
- результат - UTF-8 строка.
|
||||||
Ключи передаются «непрозрачными» целыми (см. ниже). Второй вариант `awc.g(...)`
|
Ключи передаются «непрозрачными» целыми (см. ниже). Второй вариант `awc.g(...)`
|
||||||
— то же самое плюс session-gate: строка расшифровывается только после
|
- то же самое плюс session-gate: строка расшифровывается только после
|
||||||
проверки сессии через рефлексию `aln.key()`.
|
проверки сессии через рефлексию `aln.key()`.
|
||||||
|
|
||||||
3. **Контрольный поток** — тела методов разложены в машину состояний:
|
3. **Контрольный поток** - тела методов разложены в машину состояний:
|
||||||
`while (true) { switch (состояние) { case <обфуск-константа>: ...; состояние = <следующая>; } }`.
|
`while (true) { switch (состояние) { case <обфуск-константа>: ...; состояние = <следующая>; } }`.
|
||||||
Переходы между блоками — через присваивание целому и обратный `goto`.
|
Переходы между блоками - через присваивание целому и обратный `goto`.
|
||||||
Из-за этого объём декомпилированного кода раздут примерно в 25 раз
|
Из-за этого объём декомпилированного кода раздут примерно в 25 раз
|
||||||
относительно реального.
|
относительно реального.
|
||||||
|
|
||||||
|
|
@ -29,14 +29,14 @@
|
||||||
| `(A \| B) + (A & B)` | `A + B` |
|
| `(A \| B) + (A & B)` | `A + B` |
|
||||||
|
|
||||||
Так прячутся даже `0`, `1`, `16`, `255`. Например, `true`/`false` приходят как
|
Так прячутся даже `0`, `1`, `16`, `255`. Например, `true`/`false` приходят как
|
||||||
выражение, равное `1`/`0` (javac такого не генерирует — это работа обфускатора).
|
выражение, равное `1`/`0` (javac такого не генерирует - это работа обфускатора).
|
||||||
Сдвиги и маски тоже закодированы: сдвиговые константы в байткоде могут быть
|
Сдвиги и маски тоже закодированы: сдвиговые константы в байткоде могут быть
|
||||||
отрицательными — JVM маскирует количество бит по младшим 5 битам
|
отрицательными - JVM маскирует количество бит по младшим 5 битам
|
||||||
(`ishl/ishr` со сдвигом `-8` работает как сдвиг на `24`), поэтому `>> -8`
|
(`ishl/ishr` со сдвигом `-8` работает как сдвиг на `24`), поэтому `>> -8`
|
||||||
в байткоде эквивалентен `>> 24` в исходнике.
|
в байткоде эквивалентен `>> 24` в исходнике.
|
||||||
|
|
||||||
## Прочее
|
## Прочее
|
||||||
|
|
||||||
- Декой-классы обфускатора — пустышки с бессмысленными членами, в логике не участвуют.
|
- Декой-классы обфускатора - пустышки с бессмысленными членами, в логике не участвуют.
|
||||||
- Из-за имён-ключевых слов (`if`, `do`, …) декомпилятор переименовывает такие
|
- Из-за имён-ключевых слов (`if`, `do`, …) декомпилятор переименовывает такие
|
||||||
классы (`Cif`, `Cdo`) — в самом jar классов с такими именами нет.
|
классы (`Cif`, `Cdo`) - в самом jar классов с такими именами нет.
|
||||||
|
|
|
||||||
|
|
@ -71,19 +71,19 @@ Fabric загружает мод → мод сам распаковывает ev
|
||||||
|
|
||||||
Ключевые свойства:
|
Ключевые свойства:
|
||||||
|
|
||||||
1. **Мод не лежит на диске в открытом виде** — расшифровка в памяти агента,
|
1. **Мод не лежит на диске в открытом виде** - расшифровка в памяти агента,
|
||||||
`candidate` — временный mount-файл.
|
`candidate` - временный mount-файл.
|
||||||
2. **Ключ собирается из двух мест**: hex из env (лаунчер) + `java.lang.EvoShare.compute(hwid)`
|
2. **Ключ собирается из двух мест**: hex из env (лаунчер) + `java.lang.EvoShare.compute(hwid)`
|
||||||
в **подменённом JVM** — поэтому мод гоняется только на их evojdk.
|
в **подменённом JVM** - поэтому мод гоняется только на их evojdk.
|
||||||
3. **Инъекция в Fabric** — не мод в `mods/`, а байткод-трансформация внутреннего
|
3. **Инъекция в Fabric** - не мод в `mods/`, а байткод-трансформация внутреннего
|
||||||
класса Fabric (`ArgumentModCandidateFinder`) через `java.lang.classfile` API.
|
класса Fabric (`ArgumentModCandidateFinder`) через `java.lang.classfile` API.
|
||||||
Классы: `EvoAgent`, `EvoAgentHook`, `FabricDiscoveryInjector`, `PreparedMod`,
|
Классы: `EvoAgent`, `EvoAgentHook`, `FabricDiscoveryInjector`, `PreparedMod`,
|
||||||
`NestedLibraries`.
|
`NestedLibraries`.
|
||||||
4. Далее внутри мода — свой слой: `KeyAssembler` (Shamir: сервер + integrity + native + hwid),
|
4. Далее внутри мода - свой слой: `KeyAssembler` (Shamir: сервер + integrity + native + hwid),
|
||||||
`SealedBoot` (sealed.bin в этом билде отсутствует → fail-open), `SessionHeartbeat`.
|
`SealedBoot` (sealed.bin в этом билде отсутствует → fail-open), `SessionHeartbeat`.
|
||||||
|
|
||||||
## Что здесь не проверено
|
## Что здесь не проверено
|
||||||
|
|
||||||
- Тело `java.dll` / `EvoShare` — натив не реверсился (план в `todo.md`).
|
- Тело `java.dll` / `EvoShare` - натив не реверсился (план в `todo.md`).
|
||||||
- Реальное поведение при запуске (нужен MC 26.2 + Fabric 0.19.5 + их evojdk + дисплей).
|
- Реальное поведение при запуске (нужен MC 26.2 + Fabric 0.19.5 + их evojdk + дисплей).
|
||||||
- `evoguard.dll` — не реверсился.
|
- `evoguard.dll` - не реверсился.
|
||||||
|
|
|
||||||
|
|
@ -1,47 +1,47 @@
|
||||||
# Где защита EvoVis провалилась — чего нам делать нельзя
|
# Где защита EvoVis провалилась - чего нам делать нельзя
|
||||||
|
|
||||||
Разобрано полностью за один проход, без единого рантайм-хака. Каждый пункт —
|
Разобрано полностью за один проход, без единого рантайм-хака. Каждый пункт -
|
||||||
что именно дало ключ атаки.
|
что именно дало ключ атаки.
|
||||||
|
|
||||||
## 1. Ключ шифрования строк лежит рядом со шифротекстом
|
## 1. Ключ шифрования строк лежит рядом со шифротекстом
|
||||||
|
|
||||||
`awc.d("<cipher>", <opaque-выражение>)` — ключ это **константное выражение в вызове**.
|
`awc.d("<cipher>", <opaque-выражение>)` - ключ это **константное выражение в вызове**.
|
||||||
Ни secrets, ни runtime-derivation: выражение просто сворачивается в `int` (любой
|
Ни secrets, ни runtime-derivation: выражение просто сворачивается в `int` (любой
|
||||||
парсер вроде `scripts/expr.rs`, ~200 строк). Итог: **8993 вызова расшифрованы
|
парсер вроде `scripts/expr.rs`, ~200 строк). Итог: **8993 вызова расшифрованы
|
||||||
детерминированно, 0 обращений к JVM**.
|
детерминированно, 0 обращений к JVM**.
|
||||||
|
|
||||||
Хуже: шифр один на весь продукт (LCG с фиксированными `a/c`), т. е. зная одну
|
Хуже: шифр один на весь продукт (LCG с фиксированными `a/c`), т. е. зная одну
|
||||||
пару «шифротекст → текст», остальные не нужно даже сворачивать — можно учиться.
|
пару «шифротекст → текст», остальные не нужно даже сворачивать - можно учиться.
|
||||||
|
|
||||||
## 2. Обфусцированы только типы, но не члены
|
## 2. Обфусцированы только типы, но не члены
|
||||||
|
|
||||||
`onDraw`, `getProjectiles`, `BreakingBad`, `key()`, `isAuthorized()`, `setUser` —
|
`onDraw`, `getProjectiles`, `BreakingBad`, `key()`, `isAuthorized()`, `setUser` -
|
||||||
открыто. Семантика класса читается по одному `javap -p`. Класс с тремя понятными
|
открыто. Семантика класса читается по одному `javap -p`. Класс с тремя понятными
|
||||||
методами опознаётся за секунду, даже если имя `wb`.
|
методами опознаётся за секунду, даже если имя `wb`.
|
||||||
|
|
||||||
## 3. Метаданные и отладочная информация не тронуты
|
## 3. Метаданные и отладочная информация не тронуты
|
||||||
|
|
||||||
- `SourceFile: "StringRuntime.java"` / `"KeyAssembler.java"` / `"PayloadCrypto.java"` —
|
- `SourceFile: "StringRuntime.java"` / `"KeyAssembler.java"` / `"PayloadCrypto.java"` -
|
||||||
оригинальные имена 160 классов (все миксины — 100%).
|
оригинальные имена 160 классов (все миксины - 100%).
|
||||||
- `LineNumberTable` (10 218) + `LocalVariableTable` (151) — имена локальных переменных.
|
- `LineNumberTable` (10 218) + `LocalVariableTable` (151) - имена локальных переменных.
|
||||||
- `@rn(name=..., description=...)` — полный каталог всех 57 модулей с описаниями.
|
- `@rn(name=..., description=...)` - полный каталог всех 57 модулей с описаниями.
|
||||||
- `fabric.mod.json`, `*.mixins.json` (пути и состав), `accesswidener`, ресурсы.
|
- `fabric.mod.json`, `*.mixins.json` (пути и состав), `accesswidener`, ресурсы.
|
||||||
|
|
||||||
Все эти вещи дешевле всего чинить и дороже всего отдавать.
|
Все эти вещи дешевле всего чинить и дороже всего отдавать.
|
||||||
|
|
||||||
## 4. Flattening сделан «по учебнику»
|
## 4. Flattening сделан «по учебнику»
|
||||||
|
|
||||||
Один шаблон на весь продукт: `while(true) { switch (i + i2) }`, номер состояния —
|
Один шаблон на весь продукт: `while(true) { switch (i + i2) }`, номер состояния -
|
||||||
сумма двух opaque-полей, переходы — присваивание пары. Один regex находит 3530
|
сумма двух opaque-полей, переходы - присваивание пары. Один regex находит 3530
|
||||||
автоматов. Декомпилятор восстанавливает CFG, потому что:
|
автоматов. Декомпилятор восстанавливает CFG, потому что:
|
||||||
- `switch` по **константным** меткам (метки не закодированы);
|
- `switch` по **константным** меткам (метки не закодированы);
|
||||||
- все переходы — прямые присваивания в одном basic block;
|
- все переходы - прямые присваивания в одном basic block;
|
||||||
- нет вычислимых на входе предикатов, нет переходов через таблицы/массивы;
|
- нет вычислимых на входе предикатов, нет переходов через таблицы/массивы;
|
||||||
- нет обфускации самих тел блоков (тела — обычный код).
|
- нет обфускации самих тел блоков (тела - обычный код).
|
||||||
|
|
||||||
## 5. Opaque-предикаты сводятся к `A+B`
|
## 5. Opaque-предикаты сводятся к `A+B`
|
||||||
|
|
||||||
`(A^B)+((A&B)<<1)` и `(A|B)+(A&B)` — это школьные тождества. Распознаются
|
`(A^B)+((A&B)<<1)` и `(A|B)+(A&B)` - это школьные тождества. Распознаются
|
||||||
регуляркой, сворачиваются в один проход, заодно выдают «бесплатный» фолдинг
|
регуляркой, сворачиваются в один проход, заодно выдают «бесплатный» фолдинг
|
||||||
остальных констант. Ни одного настоящего opaque-предиката (зависящего от состояния
|
остальных констант. Ни одного настоящего opaque-предиката (зависящего от состояния
|
||||||
памяти/окружения и всегда истинного) в коде нет.
|
памяти/окружения и всегда истинного) в коде нет.
|
||||||
|
|
@ -55,13 +55,13 @@
|
||||||
## 7. Каждая защита живёт отдельно и называется своим именем
|
## 7. Каждая защита живёт отдельно и называется своим именем
|
||||||
|
|
||||||
`StringRuntime`, `KeyAssembler`, `AttestClient`, `RuntimeGuard`, `SealedBoot`,
|
`StringRuntime`, `KeyAssembler`, `AttestClient`, `RuntimeGuard`, `SealedBoot`,
|
||||||
`NativeGuard` — названия прямо говорят атакующему, куда смотреть. Плюс open-source
|
`NativeGuard` - названия прямо говорят атакующему, куда смотреть. Плюс open-source
|
||||||
идентификаторы: `aln` упомянут в рефлексии строкой, `awc` — по имени вызова.
|
идентификаторы: `aln` упомянут в рефлексии строкой, `awc` - по имени вызова.
|
||||||
|
|
||||||
## 8. Вторичные компоненты вообще без защиты
|
## 8. Вторичные компоненты вообще без защиты
|
||||||
|
|
||||||
- `evo-agent.jar` (расшифровщик payload) — открытые строки, `SourceFile`, без обфускации.
|
- `evo-agent.jar` (расшифровщик payload) - открытые строки, `SourceFile`, без обфускации.
|
||||||
- Лаунчер .NET — статическая XOR-таблица `b ^ i ^ 0xAA`, имена сервисов не тронуты.
|
- Лаунчер .NET - статическая XOR-таблица `b ^ i ^ 0xAA`, имена сервисов не тронуты.
|
||||||
- Защита ключа в нативе сильная, но **код мода** защищён слабее, чем ключ.
|
- Защита ключа в нативе сильная, но **код мода** защищён слабее, чем ключ.
|
||||||
|
|
||||||
## 9. Шифр без аутентификации и без per-string salt
|
## 9. Шифр без аутентификации и без per-string salt
|
||||||
|
|
@ -71,6 +71,6 @@ LCG+XOR: нет MAC, нет случайного сида на строку, н
|
||||||
|
|
||||||
## Опровержение (чтобы не повторять чужие ошибки в выводах)
|
## Опровержение (чтобы не повторять чужие ошибки в выводах)
|
||||||
|
|
||||||
- «Сдвиги в байткоде могут быть отрицательными (`>> -8` == `>> 24`)» — по всем
|
- «Сдвиги в байткоде могут быть отрицательными (`>> -8` == `>> 24`)» - по всем
|
||||||
1277 классам **0** таких случаев (32 559 сдвигов, константные — всегда `1`).
|
1277 классам **0** таких случаев (32 559 сдвигов, константные - всегда `1`).
|
||||||
- `awc.g(...)` (session-gate) в этом билде **не вызывается ни разу**.
|
- `awc.g(...)` (session-gate) в этом билде **не вызывается ни разу**.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue