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
34
crates/noxis/Cargo.toml
Normal file
34
crates/noxis/Cargo.toml
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
[package]
|
||||
name = "noxis"
|
||||
description = "CLI обфускатора JVM-байткода: obfuscate, analyze, patterns"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[[bin]]
|
||||
name = "noxis"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
clap = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
noxis-analysis = { workspace = true }
|
||||
noxis-classfile = { workspace = true }
|
||||
noxis-core = { workspace = true }
|
||||
noxis-jar = { workspace = true }
|
||||
noxis-passes = { workspace = true }
|
||||
noxis-verify = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
ristretto_classfile = "0.34"
|
||||
|
||||
[build-dependencies]
|
||||
noxis-structure-check = { workspace = true }
|
||||
3
crates/noxis/build.rs
Normal file
3
crates/noxis/build.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
fn main() {
|
||||
noxis_structure_check::check();
|
||||
}
|
||||
73
crates/noxis/examples/dbg.rs
Normal file
73
crates/noxis/examples/dbg.rs
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
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");
|
||||
}
|
||||
68
crates/noxis/src/cmd/analyze.rs
Normal file
68
crates/noxis/src/cmd/analyze.rs
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
//! Команда `analyze`: разбор jar без изменений.
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use noxis_analysis::Analysis;
|
||||
use noxis_core::Config;
|
||||
|
||||
/// Аргументы анализа.
|
||||
#[derive(Debug, clap::Args)]
|
||||
pub struct Args {
|
||||
/// Входной jar.
|
||||
pub input: PathBuf,
|
||||
/// TOML-конфиг с keep-паттернами (без него — пустой keep).
|
||||
#[arg(short, long)]
|
||||
pub config: Option<PathBuf>,
|
||||
}
|
||||
|
||||
/// Показать классы, entrypoint'ы и keep-правила.
|
||||
pub fn run(args: Args) -> Result<()> {
|
||||
let config = match &args.config {
|
||||
Some(path) => {
|
||||
let text = std::fs::read_to_string(path)
|
||||
.with_context(|| format!("конфиг {}", path.display()))?;
|
||||
Config::from_toml(&text).map_err(|error| anyhow::anyhow!("{error}"))?
|
||||
}
|
||||
None => Config::default(),
|
||||
};
|
||||
let entries = noxis_jar::read_file(&args.input)
|
||||
.with_context(|| format!("чтение {}", args.input.display()))?;
|
||||
let program =
|
||||
noxis_jar::load(entries).map_err(|error| anyhow::anyhow!("разбор jar: {error}"))?;
|
||||
let analysis =
|
||||
Analysis::run(&program, &config).map_err(|error| anyhow::anyhow!("анализ: {error}"))?;
|
||||
|
||||
println!("классов: {}", program.classes.len());
|
||||
println!("ресурсов: {}", program.resources.len());
|
||||
println!("locked классов: {}", analysis.locked_classes.len());
|
||||
println!(
|
||||
"entrypoints.main: {}",
|
||||
analysis.entrypoints.main_classes.len()
|
||||
);
|
||||
for name in &analysis.entrypoints.main_classes {
|
||||
println!(" main: {name}");
|
||||
}
|
||||
println!(
|
||||
"reflective_names: {}",
|
||||
analysis.entrypoints.reflective_names.len()
|
||||
);
|
||||
println!("keep-правил: {}", analysis.keep.len());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn analyze_missing_jar_fails() {
|
||||
let args = Args {
|
||||
input: PathBuf::from("definitely-not-here.jar"),
|
||||
config: None,
|
||||
};
|
||||
assert!(run(args).is_err());
|
||||
}
|
||||
}
|
||||
5
crates/noxis/src/cmd/mod.rs
Normal file
5
crates/noxis/src/cmd/mod.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
//! Команды CLI.
|
||||
|
||||
pub mod analyze;
|
||||
pub mod obfuscate;
|
||||
pub mod patterns;
|
||||
232
crates/noxis/src/cmd/obfuscate.rs
Normal file
232
crates/noxis/src/cmd/obfuscate.rs
Normal file
|
|
@ -0,0 +1,232 @@
|
|||
//! Команда `obfuscate`: jar → конвейер → jar + маппинги + отчёт.
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use noxis_core::{Config, Context, PassId};
|
||||
|
||||
/// Аргументы обфускации.
|
||||
#[derive(Debug, clap::Args)]
|
||||
pub struct Args {
|
||||
/// Входной jar.
|
||||
pub input: PathBuf,
|
||||
/// Выходной jar.
|
||||
#[arg(short, long)]
|
||||
pub output: PathBuf,
|
||||
/// TOML-конфиг (без него — профиль Standard).
|
||||
#[arg(short, long)]
|
||||
pub config: Option<PathBuf>,
|
||||
/// Мастер-сид (детерминированность по умолчанию — из конфига).
|
||||
#[arg(long)]
|
||||
pub seed: Option<u64>,
|
||||
/// Куда положить JSON-отчёт (по умолчанию рядом с выходом).
|
||||
#[arg(long)]
|
||||
pub report: Option<PathBuf>,
|
||||
/// Выключить пасс переименования.
|
||||
#[arg(long)]
|
||||
pub no_rename: bool,
|
||||
/// Выключить пасс ресурсов.
|
||||
#[arg(long)]
|
||||
pub no_resources: bool,
|
||||
/// Выключить пасс строк.
|
||||
#[arg(long)]
|
||||
pub no_strings: bool,
|
||||
/// Выключить пасс констант.
|
||||
#[arg(long)]
|
||||
pub no_constants: bool,
|
||||
/// Выключить пасс шума.
|
||||
#[arg(long)]
|
||||
pub no_noise: bool,
|
||||
/// Выключить пасс отладочной информации.
|
||||
#[arg(long)]
|
||||
pub no_debug_info: bool,
|
||||
/// Выключить пасс control-flow (ещё не реализован — честная ошибка).
|
||||
#[arg(long)]
|
||||
pub no_flow: bool,
|
||||
}
|
||||
|
||||
/// Прогнать обфускацию.
|
||||
pub fn run(args: Args) -> Result<()> {
|
||||
if args.no_flow {
|
||||
anyhow::bail!("пасс flow ещё не реализован (см. todo.md §3.4)");
|
||||
}
|
||||
let mut config = match &args.config {
|
||||
Some(path) => {
|
||||
let text = std::fs::read_to_string(path)
|
||||
.with_context(|| format!("конфиг {}", path.display()))?;
|
||||
Config::from_toml(&text).map_err(|error| anyhow::anyhow!("{error}"))?
|
||||
}
|
||||
None => Config::default(),
|
||||
};
|
||||
if let Some(seed) = args.seed {
|
||||
config.seed = Some(seed);
|
||||
}
|
||||
for (flag, id) in [
|
||||
(args.no_rename, PassId::Rename),
|
||||
(args.no_resources, PassId::Resources),
|
||||
(args.no_strings, PassId::Strings),
|
||||
(args.no_constants, PassId::Constants),
|
||||
(args.no_noise, PassId::Noise),
|
||||
(args.no_debug_info, PassId::DebugInfo),
|
||||
] {
|
||||
if flag && !config.disable.contains(&id) {
|
||||
config.disable.push(id);
|
||||
}
|
||||
}
|
||||
|
||||
let entries = noxis_jar::read_file(&args.input)
|
||||
.with_context(|| format!("чтение {}", args.input.display()))?;
|
||||
let (entries, stripped) = noxis_jar::strip_signatures(entries);
|
||||
let mut program =
|
||||
noxis_jar::load(entries).map_err(|error| anyhow::anyhow!("разбор jar: {error}"))?;
|
||||
|
||||
let mut ctx = Context::new(config);
|
||||
let report = noxis_passes::standard_pipeline()
|
||||
.run(&mut ctx, &mut program)
|
||||
.map_err(|error| anyhow::anyhow!("конвейер: {error}"))?;
|
||||
let entries =
|
||||
noxis_jar::save(&program).map_err(|error| anyhow::anyhow!("сборка jar: {error}"))?;
|
||||
noxis_jar::write_file(&args.output, &entries)
|
||||
.with_context(|| format!("запись {}", args.output.display()))?;
|
||||
|
||||
let report_path = args
|
||||
.report
|
||||
.unwrap_or_else(|| args.output.with_extension("report.json"));
|
||||
std::fs::write(
|
||||
&report_path,
|
||||
serde_json::to_string_pretty(&SerializableReport::from(&report, &ctx))?,
|
||||
)
|
||||
.with_context(|| format!("отчёт {}", report_path.display()))?;
|
||||
std::fs::write(
|
||||
args.output.with_extension("mapping.txt"),
|
||||
ctx.mapping.to_text(),
|
||||
)?;
|
||||
|
||||
println!(
|
||||
"noxis: {} → {} (классов: {}, подписей срезано: {}, отчёт: {})",
|
||||
args.input.display(),
|
||||
args.output.display(),
|
||||
report.counters.get("classes").copied().unwrap_or(0),
|
||||
stripped,
|
||||
report_path.display()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Отчёт в JSON: сид, пассы, счётчики, маппинги текстом.
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
struct SerializableReport {
|
||||
seed: u64,
|
||||
passes: Vec<PassEntry>,
|
||||
counters: std::collections::BTreeMap<String, usize>,
|
||||
notes: Vec<String>,
|
||||
mapping: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
struct PassEntry {
|
||||
pass: String,
|
||||
transformed: usize,
|
||||
skipped: usize,
|
||||
notes: Vec<String>,
|
||||
}
|
||||
|
||||
impl SerializableReport {
|
||||
fn from(report: &noxis_core::Report, ctx: &Context) -> Self {
|
||||
Self {
|
||||
seed: report.seed,
|
||||
passes: report
|
||||
.passes
|
||||
.iter()
|
||||
.map(|pass| PassEntry {
|
||||
pass: pass.pass.to_string(),
|
||||
transformed: pass.transformed,
|
||||
skipped: pass.skipped,
|
||||
notes: pass.notes.clone(),
|
||||
})
|
||||
.collect(),
|
||||
counters: report.counters.clone(),
|
||||
notes: report.notes.clone(),
|
||||
mapping: ctx.mapping.to_text(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(clippy::expect_used, clippy::unwrap_used)]
|
||||
|
||||
use super::*;
|
||||
|
||||
fn sample_jar(path: &std::path::Path) {
|
||||
use noxis_classfile::{ClassBuilder, Code, Insn, Program, Version};
|
||||
|
||||
let code = Code {
|
||||
max_stack: 0,
|
||||
max_locals: 0,
|
||||
insns: vec![Insn::Return(None)],
|
||||
exception_table: Vec::new(),
|
||||
frames: Vec::new(),
|
||||
attributes: Vec::new(),
|
||||
};
|
||||
let 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 entries = noxis_jar::save(&program).expect("save");
|
||||
noxis_jar::write_file(path, &entries).expect("write jar");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn obfuscate_end_to_end() {
|
||||
let dir = std::env::temp_dir().join(format!("noxis-cli-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(&dir).expect("mkdir");
|
||||
let input = dir.join("in.jar");
|
||||
let output = dir.join("out.jar");
|
||||
sample_jar(&input);
|
||||
run(Args {
|
||||
input: input.clone(),
|
||||
output: output.clone(),
|
||||
config: None,
|
||||
seed: Some(5),
|
||||
report: None,
|
||||
no_rename: false,
|
||||
no_resources: false,
|
||||
no_strings: false,
|
||||
no_constants: false,
|
||||
no_noise: false,
|
||||
no_debug_info: false,
|
||||
no_flow: false,
|
||||
})
|
||||
.expect("obfuscate");
|
||||
assert!(output.exists(), "нет выходного jar");
|
||||
assert!(dir.join("out.report.json").exists(), "нет отчёта");
|
||||
assert!(dir.join("out.mapping.txt").exists(), "нет маппингов");
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_flow_is_honest_error() {
|
||||
let args = Args {
|
||||
input: PathBuf::from("in.jar"),
|
||||
output: PathBuf::from("out.jar"),
|
||||
config: None,
|
||||
seed: None,
|
||||
report: None,
|
||||
no_rename: false,
|
||||
no_resources: false,
|
||||
no_strings: false,
|
||||
no_constants: false,
|
||||
no_noise: false,
|
||||
no_debug_info: false,
|
||||
no_flow: true,
|
||||
};
|
||||
assert!(run(args).is_err());
|
||||
}
|
||||
}
|
||||
38
crates/noxis/src/cmd/patterns.rs
Normal file
38
crates/noxis/src/cmd/patterns.rs
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
//! Команда `patterns`: красный список и его прогон по jar'у.
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
|
||||
/// Аргументы просмотра/проверки паттернов.
|
||||
#[derive(Debug, clap::Args)]
|
||||
pub struct Args {
|
||||
/// Показать только список (без проверки).
|
||||
#[arg(long)]
|
||||
pub list: bool,
|
||||
/// Jar для проверки на секреты открытым текстом.
|
||||
#[arg(short, long)]
|
||||
pub input: Option<PathBuf>,
|
||||
}
|
||||
|
||||
/// Показать список или прогнать его по jar'у.
|
||||
pub fn run(args: Args) -> Result<()> {
|
||||
if args.list || args.input.is_none() {
|
||||
for pattern in noxis_verify::RED_PATTERNS {
|
||||
println!("{pattern}");
|
||||
}
|
||||
}
|
||||
let Some(input) = args.input else {
|
||||
return Ok(());
|
||||
};
|
||||
let entries =
|
||||
noxis_jar::read_file(&input).with_context(|| format!("чтение {}", input.display()))?;
|
||||
let program =
|
||||
noxis_jar::load(entries).map_err(|error| anyhow::anyhow!("разбор jar: {error}"))?;
|
||||
let hits = noxis_verify::scan_secrets(&program);
|
||||
for hit in &hits {
|
||||
println!("{}: [{}] {:.48}", hit.location, hit.pattern, hit.sample);
|
||||
}
|
||||
println!("совпадений: {}", hits.len());
|
||||
Ok(())
|
||||
}
|
||||
3
crates/noxis/src/lib.rs
Normal file
3
crates/noxis/src/lib.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
//! Библиотека CLI: команды как вызываемые функции (тонкий `main` — в `main.rs`).
|
||||
|
||||
pub mod cmd;
|
||||
35
crates/noxis/src/main.rs
Normal file
35
crates/noxis/src/main.rs
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
//! CLI обфускатора: `obfuscate`, `analyze`, `patterns`.
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{Parser, Subcommand};
|
||||
use noxis::cmd::{analyze, obfuscate, patterns};
|
||||
|
||||
/// Обфускатор JVM-байткода на Rust.
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(name = "noxis", version, about)]
|
||||
struct Cli {
|
||||
#[command(subcommand)]
|
||||
command: Commands,
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
enum Commands {
|
||||
/// Обфусцировать jar: конвейер, маппинги, JSON-отчёт.
|
||||
Obfuscate(obfuscate::Args),
|
||||
/// Разобрать jar: классы, entrypoint'ы, keep-правила.
|
||||
Analyze(analyze::Args),
|
||||
/// Красный список паттернов: показать или прогнать по jar'у.
|
||||
Patterns(patterns::Args),
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
|
||||
.with_writer(std::io::stderr)
|
||||
.init();
|
||||
match Cli::parse().command {
|
||||
Commands::Obfuscate(args) => obfuscate::run(args),
|
||||
Commands::Analyze(args) => analyze::run(args),
|
||||
Commands::Patterns(args) => patterns::run(args),
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue