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
244
crates/noxis-analysis/src/entrypoints.rs
Normal file
244
crates/noxis-analysis/src/entrypoints.rs
Normal 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"));
|
||||
}
|
||||
}
|
||||
250
crates/noxis-analysis/src/hierarchy.rs
Normal file
250
crates/noxis-analysis/src/hierarchy.rs
Normal 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(¤t) {
|
||||
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(¤t)
|
||||
&& class.method(name, descriptor).is_some()
|
||||
{
|
||||
out.push(current.clone());
|
||||
}
|
||||
stack.extend(self.parents(¤t));
|
||||
}
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
237
crates/noxis-analysis/src/keep.rs
Normal file
237
crates/noxis-analysis/src/keep.rs
Normal 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(®ex::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"));
|
||||
}
|
||||
}
|
||||
17
crates/noxis-analysis/src/lib.rs
Normal file
17
crates/noxis-analysis/src/lib.rs
Normal 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};
|
||||
Loading…
Add table
Add a link
Reference in a new issue