How to Use Ghidra to Analyse Shellcode and Extract Cobalt Strike Command & Control Servers

How to Use Ghidra to Analyse Shellcode and Extract Cobalt Strike Command & Control Servers

Original text: “How to Use Ghidra to Analyse Shellcode and Extract Cobalt Strike Command & Control Servers”Matthew, Embee Research (Dec 08, 2023). Images reproduced verbatim with attribution captions.

Executive Summary

Cobalt Strike shellcode is one of the most widely deployed payloads in commodity malware campaigns, yet its analysis remains non-trivial for practitioners new to static reverse engineering. The shellcode uses API hashing to obscure every Windows API call it makes, meaning none of the resolved function names appear as readable strings in the binary. Without knowing how to trace these hash resolutions — either through Google lookups or a debugger — analysts cannot determine what the shellcode is doing or extract its embedded command-and-control server.

This walkthrough demonstrates a complete static-plus-dynamic methodology using Ghidra and x32dbg. Starting from a raw shellcode blob (SHA256: 26f9955137d96222533b01d3985c0b1943a7586c167eceeaa4be808373f7dd30), the guide covers: loading shellcode correctly into Ghidra, manually kicking off disassembly and decompilation, identifying the API hash-resolving routine, cross-referencing hash values with known lookup tables, and using x32dbg with BlobRunner to observe resolved function names and arguments at runtime — ultimately recovering the embedded C2 address. Bonus material covers the ROR13 hashing algorithm, the POP EBP trampoline pattern, and improving Ghidra’s decompiler output by retyping TEB32 and PEB structures.

Shellcode analysis workflow overview
Malware analysis workflow covering the stages from sample acquisition to C2 extraction. Source: original article.

Obtaining the Sample

The shellcode sample is available on Malware Bazaar (password: infected).

SHA256: 26f9955137d96222533b01d3985c0b1943a7586c167eceeaa4be808373f7dd30

Any Cobalt Strike or Metasploit shellcode file can be substituted — both share essentially the same structure, so the techniques below apply broadly.

Loading Shellcode Into Ghidra

Loading raw shellcode differs from loading a PE/EXE. When Ghidra presents the architecture dialog, select any option that specifies x86, 32, little. For Windows shellcode, the Visual Studio compiler option is ideal, but for most shellcode the important settings are architecture (x86), word-size (32-bit), and endianness (little-endian). Accept all remaining defaults.

Ghidra architecture selection dialog for shellcode
Selecting x86, 32-bit, little-endian in the Ghidra language selection dialog. Source: original article.

Disassembling the Shellcode

After the initial analysis pass, Ghidra will not disassemble the shellcode automatically — there are no file headers to indicate where code starts. Fix this by selecting the very first byte and pressing D (or right-clicking and choosing Disassemble). This instructs Ghidra to start decoding bytes as x86 instructions from that address.

Ghidra disassemble option on first byte
Right-click context menu showing the Disassemble option on the first byte of the shellcode. Source: original article.

After disassembling, the left-hand listing panel is populated with instructions. The right-hand decompiler may still show nothing at this stage.

Ghidra listing view after disassembly
The Ghidra listing panel populated with disassembled x86 instructions after pressing D on the first byte. Source: original article.
Ghidra primary window with code populated
Main Ghidra window showing the populated listing panel with shellcode instructions. Source: original article.

Defining a Function to Enable the Decompiler

The decompiler requires a defined function boundary to generate C pseudocode. Right-click on the first byte and choose Create Function, or press F as a shortcut. Once a function is defined at offset 0x0, the decompiler panel on the right will populate.

Ghidra Create Function context menu
Right-click context menu showing the Create Function option on the first byte of shellcode. Source: original article.
Ghidra decompiler view populated after function definition
The decompiler panel now shows C pseudocode after a function was defined at the start of the shellcode. Source: original article.

Locating API Function Calls

Shellcode almost universally resolves Windows API functions by hashing their names at runtime rather than linking to them statically. This means no readable function names appear anywhere in the binary. All calls go through a hash-resolution routine, and the argument to each call is an integer hash value rather than a pointer to a name string.

Clicking into the first function call — FUN_0000008f — reveals where these hashes are used.

Ghidra showing first function call FUN_0000008f
The first function call in the shellcode, FUN_0000008f, which contains the API hash resolution logic. Source: original article.
Ghidra with API hash values highlighted
Two API hash values visible inside FUN_0000008f. Only those passed as arguments to a function are genuine hashes; the first hex-like value is hex-encoded text. Source: original article.
Ghidra code pointer reference for API hash call
The decompiler showing a code * reference (unaff_retaddr) used as the target for API calls resolved by hash. Source: original article.

In the disassembly view, the pattern is: a PUSH <hash_value> instruction immediately followed by a CALL RBP. The called routine receives the hash and returns the resolved function address.

Ghidra PUSH hash CALL RBP pattern for API resolution
Disassembly view showing the PUSH <hash> / CALL RBP pattern used by Cobalt Strike shellcode to invoke the hash resolver. Source: original article.

Resolving Hashes With Google

For widely-used shellcode frameworks such as Cobalt Strike and Metasploit, hash values have been published in public lookup tables. Searching the hash directly often returns the matching function name. Searching for 0x726774c confirms it resolves to LoadLibraryA.

Google search confirming hash 0x726774c resolves to LoadLibraryA
Search results confirming that API hash 0x726774c corresponds to the LoadLibraryA Windows API function. Source: original article.

After confirming the name, add a Ghidra comment to annotate the hash with the resolved function.

Ghidra comment added for LoadLibraryA hash
A comment annotating the 0x726774c hash in Ghidra with the resolved name LoadLibraryA. Source: original article.

The same approach works for 0xa779563a, which resolves to InternetOpenA.

Google search confirming hash 0xa779563a resolves to InternetOpenA
Search results confirming that API hash 0xa779563a corresponds to InternetOpenA. Source: original article.
Ghidra comment added for InternetOpenA hash
A Ghidra comment annotating hash 0xa779563a with the resolved name InternetOpenA. Source: original article.

These two resolved functions — LoadLibraryA and InternetOpenA — align with what SpeakEasy emulation reports when run against the same sample.

SpeakEasy emulation output showing LoadLibraryA and InternetOpenA
SpeakEasy dynamic emulation output confirming the first two resolved API calls match the Google-based hash resolution. Source: original article.

Note: The Hex-Encoded Library Name

One of the values that initially looks like an API hash is actually the DLL name wininet encoded as a hex string — the argument to LoadLibraryA. Recognizing this avoids a fruitless hash lookup.

Hex-encoded wininet string in shellcode
The hex-encoded wininet library name that is passed as an argument to LoadLibraryA, distinguishable from actual API hash values. Source: original article.
Ghidra decompiler showing hex-encoded library name
Decompiler output showing the hex value that decodes to the wininet DLL name. Source: original article.

Resolving API Hashes With a Debugger (x32dbg)

The Google lookup method fails for custom, modified, or undocumented hash algorithms. The universal fallback is to use a debugger: run the shellcode up to the point where each hash resolves, and read the resulting function address directly from a register.

The key is identifying where the resolved hash is finally executed. Inside FUN_0000008f, the resolved address ends up in EAX, and execution transfers there via a JMP EAX instruction.

Ghidra decompiler showing JMP EAX resolved function call
The decompiler view showing the code * value that corresponds to the JMP EAX at the end of the hash-resolution function. Source: original article.

The Graph View makes the JMP EAX location easier to find: the majority of the function handles hash computation, and the instruction that dispatches to the resolved function sits at the very end of the graph.

Ghidra graph view of initial hash resolution function
Ghidra Graph View showing the control-flow structure of FUN_0000008f, with the hash-resolution loop dominating and the JMP EAX at the tail. Source: original article.
Ghidra graph view zoomed showing JMP EAX at function end
Zoomed graph view showing the JMP EAX dispatch instruction at the end of the hash-resolution function. Source: original article.

Loading Shellcode With BlobRunner

To run the shellcode under a debugger, use BlobRunner from OALabs. BlobRunner loads the shellcode blob into memory, prints the load address, and waits for a keypress — giving time to attach a debugger before execution begins. Download the 32-bit version (not blobrunner64) from the OALabs GitHub releases page.

BlobRunner GitHub release page from OALabs
The OALabs BlobRunner GitHub releases page. Download the 32-bit version for 32-bit shellcode analysis. Source: original article.

Transfer BlobRunner to a Windows analysis VM and run it against the shellcode file:

blobrunner.exe <shellcode_filename>
Running BlobRunner against shellcode file in command prompt
Command prompt showing BlobRunner invoked with the shellcode filename as its argument. Source: original article.
BlobRunner output showing shellcode loaded at 0x001e0000
BlobRunner output indicating the shellcode was loaded at address 0x001e0000 and is waiting for a keypress. Source: original article.

Open x32dbg, go to File → Attach, and select the blobrunner process.

x32dbg File Attach menu selecting blobrunner process
The x32dbg Attach dialog used to attach the debugger to the waiting blobrunner process. Source: original article.

With the debugger attached, set a breakpoint at the shellcode base address reported by BlobRunner, and a second breakpoint at the JMP EAX location (base + 0x86 in this sample):

bp 0x001e0000
bp 0x001e0000 + 0x86
x32dbg breakpoint set at shellcode base address 0x001e0000
x32dbg command bar used to set a breakpoint at the shellcode base address returned by BlobRunner. Source: original article.
x32dbg breakpoint set at JMP EAX offset 0x86
x32dbg breakpoint set at base + 0x86, the offset of the JMP EAX dispatch instruction within the hash-resolution function. Source: original article.

Switch back to the BlobRunner window and press any key to start shellcode execution.

BlobRunner waiting for keypress to begin shellcode execution
BlobRunner prompt waiting for a keypress before handing control to the loaded shellcode. Source: original article.
x32dbg breakpoint hit at shellcode entry point
x32dbg stopped at the first breakpoint at the shellcode entry point 0x001e0000. Source: original article.

Press F7 twice to step into FUN_0000008f, then set breakpoints on the first two CALL EBP instructions inside that function.

x32dbg breakpoints set on first two Call EBP instructions
Breakpoints placed on the two CALL EBP instructions inside the hash-resolution function. Source: original article.

Observing Hash Values and Resolved Names in Memory

Press F9 to continue. x32dbg stops at the first CALL EBP. The stack window shows hash argument 0x726774c being passed to the hash resolver.

x32dbg stack showing hash value 0x726774c at first Call EBP
x32dbg stack window at the first CALL EBP breakpoint, showing hash value 0x726774c on the stack. Source: original article.

Press F9 again to continue to the JMP EAX breakpoint. The registers panel on the right shows EAX annotated by x32dbg with the resolved name LoadLibraryA.

x32dbg JMP EAX breakpoint with EAX showing LoadLibraryA
x32dbg stopped at the JMP EAX instruction. The EAX register is annotated with LoadLibraryA, confirming the hash resolution. Source: original article.
x32dbg registers window showing LoadLibraryA decoded value in EAX
Zoomed registers panel confirming that EAX holds the address of LoadLibraryA. Source: original article.
x32dbg stack window showing wininet argument passed to LoadLibraryA
The stack window shows the wininet string passed as an argument to LoadLibraryA, confirming the DLL being loaded. Source: original article.

Decoding Additional API Hashes

Continue pressing F9 to cycle through subsequent hash resolutions. The second CALL EBP corresponds to hash 0xa779563a.

x32dbg stopped at second Call EBP for InternetOpenA hash
x32dbg at the second CALL EBP breakpoint, with hash 0xa779563a visible on the stack. Source: original article.
x32dbg stack showing InternetOpenA hash 0xa779563a
Stack view confirming hash 0xa779563a is passed to the hash-resolution routine. Source: original article.
x32dbg JMP EAX confirming 0xa779563a resolves to InternetOpenA
At the JMP EAX breakpoint, EAX is annotated with InternetOpenA, confirming the second hash resolution. Source: original article.

The next CALL EBP is at offset 0xCA from the shellcode base and carries hash 0xC69F8957.

x32dbg Call EBP at offset 0xCA with hash 0xC69F8957
x32dbg at the CALL EBP at offset 0xCA. Hash 0xC69F8957 is visible on the stack. Source: original article.
x32dbg showing InternetConnectA resolved and C2 server IP 195.211.98.91
x32dbg confirming that hash 0xC69F8957 resolves to InternetConnectA, with the C2 address 195.211.98[.]91 visible in the arguments. Source: original article.

This is where the C2 server is exposed: the argument to InternetConnectA is the remote host 195.211.98[.]91.

Cross-reference the finding back in Ghidra: press G to jump to address 0xCA and verify the hash value is annotated correctly.

Ghidra navigation dialog jumping to offset 0xCA
Ghidra Go To Address dialog used to navigate to offset 0xCA to verify the InternetConnectA hash. Source: original article.
Ghidra showing hash value at offset 0xCA
The Ghidra listing at offset 0xCA showing the InternetConnectA hash value 0xC69F8957. Source: original article.
Ghidra comment added for InternetConnectA at offset 0xCA
Ghidra comment annotating the hash at offset 0xCA with the resolved name InternetConnectA. Source: original article.

Repeating this process for every CALL EBP in the shellcode reveals all API calls and their arguments. The procedure can also be automated with x32dbg conditional breakpoints that log EAX on each hit without stopping execution.

x32dbg conditional breakpoint setup for automated API hash logging
A conditional breakpoint in x32dbg configured to log the EAX value at the JMP EAX location without pausing execution each time. Source: original article.
Complete API hash resolution list from x32dbg
The full list of resolved API hashes and their corresponding Windows function names logged by x32dbg. Source: original article.

The CALL EBP / POP EBP Trampoline Pattern

Understanding why the shellcode uses CALL EBP rather than a direct CALL instruction requires examining the very start of FUN_0000008f: it begins with a POP EBP.

Ghidra showing POP EBP at the start of FUN_0000008f
The first instruction of FUN_0000008f is POP EBP, which captures the return address pushed by the caller. Source: original article.

When the parent function calls FUN_0000008f, the CPU pushes the return address (the next instruction after the call) onto the stack. The POP EBP at the start of FUN_0000008f immediately removes that return address into EBP — so EBP now holds the address of the next instruction in the outer function, i.e., shellcode base + 0x6. Every subsequent CALL EBP is therefore a call back into the shellcode at that fixed offset, which is where the hash-resolution routine can always be re-entered without a hardcoded address.

x32dbg showing EBP value at base+0x6 during Call EBP
x32dbg register panel showing EBP = shellcode_base + 0x6 during a CALL EBP, confirming it is the hash-resolver re-entry point. Source: original article.
Ghidra instruction at base+0x6 next instruction after FUN_0000008f call
Ghidra listing showing that the instruction at shellcode base + 0x6 is the next instruction after the call to FUN_0000008f, confirming the EBP trampoline. Source: original article.

Identifying the Hashing Algorithm in the Graph View

Returning to the Ghidra Graph View for the hash-resolution function reveals a loop block — a small node with a back-edge — indicating that a sequence of operations repeats. Loop structures in shellcode hash functions are a reliable indicator of where the actual hash computation takes place.

Ghidra graph view loop block indicating the API hashing routine
Ghidra Graph View showing a small loop block inside the hash-resolution function. The loop back-edge indicates repeated hash computation steps. Source: original article.

Zooming into that loop block exposes the instruction ROR edi, 0xd. The value 0xd is 13 in decimal, which is the signature of the ROR13 hashing algorithm used by both Cobalt Strike and Metasploit shellcode.

Ghidra showing ROR edi 0xd instruction in the hash loop
The ROR edi, 0xd (rotate right by 13) instruction inside the loop block, identifying the ROR13 hashing algorithm. Source: original article.

Searching for “ror13 hashing” surfaces published pseudocode and pre-computed hash tables from vendors including Mandiant. These resources confirm the algorithm and may provide complete hash-to-function mappings for Cobalt Strike payloads. You may also encounter public blog posts demonstrating how API hashing can be modified to bypass AV detections.

Advanced: Windows Data Structures (TEB, PEB)

The first lines of the hash-resolution function access the Thread Environment Block (TEB) via the segment register FS to obtain a list of all modules currently loaded in the process. It then enumerates each module’s export table and hashes every exported function name until the target hash matches.

Ghidra showing Thread Environment Block access via FS register
The initial lines of the hash-resolution function accessing the TEB structure via the FS segment register offset to enumerate loaded DLLs. Source: original article.

The access pattern follows a chain of offsets well-documented by Nviso and others: FS:[0x30] reaches the PEB, +0xC gives the PEB_LDR_DATA pointer, and +0x14 reaches the InMemoryOrderModuleList.

Nviso diagram showing TEB PEB data structure resolution chain
Diagram illustrating the TEB → PEB → PEB_LDR_DATA → InMemoryOrderModuleList traversal used to enumerate loaded DLLs. Source: original article.

Ghidra can be improved significantly by retyping the opaque offset variables as proper Windows structures. Select the variable corresponding to FS:[0x30], right-click, and choose Retype Variable. Declare it as TEB32 * (you may need to download and import the TEB32 header file from the AllsafeCyberSecurity Ghidra data-types repository).

Ghidra retype variable context menu for TEB32 pointer
Right-click context menu in Ghidra showing the Retype Variable option used to declare the FS-relative variable as TEB32 *. Source: original article.
Ghidra TEB32 pointer declaration in retype dialog
Ghidra retype dialog with TEB32 * typed in, associating the variable with the Thread Environment Block structure. Source: original article.

Then retype the ProcessEnvironmentBlock field as PEB * to continue resolving nested structure names.

Ghidra PEB pointer declaration for ProcessEnvironmentBlock
Retype dialog showing PEB * being applied to the ProcessEnvironmentBlock variable. Source: original article.
Ghidra decompiler with improved readability after TEB PEB structure retyping
The decompiler output after retyping both TEB32 * and PEB * — named structure fields replace raw numeric offsets, making the module-enumeration logic immediately readable. Source: original article.

After both retyping operations the decompiler replaces raw numeric offsets with named structure fields, making it immediately apparent that the function is walking the module list to locate and hash exported function names.

Key Takeaways

  • Raw shellcode has no PE headers, so Ghidra will not auto-disassemble it — select the first byte and press D, then define a function with F to enable the decompiler.
  • Cobalt Strike and Metasploit shellcode resolve all Windows API calls via ROR13 hashing; no readable function names appear statically in the binary.
  • Google can quickly resolve common ROR13 hashes for well-known frameworks; for custom or modified hash logic, a debugger is required.
  • BlobRunner (OALabs) is the easiest way to load a shellcode blob under x32dbg: it prints the load address and waits, allowing breakpoints to be set before execution.
  • Setting a breakpoint at the JMP EAX dispatch point of the hash-resolution routine exposes both the resolved function name (in EAX) and its arguments (on the stack), including embedded C2 addresses.
  • The POP EBP trampoline at the start of the resolver function is a position-independent technique that stores the shellcode re-entry address in EBP without hardcoding any address.
  • Retyping TEB32 * and PEB * in Ghidra dramatically improves decompiler readability by replacing raw offset arithmetic with named Windows structure fields.

Defensive Recommendations

  • YARA rules for ROR13 patterns: the ROR edi, 0xd instruction sequence combined with nearby CALL EBP instructions is a reliable signature for Cobalt Strike/Metasploit shellcode; include it in your YARA rule set.
  • Detect API-hash resolution at load time: tools such as pe-sieve and Moneta can scan process memory for shellcode with characteristic ROR13 loops, flagging injection even before C2 contact.
  • Block or alert on shellcode loaders: BlobRunner and similar tools (donut, sRDI) are dual-use utilities nearly always present in adversary pre-exploitation; endpoint detection should alert on their process names and command-line patterns.
  • Network detection for Cobalt Strike C2: published Cobalt Strike JA3/JA3S fingerprints, HTTP malleable-profile signatures, and JARM scans can identify infrastructure like 195.211.98[.]91 before initial access.
  • Emulate first, debug second: running shellcode through SpeakEasy or similar emulators provides a fast first-pass resolution of API calls, reducing the need for interactive debugging in many cases.
  • Maintain an internal ROR13 hash database: pre-compute and store the ROR13 hash for every export in a standard Windows SDK build; this makes batch resolution of unknown samples trivial without external lookups.
  • Restrict shellcode execution surfaces: enforce memory protections (W^X, ACG, CIG) and use AMSI or ETW-based hooks to log and block shellcode injection before the resolver function can enumerate the module list.

Conclusion

Analysing Cobalt Strike shellcode in Ghidra becomes straightforward once the three foundational steps are clear: manually kick-off disassembly on the first byte, define a function to enable the decompiler, and locate the hash-resolution routine by searching for the CALL EBP pattern. From there, hash resolution is either fast (Google lookup for common frameworks) or reliable (x32dbg + BlobRunner for any custom implementation). The ROR13 algorithm’s distinctive ROR edi, 0xd loop makes it identifiable at a glance, and the PEB/TEB module-enumeration chain is a well-documented technique that Ghidra can annotate precisely with the right structure definitions. Combined, these techniques expose every API call the shellcode makes and surface embedded indicators — including C2 server addresses — with no need for a live network connection.

Original text: “How to Use Ghidra to Analyse Shellcode and Extract Cobalt Strike Command & Control Servers” by Matthew at Embee Research.

Comments are closed.