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
210
crates/noxis-core/src/config/mod.rs
Normal file
210
crates/noxis-core/src/config/mod.rs
Normal 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());
|
||||
}
|
||||
}
|
||||
185
crates/noxis-core/src/config/sections.rs
Normal file
185
crates/noxis-core/src/config/sections.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue