
A stripped ELF lands in your queue. Or a packed PE pulled out of a phishing email, imports resolving to two functions and a section table that reads like noise. You want an answer before the end of the day, and you have two ways in: read the file, or run it.
Most analysts have a default. Some go straight to the disassembler and stay there for hours, building a beautiful map of code that never executes on their machine. Others drop everything into a sandbox and treat the report as the final word. Both habits cost time. Static and dynamic analysis answer different questions, and the order you use them matters more than which one you prefer.
Two ways of asking the same question
Static analysis reads bytes without executing them. Disassemblers, decompilers, string extraction, header and entropy checks, cross-references, and the slow work of renaming functions until the code starts to make sense. You see every path the program could take, including the ones that never run on your hardware.
Dynamic analysis runs the thing under observation. Debuggers, sandboxes, API tracers, network captures, filesystem monitors. You see one path — the one the program actually took with the inputs and environment you gave it — with real values attached to every register and buffer.
The framing that keeps you honest: static tells you what is possible, dynamic tells you what happened. Strong analyses move between the two, sometimes several times in a single hour.
Start static when the binary will not cooperate
- It is packed or obfuscated. Entropy near the top of the scale, imports that resolve to almost nothing, sections named like UPX0. You want to know that before spending a VM on it.
- It refuses to run. Anti-VM checks, a C2 server that no longer answers, a license file that is missing. Static reads the code regardless of what the code thinks about its environment.
- You cannot execute it safely or legally. Firmware images, early bootloader stages, kernel drivers on production hardware, code pulled off a device you do not own.
- You need coverage, not one sample path. Every function that references a particular crypto constant. Every handler for a specific opcode. A disassembler answers in seconds what a debugger would take an afternoon to walk.
- The binary is enormous. Mapping tens of megabytes of statically linked Go or Rust is tedious, but it still beats stepping through it instruction by instruction.
Static has its own traps. Indirect jumps through computed tables break recursive disassembly. A decompiler's C output is a hypothesis about intent, not source code. Heavy optimization inlines, reorders and merges variables until the names lie to you. Treat every symbol you did not assign yourself as provisional.
Start dynamic when you need ground truth
- Triage. You do not yet know whether the sample is malicious, and a sandbox tells you about network, process and filesystem behavior in minutes.
- Unpacking. Let the stub decrypt itself, then dump process memory. Packing is a runtime problem far more often than a static one.
- Protocol reversing. Run the client against a fake server, capture the traffic, and use the bytes as an anchor for the static work that follows.
- Anti-analysis you cannot untangle. Custom virtual machines, self-modifying code, encrypted string tables. Watch it run and see what it touches.
- Crash triage. A stack trace and a register dump point at the interesting code faster than reading symbol-less disassembly.
The trade-offs are real. One path per run. Environment sensitivity. Timing conditions that never reproduce. Code that sleeps for three days or waits for a specific command-line argument. And an API trace tells you that a process was created, never why.
If your static notes are a map and your dynamic notes are a route, you want both before you claim to know the territory.
The tools, and what each is actually good for
Disassemblers
IDA Pro, Ghidra, Binary Ninja, radare2 and rizin. These give you structure: functions, cross-references, call graphs, and a place to record what you learn. Recursive-descent algorithms handle normal compiler output well; linear sweep trips over data embedded in code sections.
Decompilers
Ghidra's decompiler, Hex-Rays, and similar tools. Read the output for intent — loop bounds, comparison logic, structure layout — then confirm the details in assembly. Optimization happily turns a bounds check into something you would never write by hand.
Debuggers and instrumentation
x64dbg and WinDbg on Windows, GDB and lldb elsewhere, plus Frida when you want to hook a function without attaching a debugger. Breakpoints are how a static hypothesis becomes a fact: read the register, watch the buffer, patch the branch, run it again.
Sandboxes and tracers
Automated sandboxes for triage, strace and ltrace on Linux, Procmon on Windows, Wireshark or tcpdump for traffic. Broad coverage, thin detail. Use them to generate questions for the debugger, not to answer them.
A workflow that holds up under a deadline
- Triage first, ten minutes. Format, architecture, packing, imports, strings, entropy. Cheap tools, large payoff.
- Map statically. Find entry and the real main, follow the imports that matter, rename everything you understand as you go.
- Write your hypothesis in one sentence. "Beacons to a hardcoded address and drops a file in AppData." If you cannot write that sentence, you are not ready to run anything.
- Confirm dynamically in an isolated VM under a snapshot. No shared folders, no real credentials, no bridged network where you can avoid it.
- Take the surprises back to static. The branch that never fired is usually the interesting one.
- Document as you work. Renamed functions and comments are the only reason the next run takes ten minutes instead of two hours.
Your first hour, in order
Spend fifteen minutes statically before you execute anything. Get the format, the imports, the strings, and a rough sense of where the interesting code lives. Then form a hypothesis and run the sample in a disposable VM. Come back to the disassembler with the questions the run created.
A concrete loop: a license check. Static analysis locates the comparison and the branch that follows it. A debugger breakpoint on that comparison hands you the correct value sitting in a register. Patch the branch or supply the value, and the behavior is confirmed. Neither technique alone gets there quickly.
Two rules of thumb cover most cases. If the binary is packed or will not run, start static — or at least start static long enough to understand why. If it is unpacked, talks to the network, or you need to know whether it is malicious at all, start dynamic. When you are stuck in both directions, the answer is usually instrumentation: hook the function, log the arguments, and let the program tell you what it is doing.
Photo: Tima Miroshnichenko / Pexels

