use std::{any::Any, collections::HashMap, sync::Arc};
use log::debug;
use crate::{
analysis::{ConstValue, SsaFunction, SsaOp, SsaVarId},
assembly::{Immediate, Operand},
cilassembly::GeneratorConfig,
compiler::{EventKind, EventLog, PassPhase, SsaPass},
deobfuscation::{
context::AnalysisContext,
passes::jiejienet::{ResourceRestorationPass, ResourceTarget},
techniques::{
Detection, Detections, Evidence, Technique, TechniqueCategory, WorkingAssembly,
},
utils::get_field_data_size,
},
error::Error,
metadata::{
signatures::TypeSignature,
tables::{FieldRvaRaw, ManifestResourceAttributes, ManifestResourceBuilder},
token::Token,
typesystem::wellknown,
validation::ValidationConfig,
},
utils::decompress_gzip,
CilObject, Result,
};
#[derive(Debug)]
pub struct ResourceFindings {
pub host_type: Token,
pub stream_type: Token,
pub interception_methods: Vec<Token>,
pub get_content_method: Option<Token>,
pub xor_key: Option<u8>,
pub resource_entries: Vec<ResourceEntry>,
pub redirects: HashMap<Token, ResourceTarget>,
}
#[derive(Debug)]
pub struct ResourceEntry {
pub name: String,
pub data_method_token: Token,
}
pub struct JiejieNetResources;
impl Technique for JiejieNetResources {
fn id(&self) -> &'static str {
"jiejienet.resources"
}
fn name(&self) -> &'static str {
"JIEJIE.NET Resource Encryption"
}
fn category(&self) -> TechniqueCategory {
TechniqueCategory::Value
}
fn detect(&self, _assembly: &CilObject) -> Detection {
Detection::new_empty()
}
fn detect_ssa(&self, ctx: &AnalysisContext, assembly: &CilObject) -> Detection {
let Some(structure) = detect_resource_structure(assembly) else {
return Detection::new_empty();
};
let Some(get_content_token) = structure.get_content_method else {
return Detection::new_empty();
};
let Some(ssa_ref) = ctx.ssa_functions.get(&get_content_token) else {
return Detection::new_empty();
};
let entries = extract_resource_entries_ssa(&ssa_ref, assembly);
if entries.is_empty() {
return Detection::new_empty();
}
debug!(
"JIEJIE.NET resources: SSA-detected {} resource entries in SMF_GetContent",
entries.len()
);
let data_container_types: Vec<Token> = entries
.iter()
.filter_map(|entry| {
assembly
.method(&entry.data_method_token)
.and_then(|method| {
method
.declaring_type
.get()
.and_then(|type_ref| type_ref.token())
})
})
.collect();
let redirects = build_redirect_map(&structure.interception_methods, assembly);
debug!(
"JIEJIE.NET resources: built {} interception→BCL redirects during detection",
redirects.len()
);
let evidence = vec![Evidence::Structural(format!(
"Resource interception: nested Stream subclass + {} interception methods, {} resources (SSA)",
structure.interception_methods.len(),
entries.len(),
))];
let findings = ResourceFindings {
host_type: structure.host_type,
stream_type: structure.stream_type,
interception_methods: structure.interception_methods,
get_content_method: structure.get_content_method,
xor_key: structure.xor_key,
resource_entries: entries,
redirects,
};
let mut detection = Detection::new_detected(
evidence,
Some(Box::new(findings) as Box<dyn Any + Send + Sync>),
);
detection.cleanup_mut().add_type(structure.stream_type);
detection.cleanup_mut().add_type(structure.host_type);
for type_token in data_container_types {
detection.cleanup_mut().add_type(type_token);
}
detection
}
fn byte_transform(
&self,
assembly: &mut WorkingAssembly,
detection: &Detection,
_detections: &Detections,
) -> Option<Result<EventLog>> {
let events = EventLog::new();
let Some(findings) = detection.findings::<ResourceFindings>() else {
return Some(Ok(events));
};
let Some(xor_key) = findings.xor_key else {
log::warn!("JIEJIE.NET resources: no XOR key found, skipping decryption");
return Some(Ok(events));
};
if findings.resource_entries.is_empty() {
return Some(Ok(events));
}
let co = match assembly.cilobject() {
Ok(v) => v,
Err(e) => return Some(Err(e)),
};
let bytes = co.file().data().to_vec();
let cilobject =
match CilObject::from_mem_with_validation(bytes, ValidationConfig::analysis()) {
Ok(v) => v,
Err(e) => return Some(Err(e)),
};
let mut decrypted_resources = Vec::new();
for entry in &findings.resource_entries {
match extract_and_decrypt_resource(&cilobject, entry, xor_key) {
Ok(data) => {
log::info!(
"JIEJIE.NET resources: decrypted '{}' ({} bytes)",
entry.name,
data.len()
);
decrypted_resources.push((entry.name.clone(), data));
}
Err(e) => {
log::warn!(
"JIEJIE.NET resources: failed to decrypt '{}': {}",
entry.name,
e
);
}
}
}
if decrypted_resources.is_empty() {
events.record(EventKind::ArtifactRemoved).message(
"Resource encryption detected but no resources could be decrypted".to_string(),
);
return Some(Ok(events));
}
let mut cil_assembly = cilobject.into_assembly();
let mut inserted_count = 0;
for (name, data) in &decrypted_resources {
let builder = ManifestResourceBuilder::new()
.name(name)
.flags(ManifestResourceAttributes::PUBLIC)
.resource_data(data);
match builder.build(&mut cil_assembly) {
Ok(_) => {
inserted_count += 1;
log::info!(
"JIEJIE.NET resources: inserted resource '{}' ({} bytes)",
name,
data.len()
);
}
Err(e) => {
log::warn!(
"JIEJIE.NET resources: failed to insert resource '{}': {}",
name,
e
);
}
}
}
if inserted_count > 0 {
events.record(EventKind::ArtifactRemoved).message(format!(
"Decrypted and restored {} embedded resource(s)",
inserted_count,
));
}
let new_assembly = match cil_assembly
.into_cilobject_with(ValidationConfig::analysis(), GeneratorConfig::default())
{
Ok(v) => v,
Err(e) => return Some(Err(e)),
};
assembly.replace_assembly(new_assembly);
Some(Ok(events))
}
fn requires_regeneration(&self) -> bool {
true
}
fn ssa_phase(&self) -> Option<PassPhase> {
Some(PassPhase::Simplify)
}
fn create_pass(
&self,
_ctx: &AnalysisContext,
detection: &Detection,
_assembly: &Arc<CilObject>,
) -> Vec<Box<dyn SsaPass>> {
let Some(findings) = detection.findings::<ResourceFindings>() else {
return Vec::new();
};
if findings.interception_methods.is_empty() {
return Vec::new();
}
vec![Box::new(ResourceRestorationPass::new(
findings.redirects.clone(),
))]
}
}
struct ResourceStructure {
host_type: Token,
stream_type: Token,
interception_methods: Vec<Token>,
get_content_method: Option<Token>,
xor_key: Option<u8>,
}
fn build_redirect_map(
interception_tokens: &[Token],
assembly: &CilObject,
) -> HashMap<Token, ResourceTarget> {
let mut redirects = HashMap::new();
for &token in interception_tokens {
if let Some(target) = find_original_bcl_call(assembly, token) {
redirects.insert(token, target);
}
}
redirects
}
fn find_original_bcl_call(
assembly: &CilObject,
interception_token: Token,
) -> Option<ResourceTarget> {
let method = assembly.method(&interception_token)?;
let instructions: Vec<_> = method.instructions().collect();
for instr in &instructions {
if instr.mnemonic == "callvirt" || instr.mnemonic == "call" {
if let Operand::Token(token) = &instr.operand {
if let Some(member_ref) = assembly.member_ref(token) {
let type_name = member_ref.declaredby.fullname().unwrap_or_default();
if type_name.ends_with("Assembly")
&& (member_ref.name.starts_with("GetManifestResource")
|| member_ref.name == "GetName"
|| member_ref.name == "GetExecutingAssembly"
|| member_ref.name == "GetCallingAssembly")
{
return Some(ResourceTarget {
target_token: *token,
is_virtual: instr.mnemonic == "callvirt",
});
}
}
}
}
}
None
}
fn detect_resource_structure(assembly: &CilObject) -> Option<ResourceStructure> {
let mut best: Option<ResourceStructure> = None;
let mut best_has_content = false;
for type_entry in assembly.types().iter() {
let cil_type = type_entry.value();
let mut stream_type_token: Option<Token> = None;
for (_, nested_ref) in cil_type.nested_types.iter() {
let Some(nested) = nested_ref.upgrade() else {
continue;
};
if nested.methods.count() >= 10 {
let has_byte_array_method = nested.methods.iter().any(|(_, method_ref)| {
let Some(method) = method_ref.upgrade() else {
return false;
};
method
.signature
.params
.iter()
.any(|p| matches!(p.base, TypeSignature::SzArray(_)))
});
if has_byte_array_method {
stream_type_token = Some(nested.token);
break;
}
}
}
let Some(stream_token) = stream_type_token else {
continue;
};
let mut interception_methods: Vec<Token> = Vec::new();
let mut get_content_method: Option<Token> = None;
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;
}
let sig = &method.signature;
if !method.is_static() {
continue;
}
if sig.params.len() == 1
&& matches!(sig.params[0].base, TypeSignature::String)
&& matches!(sig.return_type.base, TypeSignature::SzArray(_))
{
get_content_method = Some(method.token);
continue;
}
let returns_class_or_array = matches!(
sig.return_type.base,
TypeSignature::Class(_) | TypeSignature::SzArray(_)
);
if !returns_class_or_array {
continue;
}
let has_class_param = sig
.params
.iter()
.any(|p| matches!(p.base, TypeSignature::Class(_)));
if has_class_param && !sig.params.is_empty() {
interception_methods.push(method.token);
}
}
if interception_methods.len() < 2 {
continue;
}
let xor_key = extract_xor_key_from_stream(assembly, stream_token);
let structure = ResourceStructure {
host_type: cil_type.token,
stream_type: stream_token,
interception_methods,
get_content_method,
xor_key,
};
let has_content = structure.get_content_method.is_some();
if has_content && !best_has_content {
best = Some(structure);
best_has_content = true;
} else if best.is_none() {
best = Some(structure);
}
if best_has_content {
break;
}
}
best
}
fn extract_xor_key_from_stream(assembly: &CilObject, stream_type_token: Token) -> Option<u8> {
let stream_type = assembly.types().get(&stream_type_token)?;
for (_, method_ref) in stream_type.methods.iter() {
let Some(method) = method_ref.upgrade() else {
continue;
};
let sig = &method.signature;
if method.is_static()
|| sig.params.len() != 3
|| !matches!(sig.return_type.base, TypeSignature::I4)
{
continue;
}
let first_is_array = matches!(&sig.params[0].base, TypeSignature::SzArray(_));
if !first_is_array {
continue;
}
let instructions: Vec<_> = method.instructions().collect();
for (i, instr) in instructions.iter().enumerate() {
if instr.mnemonic != "xor" {
continue;
}
if i == 0 {
continue;
}
for j in (0..i).rev() {
let prev = &instructions[j];
if prev.mnemonic.starts_with("ldc.i4") {
if let Operand::Immediate(imm) = &prev.operand {
let key_value = match imm {
Immediate::Int8(v) => *v as u8,
Immediate::Int32(v) => *v as u8,
Immediate::UInt8(v) => *v,
Immediate::UInt32(v) => *v as u8,
_ => continue,
};
return Some(key_value);
}
}
if i - j > 3 {
break;
}
}
}
}
None
}
fn extract_resource_entries_ssa(ssa: &SsaFunction, assembly: &CilObject) -> Vec<ResourceEntry> {
let mut entries = Vec::new();
let blocks = ssa.blocks();
for (block_idx, block) in blocks.iter().enumerate() {
let mut resource_name: Option<String> = None;
for instr in block.instructions() {
let (method_token, args) = match instr.op() {
SsaOp::CallVirt { method, args, .. } | SsaOp::Call { method, args, .. } => {
(method.token(), args)
}
_ => continue,
};
if !is_string_equals(assembly, method_token) {
continue;
}
for arg in args {
if let Some(name) = trace_to_string_const(ssa, *arg, assembly) {
resource_name = Some(name);
break;
}
}
}
let Some(name) = resource_name else {
continue;
};
let successor_idx = block_idx + 1;
if successor_idx >= blocks.len() {
continue;
}
let successor = &blocks[successor_idx];
for instr in successor.instructions() {
let method_token = match instr.op() {
SsaOp::Call { method, .. } => method.token(),
_ => continue,
};
if let Some(called_method) = assembly.method(&method_token) {
if matches!(
called_method.signature.return_type.base,
TypeSignature::SzArray(_)
) && called_method.signature.params.is_empty()
&& called_method.is_static()
{
entries.push(ResourceEntry {
name: name.clone(),
data_method_token: method_token,
});
break;
}
}
}
}
entries
}
fn is_string_equals(assembly: &CilObject, token: Token) -> bool {
if let Some(member_ref) = assembly.member_ref(&token) {
if member_ref.name == "Equals" || member_ref.name == "op_Equality" {
if let Some(type_name) = member_ref.declaredby.fullname() {
return type_name == "System.String" || type_name.ends_with(".String");
}
}
}
false
}
fn trace_to_string_const(ssa: &SsaFunction, var: SsaVarId, assembly: &CilObject) -> Option<String> {
const MAX_DEPTH: usize = 10;
fn trace_impl(
ssa: &SsaFunction,
var: SsaVarId,
assembly: &CilObject,
depth: usize,
) -> Option<String> {
if depth >= MAX_DEPTH {
return None;
}
let def = ssa.get_definition(var)?;
match def {
SsaOp::Const {
value: ConstValue::String(us_offset),
..
} => assembly
.userstrings()
.and_then(|us| us.get(*us_offset as usize).ok())
.map(|s| s.to_string_lossy().to_string()),
SsaOp::Const {
value: ConstValue::DecryptedString(s),
..
} => Some(s.clone()),
SsaOp::Copy { src, .. } => trace_impl(ssa, *src, assembly, depth + 1),
_ => None,
}
}
trace_impl(ssa, var, assembly, 0)
}
fn extract_and_decrypt_resource(
assembly: &CilObject,
entry: &ResourceEntry,
xor_key: u8,
) -> Result<Vec<u8>> {
let method = assembly.method(&entry.data_method_token).ok_or_else(|| {
Error::Deobfuscation(format!(
"Data method 0x{:08X} not found",
entry.data_method_token.value()
))
})?;
let mut field_token: Option<Token> = None;
let mut array_size: Option<usize> = None;
let instructions: Vec<_> = method.instructions().collect();
for instr in &instructions {
if instr.mnemonic == "ldtoken" {
if let Operand::Token(token) = &instr.operand {
field_token = Some(*token);
}
}
if instr.mnemonic.starts_with("ldc.i4") {
if let Operand::Immediate(imm) = &instr.operand {
let size = match imm {
Immediate::Int32(v) => *v as usize,
Immediate::UInt32(v) => *v as usize,
Immediate::Int8(v) => *v as usize,
Immediate::UInt8(v) => *v as usize,
_ => continue,
};
array_size = Some(size);
}
}
}
let field_token = field_token.ok_or_else(|| {
Error::Deobfuscation(format!(
"No ldtoken found in data method 0x{:08X}",
entry.data_method_token.value()
))
})?;
let tables = assembly
.tables()
.ok_or_else(|| Error::Deobfuscation("No metadata tables".to_string()))?;
let fieldrva_table = tables
.table::<FieldRvaRaw>()
.ok_or_else(|| Error::Deobfuscation("No FieldRVA table".to_string()))?;
let field_rid = field_token.row();
let rva_entry = fieldrva_table
.iter()
.find(|row| row.field == field_rid)
.ok_or_else(|| {
Error::Deobfuscation(format!("No FieldRVA entry for field RID 0x{:X}", field_rid))
})?;
let data_size = get_field_data_size(assembly, field_rid)
.or(array_size)
.ok_or_else(|| {
Error::Deobfuscation(format!(
"Cannot determine data size for field RID 0x{:X}",
field_rid
))
})?;
let file = assembly.file();
let offset = file.rva_to_offset(rva_entry.rva as usize)?;
let raw_data = file.data_slice(offset, data_size)?;
if raw_data.len() < 4 {
return Err(Error::Deobfuscation(format!(
"Resource data too short ({} bytes) for '{}'",
raw_data.len(),
entry.name
)));
}
let gzip_len = u32::from_le_bytes([raw_data[0], raw_data[1], raw_data[2], raw_data[3]]);
let payload = &raw_data[4..];
let content = if gzip_len > 0 {
let decompressed = decompress_gzip(payload).map_err(|e| {
Error::Deobfuscation(format!(
"GZip decompression failed for '{}': {}",
entry.name, e
))
})?;
xor_decrypt(&decompressed, xor_key)
} else {
xor_decrypt(payload, xor_key)
};
Ok(content)
}
fn xor_decrypt(data: &[u8], key: u8) -> Vec<u8> {
data.iter().map(|b| b ^ key).collect()
}
#[cfg(test)]
mod tests {
use crate::{
deobfuscation::techniques::{
jiejienet::resources::{
detect_resource_structure, extract_xor_key_from_stream, JiejieNetResources,
},
Technique,
},
test::helpers::load_sample,
};
#[test]
fn test_detect_returns_empty() {
let asm = load_sample("tests/samples/packers/jiejie/source/jiejie_resources_only.exe");
let technique = JiejieNetResources;
let detection = technique.detect(&asm);
assert!(!detection.is_detected(), "detect() should return empty");
}
#[test]
fn test_structural_detection() {
let asm = load_sample("tests/samples/packers/jiejie/source/jiejie_resources_only.exe");
let structure = detect_resource_structure(&asm);
assert!(structure.is_some(), "Should find resource structure");
let structure = structure.unwrap();
assert!(
structure.interception_methods.len() >= 2,
"Should find at least 2 interception methods, found {}",
structure.interception_methods.len()
);
assert!(
structure.get_content_method.is_some(),
"Should find SMF_GetContent method"
);
}
#[test]
fn test_xor_key_extraction() {
let asm = load_sample("tests/samples/packers/jiejie/source/jiejie_resources_only.exe");
let structure = detect_resource_structure(&asm).expect("Should find structure");
let xor_key = extract_xor_key_from_stream(&asm, structure.stream_type);
assert!(xor_key.is_some(), "Should extract XOR key");
}
#[test]
fn test_detect_negative_controlflow_only() {
let asm = load_sample("tests/samples/packers/jiejie/source/jiejie_controlflow_only.exe");
let structure = detect_resource_structure(&asm);
assert!(
structure.is_none(),
"Should not detect resources in controlflow-only sample"
);
}
#[test]
fn test_detect_negative_original() {
let asm = load_sample("tests/samples/packers/jiejie/source/original.exe");
let structure = detect_resource_structure(&asm);
assert!(structure.is_none(), "Should not detect in original");
}
}