Initial commit: noxis JVM bytecode obfuscator
Workspace of 9 crates (noxis, noxis-core, noxis-classfile, noxis-analysis, noxis-passes, noxis-runtime, noxis-jar, noxis-verify, noxis-structure-check). AGPL-3.0-only license, Rust 2024 edition, unsafe_code denied.
This commit is contained in:
commit
a641e69455
140 changed files with 17197 additions and 0 deletions
94
crates/noxis-core/src/pipeline/context.rs
Normal file
94
crates/noxis-core/src/pipeline/context.rs
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
//! Контекст прогона: конфиг, мастер-сид, маппинги, заметки.
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::mapping::Mapping;
|
||||
use crate::rng::{Rng, derive_seed};
|
||||
|
||||
/// Всё состояние, которое пассы читают и дописывают.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Context {
|
||||
pub config: Config,
|
||||
/// Мастер-сид прогона (всегда известен и попадает в отчёт).
|
||||
pub master_seed: u64,
|
||||
pub mapping: Mapping,
|
||||
/// Заметки пассов: почему что-то пропущено, что не удалось.
|
||||
pub notes: Vec<String>,
|
||||
}
|
||||
|
||||
impl Context {
|
||||
/// Новый контекст. `seed` берётся из конфига или генерируется.
|
||||
pub fn new(mut config: Config) -> Self {
|
||||
config.sanitize();
|
||||
let master_seed = config.seed.unwrap_or_else(fresh_seed);
|
||||
Self {
|
||||
config,
|
||||
master_seed,
|
||||
mapping: Mapping::default(),
|
||||
notes: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Детерминированный RNG для домена (`"rename"`, `"strings/0"`, ...).
|
||||
///
|
||||
/// Доменное разделение: разные пассы не должны вытягивать одну
|
||||
/// последовательность — иначе их «случайности» статистически коррелируют.
|
||||
pub fn rng(&self, domain: &str) -> Rng {
|
||||
Rng::from_seed(derive_seed(self.master_seed, domain, 0))
|
||||
}
|
||||
|
||||
/// RNG с порядковым номером внутри домена.
|
||||
pub fn rng_nth(&self, domain: &str, index: u64) -> Rng {
|
||||
Rng::from_seed(derive_seed(self.master_seed, domain, index))
|
||||
}
|
||||
|
||||
/// Заметка в отчёт (видна в JSON-отчёте и в логе).
|
||||
pub fn note(&mut self, msg: impl Into<String>) {
|
||||
let msg = msg.into();
|
||||
tracing::debug!(target: "noxis", "{msg}");
|
||||
self.notes.push(msg);
|
||||
}
|
||||
}
|
||||
|
||||
/// Нестабильный сид, когда пользователь его не задал.
|
||||
fn fresh_seed() -> u64 {
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
let nanos = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map(|d| d.as_nanos() as u64)
|
||||
.unwrap_or(0x5EED_C0DE);
|
||||
let mut r = Rng::from_seed(nanos ^ 0xA5A5_A5A5_A5A5_A5A5);
|
||||
r.next_u64()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn rng_is_reproducible_from_config_seed() {
|
||||
let cfg = Config {
|
||||
seed: Some(777),
|
||||
..Config::default()
|
||||
};
|
||||
let a = Context::new(cfg.clone());
|
||||
let b = Context::new(cfg);
|
||||
assert_eq!(a.master_seed, b.master_seed);
|
||||
let av: Vec<u64> = {
|
||||
let mut r = a.rng("rename");
|
||||
(0..8).map(|_| r.next_u64()).collect()
|
||||
};
|
||||
let bv: Vec<u64> = {
|
||||
let mut r = b.rng("rename");
|
||||
(0..8).map(|_| r.next_u64()).collect()
|
||||
};
|
||||
assert_eq!(av, bv);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn note_is_recorded() {
|
||||
let mut ctx = Context::new(Config::default());
|
||||
ctx.note("тест");
|
||||
assert_eq!(ctx.notes.len(), 1);
|
||||
}
|
||||
}
|
||||
146
crates/noxis-core/src/pipeline/mod.rs
Normal file
146
crates/noxis-core/src/pipeline/mod.rs
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
//! Конвейер пассов: единый порядок исполнения и отчёт.
|
||||
|
||||
pub mod context;
|
||||
pub mod report;
|
||||
|
||||
use serde::Serialize;
|
||||
use noxis_classfile::Program;
|
||||
|
||||
use self::context::Context;
|
||||
use self::report::Report;
|
||||
use crate::config::PassId;
|
||||
use crate::error::CoreError;
|
||||
|
||||
/// Результат одного пасса.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct PassReport {
|
||||
pub pass: PassId,
|
||||
/// Сколько объектов трансформировано.
|
||||
pub transformed: usize,
|
||||
/// Сколько объектов сознательно пропущено.
|
||||
pub skipped: usize,
|
||||
/// Человекочитаемые заметки пасса.
|
||||
pub notes: Vec<String>,
|
||||
}
|
||||
|
||||
impl PassReport {
|
||||
/// Пустой отчёт (пасс ещё не реализован).
|
||||
pub fn not_implemented(pass: PassId, todo: &'static str) -> Self {
|
||||
Self {
|
||||
pass,
|
||||
transformed: 0,
|
||||
skipped: 0,
|
||||
notes: vec![format!("не реализовано: {todo}")],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Единый контракт трансформации программы.
|
||||
pub trait Pass: Send + Sync {
|
||||
/// Идентификатор пасса (совпадает с ключом в конфиге).
|
||||
fn id(&self) -> PassId;
|
||||
|
||||
/// Применить пасс. Порядок пассов задаёт [`Pipeline`].
|
||||
fn apply(&self, ctx: &mut Context, program: &mut Program) -> Result<PassReport, CoreError>;
|
||||
}
|
||||
|
||||
/// Упорядоченная цепочка пассов.
|
||||
///
|
||||
/// Порядок — не деталь: см. `docs/architecture.md` (rename → resources →
|
||||
/// strings → constants → flow → noise → debug), иначе часть техник ломает
|
||||
/// предыдущие или работает по уже мёртвым именам.
|
||||
#[derive(Default)]
|
||||
pub struct Pipeline {
|
||||
passes: Vec<Box<dyn Pass>>,
|
||||
}
|
||||
|
||||
impl Pipeline {
|
||||
/// Пустой конвейер.
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Добавить пасс в конец.
|
||||
pub fn push(mut self, pass: Box<dyn Pass>) -> Self {
|
||||
self.passes.push(pass);
|
||||
self
|
||||
}
|
||||
|
||||
/// Число пассов в цепочке.
|
||||
pub fn len(&self) -> usize {
|
||||
self.passes.len()
|
||||
}
|
||||
|
||||
/// Пуст ли конвейер.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.passes.is_empty()
|
||||
}
|
||||
|
||||
/// Прогнать всю программу через все пассы.
|
||||
pub fn run(&self, ctx: &mut Context, program: &mut Program) -> Result<Report, CoreError> {
|
||||
let mut report = Report {
|
||||
seed: ctx.master_seed,
|
||||
..Report::default()
|
||||
};
|
||||
report.bump("classes", program.classes.len());
|
||||
report.bump("resources", program.resources.len());
|
||||
|
||||
for pass in &self.passes {
|
||||
let id = pass.id();
|
||||
tracing::info!(target: "noxis", pass = %id, "пасс запущен");
|
||||
let result = pass.apply(ctx, program)?;
|
||||
tracing::info!(
|
||||
target: "noxis",
|
||||
pass = %id,
|
||||
transformed = result.transformed,
|
||||
skipped = result.skipped,
|
||||
"пасс завершён"
|
||||
);
|
||||
report.passes.push(result);
|
||||
}
|
||||
|
||||
report.notes = std::mem::take(&mut ctx.notes);
|
||||
Ok(report)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
||||
use super::*;
|
||||
|
||||
struct Stub(PassId);
|
||||
|
||||
impl Pass for Stub {
|
||||
fn id(&self) -> PassId {
|
||||
self.0
|
||||
}
|
||||
|
||||
fn apply(
|
||||
&self,
|
||||
_ctx: &mut Context,
|
||||
_program: &mut Program,
|
||||
) -> Result<PassReport, CoreError> {
|
||||
Ok(PassReport {
|
||||
pass: self.0,
|
||||
transformed: 1,
|
||||
skipped: 0,
|
||||
notes: Vec::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runs_in_order_and_collects_reports() {
|
||||
let pipeline = Pipeline::new()
|
||||
.push(Box::new(Stub(PassId::Rename)))
|
||||
.push(Box::new(Stub(PassId::Strings)));
|
||||
let mut ctx = Context::new(crate::config::Config::default());
|
||||
let mut program = Program::default();
|
||||
let report = pipeline.run(&mut ctx, &mut program).unwrap();
|
||||
assert_eq!(report.passes.len(), 2);
|
||||
assert_eq!(report.passes[0].pass, PassId::Rename);
|
||||
assert_eq!(report.passes[1].pass, PassId::Strings);
|
||||
assert_eq!(report.counters.get("classes"), Some(&0));
|
||||
}
|
||||
}
|
||||
20
crates/noxis-core/src/pipeline/report.rs
Normal file
20
crates/noxis-core/src/pipeline/report.rs
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
//! Итог прогона: пассы + счётчики — сериализуется в JSON.
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
/// Итог прогона: пассы + счётчики — сериализуется в JSON.
|
||||
#[derive(Debug, Clone, Default, Serialize)]
|
||||
pub struct Report {
|
||||
pub seed: u64,
|
||||
pub passes: Vec<crate::pipeline::PassReport>,
|
||||
pub notes: Vec<String>,
|
||||
/// Произвольные счётчики (`classes`, `strings_encrypted`, ...).
|
||||
pub counters: std::collections::BTreeMap<String, usize>,
|
||||
}
|
||||
|
||||
impl Report {
|
||||
/// Увеличить счётчик.
|
||||
pub fn bump(&mut self, key: &str, by: usize) {
|
||||
*self.counters.entry(key.to_string()).or_insert(0) += by;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue