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. 1.
    Check that the length of the URI is 4 characters or greater
  2. 2.
    Remove any occurrences of the "/" character from the URI
  3. 3.
    Convert each character in the URI to it's integer representation and add up the results
  4. 4.
    Divide the result by 256 and return the remainder
  5. 5.
    Check the remainder as follows:
    1. 1.
      If the remainder == 92 then the request is a valid x86 staging request
    2. 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:
1
#!/usr/bin/python3
2
​
3
import rstr
4
​
5
def generate_checksum8_uri(arch):
6
# x86 = 92
7
# x64 = 93
8
value = 0
9
while value != arch:
10
rand = rstr.xeger(r'[A-Za-z0-9]{4}')
11
value = (sum([ord(ch) for ch in rand]) % 0x100)
12
​
13
return "/" + str(rand)
14
​
15
x86_uri = generate_checksum8_uri(92)
16
x64_uri = generate_checksum8_uri(93)
17
​
18
print("Generated x86 URI: %s" %(x86_uri))
19
print("Generated x64 URI: %s" %(x64_uri))
Copied!
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:
1
#!/usr/bin/python3
2
​
3
import requests
4
import rstr
5
import sys
6
import urllib3
7
​
8
if len(sys.argv) < 2:
9
print("Usage: %s http[s]://<server_address>" % (sys.argv[0]))
10
sys.exit(1)
11
​
12
url = sys.argv[1]
13
​
14
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
15
headers = {"User-Agent":""}
16
​
17
def generate_checksum8_uri(arch):
18
# x86 = 92
19
# x64 = 93
20
value = 0
21
while value != arch:
22
rand = rstr.xeger(r'[A-Za-z0-9]{4}')
23
value = (sum([ord(ch) for ch in rand]) % 0x100)
24
​
25
return "/" + str(rand)
26
​
27
def get_shellcode(url,uri):
28
f_url = url + uri
29
try:
30
resp = requests.get(f_url, timeout=5, headers=headers, verify=False)
31
except requests.exceptions.RequestException as e:
32
print('[!] Connection error %s' % (e))
33
sys.exit(1)
34
​
35
if(resp.status_code==200):
36
print('[+] Got response from %s' % (url))
37
with open("/tmp/out","wb") as output:
38
output.write(resp.content)
39
print('[+] Response written to /tmp/out, response might be shellcode')
40
else:
41
print('[!] Server returned non-200 response')
42
sys.exit(1)
43
​
44
print('[+] Generating x86 check8 uri')
45
# Just x86 for this example
46
uri = generate_checksum8_uri(92)
47
​
48
print('[+] Making request to suspected C2 server')
49
get_shellcode(url,uri)
Copied!
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:
GitHub - mandiant/speakeasy: Windows kernel and user mode emulation.
GitHub
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:
1
[[email protected] speakeasy]# python3 run_speakeasy.py -t /tmp/out -r -a x86
2
...
3
0x93ad: 'kernel32.GetProcAddress(0x7bc00000, "HttpSendRequestA")' -> 0xfeee00a6
4
0x93ad: 'kernel32.GetProcAddress(0x7bc00000, "InternetOpenA")' -> 0xfeee00a7
5
0x93ad: 'kernel32.GetProcAddress(0x7bc00000, "InternetCloseHandle")' -> 0xfeee00a8
6
0x93ad: 'kernel32.GetProcAddress(0x7bc00000, "InternetQueryOptionA")' -> 0xfeee00a9
7
...
8
0x564e9: 'advapi32.GetUserNameA("speakeasy_user", 0x1203ecc)' -> 0x1
9
0x564f9: 'kernel32.GetComputerNameA(0x3fb1b4, 0x1203ecc)' -> 0x1
10
0x525d0: 'ws2_32.WSAStartup(0x202, 0x1203d00)' -> 0x0
11
0x5261e: 'ws2_32.gethostname(0x3fb3b4, 0x100)' -> 0x0
12
0x5262b: 'ws2_32.gethostbyname("speakeasy_host")' -> 0x3fb4c0
13
...
14
-> 0x51ff2: 'wininet.InternetOpenA('Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; Trident/6.0)', 0x0, 0x0, 0x0, 0x0)' -> 0x1c4
15
0x5200d: 'wininet.InternetSetOptionA(0x1c4, 0x5, 0x1203ef0, 0x4)' -> 0x1
16
0x5201d: 'wininet.InternetSetOptionA(0x1c4, 0x6, 0x1203ef0, 0x4)' -> 0x1
17
...
18
0x5200d: 'wininet.InternetSetOptionA(0x1f4, 0x5, 0x1203ef0, 0x4)' -> 0x1
19
0x5201d: 'wininet.InternetSetOptionA(0x1f4, 0x6, 0x1203ef0, 0x4)' -> 0x1
20
-> 0x52039: 'wininet.InternetConnectA(0x1f4, '<C2_SERVER>', 0x50, 0x0, 0x0, 0x3, 0x0, 0x82c40)' -> 0x1f8
21
0x644e2: 'kernel32.HeapAlloc(0x3f8200, 0x0, 0x10)' -> 0x7e5020
22
0x644e2: 'kernel32.HeapAlloc(0x3f8200, 0x0, 0x6c00)' -> 0x39000
23
0x68153: 'kernel32.GetLastError()' -> 0x0
24
...
25
0x68000: 'kernel32.TlsGetValue(0x0)' -> 0xfeee00ac
26
0x681bd: 'kernel32.SetLastError(0x0)' -> None
27
-> 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
28
...
Copied!
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:
GitHub - Sentinel-One/CobaltStrikeParser
GitHub
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:
1
START_PATTERNS = {
2
3: b'\x69\x68\x69\x68\x69\x6b..\x69\x6b\x69\x68\x69\x6b..\x69\x6a',
3
4: b'\x2e\x2f\x2e\x2f\x2e\x2c..\x2e\x2c\x2e\x2f\x2e\x2c..\x2e'
4
}
5
START_PATTERN_DECODED = b'\x00\x01\x00\x01\x00\x02..\x00\x02\x00\x01\x00\x02..\x00'
Copied!
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:
1
[[email protected] speakeasy]# python3 run_speakeasy.py -t /tmp/out -r -a x86 -d /tmp/output.zip
2
0x98ad: 'kernel32.VirtualAlloc(0x0, 0x3d000, 0x3000, "PAGE_EXECUTE_READWRITE")' -> 0x50000
3
0x92e1: 'kernel32.LoadLibraryA("KERNEL32.dll")' -> 0x77000000
4
0x93ad: 'kernel32.GetProcAddress(0x77000000, "GetThreadContext")' -> 0xfeee0000
5
<SNIP>
6
* User exited
7
Traceback (most recent call last):
8
File "_ctypes/callbacks.c", line 234, in 'calling callback function'
9
File "/tmp/speakeasy/speakeasy/common.py", line 81, in _wrap_code_cb
10
def _wrap_code_cb(self, emu, addr, size, ctx=[]):
11
KeyboardInterrupt
12
* Finished emulating
13
* Saving memory dump archive to /tmp/output.zip
Copied!
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:
1
[[email protected] CobaltStrikeParser]# python3 parse_beacon_config.py /tmp/output/emu.shellcode.71244efa5322cebbff97c7bbcc54c37a9bbga516fee0a563.0x1000.mem
2
BeaconType - HTTP
3
Port - 80
4
SleepTime - 60000
5
MaxGetSize - 1048576
6
Jitter - 0
7
MaxDNS - Not Found
8
C2Server - <c2 server>,/__utm.gif
9
UserAgent - Not Found
10
HttpPostUri - /submit.php
11
Malleable_C2_Instructions - Empty
12
HttpGet_Metadata - Not Found
13
HttpPost_Metadata - Not Found
14
HttpPost_Metadata - Not Found
15
PipeName - Not Found
16
DNS_Idle - Not Found
17
DNS_Sleep - Not Found
18
SSH_Host - Not Found
19
SSH_Port - Not Found
20
SSH_Username - Not Found
21
SSH_Password_Plaintext - Not Found
22
SSH_Password_Pubkey - Not Found
23
HttpGet_Verb - GET
24
HttpPost_Verb - POST
25
HttpPostChunk - 0
26
Spawnto_x86 - %windir%\syswow64\rundll32.exe
27
Spawnto_x64 - %windir%\sysnative\rundll32.exe
28
CryptoScheme - 0
29
Proxy_Config - Not Found
30
Proxy_User - Not Found
31
Proxy_Password - Not Found
32
Proxy_Behavior - Use IE settings
33
Watermark - 12345678
34
bStageCleanup - False
35
bCFGCaution - False
36
KillDate - 0
37
bProcInject_StartRWX - True
38
bProcInject_UseRWX - True
39
bProcInject_MinAllocSize - 0
40
ProcInject_PrependAppend_x86 - Empty
41
ProcInject_PrependAppend_x64 - Empty
42
ProcInject_Execute - CreateThread
43
SetThreadContext
44
CreateRemoteThread
45
RtlCreateUserThread
46
ProcInject_AllocationMethod - VirtualAllocEx
47
bUsesCookies - True
48
HostHeader -
Copied!
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

GitHub - mandiant/speakeasy: Windows kernel and user mode emulation.
GitHub
GitHub - Sentinel-One/CobaltStrikeParser
GitHub
Striking Back at Retired Cobalt Strike: A look at a legacy vulnerability
NCC Group Research
Empire/http.py at e37fb2eef8ff8f5a0a689f1589f424906fe13055 Β· EmpireProject/Empire
GitHub
​
Last modified 10mo ago