Malware Unpacking With Hardware Breakpoints – Cobalt Strike Shellcode Loader

This post demonstrates how to extract and validate Cobalt Strike shellcode from a compiled EXE using x64dbg for dynamic debugging and Ghidra for static analysis, with emulation in Speakeasy and manual decoding in CyberChef. The walkthrough covers setting API breakpoints (VirtualAlloc/VirtualProtect), using hardware breakpoints to detect memory writes, locating the XOR decryption routine and 4-byte key in Ghidra, and recovering the unpacked shellcode and C2 indicators. #CobaltStrike #x64dbg

Keypoints

  • Sample is a compiled EXE that decodes embedded Cobalt Strike shellcode with a simple XOR loop using a 4-byte key and writes it to a VirtualAlloc buffer.
  • Use x64dbg to set API breakpoints on VirtualAlloc and VirtualProtect, execute until return to capture the allocated buffer address, and follow that address in the memory dump.
  • Create a hardware (data) breakpoint on the allocated buffer to detect when the shellcode is written; the buffer begins with 0xFC, a common shellcode marker.
  • Validate the buffer by following in the disassembler and emulating the saved memdump.bin with Speakeasy (speakeasy -t memdump.bin -r -a x64) to reveal C2 and HTTP indicators.
  • In Ghidra, locate the decryption loop by searching for the instruction bytes observed in the debugger, identify the XOR loop and the 4-byte key (32 2f 0d 96), and locate the encrypted blob via entropy view.
  • Manually decode the encrypted section in CyberChef using the recovered 4-byte XOR key to reconstruct the shellcode; use the decryption bytes to hunt for related samples and build a YARA rule.

MITRE Techniques

  • [T1055] Process Injection – Allocates memory, writes decoded shellcode, changes protections and executes it via CreateThread: [‘VirtualAlloc’ … ‘VirtualProtect’ … ‘CreateThread’]
  • [T1140] Deobfuscate/Decode Files or Information – Malware performs a looping XOR decode using a 4-byte key to reveal packed shellcode: [‘XOR’ instruction … ‘4-byte key’]
  • [T1071.001] Application Layer Protocol: Web Protocols – Shellcode contains HTTP-based C2 indicators including an IP, partial URL and User-Agent used for C2 communications: [‘C2 address of 116.62[.]138.47’, ‘partial url of /8yHd’, ‘User-Agent: Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; InfoPath.2; .NET CLR 2.0.50727)’]

Indicators of Compromise

  • [SHA256] sample identifier – 99986d438ec146bbb8b5faa63ce47264750a8fdf508a4d4250a8e1e3d58377fd
  • [C2 IP / URL] emulated shellcode network indicators – 116.62[.]138.47, partial path /8yHd
  • [HTTP User-Agent] C2 fingerprint – “Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; InfoPath.2; .NET CLR 2.0.50727)”
  • [Decryption key bytes] XOR key used in decoding – 32 2f 0d 96
  • [Saved dump / filenames] extracted shellcode file – memdump.bin; sample archive protected with password ‘infected’

Save and extract the provided sample, then open it in x64dbg and run to the entry point. Set API breakpoints on VirtualAlloc and VirtualProtect (bp VirtualAlloc, bp VirtualProtect) and continue execution. When VirtualAlloc returns, use “Execute Until Return” to capture the allocated buffer address in RAX, right-click the address and “Follow in Dump” to view the memory content.

Create a hardware (data) breakpoint on the first byte of the allocated buffer (Right Click → Breakpoint → Hardware, Access → Byte) so execution will pause when the unpacking routine writes into that region. Let the program continue until the hardware breakpoint triggers; you should observe the buffer filling and commonly the first byte 0xFC, indicating shellcode. Use “Follow in Disassembler” on the 0xFC to validate the bytes disassemble cleanly, then save the memory region to a file (e.g., memdump.bin).

Emulate memdump.bin with Speakeasy (speakeasy -t memdump.bin -r -a x64) to extract runtime indicators such as C2 IP, URL path and User-Agent. For static recovery, load the original EXE in Ghidra, copy the byte sequence of the suspected XOR loop observed in x64dbg (Right-Click → Binary → Edit to copy bytes), and search the program memory (Search → Memory, hex, All Blocks) to locate the decryption function. Inspect the decompiled for-loop to confirm the XOR operation and find where the 4-byte key (observed as 32 2f 0d 96) is passed (Show References To → follow param_3). Use Ghidra’s entropy view to find the high-entropy encrypted blob, copy the bytes (Copy Special → Byte String), and decode them in CyberChef with the recovered 4-byte XOR key to reconstruct the unpacked shellcode. Finally, use the decryption-loop bytes to hunt for similar samples (unpac.me, VT) and create a YARA rule based on the unique decoding sequence to detect related Cobalt Strike loaders.

Read more: https://embee-research.ghost.io/unpacking-malware-with-hardware-breakpoints-cobalt-strike/