Runbook: runtime and memory analysis

Static analysis tells you what a binary contains. Runtime analysis tells you what it does. For packed or encrypted samples, static analysis is often not enough: the real code only exists in memory after the unpacker runs. This runbook covers extracting that material.

Frida

Frida is a dynamic instrumentation toolkit that injects a JavaScript engine into a running process and lets you intercept function calls, read and write memory, and trace execution without a traditional debugger.

Install:

pip install frida-tools

Attach to a running process by name or PID:

frida Calculator
frida -p 1234

Or spawn a process under Frida’s control from the start:

frida -f target.exe --no-pause

Intercepting API calls

The primary use is hooking imports to observe behaviour before the binary has a chance to detect analysis. Hook CreateFile to log every file the binary touches:

Interceptor.attach(Module.getExportByName('kernel32.dll', 'CreateFileW'), {
    onEnter: function(args) {
        console.log('CreateFileW: ' + args[0].readUtf16String());
    }
});

Hook VirtualAlloc to catch allocations that are likely to receive unpacked code:

Interceptor.attach(Module.getExportByName('kernel32.dll', 'VirtualAlloc'), {
    onEnter: function(args) {
        this.size = args[1].toInt32();
        this.protect = args[3].toInt32();
    },
    onLeave: function(retval) {
        if (this.protect === 0x40) { // PAGE_EXECUTE_READWRITE
            console.log('RWX alloc of ' + this.size + ' bytes at ' + retval);
        }
    }
});

PAGE_EXECUTE_READWRITE allocations are a reliable indicator of an unpacker writing and then executing code. Log the address, then dump the region after the packer has finished.

Dumping memory regions

Once you have the address of a region containing unpacked or decrypted content, read it out:

var addr = ptr('0x1A2B3C4D');
var size = 0x10000;
var data = Memory.readByteArray(addr, size);

// write to a file via the frida-compile / Python side
send(data);

On the Python side, receive and write to disk:

import frida, sys

def on_message(message, data):
    if data:
        with open('dump.bin', 'wb') as f:
            f.write(data)

session = frida.attach('target.exe')
script = session.create_script(open('hook.js').read())
script.on('message', on_message)
script.load()
sys.stdin.read()

Analyse the dumped region as a standalone binary with rabin2 -I dump.bin to check whether it is a valid PE or ELF. If it is, load it into Ghidra directly.

Qiling

Qiling is a binary emulation framework that runs binaries in an instrumented environment without requiring the target OS or hardware. It is useful when you cannot or do not want to execute the sample on a live system.

Install:

pip install qiling

Emulate a Windows PE on Linux:

from qiling import Qiling
from qiling.const import QL_VERBOSE

ql = Qiling(['target.exe'], r'rootfs/x8664_windows', verbose=QL_VERBOSE.DEBUG)
ql.run()

rootfs is a directory tree containing the Windows DLLs Qiling needs. The project provides pre-built rootfs images.

Hooking in Qiling

Hook a Windows API to extract arguments:

def hook_createfile(ql, address, params):
    print('CreateFile called with:', params['lpFileName'])

ql.os.set_api('CreateFileW', hook_createfile)
ql.run()

Hook an address to dump memory at a point after unpacking is complete:

def dump_at(ql, address, data):
    mem = ql.mem.read(0x401000, 0x10000)
    with open('unpacked.bin', 'wb') as f:
        f.write(mem)

ql.hook_address(dump_at, 0x40150A)
ql.run()

Extracting configs and C2 addresses

Decrypted configuration blocks typically contain C2 addresses, mutex names, campaign IDs, and sleep intervals. They appear in memory after the unpacker runs, often shortly before the first network call.

Hook connect or WSAConnect (Windows) to log C2 addresses at the point of connection:

Interceptor.attach(Module.getExportByName('ws2_32.dll', 'connect'), {
    onEnter: function(args) {
        var sockaddr = args[1];
        var family = sockaddr.readU16();
        if (family === 2) { // AF_INET
            var port = sockaddr.add(2).readU16be();
            var ip = sockaddr.add(4).readU32();
            console.log('connect: ' +
                ((ip & 0xff)) + '.' + ((ip >> 8) & 0xff) + '.' +
                ((ip >> 16) & 0xff) + '.' + ((ip >> 24) & 0xff) +
                ':' + port);
        }
    }
});

For configs stored in a structured block, once you have the base address from the VirtualAlloc hook, scan for common indicators: null-terminated strings, IP address patterns, known magic bytes used by specific malware families.

Notes

Frida requires the target to run. If the binary detects a debugger or virtual environment, address that before attaching. Common checks to patch: IsDebuggerPresent, CheckRemoteDebuggerPresent, CPUID-based VM detection, timing checks using GetTickCount or QueryPerformanceCounter.

Qiling sidesteps most of these checks because it is not running the binary on real hardware. The trade-off is that complex binaries with many API dependencies may fail to emulate correctly without manual shim work.