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,210 @@
//! Конфигурация обфускации: профили, список пассов, keep-правила.
pub mod sections;
use serde::{Deserialize, Serialize};
use self::sections::{
ConstantsConfig, DebugConfig, FlowConfig, NoiseConfig, RenameConfig, StringsConfig,
};
use crate::error::CoreError;
/// Идентификатор пасса (он же ключ в конфиге, kebab-case).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum PassId {
/// Переименование классов, членов, локальных переменных, пакетов.
Rename,
/// Шифрование строк + генерация рантайм-декодера.
Strings,
/// Обфускация числовых констант и настоящие opaque-предикаты.
Constants,
/// Обфускация контрольного потока.
Flow,
/// Junk-классы, junk-аннотации, ложный шум.
Noise,
/// Отладочная информация: strip / spoof.
DebugInfo,
/// Ресурсы и метаданные архива (JSON, mixins, refmap, services, AW).
Resources,
}
impl PassId {
/// Строковое имя для отчётов и логов.
pub const fn as_str(self) -> &'static str {
match self {
PassId::Rename => "rename",
PassId::Strings => "strings",
PassId::Constants => "constants",
PassId::Flow => "flow",
PassId::Noise => "noise",
PassId::DebugInfo => "debug-info",
PassId::Resources => "resources",
}
}
}
impl std::fmt::Display for PassId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
/// Базовый набор пассов профиля.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Profile {
/// Только переименование + отладка. Быстро, минимальный риск сломать код.
Light,
/// Стандарт: имена, строки, константы, ресурсы, отладка, шум.
#[default]
Standard,
/// Всё включено, включая агрессивный control-flow.
Paranoid,
/// Ничего не включено явно — только `enable` из конфига.
Manual,
}
impl Profile {
/// Пассы, включённые профилем по умолчанию.
pub const fn baseline(self) -> &'static [PassId] {
match self {
Profile::Light => &[PassId::Rename, PassId::DebugInfo, PassId::Resources],
Profile::Standard => &[
PassId::Rename,
PassId::Strings,
PassId::Constants,
PassId::Noise,
PassId::DebugInfo,
PassId::Resources,
],
Profile::Paranoid => &[
PassId::Rename,
PassId::Strings,
PassId::Constants,
PassId::Flow,
PassId::Noise,
PassId::DebugInfo,
PassId::Resources,
],
Profile::Manual => &[],
}
}
}
/// Полная конфигурация (TOML).
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct Config {
pub profile: Profile,
/// Мастер-сид; `None` — случайный, но обязательно попадает в отчёт.
pub seed: Option<u64>,
/// Принудительно включить пассы вне профиля.
pub enable: Vec<PassId>,
/// Выключить пассы профиля.
pub disable: Vec<PassId>,
/// Keep-паттерны внутренних имён (`com/example/Main`, `*Service`).
pub keep: Vec<String>,
/// Keep-паттерны путей ресурсов, которые не переписываются.
pub keep_resources: Vec<String>,
pub rename: RenameConfig,
pub strings: StringsConfig,
pub constants: ConstantsConfig,
pub flow: FlowConfig,
pub noise: NoiseConfig,
pub debug: DebugConfig,
}
impl Default for Config {
fn default() -> Self {
Self {
profile: Profile::Standard,
seed: None,
enable: Vec::new(),
disable: Vec::new(),
keep: Vec::new(),
keep_resources: Vec::new(),
rename: RenameConfig::default(),
strings: StringsConfig::default(),
constants: ConstantsConfig::default(),
flow: FlowConfig::default(),
noise: NoiseConfig::default(),
debug: DebugConfig::default(),
}
}
}
impl Config {
/// Включён ли пасс с учётом профиля и явных override'ов.
pub fn is_enabled(&self, id: PassId) -> bool {
if self.disable.contains(&id) {
return false;
}
if self.enable.contains(&id) {
return true;
}
self.profile.baseline().contains(&id)
}
/// Загрузить конфиг из TOML.
pub fn from_toml(text: &str) -> Result<Self, CoreError> {
toml::from_str(text).map_err(|e| CoreError::Config(e.to_string()))
}
/// Нормализовать значения, которые легко задать нулем/пустым списком.
pub fn sanitize(&mut self) {
if self.rename.package_pool == 0 {
self.rename.package_pool = 1;
}
if self.strings.min_len == 0 {
self.strings.min_len = 1;
}
self.strings.schemes.retain(|s| !s.trim().is_empty());
self.flow.schemes.retain(|s| !s.trim().is_empty());
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used, clippy::unwrap_used)]
use super::*;
#[test]
fn profile_gate() {
let mut cfg = Config::default();
assert!(cfg.is_enabled(PassId::Rename));
assert!(cfg.is_enabled(PassId::Strings));
assert!(!cfg.is_enabled(PassId::Flow));
cfg.profile = Profile::Paranoid;
assert!(cfg.is_enabled(PassId::Flow));
cfg.disable.push(PassId::Strings);
assert!(!cfg.is_enabled(PassId::Strings));
cfg.enable.push(PassId::Flow);
cfg.profile = Profile::Light;
assert!(cfg.is_enabled(PassId::Flow));
}
#[test]
fn parses_toml() {
let cfg = Config::from_toml(
r#"
profile = "paranoid"
seed = 123
keep = ["com/example/Main"]
[rename]
package_pool = 4
"#,
)
.unwrap();
assert_eq!(cfg.profile, Profile::Paranoid);
assert_eq!(cfg.seed, Some(123));
assert_eq!(cfg.rename.package_pool, 4);
}
#[test]
fn rejects_unknown_keys() {
assert!(Config::from_toml("totally_unknown = 1").is_err());
}
}

View file

@ -0,0 +1,185 @@
//! Секции конфигурации: стили имён и настройки отдельных пассов.
use serde::{Deserialize, Serialize};
/// Стиль генерации имён.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum NameStyle {
/// `a`, `b`, ... `aa` — короткие латинские.
ShortLatin,
/// `I`, `l`, `O`, `0` — визуально одинаковые.
IlO0,
/// Случайные unicode-имена (суррогаты допустимы в modified UTF-8).
Unicode,
/// Смешанная латиница средней длины.
#[default]
MixedLatin,
/// Псевдосмысленные слова из другого проекта (дезориентация).
DecoyWords,
}
/// Настройки переименования.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct RenameConfig {
/// Переименовывать пакеты в случайные пути.
pub spread_packages: bool,
/// Размер пула случайных пакетов.
pub package_pool: usize,
/// Стиль имён классов.
pub class_style: NameStyle,
/// Стиль имён полей и методов.
pub member_style: NameStyle,
/// Стиль имён локальных переменных и параметров.
pub local_style: NameStyle,
/// Делать члены `public` (чтобы не ломать inheritance между пакетами).
pub widen_access: bool,
/// Переименовывать enum-константы.
pub rename_enum_constants: bool,
/// Переименовывать локальные переменные и параметры.
pub rename_locals: bool,
/// Переименовывать и не-private члены (агрессивно: безопасно для приложений,
/// опасно для библиотек и кода с рефлексией).
pub rename_public_members: bool,
}
impl Default for RenameConfig {
fn default() -> Self {
Self {
spread_packages: true,
package_pool: 12,
class_style: NameStyle::ShortLatin,
member_style: NameStyle::MixedLatin,
local_style: NameStyle::IlO0,
widen_access: true,
rename_enum_constants: true,
rename_locals: true,
rename_public_members: false,
}
}
}
/// Настройки шифрования строк.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct StringsConfig {
/// Схемы шифра, между которыми перемешиваются строки.
pub schemes: Vec<String>,
/// Строки короче этого не шифруются.
pub min_len: usize,
/// Дробить длинные строки на чанки (меньше похоже на пул).
pub split_above: usize,
/// Не шифровать строки, совпадающие с этими паттернами.
pub skip: Vec<String>,
/// Вычислять ключ из KDF(class, method, index), а не класть рядом с текстом.
pub derive_key: bool,
}
impl Default for StringsConfig {
fn default() -> Self {
Self {
schemes: vec!["xor-splitmix".to_string(), "xtea-ctr".to_string()],
min_len: 2,
split_above: 64,
skip: Vec::new(),
derive_key: true,
}
}
}
/// Настройки обфускации контрольного потока.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct FlowConfig {
/// Схемы, между которыми выбирается случайно по методу.
pub schemes: Vec<String>,
/// Не трогать методы больше этого числа инструкций (бюджет размера).
pub max_insns: usize,
/// Вплетать opaque-предикаты в тела блоков.
pub encrypt_block_bodies: bool,
}
impl Default for FlowConfig {
fn default() -> Self {
Self {
schemes: vec![
"state-spread".to_string(),
"computed-switch".to_string(),
"butterfly".to_string(),
"exception-goto".to_string(),
],
max_insns: 4000,
encrypt_block_bodies: false,
}
}
}
/// Настройки шифрования констант.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct ConstantsConfig {
/// Схемы разбиения (`xor`, `add`, `shift`), выбор по RNG на константу.
pub schemes: Vec<String>,
/// Вставлять opaque-предикаты (не больше этого на метод).
pub max_predicates: usize,
/// Не трогать константы в этих классах (префиксы внутренних имён).
pub skip: Vec<String>,
}
impl Default for ConstantsConfig {
fn default() -> Self {
Self {
schemes: vec!["xor".to_string(), "add".to_string(), "shift".to_string()],
max_predicates: 1,
skip: Vec::new(),
}
}
}
/// Настройки шума.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct NoiseConfig {
/// Сколько junk-классов на каждый настоящий (0 = выключено).
pub junk_ratio: usize,
/// Число junk-аннотаций на класс.
pub junk_annotations: usize,
/// Вплетать junk-классы в граф достижимости (обязательно для осмысленности).
pub weave_reachable: bool,
}
impl Default for NoiseConfig {
fn default() -> Self {
Self {
junk_ratio: 0,
junk_annotations: 0,
weave_reachable: true,
}
}
}
/// Настройки отладочной информации.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct DebugConfig {
/// Удалять `LineNumberTable`.
pub strip_line_numbers: bool,
/// Удалять `LocalVariableTable`.
pub strip_local_variables: bool,
/// Подменять `SourceFile` на ложное имя.
pub spoof_source_file: bool,
/// Удалять `Signature`/`InnerClasses`/`EnclosingMethod`.
pub strip_signatures: bool,
}
impl Default for DebugConfig {
fn default() -> Self {
Self {
strip_line_numbers: true,
strip_local_variables: true,
spoof_source_file: true,
strip_signatures: false,
}
}
}