exe-dll-diff: A Practical Guide to Comparing EXE and DLL FilesComparing executable (EXE) and dynamic-link library (DLL) files is a common task for reverse engineers, security researchers, software developers, and system integrators. Whether you’re auditing for malicious modifications, verifying a build, or tracking behavioral changes between versions, a structured approach to binary comparison helps you find meaningful differences quickly and accurately. This guide covers practical methods, tools, and workflows for performing an effective exe-dll-diff analysis.
Why compare EXE and DLL files?
Binary comparisons answer several common questions:
- Has the binary changed between builds or releases?
- Did a patch modify functionality or introduce new code paths?
- Are there injected modules, packers, or tampered resources?
- What are the exact code or data changes that explain different runtime behavior?
EXE and DLL comparisons are crucial for release verification, forensic analysis, malware triage, and regression debugging.
Overview of the types of differences you might find
Binary differences can be grouped by cause and significance:
-
Build-time differences
- Timestamps, build IDs, and non-deterministic linker output.
- Compiler optimizations producing different machine code shapes.
-
Intentional source changes
- New functions, removed functions, changed algorithm implementation.
- Modified resources (icons, version strings, manifest).
-
Post-build modifications
- Packer/encryptor additions, overlays, and appended data.
- Code injection or import table tampering.
- Resource edits (strings, dialogs).
-
Metadata and structural changes
- PE header changes, section alignment, import/export table differences.
- Relocation and debug symbol adjustments.
Understanding likely causes helps prioritize what differences matter.
Preparation: getting consistent inputs
To make comparisons useful and reduce noise:
- Collect matching builds: compare files from the same OS/architecture and as close in build environment as possible.
- Strip non-essential variability where possible:
- Use deterministic builds (reproducible builds) if you control the build system.
- If you can, compile with debug or symbol info on one side and strip both consistently for the comparison you need.
- Preserve originals: work on copies; keep both versions and a log of actions.
- Record checksums (SHA-256) before and after operations to ensure integrity.
Tools and techniques
No single tool does everything. Use a combination depending on what you want to discover.
1) Byte-level comparisons
- Tools: cmp, FC (Windows), diff, xxd + diff
- Use when you want a raw view of differences.
- Pros: Exact; shows every changed byte.
- Cons: Very noisy; build timestamps or alignment cause many irrelevant diffs.
Example workflow:
- Dump binaries as hex and compare:
xxd old.exe > old.hex xxd new.exe > new.hex diff -u old.hex new.hex
2) PE-aware differencing
- Tools: Diaphora, BinDiff, DarunGrim, r2diff (Radare2), Ghidra’s patching/diffing
- These tools parse PE structure and focus on code/data differences, function-level matches, and control-flow graph (CFG) deltas.
- Use when you need meaningful semantic diffs: function renames/moves, inlined changes, compiler reordering.
Typical steps:
- Load both binaries in IDA Pro + BinDiff or Ghidra and run automated matching.
- Inspect unmatched or significantly changed functions first.
3) Disassembly and decompilation comparisons
- Tools: IDA, Ghidra, Binary Ninja, Hopper
- Decompile both versions to pseudo-C and compare either manually or with text-diffing tools.
- Helpful for understanding higher-level algorithmic changes.
Practical tip: export decompiled code to text and use a structured diff tool that ignores whitespace and comment changes.
4) Symbol- and debug-aware comparisons
- If PDBs or DWARF exist, use them to map addresses to source-level symbols.
- Tools: IDA/Ghidra with symbol loading, LLVM tools for DWARF, Microsoft’s DIA SDK
- Symbols reduce ambiguity — you can compare function names and source-line mappings.
5) Import/Export and dependency analysis
- Tools: PEView, CFF Explorer, Dependency Walker, dumpbin
- Compare import tables and exports to find added or removed dependencies and API usage changes.
Command example:
dumpbin /imports old.exe > old_imports.txt dumpbin /imports new.exe > new_imports.txt diff -u old_imports.txt new_imports.txt
6) Runtime behavioral differencing
- Tools: Procmon, API monitor, WinDbg, strace (Linux), dynamic instrumentation (Frida, DynamoRIO)
- Execute both versions under controlled inputs and compare runtime traces, API calls, and file/network activity.
- Helpful when small binary diffs produce large behavioral changes due to config or runtime conditions.
7) File system and overlay checks
- Use hex viewers and PE parsers to inspect appended overlays, digital signatures, and embedded resources.
- Check certificate tables (signatures) using signtool or openssl for differences in signing.
A practical workflow (step-by-step)
-
Sanity checks
- Verify file types and architectures (pecheck, file).
- Compute checksums:
sha256sum old.exe new.exe
-
Quick metadata diff
- Compare PE header fields (timestamp, entry point, image size).
- Tools: pefile (Python), CFF Explorer.
-
Import/Export comparison
- Identify new or removed external dependencies.
-
Section-wise binary diff
- Extract and compare .text, .rdata, .data sections separately to isolate code vs data changes.
-
Automated function matching
- Use BinDiff/Diaphora/Ghidra to match functions; inspect unmatched ones first.
-
Decompile and review changed functions
- Prioritize functions with behavioral impact (network, file, crypto, privilege changes).
-
Runtime verification
- Run both versions in an instrumented environment with identical inputs and record traces.
- Compare logs for differing system/API calls, file I/O, or network endpoints.
-
Document findings
- Record changed functions, new imports, and any suspicious modifications with annotated evidence (screenshots, diffs, hashes).
Practical examples — what to look for
- New import of networking APIs (WinHTTP, sockets) suggests added network capability.
- Changes in cryptographic API usage (CryptoAPI, BCrypt) may indicate algorithm updates.
- Added relocations/section changes and appended data often point to packers or packer removal.
- Changed resource version strings or manifests may show repackaging or tampering.
- Function-level logic changes in authentication, privilege elevation code, or file access indicate security-relevant edits.
Handling noise: filters and normalization
To reduce irrelevant differences:
- Normalize build timestamps and debug paths when possible.
- Strip or ignore relocation-only differences.
- Use function-level matching to suppress address reordering noise.
- Compare only .text and critical data sections when resources differ heavily.
Scripts and tools like pefile (Python) can automate normalization steps (zero out TimeDateStamp, canonicalize debug directories).
Caveats and limitations
- Compiler optimizations can transform code in ways that make semantic matching difficult; what looks like many changes may be a single source change.
- Packer/obfuscator usage can hide meaningful differences until unpacked.
- Stripped binaries lack symbol context, increasing manual effort.
- Dynamic behavior may not be apparent in static comparisons; combining static and dynamic analysis is best practice.
Automation and scaling
For repeated comparisons across many builds:
- Automate with scripts (Python + pefile, r2pipe) to extract metadata, run diffs, and produce a report.
- Integrate comparisons into CI: reject builds with unexpected import/export changes or added suspicious sections.
- Store diff results and evidence in a searchable database for audit trails.
Example Python libraries:
- pefile — inspect and extract PE structures.
- lief — modify, parse, and rebuild binaries.
- r2pipe — control radare2 for automated disassembly and diffing.
Quick reference checklist
- Verify architecture and checksums.
- Compare PE header metadata.
- Diff imports/exports.
- Compare .text vs .data vs resources separately.
- Run automated function-level diffing.
- Decompile and inspect important changed functions.
- Execute both versions under identical conditions for runtime comparison.
- Normalize/remove build noise where possible.
- Document and archive diffs and evidence.
Further reading and resources
- Documentation for BinDiff, Diaphora, Ghidra, IDA Pro, and Radare2.
- pefile and lief project pages for scripting PE analysis.
- Malware analysis/playbooks that cover static and dynamic triage.
Performing exe-dll-diff analysis is part art, part automation. Combine structural PE awareness with semantic matching and runtime testing to separate noise from meaningful change. With a repeatable workflow, you’ll reduce time-to-evidence and increase confidence in identifying intentional or malicious modifications.
Leave a Reply