Advanced Binary Deobfuscation

repository·master·Indexed 22 days ago

https://github.com/malrev/abd

Course 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.

Tokens
15.6K
Snippets
48
Records
55
Agent score
77%

What's inside malrev-abd

  1. Setup the ZeusVM environment

    master

    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.zip
  2. Initialize Miasm for x86 binary analysis

    master

    To 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)
  3. Define custom VM semantics and virtual registers

    master

    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:

    • Map ECX memory to a virtual PC.
    • Map ESP offsets to a return address.
    • Map specific memory offsets to virtual registers (REG0-REG4).
    • Map immediate values (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)
  4. Quick Start: Set up the Advanced Binary Deobfuscation environment

    master

    To follow the course materials, set up an Ubuntu 18.04 environment with Miasm, Z3, and Jupyter Notebook. Follow these steps:

    1. Install VirtualBox.
    2. Download and install the Ubuntu 18.04.3 Image in VirtualBox.
    3. Clone this repository.
    4. Run the setup script from the root of the repository: ./setup.sh ./.
    5. Install IDA Freeware.
    6. Refer to Advanced-Binary-Deobfuscation.pdf for course content.
    # Clone the repository and run the setup script
    git clone <repository_url>
    cd abd
    ./setup.sh ./
  5. Prerequisite Knowledge for Advanced Binary Deobfuscation

    master

    This course is designed for security analysts and researchers. To successfully engage with the materials, you should possess:

    • A robust skill set in x86/x64 architecture.
    • Basic experience with C/C++ and Python.
    • A basic understanding of low-level Computer Science concepts (e.g., OSs, Compilers, interpreters, linkers, and loaders).

    Recommended resources to bridge knowledge gaps:

  6. Identify unexecuted basic blocks

    master

    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.

    1. Iterate through final_states and collect all LocKey objects found in path_history into an executed_lockey list.
    2. Iterate through all blocks in 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.

  7. Manage DSE execution using snapshots

    master

    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)
  8. Perform Equivalence Checking between binary code blocks

    master

    Equivalence checking determines if two basic blocks of assembly code are functionally identical. This process follows a two-step hierarchy:

    1. Syntax Comparison: A fast check to see if the instruction sequences are identical in structure. If the syntax matches, the blocks are assumed to be semantically equivalent.
    2. Semantic Comparison: If syntax differs, the blocks are analyzed using symbolic execution. This involves converting the instructions into Intermediate Representation (IR), executing them symbolically using a 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)
  9. Perform SMT-based path exploration with SymbolicExecutionEngine

    master

    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:

    1. Calculate the conditions for both the true and false paths.
    2. Determine the destination addresses for both paths using sb.eval_expr and expr_simp.
    3. Use check_path_feasibility() to verify if a path is mathematically possible before continuing the walk.
    4. If a path is infeasible, record it as a FinalState with result=False.
    5. If a path reaches a target address (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))
  10. Simplify binary IR using Miasm's DeadRemoval and CST Propagation

    master

    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.

    Workflow Steps:

    1. Initialize Environment: Create a LocationDB, a Machine instance (e.g., 'x86_32'), and a Container from the binary file.
    2. Generate IR: Use the machine's disassembly engine to create an assembly configuration (asmcfg) and convert it into an IR configuration (ircfg) using ir_arch.new_ircfg_from_asmcfg(asmcfg).
    3. Constant Propagation: Use propagate_cst_expr to propagate constant expressions through the IR, starting from a set of entry points and initial register states.
    4. Iterative Simplification: Use a 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)
  11. Explore Control Flow Graphs (CFG) using Symbolic Execution

    master

    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)
  12. Use Z3 for Boolean and Integer Constraint Solving

    master

    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())