Cobalt Strike Staging and Extracting Configuration Information

This post covers how Cobalt Strike staging works, how to replicate a staging request to obtain beacon shellcode, and then how to extract the Cobalt Strike config from the shellcode.

How Cobalt Strike Staging Works

By default Cobalt Strike exposes its stager shellcode via a valid checksum8 request (the same request format used in the Metasploit staging process). To determine if a web server request is a valid staging request, Cobalt Strike does the following:

  1. Check that the length of the URI is 4 characters or greater

  2. Remove any occurrences of the "/" character from the URI

  3. Convert each character in the URI to it's integer representation and add up the results

  4. Divide the result by 256 and return the remainder

  5. Check the remainder as follows:

    1. If the remainder == 92 then the request is a valid x86 staging request

    2. if the remainder == 93 AND the request is 4 alphanumeric characters, then the request is a valid x64 request

It's worth noting that Cobalt Strike includes a number of staging configuration options via its malleable C2 profile. These settings can change how staging behaves, and can also disable staging completely. This process outlined in this section is the default Cobalt Strike staging process.

A python implementation to generate a valid Cobalt Strike checksum8 request is shown below:

#!/usr/bin/python3
import rstr
def generate_checksum8_uri(arch):
# x86 = 92
# x64 = 93
value = 0
while value != arch:
rand = rstr.xeger(r'[A-Za-z0-9]{4}')
value = (sum([ord(ch) for ch in rand]) % 0x100)
return "/" + str(rand)
x86_uri = generate_checksum8_uri(92)
x64_uri = generate_checksum8_uri(93)
print("Generated x86 URI: %s" %(x86_uri))
print("Generated x64 URI: %s" %(x64_uri))

If the request is a valid staging request, then Cobalt Strike returns a x86/x64 shellcode blob, which under normal circumstances is typically loaded into memory by the stager and executed on the victim endpoint.

Now that we know the default 'verification' process for staging requests used by Cobalt Strike, we can use this information to attempt to retrieve the shellcode from suspected Cobalt Strike servers. The Python code below shows how to do this:

#!/usr/bin/python3
import requests
import rstr
import sys
import urllib3
if len(sys.argv) < 2:
print("Usage: %s http[s]://<server_address>" % (sys.argv[0]))
sys.exit(1)
url = sys.argv[1]
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
headers = {"User-Agent":""}
def generate_checksum8_uri(arch):
# x86 = 92
# x64 = 93
value = 0
while value != arch:
rand = rstr.xeger(r'[A-Za-z0-9]{4}')
value = (sum([ord(ch) for ch in rand]) % 0x100)
return "/" + str(rand)
def get_shellcode(url,uri):
f_url = url + uri
try:
resp = requests.get(f_url, timeout=5, headers=headers, verify=False)
except requests.exceptions.RequestException as e:
print('[!] Connection error %s' % (e))
sys.exit(1)
if(resp.status_code==200):
print('[+] Got response from %s' % (url))
with open("/tmp/out","wb") as output:
output.write(resp.content)
print('[+] Response written to /tmp/out, response might be shellcode')
else:
print('[!] Server returned non-200 response')
sys.exit(1)
print('[+] Generating x86 check8 uri')
# Just x86 for this example
uri = generate_checksum8_uri(92)
print('[+] Making request to suspected C2 server')
get_shellcode(url,uri)

If a server returns shellcode as the result of a checksum8 request, we can not only conclude that the server is likely to be a Cobalt Strike server, but we can also extract additional configuration information from the shellcode. Below is an example of the above script working as intended and successfully extracting shellcode from a Cobalt Strike server:

It's worth noting that interacting directly with suspected attacker infrastructure is typically a bad idea as it may tip off an attacker to your presence.

Emulating the Shellcode

Now that we have the shellcode blob extracted, we want to extract as much information as possible to learn more about the configuration of the Cobalt Strike server. This may allow us to identify relevant indicators of compromise, and may also serve as a mechanism to track a threat actor through their Cobalt Strike configuration.

There are a number of ways to emulate shellcode including SCDBG, JMP2IT and Shellcode2EXE. However, my favourite tools to use is FireEye's excellent SpeakEasy emulation framework:

We could either implement the SpeakEasy python library to extend our current Python code, or we can run the standalone "run_speakeasy" python script included in the repository:

[[email protected] speakeasy]# python3 run_speakeasy.py -t /tmp/out -r -a x86
...
0x93ad: 'kernel32.GetProcAddress(0x7bc00000, "HttpSendRequestA")' -> 0xfeee00a6
0x93ad: 'kernel32.GetProcAddress(0x7bc00000, "InternetOpenA")' -> 0xfeee00a7
0x93ad: 'kernel32.GetProcAddress(0x7bc00000, "InternetCloseHandle")' -> 0xfeee00a8
0x93ad: 'kernel32.GetProcAddress(0x7bc00000, "InternetQueryOptionA")' -> 0xfeee00a9
...
0x564e9: 'advapi32.GetUserNameA("speakeasy_user", 0x1203ecc)' -> 0x1
0x564f9: 'kernel32.GetComputerNameA(0x3fb1b4, 0x1203ecc)' -> 0x1
0x525d0: 'ws2_32.WSAStartup(0x202, 0x1203d00)' -> 0x0
0x5261e: 'ws2_32.gethostname(0x3fb3b4, 0x100)' -> 0x0
0x5262b: 'ws2_32.gethostbyname("speakeasy_host")' -> 0x3fb4c0
...
-> 0x51ff2: 'wininet.InternetOpenA('Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; Trident/6.0)', 0x0, 0x0, 0x0, 0x0)' -> 0x1c4
0x5200d: 'wininet.InternetSetOptionA(0x1c4, 0x5, 0x1203ef0, 0x4)' -> 0x1
0x5201d: 'wininet.InternetSetOptionA(0x1c4, 0x6, 0x1203ef0, 0x4)' -> 0x1
...
0x5200d: 'wininet.InternetSetOptionA(0x1f4, 0x5, 0x1203ef0, 0x4)' -> 0x1
0x5201d: 'wininet.InternetSetOptionA(0x1f4, 0x6, 0x1203ef0, 0x4)' -> 0x1
-> 0x52039: 'wininet.InternetConnectA(0x1f4, '<C2_SERVER>', 0x50, 0x0, 0x0, 0x3, 0x0, 0x82c40)' -> 0x1f8
0x644e2: 'kernel32.HeapAlloc(0x3f8200, 0x0, 0x10)' -> 0x7e5020
0x644e2: 'kernel32.HeapAlloc(0x3f8200, 0x0, 0x6c00)' -> 0x39000
0x68153: 'kernel32.GetLastError()' -> 0x0
...
0x68000: 'kernel32.TlsGetValue(0x0)' -> 0xfeee00ac
0x681bd: 'kernel32.SetLastError(0x0)' -> None
-> 0x524b9: 'wininet.HttpOpenRequestA(0x1f8, 'GET", "/__utm.gif', 0x0, 0x0, 0x1203ec0, "INTERNET_FLAG_DONT_CACHE | INTERNET_FLAG_KEEP_CONNECTION | INTERNET_FLAG_NO_COOKIES | INTERNET_FLAG_NO_UI | INTERNET_FLAG_RELOAD", 0x82c40)' -> 0x1fc
...

Potentially interesting API calls are marked with "->" at the start of each line

Using the "run_speakeasy" tool to emulate the shellcode and allowing it to run for a few moments reveals some interesting information about the shellcode behaviour, such as:

  • Resolving a number of APIs using the GetProcAddress API call

  • Gathering information about the host by using the GetUserNameA and GetHostByName API Calls

  • Setting up various networking API calls:

    • wininet.InternetOpenA - which gives us the user agent used during beaconing

    • wininet.InternetConnectA - which gives us the address of the C2 server used for beaconing

    • wininet.HttpOpenRequestA - which gives us the URI used for beaconing

After a few moments we can see that the emulation enters a loop where it attempts to set up a network connection to the C2 server to enable command and control activity. There's a lot of valuable information that can be gained by just examining that API calls made by the shellcode, but to really get the most valuable information we want to extract the Cobalt Strike config.

Extracting the Config

By default Cobalt Strike beacons contain configuration information to specify how they should behave. For example, a beacon configuration will specify the C2 servers to communicate with, how often to connect to the servers, what URI to use during beaconing, and other information such as how to inject into other processes and even the subscription watermark of the Cobalt Strike license.

There are a few different Config Parsers floating around online, but one of the best one's was created by Sentinal-One and is available here:

Inspecting the parser code we can see that it looks for one of three byte patterns in order to identify the presence of a Cobalt Strike config. If any of the byte patterns are found, then the parser will attempt to decode and print the configuration information of the Cobalt Strike beacon. The byte patterns that the parser looks for are:

START_PATTERNS = {
3: b'\x69\x68\x69\x68\x69\x6b..\x69\x6b\x69\x68\x69\x6b..\x69\x6a',
4: b'\x2e\x2f\x2e\x2f\x2e\x2c..\x2e\x2c\x2e\x2f\x2e\x2c..\x2e'
}
START_PATTERN_DECODED = b'\x00\x01\x00\x01\x00\x02..\x00\x02\x00\x01\x00\x02..\x00'

The first two patterns reflect the two different XOR keys used in version 3 (0x69) and version 4 (0x2e).

Running the parser over the shellcode we extracted from the Cobalt Strike server results in the following message:

It looks like there is no configuration block in our shellcode, or perhaps it's encrypted and is decrypted and loaded into memory when the shellcode runs? To test this hypothesis we could manually inspect the shellcode to try and identify the decryption routine, or we could use the SpeakEasy framework again to do the heavy lifting. Running the run_speakeasy tool again and using the "-d" option allows us to save a copy of the memory dump after emulation:

[[email protected] speakeasy]# python3 run_speakeasy.py -t /tmp/out -r -a x86 -d /tmp/output.zip
0x98ad: 'kernel32.VirtualAlloc(0x0, 0x3d000, 0x3000, "PAGE_EXECUTE_READWRITE")' -> 0x50000
0x92e1: 'kernel32.LoadLibraryA("KERNEL32.dll")' -> 0x77000000
0x93ad: 'kernel32.GetProcAddress(0x77000000, "GetThreadContext")' -> 0xfeee0000
<SNIP>
* User exited
Traceback (most recent call last):
File "_ctypes/callbacks.c", line 234, in 'calling callback function'
File "/tmp/speakeasy/speakeasy/common.py", line 81, in _wrap_code_cb
def _wrap_code_cb(self, emu, addr, size, ctx=[]):
KeyboardInterrupt
* Finished emulating
* Saving memory dump archive to /tmp/output.zip

Unzipping the resulting ZIP file shows in a number of files relating to the emulation of the shellcode. If we inspect the emu.shellcode.XXX.mem file we can see that it contains an embedded PE file:

To explore this PE file further, we could carve it out of this file and load it into our favourite static/dynamic analysis tools, but to keep to the purpose of this blog post we can work under the assumption that during the emulation process this PE file was decrypted and loaded into memory, and is likely to contain the Cobalt Strike configuration information. To check this assumption we can re-run the Sentinal-One parser over this file:

[[email protected] CobaltStrikeParser]# python3 parse_beacon_config.py /tmp/output/emu.shellcode.71244efa5322cebbff97c7bbcc54c37a9bbga516fee0a563.0x1000.mem
BeaconType - HTTP
Port - 80
SleepTime - 60000
MaxGetSize - 1048576
Jitter - 0
MaxDNS - Not Found
C2Server - <c2 server>,/__utm.gif
UserAgent - Not Found
HttpPostUri - /submit.php
Malleable_C2_Instructions - Empty
HttpGet_Metadata - Not Found
HttpPost_Metadata - Not Found
HttpPost_Metadata - Not Found
PipeName - Not Found
DNS_Idle - Not Found
DNS_Sleep - Not Found
SSH_Host - Not Found
SSH_Port - Not Found
SSH_Username - Not Found
SSH_Password_Plaintext - Not Found
SSH_Password_Pubkey - Not Found
HttpGet_Verb - GET
HttpPost_Verb - POST
HttpPostChunk - 0
Spawnto_x86 - %windir%\syswow64\rundll32.exe
Spawnto_x64 - %windir%\sysnative\rundll32.exe
CryptoScheme - 0
Proxy_Config - Not Found
Proxy_User - Not Found
Proxy_Password - Not Found
Proxy_Behavior - Use IE settings
Watermark - 12345678
bStageCleanup - False
bCFGCaution - False
KillDate - 0
bProcInject_StartRWX - True
bProcInject_UseRWX - True
bProcInject_MinAllocSize - 0
ProcInject_PrependAppend_x86 - Empty
ProcInject_PrependAppend_x64 - Empty
ProcInject_Execute - CreateThread
SetThreadContext
CreateRemoteThread
RtlCreateUserThread
ProcInject_AllocationMethod - VirtualAllocEx
bUsesCookies - True
HostHeader -

As we can see from the output above, the parser has successfully identified and extracted the Cobalt Strike config information. This config contains a lot of valuable information that allows us to not only determine how the beacon will behave once active on a victim machine, but also information like the Watermark which is supposed to be unique to each Cobalt Strike licence.

Most threat actors either use stolen/cracked versions of Cobalt Strike, or simply patch out the watermark value to disrupt attribution attempts.

References