use std::mem;
use analyssa::BitSet;
use rustc_hash::FxHashSet;
use crate::{
analysis::{
cff_taint_config, ConstValue, EvaluatorMark, SsaEvaluator, SsaFunction, SsaOp, SsaVarId,
SsaVariable, TaintAnalysis, VariableOrigin,
},
deobfuscation::passes::unflattening::{
tracer::{helpers, types::TracedDispatcher},
UnflattenConfig,
},
CilObject,
};
const GLOBAL_VISIT_BUDGET_FACTOR: usize = 4;
struct BlockFacts {
dispatch_target: Vec<bool>,
other_dispatcher: Vec<bool>,
const_producer: Vec<Option<usize>>,
overflow_site: Vec<Option<bool>>,
}
impl BlockFacts {
fn new(ssa: &SsaFunction, dispatcher: Option<&TracedDispatcher>) -> Self {
let count = ssa.block_count();
let mut dispatch_target = vec![false; count];
if let Some(d) = dispatcher {
for &target in d.targets.iter().chain(std::iter::once(&d.default)) {
if let Some(slot) = dispatch_target.get_mut(target) {
*slot = true;
}
}
}
let mut const_producer = vec![None; count];
for block in ssa.blocks() {
if let Some(slot) = const_producer.get_mut(block.id()) {
*slot = helpers::const_producer_target(block);
}
}
Self {
dispatch_target,
other_dispatcher: vec![false; count],
const_producer,
overflow_site: vec![None; count],
}
}
}
pub struct TreeTraceContext<'a> {
ssa: &'a SsaFunction,
evaluator: SsaEvaluator<'a>,
assembly: Option<&'a CilObject>,
dispatcher: Option<TracedDispatcher>,
state_tainted: BitSet,
next_node_id: usize,
total_visits: usize,
global_visits: usize,
max_global_visits: usize,
visited_states: FxHashSet<(usize, i64)>,
visited_journal: Vec<(usize, i64)>,
visit_journaling: bool,
last_case_index: usize,
visited_case_counts: Vec<u8>,
last_case_state: Vec<Option<i64>>,
max_block_visits: usize,
max_tree_depth: usize,
other_dispatcher_blocks: Vec<usize>,
no_fork: bool,
facts: BlockFacts,
vars_by_local: Vec<Vec<SsaVarId>>,
}
impl<'a> TreeTraceContext<'a> {
pub fn new(
ssa: &'a SsaFunction,
config: &UnflattenConfig,
assembly: Option<&'a CilObject>,
) -> Self {
let mut vars_by_local: Vec<Vec<SsaVarId>> = Vec::new();
for var in ssa.variables() {
if let VariableOrigin::Local(idx) = var.origin() {
let idx = usize::from(idx);
if vars_by_local.len() <= idx {
vars_by_local.resize_with(idx.saturating_add(1), Vec::new);
}
if let Some(slot) = vars_by_local.get_mut(idx) {
slot.push(var.id());
}
}
}
Self {
ssa,
evaluator: SsaEvaluator::new(ssa, config.pointer_size),
assembly,
dispatcher: None,
state_tainted: BitSet::new(ssa.var_id_capacity()),
next_node_id: 0,
total_visits: 0,
global_visits: 0,
max_global_visits: config
.max_block_visits
.saturating_mul(GLOBAL_VISIT_BUDGET_FACTOR),
visited_states: FxHashSet::default(),
visited_journal: Vec::new(),
visit_journaling: false,
last_case_index: usize::MAX,
visited_case_counts: Vec::new(),
last_case_state: Vec::new(),
max_block_visits: config.max_block_visits,
max_tree_depth: config.max_tree_depth,
other_dispatcher_blocks: Vec::new(),
no_fork: false,
facts: BlockFacts::new(ssa, None),
vars_by_local,
}
}
pub fn with_dispatcher(
ssa: &'a SsaFunction,
dispatcher: TracedDispatcher,
config: &UnflattenConfig,
assembly: Option<&'a CilObject>,
) -> Self {
let mut ctx = Self::new(ssa, config, assembly);
if let Some(state_var) = dispatcher.state_var {
let state_origin = ssa.variable(state_var).map(SsaVariable::origin);
let taint_config = cff_taint_config(ssa, dispatcher.block, state_origin);
let mut taint = TaintAnalysis::new(taint_config);
taint.add_tainted_var(state_var);
if let Some(disp_block) = ssa.block(dispatcher.block) {
for phi in disp_block.phi_nodes() {
if phi.result() == state_var {
for op in phi.operands() {
taint.add_tainted_var(op.value());
}
}
}
}
taint.propagate(ssa);
for var in taint.tainted_variables() {
ctx.state_tainted.insert(var.index());
}
}
if let (Some(state_var), Some(initial)) = (dispatcher.state_var, dispatcher.initial_state) {
let entry_pred = {
let mut pred = 0usize;
let mut current = 0usize;
for _ in 0..20 {
if current == dispatcher.block {
break;
}
pred = current;
match ssa.block(current).and_then(|b| b.terminator_op()) {
Some(SsaOp::Jump { target }) => current = *target,
_ => break,
}
}
pred
};
if let Some(disp_block) = ssa.block(dispatcher.block) {
for phi in disp_block.phi_nodes() {
if phi.result() == state_var {
for op in phi.operands() {
if op.predecessor() == entry_pred {
#[allow(clippy::cast_possible_truncation)]
ctx.evaluator
.set_concrete(op.value(), ConstValue::I32(initial as i32));
}
}
}
}
}
}
ctx.visited_case_counts = vec![0u8; dispatcher.targets.len().saturating_add(1)];
ctx.last_case_state = vec![None; dispatcher.targets.len().saturating_add(1)];
ctx.facts = BlockFacts::new(ssa, Some(&dispatcher));
ctx.dispatcher = Some(dispatcher);
ctx
}
pub fn ssa(&self) -> &'a SsaFunction {
self.ssa
}
pub fn evaluator(&self) -> &SsaEvaluator<'a> {
&self.evaluator
}
pub fn evaluator_mut(&mut self) -> &mut SsaEvaluator<'a> {
&mut self.evaluator
}
pub fn assembly(&self) -> Option<&'a CilObject> {
self.assembly
}
pub fn next_id(&mut self) -> usize {
let id = self.next_node_id;
self.next_node_id = self.next_node_id.saturating_add(1);
id
}
pub fn is_dispatcher_block(&self, block: usize) -> bool {
self.dispatcher.as_ref().is_some_and(|d| d.block == block)
}
pub fn is_dispatch_target(&self, block: usize) -> bool {
self.facts
.dispatch_target
.get(block)
.copied()
.unwrap_or(false)
}
pub fn state_var(&self) -> Option<SsaVarId> {
self.dispatcher.as_ref().and_then(|d| d.state_var)
}
pub fn dispatcher_block(&self) -> Option<usize> {
self.dispatcher.as_ref().map(|d| d.block)
}
pub fn is_other_dispatcher(&self, block: usize) -> bool {
self.facts
.other_dispatcher
.get(block)
.copied()
.unwrap_or(false)
}
pub fn set_other_dispatcher_blocks(&mut self, blocks: Vec<usize>) {
for slot in &mut self.facts.other_dispatcher {
*slot = false;
}
for &block in &blocks {
if let Some(slot) = self.facts.other_dispatcher.get_mut(block) {
*slot = true;
}
}
for slot in &mut self.facts.overflow_site {
*slot = None;
}
self.other_dispatcher_blocks = blocks;
}
pub fn const_producer_target(&self, block: usize) -> Option<usize> {
self.facts.const_producer.get(block).copied().flatten()
}
pub fn overflow_dispatch_site(
&mut self,
block: usize,
compute: impl FnOnce(&Self) -> bool,
) -> bool {
if let Some(cached) = self.facts.overflow_site.get(block).copied().flatten() {
return cached;
}
let answer = compute(self);
if let Some(slot) = self.facts.overflow_site.get_mut(block) {
*slot = Some(answer);
}
answer
}
pub fn vars_for_local(&self, local_idx: u16) -> &[SsaVarId] {
self.vars_by_local
.get(usize::from(local_idx))
.map_or(&[], Vec::as_slice)
}
pub fn is_tainted(&self, var: SsaVarId) -> bool {
self.state_tainted.contains(var.index())
}
pub fn any_tainted(&self, vars: &[SsaVarId]) -> bool {
vars.iter().any(|v| self.is_tainted(*v))
}
pub fn taint(&mut self, var: SsaVarId) {
self.state_tainted.insert(var.index());
}
pub fn state_tainted(&self) -> &BitSet {
&self.state_tainted
}
pub fn state_tainted_mut(&mut self) -> &mut BitSet {
&mut self.state_tainted
}
pub fn propagate_taint_forward(&mut self) {
helpers::propagate_taint_forward(self.ssa, &mut self.state_tainted);
}
pub fn current_state(&self) -> Option<i64> {
self.dispatcher
.as_ref()
.and_then(|d| d.state_var)
.and_then(|v| self.evaluator.get_concrete(v))
.and_then(ConstValue::as_i64)
}
fn visit_state(&self) -> i64 {
self.current_state().unwrap_or_else(|| {
let count = self
.visited_case_counts
.get(self.last_case_index)
.copied()
.map_or(0, i64::from);
(self.last_case_index as i64)
.wrapping_mul(256)
.wrapping_add(count)
})
}
pub fn is_visited(&self, block: usize) -> bool {
self.visited_states.contains(&(block, self.visit_state()))
}
pub fn mark_visited(&mut self, block: usize) {
let key = (block, self.visit_state());
if self.visited_states.insert(key) && self.visit_journaling {
self.visited_journal.push(key);
}
}
pub fn check_visit_budget(&mut self) -> bool {
self.total_visits = self.total_visits.saturating_add(1);
self.global_visits = self.global_visits.saturating_add(1);
self.total_visits > self.max_block_visits || self.global_visits > self.max_global_visits
}
pub fn max_tree_depth(&self) -> usize {
self.max_tree_depth
}
pub fn record_case_dispatch(&mut self, case_idx: usize) {
if let Some(slot) = self.visited_case_counts.get_mut(case_idx) {
*slot = slot.saturating_add(1);
}
self.last_case_index = case_idx;
}
pub fn case_state_is_stuck(&self, case_idx: usize, current_state: i64) -> bool {
self.last_case_state
.get(case_idx)
.and_then(|slot| *slot)
.is_some_and(|prev| prev == current_state)
}
pub fn record_case_state(&mut self, case_idx: usize, state: i64) {
if let Some(slot) = self.last_case_state.get_mut(case_idx) {
*slot = Some(state);
}
}
pub fn is_case_loop(&self, case_idx: usize, targets_len: usize) -> bool {
let loop_threshold = (targets_len / 2).max(2) as u8;
self.visited_case_counts
.get(case_idx)
.is_some_and(|count| *count >= loop_threshold)
}
pub fn no_fork(&self) -> bool {
self.no_fork
}
pub fn snapshot(&mut self) -> ContextSnapshot {
self.visit_journaling = true;
ContextSnapshot {
evaluator: self.evaluator.checkpoint(),
visited_mark: self.visited_journal.len(),
last_case_index: self.last_case_index,
visited_case_counts: self.visited_case_counts.clone(),
last_case_state: self.last_case_state.clone(),
}
}
pub fn restore(&mut self, snap: ContextSnapshot) {
self.evaluator.rollback(snap.evaluator);
while self.visited_journal.len() > snap.visited_mark {
let Some(key) = self.visited_journal.pop() else {
break;
};
self.visited_states.remove(&key);
}
self.last_case_index = snap.last_case_index;
self.visited_case_counts = snap.visited_case_counts;
self.last_case_state = snap.last_case_state;
}
pub fn case_counts_snapshot(&self) -> Vec<u8> {
self.visited_case_counts.clone()
}
pub fn set_case_counts(&mut self, counts: Vec<u8>) {
self.visited_case_counts = counts;
}
pub fn enter_expr_switch_false_arm(&mut self) -> (usize, bool) {
let saved = (self.total_visits, self.no_fork);
self.total_visits = 0;
self.no_fork = true;
saved
}
pub fn exit_expr_switch_false_arm(&mut self, saved: (usize, bool)) {
self.total_visits = saved.0;
self.no_fork = saved.1;
}
pub fn take_dispatcher(&mut self) -> Option<TracedDispatcher> {
self.dispatcher.take()
}
pub fn take_state_tainted(&mut self) -> BitSet {
mem::take(&mut self.state_tainted)
}
pub fn unvisited_handler_blocks(&self) -> Vec<usize> {
self.ssa
.exception_handlers()
.iter()
.filter_map(|h| h.handler_start_block)
.filter(|&block| {
block < self.ssa.block_count()
&& !self.visited_states.iter().any(|(b, _)| *b == block)
})
.collect()
}
pub fn fork_for_handler(&self, node_id_offset: usize) -> Self {
let case_count_len = self.visited_case_counts.len();
Self {
ssa: self.ssa,
evaluator: SsaEvaluator::new(self.ssa, self.evaluator.pointer_size()),
assembly: self.assembly,
dispatcher: self.dispatcher.clone(),
state_tainted: self.state_tainted.clone(),
next_node_id: node_id_offset,
total_visits: 0,
global_visits: 0,
max_global_visits: self.max_global_visits,
visited_states: FxHashSet::default(),
visited_journal: Vec::new(),
visit_journaling: false,
last_case_index: usize::MAX,
visited_case_counts: vec![0u8; case_count_len],
last_case_state: vec![None; case_count_len],
max_block_visits: self.max_block_visits,
max_tree_depth: self.max_tree_depth,
other_dispatcher_blocks: self.other_dispatcher_blocks.clone(),
no_fork: false,
facts: BlockFacts {
dispatch_target: self.facts.dispatch_target.clone(),
other_dispatcher: self.facts.other_dispatcher.clone(),
const_producer: self.facts.const_producer.clone(),
overflow_site: self.facts.overflow_site.clone(),
},
vars_by_local: self.vars_by_local.clone(),
}
}
pub fn advance_node_id(&mut self, new_id: usize) {
self.next_node_id = new_id;
}
pub fn max_block_visits(&self) -> usize {
self.max_block_visits
}
}
pub struct ContextSnapshot {
evaluator: EvaluatorMark,
visited_mark: usize,
last_case_index: usize,
visited_case_counts: Vec<u8>,
last_case_state: Vec<Option<i64>>,
}
impl ContextSnapshot {
pub fn clone_snapshot(&self) -> Self {
Self {
evaluator: self.evaluator.clone(),
visited_mark: self.visited_mark,
last_case_index: self.last_case_index,
visited_case_counts: self.visited_case_counts.clone(),
last_case_state: self.last_case_state.clone(),
}
}
}