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:
Loki 2026-10-08 22:01:03 +02:00
commit a641e69455
140 changed files with 17197 additions and 0 deletions

View file

@ -0,0 +1,20 @@
[package]
name = "noxis-analysis"
description = "Иерархия классов, entrypoint'ы, reflection-сайты, keep-правила"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
[dependencies]
regex = { workspace = true }
noxis-classfile = { workspace = true }
noxis-core = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true }
[lints]
workspace = true
[build-dependencies]
noxis-structure-check = { workspace = true }

View file

@ -0,0 +1,3 @@
fn main() {
noxis_structure_check::check();
}

View file

@ -0,0 +1,244 @@
//! Entrypoint'ы и точки, где имя класса/метода видно снаружи.
use std::collections::BTreeSet;
use noxis_classfile::Program;
use noxis_classfile::insn::{Const, Insn};
/// Всё, что нельзя переименовывать «как есть» и что нужно держать в keep-сете.
#[derive(Debug, Clone, Default)]
pub struct Entrypoints {
/// Классы с `public static void main(String[])`.
pub main_classes: BTreeSet<String>,
/// `native`-методы: имя связано с JNI-схемой (`Java_pkg_Class_method`).
pub native_methods: BTreeSet<(String, String, String)>,
/// Интерфейсы из `META-INF/services/*`.
pub service_interfaces: BTreeSet<String>,
/// Имена, встречающиеся в рефлексивных вызовах (`Class.forName` и т.п.).
pub reflective_names: BTreeSet<String>,
/// Имена членов (`getMethod`/`getField`-литералы): их нельзя переименовывать.
pub reflective_member_names: BTreeSet<String>,
/// Поля с флагом `transient`+`serialVersionUID`-контекст: сериализуемые классы
/// обязаны сохранить имя класса и полей (иначе ломается Java-сериализация).
pub serializable_classes: BTreeSet<String>,
}
impl Entrypoints {
/// Собрать entrypoint'ы по программе (без keep-правил — их добавляет [`Self::merge`]).
pub fn collect(program: &Program) -> Self {
let mut out = Self::default();
for class in &program.classes {
if class.this_class == "java/lang/Object" {
continue;
}
if implements_serializable(program, &class.this_class) {
out.serializable_classes.insert(class.this_class.clone());
}
for method in &class.methods {
const ACC_STATIC: u16 = 0x0008;
const ACC_NATIVE: u16 = 0x0100;
if method.access_flags & ACC_NATIVE != 0 {
out.native_methods.insert((
class.this_class.clone(),
method.name.clone(),
method.descriptor.clone(),
));
}
if method.name == "main"
&& method.descriptor == "([Ljava/lang/String;)V"
&& method.access_flags & ACC_STATIC != 0
{
out.main_classes.insert(class.this_class.clone());
}
if let Some(code) = &method.code {
collect_reflective_names(&mut out, code);
}
}
}
out.service_interfaces = collect_service_interfaces(program);
out
}
/// Объединить с другим набором (например, из keep-правил конфига).
pub fn merge(&mut self, other: &Entrypoints) {
self.main_classes.extend(other.main_classes.iter().cloned());
self.native_methods
.extend(other.native_methods.iter().cloned());
self.service_interfaces
.extend(other.service_interfaces.iter().cloned());
self.reflective_names
.extend(other.reflective_names.iter().cloned());
self.reflective_member_names
.extend(other.reflective_member_names.iter().cloned());
self.serializable_classes
.extend(other.serializable_classes.iter().cloned());
}
/// Класс, который нельзя переименовывать.
pub fn keeps_class(&self, internal_name: &str) -> bool {
self.main_classes.contains(internal_name)
|| self.serializable_classes.contains(internal_name)
|| self.reflective_names.contains(internal_name)
}
/// Член (метод/поле), который нельзя переименовывать.
pub fn keeps_member(&self, name: &str) -> bool {
self.reflective_member_names.contains(name)
}
}
/// `implements java.io.Serializable` (по цепочке предков, в пределах программы).
fn implements_serializable(program: &Program, start: &str) -> bool {
let mut stack = vec![start.to_string()];
let mut seen = std::collections::HashSet::new();
while let Some(name) = stack.pop() {
if !seen.insert(name.clone()) {
continue;
}
if name == "java/io/Serializable" {
return true;
}
let Some(class) = program.class(&name) else {
// Внешний предок: считаем, что не знаем — консервативно не keep'им.
continue;
};
if let Some(sup) = &class.super_class {
stack.push(sup.clone());
}
stack.extend(class.interfaces.iter().cloned());
}
false
}
/// `META-INF/services/<iface>` → внутреннее имя интерфейса.
fn collect_service_interfaces(program: &Program) -> BTreeSet<String> {
let mut out = BTreeSet::new();
for resource in &program.resources {
let Some(name) = resource.path.strip_prefix("META-INF/services/") else {
continue;
};
if name.is_empty() {
continue;
}
out.insert(name.replace('.', "/"));
}
out
}
/// Линейный эвристический поиск имён в рефлексивных API.
///
/// Полноценный dataflow не нужен: если рядом с вызовом `Class.forName(...)` /
/// `getMethod(...)` лежит строковый литерал — это имя видно статически,
/// значит, его нельзя переименовывать.
fn collect_reflective_names(out: &mut Entrypoints, code: &noxis_classfile::Code) {
/// API, чьи аргументы — **имена классов**.
const CLASS_API: &[(&str, &str)] = &[
("java/lang/Class", "forName"),
("java/lang/ClassLoader", "loadClass"),
];
/// API, чьи аргументы — **имена членов** (поле/метод).
const MEMBER_API: &[(&str, &str)] = &[
("java/lang/Class", "getMethod"),
("java/lang/Class", "getDeclaredMethod"),
("java/lang/Class", "getField"),
("java/lang/Class", "getDeclaredField"),
];
let mut last_string: Option<&str> = None;
for insn in &code.insns {
if let Insn::Const(Const::String(s)) = insn {
last_string = Some(s.as_str());
continue;
}
if let Insn::Invoke { owner, name, .. } = insn {
let is_class_api = CLASS_API.iter().any(|(o, n)| o == owner && n == name);
let is_member_api = MEMBER_API.iter().any(|(o, n)| o == owner && n == name);
if let (true, Some(s)) = (is_class_api, last_string) {
out.reflective_names.insert(s.replace('.', "/"));
}
if let (true, Some(s)) = (is_member_api, last_string) {
out.reflective_member_names.insert(s.to_string());
}
last_string = None;
continue;
}
// Инструкции, не относящиеся к строке/вызову, обрывают «соседность».
if !matches!(insn, Insn::Mark(_) | Insn::Nop) {
last_string = None;
}
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used, clippy::unwrap_used)]
use super::*;
use noxis_classfile::insn::InvokeKind;
use noxis_classfile::{ClassBuilder, Insn, empty_code};
#[test]
fn finds_main_and_native() {
let main = ClassBuilder::new("app/Main", None)
.method_with_code(0x0009, "main", "([Ljava/lang/String;)V", empty_code(1))
.build();
let mut native_class = ClassBuilder::new("app/Jni", None).build();
native_class.methods.push(noxis_classfile::Method {
access_flags: 0x0100, // ACC_NATIVE
name: "ping".into(),
descriptor: "()I".into(),
code: None,
attributes: Vec::new(),
});
let program = Program {
classes: vec![main, native_class],
resources: Vec::new(),
};
let ep = Entrypoints::collect(&program);
assert!(ep.main_classes.contains("app/Main"));
assert!(ep.native_methods.contains(&(
"app/Jni".to_string(),
"ping".to_string(),
"()I".to_string()
)));
assert!(ep.keeps_class("app/Main"));
}
#[test]
fn collects_service_interface() {
let program = Program {
classes: Vec::new(),
resources: vec![noxis_classfile::Resource {
path: "META-INF/services/com.example.Plugin".into(),
data: b"app.Impl\n".to_vec(),
}],
};
let ep = Entrypoints::collect(&program);
assert!(ep.service_interfaces.contains("com/example/Plugin"));
}
#[test]
fn picks_reflective_literal() {
let mut code = empty_code(1);
code.insns = vec![
Insn::Const(Const::String("app.Secret".into())),
Insn::Invoke {
kind: InvokeKind::Static,
owner: "java/lang/Class".into(),
name: "forName".into(),
descriptor: "(Ljava/lang/String;)Ljava/lang/Class;".into(),
itf: false,
},
];
let class = ClassBuilder::new("app/Caller", None)
.method_with_code(0x0009, "go", "()V", code)
.build();
let program = Program {
classes: vec![class],
resources: Vec::new(),
};
let ep = Entrypoints::collect(&program);
assert!(ep.reflective_names.contains("app/Secret"));
}
}

View file

@ -0,0 +1,250 @@
//! Иерархия классов: наследование, переопределения, безопасность переименования.
use std::collections::{HashMap, HashSet};
use noxis_classfile::Program;
/// Узел иерархии: суперкласс и интерфейсы (по внутренним именам).
#[derive(Debug, Clone, Default)]
pub struct HierarchyNode {
pub super_class: Option<String>,
pub interfaces: Vec<String>,
}
/// Построенная иерархия классов.
#[derive(Debug, Clone, Default)]
pub struct ClassHierarchy {
nodes: HashMap<String, HierarchyNode>,
}
impl ClassHierarchy {
/// Построить иерархию из программы (только по имеющимся классам).
pub fn build(program: &Program) -> Self {
let mut nodes = HashMap::with_capacity(program.classes.len());
for class in &program.classes {
nodes.insert(
class.this_class.clone(),
HierarchyNode {
super_class: class.super_class.clone(),
interfaces: class.interfaces.clone(),
},
);
}
Self { nodes }
}
/// Прямой суперкласс (для LUB при выводе кадров).
pub fn superclass(&self, internal_name: &str) -> Option<String> {
self.nodes
.get(internal_name)
.and_then(|n| n.super_class.clone())
}
/// Есть ли класс в иерархии (в том числе из внешнего classpath — нет).
pub fn contains(&self, internal_name: &str) -> bool {
self.nodes.contains_key(internal_name)
}
/// Прямые предки: суперкласс + интерфейсы.
pub fn parents(&self, internal_name: &str) -> Vec<String> {
let mut out = Vec::new();
if let Some(node) = self.nodes.get(internal_name) {
out.extend(node.super_class.iter().cloned());
out.extend(node.interfaces.iter().cloned());
}
out
}
/// Является ли `child` подклассом `parent` (в пределах программы).
///
/// Неполная иерархия (внешние зависимости не грузим) — это осознанно:
/// обфускатор работает по артефакту, а не по всему classpath.
pub fn is_subclass_of(&self, child: &str, parent: &str) -> bool {
if child == parent {
return true;
}
let mut seen = HashSet::new();
let mut stack = vec![child.to_string()];
while let Some(current) = stack.pop() {
if !seen.insert(current.clone()) {
continue;
}
for p in self.parents(&current) {
if p == parent {
return true;
}
stack.push(p);
}
}
false
}
/// Все классы программы, которые наследуют `parent` (включая его самого).
pub fn descendants(&self, parent: &str) -> Vec<String> {
self.nodes
.keys()
.filter(|name| self.is_subclass_of(name, parent))
.cloned()
.collect()
}
/// Классы, объявляющие метод с данным именем/дескриптором, вверх по цепочке.
pub fn declaring_chain(
&self,
program: &Program,
start: &str,
name: &str,
descriptor: &str,
) -> Vec<String> {
let mut out = Vec::new();
let mut seen = HashSet::new();
let mut stack = vec![start.to_string()];
while let Some(current) = stack.pop() {
if !seen.insert(current.clone()) {
continue;
}
if let Some(class) = program.class(&current)
&& class.method(name, descriptor).is_some()
{
out.push(current.clone());
}
stack.extend(self.parents(&current));
}
out
}
}
impl noxis_classfile::Hierarchy for ClassHierarchy {
fn superclass(&self, internal_name: &str) -> Option<String> {
self.superclass(internal_name)
}
}
/// Почему метод нельзя/можно переименовывать (аналог `InheritanceAnalyzer`).
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RenameSafety {
/// Переименовывать можно: это точка, где имя не видно снаружи.
Safe,
/// Конструктор/статический блок: имя зафиксировано форматом JVM.
FixedName,
/// Виртуальный метод, который может переопределяться вне артефакта.
VirtualExternal,
/// Рефлексия/сериализация может обратиться по имени.
ReferencedByName,
/// Метод не найден в классе.
Missing,
}
/// Оценка безопасности переименования метода.
///
/// Правила (сознательно консервативные, как в `OBF/InheritanceAnalyzer`):
/// - `<init>`/`<clinit>` — никогда;
/// - `private`/`static`/`final`/`<package-private>` внутри финального класса — можно;
/// - `public`/`protected`/`package-private`, если класс не `final` и есть
/// наследники (внутри артефакта или нет) — нельзя, пока нет доказательства.
pub fn method_rename_safety(
program: &Program,
hierarchy: &ClassHierarchy,
owner: &str,
name: &str,
descriptor: &str,
externally_referenced: bool,
) -> RenameSafety {
if name == "<init>" || name == "<clinit>" {
return RenameSafety::FixedName;
}
let Some(class) = program.class(owner) else {
return RenameSafety::Missing;
};
if externally_referenced {
return RenameSafety::ReferencedByName;
}
let Some(method) = class.method(name, descriptor) else {
return RenameSafety::Missing;
};
const ACC_PRIVATE: u16 = 0x0002;
const ACC_STATIC: u16 = 0x0008;
const ACC_FINAL: u16 = 0x0010;
const ACC_PUBLIC: u16 = 0x0001;
const ACC_PROTECTED: u16 = 0x0004;
let flags = method.access_flags;
if flags & ACC_PRIVATE != 0 || flags & ACC_STATIC != 0 || flags & ACC_FINAL != 0 {
return RenameSafety::Safe;
}
// Не-private без `final`: ищем переопределения в иерархии.
let chain = hierarchy.declaring_chain(program, owner, name, descriptor);
let has_subclasses = chain
.iter()
.any(|c| c != owner && hierarchy.is_subclass_of(c, owner));
if has_subclasses {
return RenameSafety::VirtualExternal;
}
if flags & (ACC_PUBLIC | ACC_PROTECTED) != 0 {
// Публичный метод финального/листьевого класса: внутри артефакта
// переопределений нет, но снаружи могут зваться по имени.
return RenameSafety::VirtualExternal;
}
RenameSafety::Safe
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used, clippy::unwrap_used)]
use super::*;
use noxis_classfile::ClassBuilder;
fn sample_program() -> Program {
let base = ClassBuilder::new("a/Base", None)
.method_with_code(0x0001, "open", "()V", noxis_classfile::empty_code(0))
.method_with_code(0x0002, "hidden", "()V", noxis_classfile::empty_code(0))
.method_with_code(0x0001, "<init>", "()V", noxis_classfile::empty_code(0))
.build();
let derived = ClassBuilder::new("a/Der", Some("a/Base"))
.method_with_code(0x0001, "open", "()V", noxis_classfile::empty_code(0))
.build();
Program {
classes: vec![base, derived],
resources: Vec::new(),
}
}
#[test]
fn hierarchy_links_superclass() {
let program = sample_program();
let h = ClassHierarchy::build(&program);
assert!(h.is_subclass_of("a/Der", "a/Base"));
assert!(!h.is_subclass_of("a/Base", "a/Der"));
}
#[test]
fn private_method_is_safe() {
let program = sample_program();
let h = ClassHierarchy::build(&program);
assert_eq!(
method_rename_safety(&program, &h, "a/Base", "hidden", "()V", false),
RenameSafety::Safe
);
}
#[test]
fn constructor_is_never_renamed() {
let program = sample_program();
let h = ClassHierarchy::build(&program);
assert_eq!(
method_rename_safety(&program, &h, "a/Base", "<init>", "()V", false),
RenameSafety::FixedName
);
}
#[test]
fn virtual_method_with_subclass_is_unsafe() {
let program = sample_program();
let h = ClassHierarchy::build(&program);
assert_eq!(
method_rename_safety(&program, &h, "a/Base", "open", "()V", false),
RenameSafety::VirtualExternal
);
}
}

View file

@ -0,0 +1,237 @@
//! Keep-правила: что нельзя переименовывать (glob-паттерны + entrypoint'ы).
use regex::Regex;
use noxis_classfile::{Attribute, Const, Program};
use crate::entrypoints::Entrypoints;
use noxis_core::Config;
/// Скомпилированный glob: `*` — любые символы кроме `/`, `**` — всё, `?` — один.
fn compile(pattern: &str) -> Result<Regex, regex::Error> {
let mut out = String::from("^");
let chars: Vec<char> = pattern.chars().collect();
let mut i = 0;
while i < chars.len() {
match chars[i] {
'*' => {
if i + 1 < chars.len() && chars[i + 1] == '*' {
out.push_str(".*");
i += 2;
} else {
out.push_str("[^/]*");
i += 1;
}
}
'?' => {
out.push_str("[^/]");
i += 1;
}
c => {
out.push_str(&regex::escape(&c.to_string()));
i += 1;
}
}
}
out.push('$');
Regex::new(&out)
}
/// Правила «не трогать»: явные паттерны конфига + entrypoint'ы.
#[derive(Debug, Default, Clone)]
pub struct KeepRules {
class_patterns: Vec<Regex>,
resource_patterns: Vec<Regex>,
/// Точные имена, которые нельзя трогать (entrypoint'ы, рефлексия, ...).
fixed: std::collections::BTreeSet<String>,
/// Имена членов, защищённые рефлексией.
fixed_members: std::collections::BTreeSet<String>,
}
impl KeepRules {
/// Собрать правила из конфига.
pub fn from_config(config: &Config) -> Result<Self, RuleError> {
let mut class_patterns = Vec::with_capacity(config.keep.len());
for pattern in &config.keep {
class_patterns.push(compile(pattern).map_err(|e| RuleError::Pattern {
pattern: pattern.clone(),
msg: e.to_string(),
})?);
}
let mut resource_patterns = Vec::with_capacity(config.keep_resources.len());
for pattern in &config.keep_resources {
resource_patterns.push(compile(pattern).map_err(|e| RuleError::Pattern {
pattern: pattern.clone(),
msg: e.to_string(),
})?);
}
Ok(Self {
class_patterns,
resource_patterns,
fixed: std::collections::BTreeSet::new(),
fixed_members: std::collections::BTreeSet::new(),
})
}
/// Запретить переименование члена с таким именем (в любом классе).
pub fn keep_member(&mut self, name: impl Into<String>) {
self.fixed_members.insert(name.into());
}
/// Добавить точное имя класса (из анализа программы).
pub fn keep_exact(&mut self, internal_name: impl Into<String>) {
self.fixed.insert(internal_name.into());
}
/// Объединить с найденными entrypoint'ами.
pub fn absorb(&mut self, entrypoints: &Entrypoints) {
for name in &entrypoints.main_classes {
self.keep_exact(name.clone());
}
for name in &entrypoints.reflective_names {
self.keep_exact(name.clone());
}
for name in &entrypoints.serializable_classes {
self.keep_exact(name.clone());
}
for (owner, _, _) in &entrypoints.native_methods {
self.keep_exact(owner.clone());
}
for name in &entrypoints.service_interfaces {
self.keep_exact(name.clone());
}
for name in &entrypoints.reflective_member_names {
self.keep_member(name.clone());
}
}
/// Методы, на которые ссылаются `BootstrapMethods` (`invokedynamic`).
///
/// Переименование такого метода без правки bootstrap-записи ломает класс,
/// а правка — задача P2 (см. `todo.md`): пока просто не трогаем имена.
pub fn absorb_bootstrap(&mut self, program: &Program) {
for class in &program.classes {
for method in &class.methods {
for attr in &method.attributes {
if let Attribute::BootstrapMethods(methods) = attr {
for bsm in methods {
self.keep_member(bsm.name.clone());
for arg in &bsm.args {
if let Const::MethodHandle { name, .. } = arg {
self.keep_member(name.clone());
}
}
}
}
}
}
}
}
/// Нужно ли оставить класс с данным внутренним именем.
pub fn keeps_class(&self, internal_name: &str) -> bool {
self.fixed.contains(internal_name)
|| self
.class_patterns
.iter()
.any(|re| re.is_match(internal_name))
}
/// Нужно ли оставить член с таким именем (не переименовывать).
pub fn keeps_member(&self, name: &str) -> bool {
self.fixed_members.contains(name)
}
/// Нужно ли оставить ресурс с данным путём.
pub fn keeps_resource(&self, path: &str) -> bool {
self.resource_patterns.iter().any(|re| re.is_match(path))
}
/// Число правил (для отчёта).
pub fn len(&self) -> usize {
self.class_patterns.len()
+ self.resource_patterns.len()
+ self.fixed.len()
+ self.fixed_members.len()
}
/// Есть ли вообще правила.
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
/// Ошибка компиляции паттерна.
#[derive(Debug, thiserror::Error)]
pub enum RuleError {
#[error("некорректный паттерн `{pattern}`: {msg}")]
Pattern { pattern: String, msg: String },
}
/// Полный анализ перед обфускацией.
#[derive(Debug, Clone)]
pub struct Analysis {
pub entrypoints: Entrypoints,
pub keep: KeepRules,
/// Классы, которые не переименовываются.
pub locked_classes: std::collections::BTreeSet<String>,
}
impl Analysis {
/// Прогнать весь анализ по программе.
pub fn run(program: &Program, config: &Config) -> Result<Self, RuleError> {
let entrypoints = Entrypoints::collect(program);
let mut keep = KeepRules::from_config(config)?;
keep.absorb(&entrypoints);
keep.absorb_bootstrap(program);
let locked_classes = program
.classes
.iter()
.map(|c| c.this_class.clone())
.filter(|name| keep.keeps_class(name))
.collect();
Ok(Self {
entrypoints,
keep,
locked_classes,
})
}
/// Класс запрещено переименовывать.
pub fn is_locked(&self, internal_name: &str) -> bool {
self.locked_classes.contains(internal_name)
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used, clippy::unwrap_used)]
use super::*;
#[test]
fn glob_semantics() {
let rules = KeepRules {
class_patterns: vec![
compile("com/example/*").unwrap(),
compile("**/internal/**").unwrap(),
],
resource_patterns: vec![compile("META-INF/services/*").unwrap()],
fixed: std::collections::BTreeSet::new(),
fixed_members: std::collections::BTreeSet::new(),
};
assert!(rules.keeps_class("com/example/Main"));
assert!(!rules.keeps_class("com/example/sub/Main"));
assert!(rules.keeps_class("x/internal/y/Z"));
assert!(rules.keeps_resource("META-INF/services/a.b.C"));
assert!(!rules.keeps_resource("assets/mc/mod.json"));
}
#[test]
fn exact_names_win() {
let mut rules = KeepRules::default();
rules.keep_exact("app/Main");
assert!(rules.keeps_class("app/Main"));
assert!(!rules.keeps_class("app/Other"));
}
}

View file

@ -0,0 +1,17 @@
//! # noxis-analysis
//!
//! Анализ программы перед обфускацией: иерархия наследования, entrypoint'ы,
//! рефлексивные обращения по имени, keep-правила.
//!
//! Из этих данных пасс переименования узнаёт, **что** трогать нельзя.
//! Урок из `info/evo-weaknesses.md`: у EvoVis рефлексия по имени была
//! открытым текстом, а entrypoint'ы — в `fabric.mod.json`; здесь они
//! обязаны попадать в keep-набор автоматически.
pub mod entrypoints;
pub mod hierarchy;
pub mod keep;
pub use entrypoints::Entrypoints;
pub use hierarchy::{ClassHierarchy, RenameSafety, method_rename_safety};
pub use keep::{Analysis, KeepRules, RuleError};