use std::{any::Any, sync::Arc};
use crate::{
compiler::{PassCapability, PassPhase, SsaPass},
deobfuscation::{
context::AnalysisContext,
passes::{I32Extractor, StaticFieldResolutionPass},
techniques::{Detection, Detections, Evidence, Technique, TechniqueCategory},
},
metadata::{signatures::TypeSignature, token::Token, typesystem::wellknown},
CilObject,
};
#[derive(Debug)]
pub struct ConstantsFindings {
pub container_type: Token,
pub field_tokens: Vec<Token>,
pub cctor_token: Option<Token>,
}
pub struct JiejieNetConstants;
const MIN_FIELDS: usize = 10;
impl Technique for JiejieNetConstants {
fn id(&self) -> &'static str {
"jiejienet.constants"
}
fn name(&self) -> &'static str {
"JIEJIE.NET Integer Constants"
}
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.is_empty() {
continue;
}
let mut all_static_int32 = true;
let mut field_tokens: Vec<Token> = Vec::new();
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 {
field_tokens.push(field.token);
} else {
all_static_int32 = false;
break;
}
}
if !all_static_int32 || field_tokens.len() < MIN_FIELDS {
continue;
}
let mut cctor_token = None;
let mut has_delta_chain = false;
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);
let instructions: Vec<_> = method.instructions().collect();
let mut ldc_i8_count = 0;
let mut has_conv_i4 = false;
let mut has_stsfld = false;
let mut has_dup = false;
for instr in &instructions {
match instr.mnemonic {
"ldc.i8" => ldc_i8_count += 1,
"conv.i4" => has_conv_i4 = true,
"stsfld" => has_stsfld = true,
"dup" => has_dup = true,
_ => {}
}
}
has_delta_chain = ldc_i8_count >= 2 && has_conv_i4 && has_stsfld && has_dup;
break;
}
}
if !has_delta_chain {
continue;
}
let evidence = vec![Evidence::Structural(format!(
"Class with {} static initonly int32 fields and ldc.i8 delta chain .cctor",
field_tokens.len(),
))];
let findings = ConstantsFindings {
container_type: cil_type.token,
field_tokens,
cctor_token,
};
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 ssa_phase(&self) -> Option<PassPhase> {
Some(PassPhase::Value)
}
fn initialize(
&self,
ctx: &AnalysisContext,
_assembly: &CilObject,
detection: &Detection,
_detections: &Detections,
) {
let Some(findings) = detection.findings::<ConstantsFindings>() else {
return;
};
if let Some(cctor_token) = findings.cctor_token {
log::info!(
"JIEJIE.NET: registering Int32ValueContainer .cctor (0x{:08X}) as warmup, \
{} fields to resolve",
cctor_token.value(),
findings.field_tokens.len(),
);
ctx.register_warmup_method(cctor_token, vec![]);
}
}
fn create_pass(
&self,
ctx: &AnalysisContext,
detection: &Detection,
_assembly: &Arc<CilObject>,
) -> Vec<Box<dyn SsaPass>> {
let Some(findings) = detection.findings::<ConstantsFindings>() else {
return Vec::new();
};
let Some(pool) = ctx.template_pool.get().cloned() else {
return Vec::new();
};
vec![Box::new(StaticFieldResolutionPass::new(
"jiejie-int32-container",
"Replaces JIEJIE.NET Int32ValueContainer field loads with resolved constant values",
pool,
findings.cctor_token,
findings.field_tokens.clone(),
Box::new(I32Extractor),
vec![PassCapability::ResolvedStaticFields],
))]
}
}
#[cfg(test)]
mod tests {
use crate::{
deobfuscation::techniques::{
jiejienet::constants::{ConstantsFindings, JiejieNetConstants},
Technique,
},
test::helpers::load_sample,
};
#[test]
fn test_detect_positive_controlflow_no_rename() {
let asm =
load_sample("tests/samples/packers/jiejie/source/jiejie_controlflow_no_rename.exe");
let technique = JiejieNetConstants;
let detection = technique.detect(&asm);
assert!(detection.is_detected(), "Should detect Int32ValueContainer");
let findings = detection
.findings::<ConstantsFindings>()
.expect("Should have ConstantsFindings");
assert!(
findings.field_tokens.len() >= 10,
"Should find 10+ int32 fields, found {}",
findings.field_tokens.len()
);
assert!(findings.cctor_token.is_some(), "Should have .cctor token");
}
#[test]
fn test_detect_positive_renamed() {
let asm = load_sample("tests/samples/packers/jiejie/source/jiejie_default.exe");
let technique = JiejieNetConstants;
let detection = technique.detect(&asm);
assert!(
detection.is_detected(),
"Should detect container even when renamed"
);
}
#[test]
fn test_detect_negative_strings_only() {
let asm = load_sample("tests/samples/packers/jiejie/source/jiejie_strings_only.exe");
let technique = JiejieNetConstants;
let detection = technique.detect(&asm);
assert!(
!detection.is_detected(),
"Should not detect container in strings-only sample"
);
}
#[test]
fn test_detect_negative_original() {
let asm = load_sample("tests/samples/packers/jiejie/source/original.exe");
let technique = JiejieNetConstants;
let detection = technique.detect(&asm);
assert!(!detection.is_detected(), "Should not detect in original");
}
}