Setup the ZeusVM environment
masterTo begin exploring the ZeusVM binary, unzip the provided archive using the password infected:
!unzip -n -P infected zeusvm.zip#!unzip -n -P infected zeusvm.ziprepository·master·Indexed 22 days ago
https://github.com/malrev/abdCourse materials for 'Advanced Binary Deobfuscation' focusing on obfuscation principles, data-flow analysis, and SAT/SMT-based binary analysis. The content covers the use of Miasm for IR Control Flow Graph (CFG) generation, dead code removal using the DeadRemoval class, constant propagation via propagate_cst_expr, and binary execution/tracing using Miasm Jitter. It includes guides for setting up an Ubuntu 18.04 environment with Z3 and Jupyter Notebook.
To begin exploring the ZeusVM binary, unzip the provided archive using the password infected:
!unzip -n -P infected zeusvm.zip#!unzip -n -P infected zeusvm.zipTo analyze a binary (e.g., zeusvm.bin), use the Miasm Machine and Container classes to load the file, create a disassembly engine, and generate the Intermediate Representation (IR) architecture.
from miasm.analysis.machine import Machine
from miasm.analysis.binary import Container
filename = 'zeusvm.bin'
machine = Machine('x86_32')
loc_db = LocationDB()
with open(filename, 'rb') as fstream:
cont = Container.from_stream(fstream, loc_db)
bs = cont.bin_stream
mdis = machine.dis_engine(bs, loc_db=cont.loc_db)
ir_arch = machine.ir(mdis.loc_db)filename = 'zeusvm.bin'
machine = Machine('x86_32')
loc_db = LocationDB()
with open(filename, 'rb') as fstream:
cont = Container.from_stream(fstream, loc_db)
bs = cont.bin_stream
mdis = machine.dis_engine(bs, loc_db=cont.loc_db)
ir_arch = machine.ir(mdis.loc_db)When analyzing a Virtual Machine (VM) obfuscated binary, you can map physical memory/registers to virtual abstractions (like VM_PC_init or REG0) using an infos dictionary. This allows the symbolic engine to treat specific memory locations as named virtual entities.
Example mapping:
ECX memory to a virtual PC.ESP offsets to a return address.REG0-REG4).imm8, imm16, etc.) to symbolic IDs.# Example: Mapping virtual PC and registers
vm_pc_init = ExprId('VM_PC_init', 32)
ret_addr = ExprId('RET_ADDR', 32)
infos = {}
infos[expr_simp(ExprMem(regs.ECX_init, 32))] = vm_pc_init
# ... mapping virtual registers ...
for i in range(0, 5):
infos[expr_simp(ExprMem(regs.ECX_init + ExprInt(4*(i+1), 32), 32))] = ExprId('REG%d' % i, 32)To follow the course materials, set up an Ubuntu 18.04 environment with Miasm, Z3, and Jupyter Notebook. Follow these steps:
./setup.sh ./.Advanced-Binary-Deobfuscation.pdf for course content.# Clone the repository and run the setup script
git clone <repository_url>
cd abd
./setup.sh ./This course is designed for security analysts and researchers. To successfully engage with the materials, you should possess:
Recommended resources to bridge knowledge gaps:
After exploration, you can distinguish between executed and unexecuted blocks by comparing the path_history of all final_states against the total set of blocks in the ircfg.
final_states and collect all LocKey objects found in path_history into an executed_lockey list.ircfg.blocks; if a block's label is not in executed_lockey, add it to unexecuted_lockey.This allows you to identify code paths that were not reached during symbolic execution.
To iterate through multiple potential solutions or brute-force specific symbolic values, use dse.take_snapshot() to save the current state. You can then use dse.restore_snapshot(snapshot, keep_known_solutions=True) to revert to that state, allowing you to inject new concrete values into the symbolic memory using sb.jitter.eval_expr(ExprAssign(...)) and re-run the execution.
# Take a snapshot before starting the loop
snapshot = dse.take_snapshot()
while todo:
# ... process values ...
# Restore state to the snapshot for the next iteration
dse.restore_snapshot(snapshot, keep_known_solutions=True)
# Inject concrete values into symbolic memory
for i in range(8):
sb.jitter.eval_expr(ExprAssign(argv1_addr[i], arg_value[i]))
# Re-initialize and run
sb.jitter.init_run(0x1040)
sb.jitter.continue_run(step=False)Equivalence checking determines if two basic blocks of assembly code are functionally identical. This process follows a two-step hierarchy:
SymbolicExecutionEngine, and using a SMT solver (like Z3) to check if any input can produce different outputs (registers or memory) between the two blocks.To perform this, you can implement a semantic_compare function that utilizes miasm for IR translation and z3 for solving the equivalence constraints.
# High-level logic for equivalence checking
if syntax_compare(blk0, blk1):
# If syntax matches, semantics are assumed same
r_semantic = True
else:
# Otherwise, perform deep semantic analysis
r_semantic = semantic_compare(blk0, blk1, ir_arch0, ir_arch1, asmcfg)To explore code paths using symbolic execution, use the SymbolicExecutionEngine in conjunction with a recursive walking function (like codepath_walk) and an SMT solver (like z3) to check path feasibility.
When encountering a conditional branch (ExprCond), you must:
true and false paths.sb.eval_expr and expr_simp.check_path_feasibility() to verify if a path is mathematically possible before continuing the walk.FinalState with result=False.lbl_stop), record it as a FinalState with result=True.# Core logic for handling conditional branches in path exploration
if isinstance(pc, ExprCond):
# 1. Define conditions
cond_true = {pc.cond: ExprInt(1, 32)}
cond_false = {pc.cond: ExprInt(0, 32)}
# 2. Calculate destination addresses
addr_true = expr_simp(sb.eval_expr(pc.replace_expr(cond_true), {}))
addr_false = expr_simp(sb.eval_expr(pc.replace_expr(cond_false), {}))
# 3. Accumulate path conditions
conds_true = list(conds) + list(cond_true.items())
conds_false = list(conds) + list(cond_false.items())
# 4. Check feasibility and recurse
if check_path_feasibility(conds_true):
codepath_walk(addr_true, sb.symbols.copy(), conds_true, depth + 1, final_states, list(path))
else:
final_states.append(FinalState(False, sb, conds_true, path))
if check_path_feasibility(conds_false):
codepath_walk(addr_false, sb.symbols.copy(), conds_false, depth + 1, final_states, list(path))
else:
final_states.append(FinalState(False, sb, conds_false, path))You can optimize and simplify binary Intermediate Representation (IR) by combining Constant Propagation (CST) with dead code removal. This process involves loading a binary, generating an IR configuration (ircfg), propagating constant expressions, and iteratively applying dead code removal and empty assignment block removal until the IR stabilizes.
LocationDB, a Machine instance (e.g., 'x86_32'), and a Container from the binary file.asmcfg) and convert it into an IR configuration (ircfg) using ir_arch.new_ircfg_from_asmcfg(asmcfg).propagate_cst_expr to propagate constant expressions through the IR, starting from a set of entry points and initial register states.while loop to repeatedly apply DeadRemoval and remove_empty_assignblks on the ircfg until no further modifications are detected.from miasm.analysis.machine import Machine
from miasm.analysis.binary import Container
from miasm.analysis.cst_propag import propagate_cst_expr
from miasm.analysis.data_flow import DeadRemoval, remove_empty_assignblks
from miasm.core.locationdb import LocationDB
# Setup
loc_db = LocationDB()
machine = Machine('x86_32')
cont = Container.from_stream(open(filename, 'rb'), loc_db)
mdis = machine.dis_engine(cont.bin_stream, loc_db=cont.loc_db)
ir_arch = machine.ira(mdis.loc_db)
# Generate IR configuration
addr = 0x8048440
asmcfg = mdis.dis_multiblock(addr)
ircfg = ir_arch.new_ircfg_from_asmcfg(asmcfg)
# Propagate constants
entry_points = set([mdis.loc_db.get_offset_location(addr)])
init_infos = ir_arch.arch.regs.regs_init
cst_propag_link = propagate_cst_expr(ir_arch, ircfg, addr, init_infos)
# Iterative Dead Code Removal
deadrm = DeadRemoval(ir_arch)
modified = True
while modified:
modified = False
modified |= deadrm(ircfg)
modified |= remove_empty_assignblks(ircfg)You can perform symbolic execution to explore different paths in a binary's Control Flow Graph (CFG) using the explore function. This function traverses the IR (Intermediate Representation) starting from a target address, handling conditional branches by evaluating both true and false paths. It uses a SymbolicExecutionEngine to manage symbolic states and path conditions.
Key parameters for explore:
ir: The IR architecture object.start_addr: The entry point address for exploration.start_symbols: A dictionary mapping symbolic expressions (like memory or registers) to initial values.ircfg: The IR Control Flow Graph object.cond_limit: Maximum depth of conditional branches to explore (prevents infinite recursion).uncond_limit: Maximum number of unconditional blocks to process.lbl_stop: An optional address that, when reached, marks a successful path.final_states: A list to collect FinalState objects representing discovered paths.# Initialize symbols (e.g., setting ESP)
symbols_init = {
ExprMem(ExprId('ESP_init', 32), 32) : ExprInt(0xdeadbeef, 32)
}
final_states = []
# Run exploration
explore(
ir_arch,
target_addr,
symbols_init,
ircfg,
lbl_stop=0xdeadbeef,
final_states=final_states
)
# Iterate through results
for final_state in final_states:
print('Feasible path:', '->'.join([str(x) for x in final_state.path_history]))
print('\t', final_state.path_conds)This notebook demonstrates how to use the z3-solver library in Python to define Boolean variables, integer variables, and logical constraints (OR, AND, NOT) to find satisfying models.
To use Z3, import the library and initialize a Solver instance. You can then add constraints using logical operators and check for satisfiability using .check(). If the constraints are satisfiable, .model() will return the assignment of variables that satisfies the formula.
from z3 import *
# Define Boolean variables
malicious, benign = Bools('malicious, benign')
# Initialize the solver
s = Solver()
# Add logical constraints
s.add(Or(malicious, benign),
Or(Not(malicious), benign),
Or(Not(malicious), Not(benign)))
# Check satisfiability and retrieve the model
if s.check() == sat:
print(s.model())