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,112 @@
//! Кадры `StackMapTable` при чтении: вариант в вариант, дельты как есть.
//!
//! Backend хранит кадры в номерах инструкций (см. `from_bytes_code_attributes`),
//! наш IR — тоже: writer сам пересчитает их в байты при `to_bytes`.
use ristretto_classfile::ConstantPool;
use ristretto_classfile::attributes::{StackFrame, VerificationType as RtType};
use super::super::pool::class_name;
use crate::error::ClassfileError;
use crate::format::{StackMapFrame, VerificationType};
/// Кадры backend'а → наши (индексные дельты — как есть).
pub(super) fn to_frames(
frames: &[StackFrame],
pool: &ConstantPool<'_>,
) -> Result<Vec<StackMapFrame>, ClassfileError> {
frames.iter().map(|frame| to_frame(frame, pool)).collect()
}
fn to_frame(frame: &StackFrame, pool: &ConstantPool<'_>) -> Result<StackMapFrame, ClassfileError> {
Ok(match frame {
StackFrame::SameFrame { frame_type } => StackMapFrame::Same {
offset_delta: u32::from(*frame_type),
},
StackFrame::SameFrameExtended { offset_delta, .. } => StackMapFrame::Same {
offset_delta: u32::from(*offset_delta),
},
StackFrame::SameLocals1StackItemFrame { frame_type, stack } => StackMapFrame::SameLocals {
offset_delta: u32::from(*frame_type) - 64,
local: single(stack, pool)?,
},
StackFrame::SameLocals1StackItemFrameExtended {
offset_delta,
stack,
..
} => StackMapFrame::SameLocals {
offset_delta: u32::from(*offset_delta),
local: single(stack, pool)?,
},
StackFrame::ChopFrame {
frame_type,
offset_delta,
} => StackMapFrame::Chop {
offset_delta: u32::from(*offset_delta),
count: 251u8.wrapping_sub(*frame_type),
},
StackFrame::AppendFrame {
offset_delta,
locals,
..
} => StackMapFrame::Append {
offset_delta: u32::from(*offset_delta),
locals: types(locals, pool)?,
},
StackFrame::FullFrame {
offset_delta,
locals,
stack,
..
} => StackMapFrame::Full {
offset_delta: u32::from(*offset_delta),
locals: types(locals, pool)?,
stack: types(stack, pool)?,
},
})
}
/// Единственный элемент стека кадра (`same_locals_1_stack_item`).
fn single(stack: &[RtType], pool: &ConstantPool<'_>) -> Result<VerificationType, ClassfileError> {
let mut types = types(stack, pool)?.into_iter();
match (types.next(), types.next()) {
(Some(first), None) => Ok(first),
_ => Err(ClassfileError::Parse(
"в same_locals-кадре не один элемент стека".into(),
)),
}
}
fn types(
values: &[RtType],
pool: &ConstantPool<'_>,
) -> Result<Vec<VerificationType>, ClassfileError> {
values.iter().map(|value| vtype(value, pool)).collect()
}
/// Тип верификации backend'а → наш тип (имя класса берётся из constant pool).
fn vtype(value: &RtType, pool: &ConstantPool<'_>) -> Result<VerificationType, ClassfileError> {
Ok(match value {
RtType::Top => VerificationType::Top,
RtType::Integer => VerificationType::Integer,
RtType::Float => VerificationType::Float,
RtType::Double => VerificationType::Double,
RtType::Long => VerificationType::Long,
RtType::Null => VerificationType::Null,
RtType::UninitializedThis => VerificationType::UninitializedThis,
RtType::Object { cpool_index } => VerificationType::Object(class_name(pool, *cpool_index)?),
RtType::Uninitialized { offset } => VerificationType::Uninitialized(*offset),
})
}
/// Тип перехвата (`0` — catch-all для `finally`/`any`).
pub(super) fn catch_type(
pool: &ConstantPool<'_>,
index: u16,
) -> Result<Option<String>, ClassfileError> {
if index == 0 {
Ok(None)
} else {
Ok(Some(class_name(pool, index)?))
}
}

View file

@ -0,0 +1,187 @@
//! Метки тела метода: где обязаны стоять и как резолвятся.
//!
//! Backend нумерует инструкции по порядку — метки ставятся прямо на номера,
//! без байтовых смещений.
use std::collections::{BTreeMap, BTreeSet};
use ristretto_classfile::attributes::Attribute as RtAttr;
use ristretto_classfile::attributes::{ExceptionTableEntry, Instruction};
use crate::error::ClassfileError;
use crate::insn::Label;
/// Номера инструкций, на которых обязана стоять метка.
///
/// `extra` — позиции отладочных таблиц (уже номерами): вставки пассов
/// не должны оставлять их без меток.
pub(super) fn label_indexes(
code: &[Instruction],
exception_table: &[ExceptionTableEntry],
extra: &BTreeSet<u32>,
) -> Result<BTreeSet<u32>, ClassfileError> {
let mut indexes = BTreeSet::from([0u32]);
for (position, instruction) in code.iter().enumerate() {
let index = to_index(position)?;
for target in branch_indexes(instruction, index) {
indexes.insert(to_index_checked(target)?);
}
}
for entry in exception_table {
indexes.insert(u32::from(entry.range_pc.start));
indexes.insert(u32::from(entry.range_pc.end));
indexes.insert(u32::from(entry.handler_pc));
}
indexes.extend(extra.iter().copied());
Ok(indexes)
}
/// Цели ветвлений backend'а: для `if*`/`goto` — абсолютный номер, для `switch` — дельта.
fn branch_indexes(instruction: &Instruction, index: u16) -> Vec<i32> {
let base = i32::from(index);
match instruction {
Instruction::Tableswitch(table) => once(base + table.default)
.chain(table.offsets.iter().map(|target| base + target))
.collect(),
Instruction::Lookupswitch(lookup) => once(base + lookup.default)
.chain(lookup.pairs.values().map(|target| base + target))
.collect(),
Instruction::Ifeq(target)
| Instruction::Ifne(target)
| Instruction::Iflt(target)
| Instruction::Ifge(target)
| Instruction::Ifgt(target)
| Instruction::Ifle(target)
| Instruction::If_icmpeq(target)
| Instruction::If_icmpne(target)
| Instruction::If_icmplt(target)
| Instruction::If_icmpge(target)
| Instruction::If_icmpgt(target)
| Instruction::If_icmple(target)
| Instruction::If_acmpeq(target)
| Instruction::If_acmpne(target)
| Instruction::Ifnull(target)
| Instruction::Ifnonnull(target)
| Instruction::Goto(target)
| Instruction::Jsr(target) => vec![i32::from(*target)],
Instruction::Goto_w(target) | Instruction::Jsr_w(target) => vec![*target],
_ => Vec::new(),
}
}
fn once(value: i32) -> std::iter::Once<i32> {
std::iter::once(value)
}
/// Превратить отсортированный набор номеров в последовательные метки.
pub(super) fn assign(indexes: &BTreeSet<u32>) -> BTreeMap<u32, Label> {
indexes
.iter()
.enumerate()
.map(|(position, index)| (*index, position as u32))
.collect()
}
/// Метка для номера инструкции.
pub(super) fn lookup(labels: &BTreeMap<u32, Label>, index: u16) -> Result<Label, ClassfileError> {
labels
.get(&u32::from(index))
.copied()
.ok_or_else(|| ClassfileError::Parse(format!("нет метки для инструкции {index}")))
}
/// Позиции отладочных таблиц, которым нужны метки.
pub(super) fn collect_debug_labels(
attributes: &[RtAttr],
starts: &[u16],
code_length: u16,
extra: &mut BTreeSet<u32>,
) -> Result<(), ClassfileError> {
for attr in attributes {
match attr {
RtAttr::LineNumberTable { line_numbers, .. } => {
for line in line_numbers {
extra.insert(u32::from(line.start_pc));
}
}
RtAttr::LocalVariableTable { variables, .. } => {
for var in variables {
extra.insert(byte_to_index(starts, var.start_pc)?);
extra.insert(end_index(starts, var.start_pc, var.length, code_length)?);
}
}
RtAttr::LocalVariableTypeTable { variable_types, .. } => {
for var in variable_types {
extra.insert(byte_to_index(starts, var.start_pc)?);
extra.insert(end_index(starts, var.start_pc, var.length, code_length)?);
}
}
_ => {}
}
}
Ok(())
}
/// Байт → номер инструкции (только границы инструкций).
pub(super) fn byte_to_index(starts: &[u16], byte: u16) -> Result<u32, ClassfileError> {
starts
.iter()
.position(|&start| start == byte)
.and_then(|position| u32::try_from(position).ok())
.ok_or_else(|| ClassfileError::Parse(format!("смещение {byte} мимо инструкции")))
}
/// Конец диапазона `[start, start+length)`: индекс за концом.
fn end_index(
starts: &[u16],
start: u16,
length: u16,
code_length: u16,
) -> Result<u32, ClassfileError> {
let end = start
.checked_add(length)
.ok_or_else(|| ClassfileError::Parse("диапазон вне метода".into()))?;
if end == code_length {
return u32::try_from(starts.len())
.map_err(|_| ClassfileError::Parse("в теле метода больше 65535 инструкций".into()));
}
byte_to_index(starts, end)
}
/// Метка для байтового смещения отладочной таблицы.
pub(super) fn resolve_byte(
labels: &BTreeMap<u32, Label>,
starts: &[u16],
byte: u16,
) -> Result<Label, ClassfileError> {
let index = byte_to_index(starts, byte)?;
labels
.get(&index)
.copied()
.ok_or_else(|| ClassfileError::Parse(format!("нет метки для смещения {byte}")))
}
/// Метка конца диапазона отладочной таблицы.
pub(super) fn resolve_end(
labels: &BTreeMap<u32, Label>,
starts: &[u16],
start: u16,
length: u16,
code_length: u16,
) -> Result<Label, ClassfileError> {
let index = end_index(starts, start, length, code_length)?;
labels
.get(&index)
.copied()
.ok_or_else(|| ClassfileError::Parse(format!("нет метки конца {index}")))
}
fn to_index(position: usize) -> Result<u16, ClassfileError> {
u16::try_from(position)
.map_err(|_| ClassfileError::Parse("в теле метода больше 65535 инструкций".into()))
}
fn to_index_checked(index: i32) -> Result<u32, ClassfileError> {
u32::try_from(index)
.map_err(|_| ClassfileError::Parse(format!("номер инструкции {index} вне тела метода")))
}

View file

@ -0,0 +1,169 @@
//! `Code` backend'а → тело метода нашего IR: метки, исключения, кадры.
//!
//! Backend уже перевёл байтовые смещения в номера инструкций — конвертеры
//! работают прямо с ними: метки ставятся на целях переходов, границах
//! try-блоков и позициях отладочных таблиц; кадры переносятся как есть
//! (индексные дельты). Позиции `LineNumberTable`/`LocalVariableTable` —
//! тоже метки, поэтому вставки пассов их не ломают (байты досчитает writer).
mod frames;
mod labels;
use std::collections::BTreeSet;
use std::io::Cursor;
use ristretto_classfile::ConstantPool;
use ristretto_classfile::attributes::Attribute as RtAttr;
use ristretto_classfile::attributes::{ExceptionTableEntry, Instruction};
use super::attrs;
use super::insns::convert;
use crate::error::ClassfileError;
use crate::insn::Insn;
use crate::model::{
Attribute, Code, ExceptionHandler, LineNumber, LocalVariable, LocalVariableType,
};
use frames::{catch_type, to_frames};
use labels::{assign, collect_debug_labels, label_indexes, lookup, resolve_byte, resolve_end};
/// Тело метода целиком: backend `Code` → наш [`Code`].
pub(super) fn convert_code(
max_stack: u16,
max_locals: u16,
code: &[Instruction],
exception_table: &[ExceptionTableEntry],
attributes: &[RtAttr],
pool: &ConstantPool<'_>,
) -> Result<Code, ClassfileError> {
let (starts, code_length) = layout(code)?;
let mut extra = BTreeSet::new();
collect_debug_labels(attributes, &starts, code_length, &mut extra)?;
let labels = assign(&label_indexes(code, exception_table, &extra)?);
let resolve = |index: u16| lookup(&labels, index);
let mut insns = Vec::with_capacity(code.len() + labels.len());
for (position, instruction) in code.iter().enumerate() {
let index = u16::try_from(position)
.map_err(|_| ClassfileError::Parse("в теле метода больше 65535 инструкций".into()))?;
if let Some(label) = labels.get(&u32::from(index)) {
insns.push(Insn::Mark(*label));
}
insns.push(convert(instruction, index, pool, &resolve)?);
}
let end = u32::try_from(code.len())
.map_err(|_| ClassfileError::Parse("в теле метода больше 65535 инструкций".into()))?;
if let Some(label) = labels.get(&end) {
insns.push(Insn::Mark(*label));
}
let exception_table = exception_table
.iter()
.map(|entry| {
Ok(ExceptionHandler {
start: lookup(&labels, entry.range_pc.start)?,
end: lookup(&labels, entry.range_pc.end)?,
handler: lookup(&labels, entry.handler_pc)?,
catch_type: catch_type(pool, entry.catch_type)?,
})
})
.collect::<Result<_, ClassfileError>>()?;
let mut frames = Vec::new();
let mut rest = Vec::new();
for attr in attributes {
match attr {
RtAttr::StackMapTable { frames: found, .. } => {
if !frames.is_empty() {
return Err(ClassfileError::Parse(
"два StackMapTable в одном Code".into(),
));
}
frames = to_frames(found, pool)?;
}
RtAttr::LineNumberTable { line_numbers, .. } => rest.push(Attribute::LineNumberTable(
line_numbers
.iter()
.map(|line| {
Ok(LineNumber {
start: lookup(&labels, line.start_pc)?,
line: line.line_number,
})
})
.collect::<Result<_, ClassfileError>>()?,
)),
RtAttr::LocalVariableTable { variables, .. } => {
rest.push(Attribute::LocalVariableTable(
variables
.iter()
.map(|var| {
Ok(LocalVariable {
start: resolve_byte(&labels, &starts, var.start_pc)?,
end: resolve_end(
&labels,
&starts,
var.start_pc,
var.length,
code_length,
)?,
name: super::pool::utf8(pool, var.name_index)?,
descriptor: super::pool::utf8(pool, var.descriptor_index)?,
index: var.index,
})
})
.collect::<Result<_, ClassfileError>>()?,
))
}
RtAttr::LocalVariableTypeTable { variable_types, .. } => {
rest.push(Attribute::LocalVariableTypeTable(
variable_types
.iter()
.map(|var| {
Ok(LocalVariableType {
start: resolve_byte(&labels, &starts, var.start_pc)?,
end: resolve_end(
&labels,
&starts,
var.start_pc,
var.length,
code_length,
)?,
name: super::pool::utf8(pool, var.name_index)?,
signature: super::pool::utf8(pool, var.signature_index)?,
index: var.index,
})
})
.collect::<Result<_, ClassfileError>>()?,
));
}
other => rest.push(attrs::convert(other, pool)?),
}
}
Ok(Code {
max_stack,
max_locals,
insns,
exception_table,
frames,
attributes: rest,
})
}
/// Байтовые смещения инструкций и длина тела метода.
fn layout(code: &[Instruction]) -> Result<(Vec<u16>, u16), ClassfileError> {
let mut cursor = Cursor::new(Vec::new());
let mut offsets = Vec::with_capacity(code.len());
for instruction in code {
offsets.push(tell(&cursor)?);
instruction
.to_bytes(&mut cursor)
.map_err(|e| ClassfileError::Parse(e.to_string()))?;
}
Ok((offsets, tell(&cursor)?))
}
fn tell(cursor: &Cursor<Vec<u8>>) -> Result<u16, ClassfileError> {
u16::try_from(cursor.position())
.map_err(|_| ClassfileError::Parse("тело метода длиннее 64 КБ".into()))
}