use std::{any::Any, collections::HashMap, sync::Arc};
use crate::{
assembly::{Immediate, Operand},
compiler::{EventLog, PassPhase, SsaPass},
deobfuscation::{
context::AnalysisContext,
passes::jiejienet::ArrayInitRestorationPass,
techniques::{
Detection, Detections, Evidence, Technique, TechniqueCategory, WorkingAssembly,
},
},
metadata::{
signatures::TypeSignature,
tables::{FieldRvaRaw, TableId},
token::Token,
typesystem::wellknown,
},
CilObject, Result,
};
#[derive(Debug)]
pub struct ArrayFindings {
pub container_type: Token,
pub accessor_token: Token,
pub cctor_token: Option<Token>,
pub handle_count: usize,
pub init_array_method: Option<Token>,
pub init_array_target: Option<Token>,
}
pub struct JiejieNetArrays;
impl Technique for JiejieNetArrays {
fn id(&self) -> &'static str {
"jiejienet.arrays"
}
fn name(&self) -> &'static str {
"JIEJIE.NET Array Init Encryption"
}
fn category(&self) -> TechniqueCategory {
TechniqueCategory::Value
}
fn detect(&self, assembly: &CilObject) -> Detection {
for type_entry in assembly.types().iter() {
let cil_type = type_entry.value();
if cil_type.fields.count() != 1 {
continue;
}
let Some((_, field)) = cil_type.fields.iter().next() else {
continue;
};
let is_handle_array = match &field.signature.base {
TypeSignature::SzArray(elem) => matches!(*elem.base, TypeSignature::ValueType(_)),
_ => false,
};
if !is_handle_array || !field.flags.is_static() {
continue;
}
let mut accessor_token: Option<Token> = None;
let mut cctor_token: Option<Token> = None;
for (_, method_ref) in cil_type.methods.iter() {
let Some(method) = method_ref.upgrade() else {
continue;
};
if method.name == wellknown::members::CCTOR {
cctor_token = Some(method.token);
continue;
}
if method.name == ".ctor" {
continue;
}
let sig = &method.signature;
if method.is_static()
&& sig.params.len() == 1
&& matches!(sig.params[0].base, TypeSignature::I4)
&& matches!(sig.return_type.base, TypeSignature::ValueType(_))
{
accessor_token = Some(method.token);
}
}
let Some(accessor) = accessor_token else {
continue;
};
let handle_count = cctor_token
.and_then(|t| assembly.method(&t))
.map(|m| m.instructions().filter(|i| i.mnemonic == "ldtoken").count())
.unwrap_or(0);
if handle_count == 0 {
continue;
}
let (init_array_method, init_array_target) = find_my_initialize_array(assembly);
let mut evidence = vec![Evidence::Structural(format!(
"RuntimeFieldHandle[] container with {} field handles and index accessor",
handle_count,
))];
if init_array_method.is_some() {
evidence.push(Evidence::Structural(
"MyInitializeArray XOR-encrypted array init wrapper".to_string(),
));
}
let findings = ArrayFindings {
container_type: cil_type.token,
accessor_token: accessor,
cctor_token,
handle_count,
init_array_method,
init_array_target,
};
let mut detection = Detection::new_detected(
evidence,
Some(Box::new(findings) as Box<dyn Any + Send + Sync>),
);
detection.cleanup_mut().add_type(cil_type.token);
return detection;
}
Detection::new_empty()
}
fn byte_transform(
&self,
assembly: &mut WorkingAssembly,
detection: &Detection,
_detections: &Detections,
) -> Option<Result<EventLog>> {
let events = EventLog::new();
let Some(findings) = detection.findings::<ArrayFindings>() else {
return Some(Ok(events));
};
let init_method = findings.init_array_method?;
let cctor_token = findings.cctor_token?;
let co = match assembly.cilobject() {
Ok(v) => v,
Err(e) => return Some(Err(e)),
};
let field_tokens = extract_cctor_field_tokens(co, cctor_token);
if field_tokens.is_empty() {
return Some(Ok(events));
}
let decrypt_entries =
extract_init_array_call_sites(co, init_method, findings.accessor_token, &field_tokens);
if decrypt_entries.is_empty() {
return Some(Ok(events));
}
let mut field_data_entries = Vec::new();
for (field_token, xor_key) in &decrypt_entries {
if *xor_key == 0 {
continue;
}
match decrypt_field_rva_data_to_bytes(co, *field_token, *xor_key) {
Ok(Some((fieldrva_rid, data))) => {
field_data_entries.push((fieldrva_rid, data));
}
Ok(None) => {}
Err(e) => {
log::warn!(
"JIEJIE.NET arrays: failed to decrypt FieldRVA for 0x{:08X}: {}",
field_token.value(),
e,
);
}
}
}
if !field_data_entries.is_empty() {
let count = field_data_entries.len();
if let Err(e) = assembly.store_field_data(field_data_entries) {
log::warn!("JIEJIE.NET arrays: failed to store decrypted field data: {e}");
} else {
log::info!(
"JIEJIE.NET arrays: XOR-decrypted {} FieldRVA data block(s)",
count,
);
}
}
Some(Ok(events))
}
fn ssa_phase(&self) -> Option<PassPhase> {
Some(PassPhase::Value)
}
fn initialize(
&self,
_ctx: &AnalysisContext,
_assembly: &CilObject,
_detection: &Detection,
_detections: &Detections,
) {
}
fn create_pass(
&self,
_ctx: &AnalysisContext,
detection: &Detection,
assembly: &Arc<CilObject>,
) -> Vec<Box<dyn SsaPass>> {
let Some(findings) = detection.findings::<ArrayFindings>() else {
return Vec::new();
};
let Some(cctor_token) = findings.cctor_token else {
return Vec::new();
};
let field_tokens = extract_cctor_field_tokens(assembly, cctor_token);
if field_tokens.is_empty() {
return Vec::new();
}
log::info!(
"JIEJIE.NET arrays: extracted {} field tokens from .cctor, accessor=0x{:08X}{}",
field_tokens.len(),
findings.accessor_token.value(),
if findings.init_array_method.is_some() {
", MyInitializeArray detected"
} else {
""
},
);
vec![Box::new(ArrayInitRestorationPass::new(
findings.accessor_token,
field_tokens,
findings.init_array_method,
findings.init_array_target,
))]
}
}
fn extract_cctor_field_tokens(assembly: &CilObject, cctor_token: Token) -> Vec<Token> {
let Some(method) = assembly.method(&cctor_token) else {
return Vec::new();
};
method
.instructions()
.filter(|i| i.mnemonic == "ldtoken")
.filter_map(|i| match &i.operand {
Operand::Token(token) => Some(*token),
_ => None,
})
.collect()
}
fn find_my_initialize_array(assembly: &CilObject) -> (Option<Token>, Option<Token>) {
for type_entry in assembly.types().iter() {
let cil_type = type_entry.value();
for (_, method_ref) in cil_type.methods.iter() {
let Some(method) = method_ref.upgrade() else {
continue;
};
if method.name == wellknown::members::CTOR || method.name == wellknown::members::CCTOR {
continue;
}
if !method.is_static() || method.signature.params.len() != 3 {
continue;
}
let params = &method.signature.params;
if !matches!(params[0].base, TypeSignature::Class(_)) {
continue;
}
if !matches!(params[1].base, TypeSignature::ValueType(_)) {
continue;
}
if !matches!(params[2].base, TypeSignature::I4) {
continue;
}
if !matches!(method.signature.return_type.base, TypeSignature::Void) {
continue;
}
let Some(method_body) = assembly.method(&method.token) else {
continue;
};
let mut init_array_target = None;
for instr in method_body.instructions() {
if instr.mnemonic != "call" {
continue;
}
let Operand::Token(call_token) = &instr.operand else {
continue;
};
if call_token.is_table(TableId::MemberRef) {
if let Some(mr) = assembly.member_ref(call_token) {
if mr.name == "InitializeArray" {
init_array_target = Some(*call_token);
break;
}
}
}
}
if let Some(target) = init_array_target {
return (Some(method.token), Some(target));
}
}
}
(None, None)
}
fn extract_init_array_call_sites(
assembly: &CilObject,
init_method: Token,
accessor_token: Token,
field_tokens: &[Token],
) -> Vec<(Token, i32)> {
let mut entries = Vec::new();
let container_values = resolve_int32_container_values(assembly);
for type_entry in assembly.types().iter() {
let cil_type = type_entry.value();
for (_, method_ref) in cil_type.methods.iter() {
let Some(method) = method_ref.upgrade() else {
continue;
};
let Some(method_body) = assembly.method(&method.token) else {
continue;
};
let instructions: Vec<_> = method_body.instructions().collect();
for (i, instr) in instructions.iter().enumerate() {
if instr.mnemonic != "call" {
continue;
}
let Operand::Token(call_token) = &instr.operand else {
continue;
};
if *call_token != init_method {
continue;
}
let xor_key = find_preceding_i32_value(&instructions, i, &container_values);
let field_index = find_preceding_get_handle_index(
&instructions,
i,
accessor_token,
&container_values,
);
if let (Some(xor_key), Some(index)) = (xor_key, field_index) {
let index = index as usize;
if index < field_tokens.len() {
entries.push((field_tokens[index], xor_key));
}
}
}
}
}
entries
}
fn resolve_int32_container_values(assembly: &CilObject) -> HashMap<Token, i32> {
for type_entry in assembly.types().iter() {
let cil_type = type_entry.value();
if cil_type.fields.count() < 10 {
continue;
}
let mut all_static_int32 = true;
for (_, field) in cil_type.fields.iter() {
let is_static = field.flags.is_static();
let is_initonly = field.flags.is_init_only();
let is_int32 = matches!(field.signature.base, TypeSignature::I4);
if !(is_static && is_initonly && is_int32) {
all_static_int32 = false;
break;
}
}
if !all_static_int32 {
continue;
}
let cctor_token = cil_type.methods.iter().find_map(|(_, method_ref)| {
let method = method_ref.upgrade()?;
if method.name == wellknown::members::CCTOR {
Some(method.token)
} else {
None
}
});
let Some(cctor_token) = cctor_token else {
continue;
};
let values = emulate_delta_chain_cctor(assembly, cctor_token);
if !values.is_empty() {
return values;
}
}
HashMap::new()
}
fn emulate_delta_chain_cctor(assembly: &CilObject, cctor_token: Token) -> HashMap<Token, i32> {
let mut values = HashMap::new();
let Some(method) = assembly.method(&cctor_token) else {
return values;
};
let instructions: Vec<_> = method.instructions().collect();
let base_offset = instructions.first().map_or(0, |i| i.offset as u32);
let offset_map: HashMap<u32, usize> = instructions
.iter()
.enumerate()
.map(|(i, instr)| (instr.offset as u32 - base_offset, i))
.collect();
let mut stack: Vec<i64> = Vec::new();
let mut pc = 0usize;
let mut visited = 0u32;
while pc < instructions.len() {
visited += 1;
if visited > 1000 {
break; }
let instr = instructions[pc];
pc += 1;
match instr.mnemonic {
"ldc.i8" => {
if let Operand::Immediate(Immediate::Int64(v)) = &instr.operand {
stack.push(*v);
}
}
"ldc.i4" | "ldc.i4.s" | "ldc.i4.0" | "ldc.i4.1" | "ldc.i4.2" | "ldc.i4.3"
| "ldc.i4.4" | "ldc.i4.5" | "ldc.i4.6" | "ldc.i4.7" | "ldc.i4.8" | "ldc.i4.m1" => {
if let Some(v) = instr.get_ldc_i4_value() {
stack.push(i64::from(v));
}
}
"add" => {
let (Some(b), Some(a)) = (stack.pop(), stack.pop()) else {
break;
};
stack.push(a.wrapping_add(b));
}
"dup" => {
if let Some(&top) = stack.last() {
stack.push(top);
}
}
"conv.i4" => {
if let Some(top) = stack.last_mut() {
*top = i64::from(*top as i32);
}
}
"stsfld" => {
if let Operand::Token(field_token) = &instr.operand {
if let Some(val) = stack.pop() {
values.insert(*field_token, val as i32);
}
}
}
"pop" => {
stack.pop();
}
"br" | "br.s" => {
let target_il = match &instr.operand {
Operand::Target(abs) => Some(*abs as u32),
Operand::Immediate(Immediate::Int32(rel)) => {
let il_off = instr.offset as u32 - base_offset;
let instr_size = instructions
.get(pc)
.map_or(0, |next| next.offset as u32 - instr.offset as u32);
let size = if instr_size > 0 {
instr_size
} else if instr.mnemonic == "br" {
5
} else {
2
};
Some((il_off + size).wrapping_add(*rel as u32))
}
Operand::Immediate(Immediate::Int8(rel)) => {
let il_off = instr.offset as u32 - base_offset;
let instr_size = instructions
.get(pc)
.map_or(2, |next| next.offset as u32 - instr.offset as u32);
Some((il_off + instr_size).wrapping_add(*rel as i32 as u32))
}
_ => None,
};
if let Some(target) = target_il {
if let Some(&target_idx) = offset_map.get(&target) {
pc = target_idx;
} else {
break;
}
}
}
"ret" => break,
_ => {}
}
}
values
}
fn find_preceding_i32_value(
instructions: &[&crate::assembly::Instruction],
pos: usize,
container_values: &HashMap<Token, i32>,
) -> Option<i32> {
for j in (0..pos).rev() {
let instr = instructions[j];
if let Some(val) = instr.get_ldc_i4_value() {
return Some(val);
}
if instr.mnemonic == "ldsfld" {
if let Operand::Token(field_token) = &instr.operand {
if let Some(&val) = container_values.get(field_token) {
return Some(val);
}
}
}
if pos - j > 5 {
break;
}
}
None
}
fn find_preceding_get_handle_index(
instructions: &[&crate::assembly::Instruction],
pos: usize,
accessor_token: Token,
container_values: &HashMap<Token, i32>,
) -> Option<i32> {
for j in (0..pos).rev() {
let instr = instructions[j];
if instr.mnemonic == "call" {
if let Operand::Token(t) = &instr.operand {
if *t == accessor_token {
return find_preceding_i32_value(instructions, j, container_values);
}
}
}
if pos - j > 10 {
break;
}
}
None
}
fn decrypt_field_rva_data_to_bytes(
assembly: &CilObject,
field_token: Token,
xor_key: i32,
) -> Result<Option<(u32, Vec<u8>)>> {
let file = assembly.file();
let tables = assembly
.tables()
.ok_or_else(|| crate::Error::Other("No metadata tables available".to_string()))?;
let fieldrva_table = tables
.table::<FieldRvaRaw>()
.ok_or_else(|| crate::Error::Other("No FieldRVA table found".to_string()))?;
let field_rid = field_token.row();
let rva_entry = fieldrva_table
.iter()
.find(|row| row.field == field_rid)
.ok_or_else(|| {
crate::Error::Other(format!(
"No FieldRVA entry for field 0x{:08X}",
field_token.value(),
))
})?;
let rva = rva_entry.rva;
if rva == 0 {
return Ok(None);
}
let data_size = calculate_field_data_size(assembly, field_rid)?;
if data_size == 0 {
return Ok(None);
}
let offset = file.rva_to_offset(rva as usize)?;
let data = file.data_slice(offset, data_size)?;
let mut decrypted = data.to_vec();
xor_decrypt_array_data(&mut decrypted, xor_key);
Ok(Some((rva_entry.rid, decrypted)))
}
fn calculate_field_data_size(assembly: &CilObject, field_rid: u32) -> Result<usize> {
use crate::metadata::{
signatures::parse_field_signature,
tables::{ClassLayoutRaw, FieldRaw},
};
let tables = assembly
.tables()
.ok_or_else(|| crate::Error::Other("No metadata tables".to_string()))?;
let field_table = tables
.table::<FieldRaw>()
.ok_or_else(|| crate::Error::Other("No Field table".to_string()))?;
let field_row = field_table
.iter()
.find(|r| r.rid == field_rid)
.ok_or_else(|| crate::Error::Other(format!("Field {field_rid} not found")))?;
let blobs = assembly
.blob()
.ok_or_else(|| crate::Error::Other("No blob heap".to_string()))?;
let sig_data = blobs
.get(field_row.signature as usize)
.map_err(|_| crate::Error::Other(format!("Cannot read signature for field {field_rid}")))?;
let field_sig = parse_field_signature(sig_data).map_err(|e| {
crate::Error::Other(format!("Cannot parse field {field_rid} signature: {e}"))
})?;
let ptr_size = crate::metadata::typesystem::PointerSize::from_pe(assembly.file().pe().is_64bit);
if let Some(size) = field_sig.base.byte_size(ptr_size) {
return Ok(size);
}
if let TypeSignature::ValueType(type_token) = &field_sig.base {
if type_token.is_table(TableId::TypeDef) {
let row = type_token.row();
if let Some(class_layout_table) = tables.table::<ClassLayoutRaw>() {
for layout_row in class_layout_table {
if layout_row.parent == row {
return Ok(layout_row.class_size as usize);
}
}
}
}
}
Err(crate::Error::Other(format!(
"Cannot determine size for field {field_rid}"
)))
}
fn xor_decrypt_array_data(data: &mut [u8], xor_key: i32) {
let block_count = data.len() / 4;
let mut key = xor_key;
for i in (0..block_count).rev() {
let offset = i * 4;
let key_bytes = key.to_le_bytes();
data[offset] ^= key_bytes[0];
data[offset + 1] ^= key_bytes[1];
data[offset + 2] ^= key_bytes[2];
data[offset + 3] ^= key_bytes[3];
key = key.wrapping_add(13);
}
}
#[cfg(test)]
mod tests {
use crate::{
deobfuscation::techniques::{
jiejienet::arrays::{xor_decrypt_array_data, ArrayFindings, JiejieNetArrays},
Technique,
},
test::helpers::load_sample,
};
#[test]
fn test_detect_positive_controlflow() {
let asm =
load_sample("tests/samples/packers/jiejie/source/jiejie_controlflow_no_rename.exe");
let technique = JiejieNetArrays;
let detection = technique.detect(&asm);
assert!(
detection.is_detected(),
"Should detect field handle container"
);
let findings = detection
.findings::<ArrayFindings>()
.expect("Should have ArrayFindings");
assert_eq!(findings.handle_count, 3, "Should find 3 field handles");
}
#[test]
fn test_detect_negative_strings_only() {
let asm = load_sample("tests/samples/packers/jiejie/source/jiejie_strings_only.exe");
let technique = JiejieNetArrays;
let detection = technique.detect(&asm);
assert!(
!detection.is_detected(),
"Should not detect in strings-only"
);
}
#[test]
fn test_detect_negative_original() {
let asm = load_sample("tests/samples/packers/jiejie/source/original.exe");
let technique = JiejieNetArrays;
let detection = technique.detect(&asm);
assert!(!detection.is_detected(), "Should not detect in original");
}
#[test]
fn test_xor_decrypt_round_trip() {
let original = vec![0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08];
let mut data = original.clone();
let xor_key = 42;
xor_decrypt_array_data(&mut data, xor_key);
assert_ne!(data, original);
xor_decrypt_array_data(&mut data, xor_key);
assert_eq!(data, original);
}
#[test]
fn test_xor_decrypt_known_values() {
let xor_key: i32 = 0;
let mut data = vec![0x0D, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
xor_decrypt_array_data(&mut data, xor_key);
assert_eq!(data, vec![0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]);
let mut data_with_trail = vec![0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF];
xor_decrypt_array_data(&mut data_with_trail, xor_key);
assert_eq!(data_with_trail[0..4], [0x00, 0x00, 0x00, 0x00]);
assert_eq!(data_with_trail[4], 0xFF);
assert_eq!(data_with_trail[5], 0xFF);
}
}