Using x64dbg for Dynamic Analysis
Overview and Learning Objectives
x64dbg is the primary debugger used in modern malware analysis. This lesson provides a comprehensive workflow for using x64dbg effectively. By the end, you will:
- Navigate the x64dbg interface confidently and use essential shortcuts
- Set strategic breakpoints to capture malware behavior efficiently
- Use memory breakpoints to track unpacking and injection
- Work with ScyllaHide to bypass anti-debug protections
- Dump unpacked code from memory using the Scylla plugin
- Build complete behavioral timelines from debug sessions
What is x64dbg?
x64dbg is a free, open-source debugger for Windows. It supports both x86 (x32dbg) and x64 (x64dbg) binaries. It has largely replaced OllyDbg as the go-to debugger for malware analysis due to its active development, modern interface, and extensive plugin ecosystem.
Interface Overview
| Panel | Purpose | Key Information |
|---|---|---|
| CPU | Disassembly + registers | Assembly instructions, current EIP/RIP, flags |
| Dump | Raw memory hexdump | Memory contents at any address, follow pointers |
| Stack | Call stack + stack memory | Function call chain, local variables, parameters |
| Breakpoints | Active breakpoint list | All set breakpoints with conditions and status |
| Memory Map | Virtual memory layout | All mapped regions with permissions (RWX) |
| Symbols | Loaded modules/exports | DLLs, their exports, and base addresses |
| References | Search results | API references, string references, patterns |
| Call Stack | Function call history | Return addresses, calling functions |
| Log | Debug output | API traces, debug messages, plugin output |
Essential Keyboard Shortcuts
| Shortcut | Action | When to Use |
|---|---|---|
F2 | Toggle breakpoint | Set/remove breakpoint at cursor |
F7 | Step into | Follow CALL instructions into functions |
F8 | Step over | Execute function without entering it |
F9 | Run | Continue to next breakpoint |
Ctrl+F9 | Run until return | Execute until current function returns |
Ctrl+G | Go to address | Navigate to specific address or API name |
Space | Assemble | Patch the instruction at cursor |
Ctrl+F2 | Restart | Restart the debugging session |
Ctrl+P | Patches | View all applied patches |
Ctrl+B | Binary search | Search for byte patterns in memory |
Ctrl+N | Module symbols | Browse module exports/imports |
Complete Debugging Workflow
Step 1: Pre-Analysis Checks
Before loading in x64dbg, perform these checks:
# Check if DynamicBase (ASLR) is enabled using CFF Explorer or pestudio
# If enabled, disable it to get consistent addresses:
setdllcharacteristics.exe -d sample.exe
# Check architecture (32-bit vs 64-bit) to use correct debugger
# x32dbg for PE32, x64dbg for PE32+
Step 2: Load the Sample
- File > Open -- select the malware executable
- x64dbg breaks at the system entry point (in ntdll.dll)
- Press
F9to run to the program entry point (PE entry / CRT startup) - You are now at the beginning of the sample's code
Important: Some malware uses TLS callbacks that execute before the entry point. Enable Options > Preferences > Events > TLS Callbacks to catch these.
Step 3: Set Strategic Breakpoints
The fastest way to understand malware behavior is to set breakpoints on key Windows APIs. Use the command window at the bottom of the CPU tab:
; Set breakpoints using the command window (SetBPX command):
SetBPX CreateFileA
SetBPX CreateFileW
SetBPX WriteFile
SetBPX RegSetValueExA
SetBPX RegSetValueExW
SetBPX InternetConnectA
SetBPX HttpSendRequestA
SetBPX connect
SetBPX send
SetBPX VirtualAllocEx
SetBPX WriteProcessMemory
SetBPX CreateRemoteThread
SetBPX CreateProcessA
SetBPX CreateProcessW
API breakpoint categories for malware analysis:
| Goal | APIs to Monitor | What to Look For |
|---|---|---|
| File drops | CreateFileA/W, WriteFile, CopyFile | Dropped payloads, config files |
| Persistence | RegSetValueExA/W, CreateServiceA | Run keys, scheduled tasks |
| Network | InternetConnectA, HttpSendRequestA, connect, send | C2 URLs, beacons |
| Process injection | VirtualAllocEx, WriteProcessMemory, CreateRemoteThread | Injected code/DLLs |
| Execution | CreateProcessA/W, ShellExecuteA, WinExec | Child processes, commands |
| Privilege | AdjustTokenPrivileges, OpenProcessToken | Privilege escalation |
| Crypto | CryptEncrypt, CryptDecrypt, BCryptEncrypt | Data encryption |
Step 4: Run and Observe
- Press
F9to run - When a breakpoint hits, examine:
- Parameters: Check registers (x64: RCX, RDX, R8, R9) or stack (x86: [ESP+4], [ESP+8]...)
- Return address: Check the Call Stack tab to see which function made this call
- Context: Note the timestamp and sequence of events
- Press
F9to continue to next breakpoint - Build a timeline of observed API calls
Step 5: Examine Memory
When the malware allocates or writes memory:
- Note the address from
VirtualAllocreturn value (RAX/EAX) - In the Dump panel, press
Ctrl+Gand navigate to that address - Watch memory contents change after
WriteProcessMemory - Right-click > Follow in Disassembler if the memory contains executable code
- To dump the memory region to a file: right-click in Dump > Follow in Memory Map > right-click the region > Dump Memory to File
Memory Breakpoints for Unpacking
Memory breakpoints are essential for analyzing packers and self-modifying code:
- Identify the target region: After
VirtualAlloc, note the allocated address and size - Set memory breakpoint: In the Dump panel, select the memory range, right-click > Breakpoint > Memory, Write
- Execution breaks whenever that memory is written to
- After the write completes, change to Execute breakpoint: right-click > Breakpoint > Memory, Execute
- The next break reveals when the unpacked code starts running
Practical unpacking pattern:
1. Break on VirtualAlloc --> note returned address (e.g., 0x02750000)
2. Set Memory Write BP on 0x02750000
3. Run --> breaks when packer writes decrypted code
4. Remove write BP, set Memory Execute BP on same region
5. Run --> breaks when execution transfers to unpacked code
6. You are now at the Original Entry Point (OEP)
Working with ScyllaHide
ScyllaHide is an essential x64dbg plugin that hides the debugger from anti-debug checks by hooking OS functions at the kernel level.
Configuration
- Plugins > ScyllaHide > Options
- Enable all checkboxes in the first column for maximum protection:
- Hide from PEB (BeingDebugged, HeapFlags, NtGlobalFlag, StartupInfo, OsBuildNumber)
- NtSetInformationThread, NtSetInformationProcess
- NtQuerySystemInformation, NtQueryInformationProcess
- NtQueryObject, NtYieldExecution
- NtCreateThreadEx, OutputDebugStringA
- BlockInput, NtUserFindWindowEx
- NtUserBuildHwndList, NtUserQueryWindow
- NtSetDebugFilterState, NtClose
- Remove Debug Privileges
- Click OK (no restart needed)
Caution: Enabling all ScyllaHide options is generally safe, but in rare cases it may break aspects of the malware unrelated to debugger detection. If the sample behaves oddly with ScyllaHide enabled, try disabling options one at a time.
Configuring Exception Handling
Some malware uses exceptions for anti-debug. Configure x64dbg to pass them through:
- Options > Preferences > Exceptions
- Select the
00000000-FFFFFFFFfilter - Click "Do not break"
- Click Save
Note: x64dbg may not persist this setting across restarts. Re-apply before each debugging session.
Dumping Unpacked Code with Scylla
When malware unpacks itself in memory, use Scylla to dump the unpacked PE:
- Let the unpacking stub run until execution reaches the OEP
- Open Scylla: Plugins > Scylla
- Set OEP to the current instruction address (the original entry point)
- Click IAT Autosearch -- Scylla finds the Import Address Table
- Click Get Imports -- Scylla resolves all imported functions
- Review the imports -- remove any marked as invalid
- Click Dump -- save the unpacked binary to disk
- Click Fix Dump -- select the dumped file to repair its import table
- The resulting file can be analyzed statically in Ghidra, PEStudio, etc.
Building a Behavioral Timeline
As you debug, build a timeline documenting each significant action:
[T+0.0s] Entry point reached at 0x00401000
[T+0.1s] IsDebuggerPresent called -- returned 0 (ScyllaHide active)
[T+0.3s] VirtualAlloc(0, 0x10000, MEM_COMMIT, PAGE_EXECUTE_READWRITE) = 0x02750000
[T+0.5s] Decryption loop at 0x00401250 writes to 0x02750000
[T+0.8s] CreateFileA("C:\\Users\\Public\\updater.exe", GENERIC_WRITE)
[T+0.9s] WriteFile -- writes 45,056 bytes (dropped payload)
[T+1.1s] RegSetValueExA(HKCU\\...\\Run, "WindowsUpdate", "C:\\Users\\Public\\updater.exe")
[T+1.3s] InternetConnectA("1.234.27.146", 80)
[T+1.5s] HttpSendRequestA(POST /api/gate.php) -- C2 beacon
Practical Exercise
- Load a malware sample in x64dbg (use x32dbg for 32-bit samples)
- Enable ScyllaHide with all first-column checkboxes
- Configure exceptions to "Do not break"
- Set breakpoints on file, registry, network, and injection APIs
- Run the sample and document each API call with parameters
- When memory allocation is detected, set memory breakpoints to track writes
- If injection is found, dump the injected code from memory
- Build a complete behavioral timeline from your debug session
- Export patches if any were applied: Ctrl+P > Export
