courses

Advanced Dynamic Analysis

This page assumes familiarity with basic dynamic analysis (strace, ltrace, sandbox tools) and extends into techniques that reveal behavior hidden behind packers, process injection, and anti-analysis checks. For a catalogue of anti-analysis primitives and evasion signatures, see Anti-Analysis Techniques; for shellcode-specific analysis workflows, see Shellcode Analysis.


Debugger-Based Analysis

A debugger gives you interactive control over execution: you can pause at any instruction, inspect and modify registers and memory, and force execution paths that the malware would otherwise suppress. This makes it the primary tool for unpacking, injection analysis, and defeating anti-analysis checks.

GDB / pwndbg (Linux)

The pwndbg plugin (installed via course setup scripts) augments GDB with malware-analysis-friendly output: automatic stack/register display, heap introspection, and pattern searching.

Attach to a running process:

$ gdb -p <PID>

Run with arguments:

$ gdb --args ./sample arg1 arg2

Essential commands:

Command Effect
b *0xdeadbeef Breakpoint at an absolute address
b mmap Breakpoint on the mmap PLT entry
run / r Start execution
continue / c Resume after a break
ni / si Step over / step into (one instruction)
x/32xb $rsp Examine 32 bytes at RSP as hex
x/10i $rip Disassemble 10 instructions from RIP
info proc mappings Print all mapped memory regions
dump binary memory out.bin 0x400000 0x401000 Dump a memory region to a file
set $rip = 0x40107a Force-redirect execution
set *(char*)0x601020 = 0 Patch a byte in memory

Useful pwndbg commands:

Command Effect
vmmap Color-coded virtual memory layout
heap Parse the heap freelist
search -x "90 90 90" Scan mapped memory for a byte pattern
telescope $rsp 20 Dereference chain for 20 words from RSP

Breakpoint strategy for unpacking:

Packed samples call mmap or mprotect to create executable memory, then transfer control there. Set breakpoints on both and inspect what gets written before execution reaches the new region:

(gdb) catch syscall mmap mprotect
(gdb) run
# when stopped at mprotect with PROT_EXEC:
(gdb) finish    # let the call complete
(gdb) vmmap     # find the new executable region
(gdb) x/10i <new_region_addr>

x64dbg (Windows)

x64dbg is the open-source equivalent of OllyDbg, with a modern UI and active plugin ecosystem. Key navigation:

Shortcut Action
F2 Toggle breakpoint on selected instruction
F7 / F8 Step into / step over
F9 Run
Ctrl+G Go to address or symbol
Ctrl+F Find pattern in current region
Alt+M Memory map window
Ctrl+B Set hardware breakpoint on memory access

ScyllaHide plugin: Automatically patches the most common IsDebuggerPresent, PEB flags, and timing check anti-analysis techniques. Install it and enable all relevant options before loading a sample.

WinDbg (Windows kernel and user-mode)

For kernel-mode malware (rootkits, drivers), use WinDbg over a serial or network (kdnet) connection between an analysis VM and a debugger host.

Attach to kernel via network (kdnet):

On the VM (run once):

bcdedit /dbgsettings net hostip:<debugger_host_ip> port:50000
bcdedit /debug on

On the debugger host:

windbg -k net:port=50000,key=<key>

Useful kernel commands:

Command Effect
!process 0 0 List all processes
!process <addr> 7 Detailed process info including threads
!drvobj \Driver\<name> Inspect a driver object
!devobj <addr> Inspect a device object
bp <module>!<function> Breakpoint on an exported symbol
dt nt!_EPROCESS <addr> Overlay EPROCESS structure at address
lm List loaded modules
!idt Dump the Interrupt Descriptor Table

Defeating Anti-Analysis in the Debugger

When a sample exits silently or behaves differently under a debugger, work through this checklist before investing time in deeper analysis:

  1. Enable ScyllaHide or equivalent plugin before loading the sample.
  2. Patch IsDebuggerPresent to return 0:
    (x64dbg) Find IsDebuggerPresent in the imports; right-click → Patch → set return value
    
  3. Clear the PEB BeingDebugged byte (Windows only; offset +0x2 from PEB base):
    • In x64dbg: Open Memory Map, navigate to the PEB (shown in the status bar), go to offset +2, right-click → Binary Edit → set to 00.
    • In WinDbg: eb @$peb+2 0
    • Via Frida (shown in API Monitoring below): patch IsDebuggerPresent to return 0 rather than directly writing the PEB.
  4. Suppress OutputDebugString anti-debug: an exception-based technique raises DBG_PRINTEXCEPTION_C to detect a live debugger. In x64dbg: Options → Preferences → Exceptions → Add exception code 0x40010006 to the pass-through list.
  5. For timing checks: set a breakpoint immediately after the RDTSC/GetTickCount call and manually set the elapsed value to 0.
  6. For VM detection: ensure the analysis VM has VMware Tools removed or spoofed, no default snapshot names, a realistic process list (browser, office apps), and screen resolution ≥ 1024×768.
  7. For connectivity checks: run INetSim or FakeNet-NG to provide simulated services so the malware believes it has real network access.

See Anti-Analysis Techniques for a full taxonomy of evasion primitives and their detection signatures.


Dynamic Unpacking

Packers compress or encrypt the original binary and use a stub to restore it at runtime. Static analysis of a packed binary gives you the stub, not the payload. You must let the stub run and then extract the unpacked code from memory.

Identifying a Packed Binary

Finding the OEP

The Original Entry Point (OEP) is where the real code begins executing after the unpacking stub finishes. Two reliable techniques:

1. ESP trick (stack-based OEP detection)

The stub always saves and restores the context it inherited. Immediately after the packed entry point:

(in debugger, at entry point of packed binary)
- note the current ESP value
- set a hardware breakpoint on memory access (write) at ESP
- run; when the stub restores the saved context, the breakpoint fires just before it jumps to OEP

The JMP EAX or POPAD; JMP <addr> instruction you land on is typically the stub finishing and handing control to the OEP.

2. VirtualAlloc / VirtualProtect breakpoint

- Set a breakpoint on VirtualAlloc or VirtualProtect (PROT_EXEC)
- Let the stub allocate executable memory and write the payload
- Set a memory execution breakpoint on the allocated region
- The first execution hit in that region is at or near the OEP

Dumping the Unpacked Image

Once at the OEP:

Linux — with GDB:

(gdb) info proc mappings       # find the base address of the image
(gdb) dump binary memory unpacked.bin 0x400000 0x500000

Windows — Scylla (x64dbg plugin):

  1. Pause at the OEP
  2. Open Scylla (Ctrl+I)
  3. “IAT Autosearch” → “Get Imports” → “Fix Dump”

Scylla rebuilds the Import Address Table from the live process, producing a PE binary that loads correctly and can be analyzed statically.


Process Injection Detection

Many malware families inject code into legitimate host processes to evade process-based detections and hide network activity behind trusted executables. The following are the most common injection techniques and how to detect them dynamically.

Process Hollowing (RunPE)

The malware creates a suspended legitimate process (CreateProcess with CREATE_SUSPENDED), unmaps its image (NtUnmapViewOfSection), maps its own payload — preferring the original image base but relocating if necessary — then resumes execution.

Detection:

DLL Injection (Classic and Reflective)

Classic: The injector calls WriteProcessMemory to write a DLL path into the target, then CreateRemoteThread(LoadLibraryA).

Reflective: The DLL carries its own loader; CreateRemoteThread points directly into the injected buffer. No call to LoadLibraryA — the DLL does not appear in the normal module list.

Detection:

Process Doppelgänging

Uses Windows NTFS transactions (TxF) to write malicious code to a transacted file that is never committed to disk, then create a process image section from it. The process appears legitimate because it points to the “phantom” transacted file.

Detection: User-mode API tracing can catch the NtCreateTransactionNtCreateSectionNtCreateProcessEx call sequence; kernel-level visibility helps confirm and correlate. MITRE: T1055.013.

Thread Hijacking

The malware suspends a thread in the target process, rewrites its CONTEXT structure to redirect RIP/EIP to injected shellcode, then resumes it.

Detection:

Other Injection Variants

Technique Key APIs / Mechanism Detection Cue
APC Injection / Early Bird QueueUserAPC or NtQueueApcThread into a suspended thread before it runs QueueUserAPC targeting a remote thread; paired with WriteProcessMemory
Manual Mapping Malware parses and maps a PE by hand (resolve imports, apply relocations) without LoadLibrary Executable anonymous VAD region with a PE header; no backing module name in lm or dlllist
Section Mapping NtCreateSection + NtMapViewOfSection across process boundaries; bypasses WriteProcessMemory NtCreateSection / NtMapViewOfSection pair with a different target process; Volatility handles
Process Herpaderping / Ghosting Create section from a malicious file, then modify or delete the file before process creation; AV scans stale disk image Section object from a file that is subsequently modified or deleted; detected by kernel callbacks on file modification after section creation

MITRE: T1055 covers the full injection family.

Memory Forensics Triage

Once you have a memory image, Volatility provides a cross-view perspective that passive monitoring cannot match.

Capture:

# Windows — WinPMEM:
winpmem_mini_x64_rc2.exe memory.raw

# Linux — LiME kernel module:
sudo insmod lime.ko "path=/mnt/usb/memory.lime format=lime"

Triage with Volatility 3:

vol -f memory.raw windows.pslist    # walks ActiveProcessLinks
vol -f memory.raw windows.psscan    # pool-tag scan — finds DKOM-hidden processes
vol -f memory.raw windows.malfind   # executable anonymous VAD regions (injection indicator)
vol -f memory.raw windows.dlllist   # loaded modules per process
vol -f memory.raw windows.handles   # open handles (mutexes, named pipes, files)
vol -f memory.raw windows.netscan   # network connections including recently closed

Cross-referencing pslist against psscan is the fastest way to identify processes hidden by DKOM: any entry in psscan absent from pslist is unlinked from the active process list.


API Monitoring and Hooking

Monitoring API calls at runtime reveals what a sample does without requiring full debugger control.

API Monitor (Windows)

API Monitor intercepts thousands of Windows API functions and logs calls with arguments and return values.

  1. Start API Monitor as administrator
  2. Select the APIs to monitor (preset collections for crypto, networking, process creation, and file I/O are most useful for malware)
  3. Monitor a new process: enter the path and arguments
  4. Filter the call log by API category or return value to find anomalies

Key APIs to watch: VirtualAllocEx, WriteProcessMemory, CreateRemoteThread, NtUnmapViewOfSection, RegSetValueEx, WinExec, ShellExecute, CryptEncrypt, InternetConnect.

Frida (Linux and Windows)

Frida injects a JavaScript engine into a running process and lets you intercept and modify function calls programmatically. It does not require a debugger or source code.

Attach to a running process:

$ frida -p <PID>

Intercept a function by address:

Interceptor.attach(ptr("0x7f1234005678"), {
    onEnter(args) {
        console.log("called with arg0 = " + args[0]);
    },
    onLeave(retval) {
        console.log("returned " + retval);
    }
});

Intercept an exported function by name:

const openFunc = Module.getExportByName("libc.so.6", "open");
Interceptor.attach(openFunc, {
    onEnter(args) {
        console.log("open(" + args[0].readUtf8String() + ")");
    }
});

Force a function to return a specific value (bypass):

const IsDebuggerPresent = Module.getExportByName("kernel32.dll", "IsDebuggerPresent");
Interceptor.replace(IsDebuggerPresent, new NativeCallback(() => 0, "int", []));

Frida scripts can be loaded from a file (frida -p <PID> -l script.js) and reloaded live, making it far faster to iterate on bypasses than patching and rerunning.


Dynamic Binary Instrumentation (DBI)

DBI frameworks instrument every instruction (or selected instructions) as a binary executes, without modifying the file on disk. This enables:

Intel PIN

PIN is Intel’s production DBI framework. Analysis code is written in C/C++ as a Pintool and loaded as a shared library.

Count all instructions executed:

$ pin -t $PIN_ROOT/source/tools/ManualExamples/inscount0.so -- ./sample
$ cat inscount.out
Count 9347812

Generate an instruction trace:

$ pin -t $PIN_ROOT/source/tools/ManualExamples/itrace.so -- ./sample
$ head itrace.out
0x400557
0x40055a
...

For malware, trace differences between runs (e.g., when injecting different inputs) reveal which code paths are newly activated — useful for triggering dormant payloads.

DynamoRIO

DynamoRIO is an open-source alternative to PIN. The drcov client collects basic-block coverage in a format readable by the Lighthouse plugin for IDA/Ghidra:

$ drrun -t drcov -- ./sample arg
$ ls *.log    # drcov.sample.<pid>.0000.proc.log

Code Coverage with Lighthouse

Once you have a drcov log:

  1. Open the binary in IDA Pro or Ghidra
  2. In IDA: File → Load file → Code coverage file (Lighthouse plugin)
  3. Covered basic blocks are highlighted in the graph view

This immediately shows which code the sample executed — and, crucially, which branches it did not take. Unexplored branches are often where additional capability is hidden (triggered by specific conditions like time, C2 response, or configuration).

Coverage diffing workflow:

  1. Run drrun -t drcov -- ./sample benign_input → save as run_benign.log
  2. Run drrun -t drcov -- ./sample trigger_input → save as run_trigger.log
  3. Load both logs in IDA via Lighthouse (File → Load file → Code coverage file for each)
  4. Use Lighthouse’s Coverage Overview to subtract: find basic blocks covered in run_trigger but not in run_benign — these are newly activated paths
  5. Set a breakpoint at the first newly covered block and rerun with the triggering input to inspect context

DBI limitations:

DBI imposes 10–100× execution overhead and can break self-modifying code, packed stubs, and code that detects instrumentation via timing or explicit checks. Apply DBI after unpacking and defeating major anti-analysis checks, not before.


Kernel-Level Analysis

User-mode monitoring has a fundamental limitation: malware with kernel code (rootkits, bootkits, malicious drivers) can hide from it entirely. Kernel analysis is necessary when:

eBPF (Linux)

eBPF programs run in the kernel and can attach to system call entry/exit points, kprobes, and tracepoints without modifying the kernel. bpftrace provides a high-level scripting interface:

Trace all execve calls with arguments:

# bpftrace -e 'tracepoint:syscalls:sys_enter_execve { printf("%s %s\n", comm, str(args->filename)); }'

Trace connect syscalls (pid and file descriptor only):

# bpftrace -e 'tracepoint:syscalls:sys_enter_connect { printf("%s (pid %d) connect fd=%d\n", comm, pid, args->fd); }'

Note: args->uservaddr in sys_enter_connect is a user-space pointer — dereferencing it directly in a bpftrace tracepoint will produce unreliable results. To decode destination addresses, use strace -e connect -p <PID> or intercept connect() with Frida in user context instead.

eBPF runs at the kernel level, so it cannot be hidden from by user-space rootkits (though kernel rootkits can still tamper with eBPF output).

Kernel Debugging with WinDbg

With a kernel debugger attached (see Debugger-Based Analysis above):

Detect DKOM (Direct Kernel Object Manipulation):

DKOM rootkits hide processes by unlinking them from the ActiveProcessLinks list in _EPROCESS. The process still exists but ps/Task Manager skips it.

kd> !process 0 0           # walk ActiveProcessLinks — misses unlinked (DKOM-hidden) processes

For cross-view DKOM detection, pool-tag scanning is most reliably done offline. Take a memory snapshot with WinPMEM and compare in Volatility:

vol -f memory.raw windows.pslist    # linked-list walk (matches !process 0 0)
vol -f memory.raw windows.psscan    # pool-tag scan for all EPROCESS objects

Any process in psscan that is absent from pslist has been unlinked by DKOM. See Memory Forensics Triage above for capture instructions.

Inspect a driver’s dispatch table for hooks:

kd> lm m <driver_name>
kd> dt nt!_DRIVER_OBJECT <addr>
kd> dqs <MajorFunction array addr> L1c    # look for unexpected dispatch pointers

Correlating Results

Advanced dynamic analysis is most powerful when cross-referenced against static findings:

Finding Origin Implication
Encrypted buffer at 0x140023a0 Static (high-entropy data section) Break on first write to that address; collect the decryption key
LoadLibraryA and GetProcAddress as only imports Static Packed or shellcode-like; use OEP hunt before any other analysis
CreateRemoteThread in IAT Static Expect injection; monitor with API Monitor; compare process memory maps before and after
Module not in lm but executable pages visible Dynamic (WinDbg / Volatility) Reflective injection or manual mapping
NtUnmapViewOfSection in API trace Dynamic Process hollowing; compare target process on-disk vs. in-memory image
Divergent execution path at basic block 0x40109a Dynamic (Lighthouse coverage) Untriggered branch; examine condition with debugger and force it

Useful Resources