Analyzing Cobalt Strike for Fun and Profit

I am not sure what happened this year but it seems that Cobalt Strike is now the most used malware around the world, from APT41 to APT32, even the last SolarWinds supply chain attack involved Cobalt Strike. Without relaunching the heated debate on publishing offensive tools, this blog post intends to summarize what an analyst needs to know about Cobalt Strike to quickly identify and analyze it during incidents.

Finding Cobalt Strike Servers#

A few months ago, the Salesforce security team published a new active fingerprint tool called JARM. It is the active equivalent to JA3 they published last year. It generates a fingerprint based on the TLS configuration of a remote server, such as the TLS version or the TLS extensions, without considering the certificate. It is especially useful to identify custom web servers used by some tools, and Cobalt Strike is one of them.

Here is the Cobalt Strike JARM signature : 07d14d16d21d21d07c42d41d00041d24a458a375eef0c576d23a7bab9a9fb1

JARM has already been added to Shodan, BinaryEdge, and SecurityTrails, Shodan recently added indexing on ssl.jarm, so it is easy to find Cobalt Strike servers in the wild.

Let’s check Shodan with ssl.jarm:07d14d16d21d21d07c42d41d00041d24a458a375eef0c576d23a7bab9a9fb1:

Shodan has identified 5623 IP with this JARM fingerprint Cobalt Strike servers, mostly on Amazon and Digital Ocean. If we limit to port 443, we get 3423 IPs.

We can easily confirm that Cobalt Strike is still running on port 443 of the first IP using JARM:

$ python jarm.py 78.152.61.71
Domain: 78.152.61.71
Resolved IP: 78.152.61.71
JARM: 07d14d16d21d21d07c42d41d00041d24a458a375eef0c576d23a7bab9a9fb1

(This JARM signature is actually a signature of the Java Web server and specific to the JAVA 11 stack, so it includes other tools not related to Cobalt Strike (like Burp Suite) and does not include Cobalt Strike using different Java versions. Cobalt Strike recently wrote a blog post about this question.)

Getting a Cobalt Strike Payload#

Cobalt Strike uses a checksum of the url using an algorithm called checksum8 to serve the 32b or 64b version of the payload (in the same way as the metasploit server). The decompiled code of Cobalt Strike has been published several times on GitHub or elsewhere, it provides information on this checksum:

public static long checksum8(String text) {
    if (text.length() < 4) {
        return 0L;
    }
    text = text.replace("/", "");
    long sum = 0L;
    for (int x = 0; x < text.length(); x++) {
        sum += text.charAt(x);
    }

    return sum % 256L;
}

public static boolean isStager(String uri) {
    return (checksum8(uri) == 92L);
}

public static boolean isStagerX64(String uri) {
    return (checksum8(uri) == 93L && uri.matches("/[A-Za-z0-9]{4}"));
}

We can easily bruteforce the algorithm to find urls that match it in python:

from itertools import product
import string

def checksum8(strr):
    j = 0
    if len(strr) < 4:
        return 0
    strr = strr.replace("/", "")
    for c in strr:
        j += ord(c)
    return j % 256

chars = string.ascii_letters + string.digits
to_attempt = product(chars, repeat=4)
for attempt in to_attempt:
    word = ''.join(attempt)
    r = checksum8(word)
    if r == 92:
        print("{:30} - 32b checksum".format(word))
    elif r == 93:
        print("{:30} - 64b checksum".format(word))
$ python bf_checksum8.py
aaa9                           - 32b checksum
aab8                           - 32b checksum
aab9                           - 64b checksum
aac7                           - 32b checksum
aac8                           - 64b checksum
aad6                           - 32b checksum
aad7                           - 64b checksum
[...]

So /aaa9 should return the 32 bits beacon (if available) and /aab9 should return the 64 bits beacon (if available). Let’s test that on one of the Cobalt Strike servers from the Shodan list, 103.39.18.184 (AS136800 - ICIDC NETWORK - China) (one thing to know is that the Cobalt Strike server blocks unusual user agents).

$ wget --no-check-certificate https://103.39.18.184/aaa9
--2020-12-19 17:44:32--  https://103.39.18.184/aaa9
Connecting to 103.39.18.184:443... connected.
WARNING: cannot verify 103.39.18.184's certificate, issued by ‘CN=gmail.com,OU=Google Mail,O=Google GMail,L=Mountain View,ST=CA,C=US’:
  Self-signed certificate encountered.
    WARNING: certificate common name ‘gmail.com’ doesn't match requested host name ‘103.39.18.184’.
HTTP request sent, awaiting response... 404 Not Found
2020-12-19 17:44:33 ERROR 404: Not Found.


$ wget --no-check-certificate --user-agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36" https://103.39.18.184/aaa9
--2020-12-19 17:44:52--  https://103.39.18.184/aaa9
Connecting to 103.39.18.184:443... connected.
WARNING: cannot verify 103.39.18.184's certificate, issued by ‘CN=gmail.com,OU=Google Mail,O=Google GMail,L=Mountain View,ST=CA,C=US’:
  Self-signed certificate encountered.
    WARNING: certificate common name ‘gmail.com’ doesn't match requested host name ‘103.39.18.184’.
HTTP request sent, awaiting response... 200 OK
Length: 208980 (204K) [application/octet-stream]
Saving to: ‘aaa9’

aaa9                    100%[============================>] 204.08K   655KB/s    in 0.3s

2020-12-19 17:44:53 (655 KB/s) - ‘aaa9’ saved [208980/208980]


$ wget --no-check-certificate --user-agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36" https://103.39.18.184/aab9
--2020-12-19 17:44:58--  https://103.39.18.184/aab9
Connecting to 103.39.18.184:443... connected.
WARNING: cannot verify 103.39.18.184's certificate, issued by ‘CN=gmail.com,OU=Google Mail,O=Google GMail,L=Mountain View,ST=CA,C=US’:
  Self-signed certificate encountered.
    WARNING: certificate common name ‘gmail.com’ doesn't match requested host name ‘103.39.18.184’.
HTTP request sent, awaiting response... 200 OK
Length: 260679 (255K) [application/octet-stream]
Saving to: ‘aab9’

aab9                    100%[============================>] 254.57K   872KB/s    in 0.3s

2020-12-19 17:44:59 (872 KB/s) - ‘aab9’ saved [260679/260679]

So this IP 103.39.18.184 gives us two Cobalt Strike beacons:

Decrypting Cobalt Strike Beacons#

The files returned by the server are actually not PE file:

$ file *
aaa9:     data
aab9:     data

During the execution of a Cobalt Strike exploit, it downloads this beacon and run it directly in memory. So this file is a blob of data containing a shellcode at the beginning that decodes and executes the beacon.

We can easily graph this shellcode with miasm:

The first JMP goes to a call right before the encryption key and encrypted beacon. The call goes back to the shellcode and the next POP EDX gets the address of the key. The code in loc_f gets the key in EBX, the length of the payload in EAX, and store on the address of the final beacon on the stack. The loop in loc_1d goes through the beacon and xor it with the key.

We can easily reproduce it in python, the challenge is to find the base address. Here the loc_47 is the address of the call just before the key, length, and encrypted payload, so the base address is 0x47 + 5 (the length of the call instruction). The base address changes with different payloads but it is possible to find it easily by searching the last call instruction.

import struct

def xor(a, b):
    return bytearray([a[0]^b[0], a[1]^b[1], a[2]^b[2], a[3]^b[3]])

with open("aaa9", "rb") as f:
    data = f.read()

ba = 0x4c
key = data[ba:ba+4]
print("Key : {}".format(key))
size = struct.unpack("I", xor(key, data[ba+4:ba+8]))[0]
print("Size : {}".format(size))

res = bytearray()
i = ba+8
while i < (len(data) - ba - 8):
    d = data[i:i+4]
    res += xor(d, key)
    key = d
    i += 4

with open("a.out", "wb+") as f:
    f.write(res)

We get a PE file : 3c9a06b2477694919b1c77d3288984cb793a47dd328ef39e15132cd0cfb593ab.

It is surprising to see a PE file directly there because it is directly executed by the last call. The trick is that the PE file is actually modified to be at the same time a valid PE file and executable directly (something Cobalt Strike calls raw stageless payload artifact). We see here the MZ header being executed:

The DOS header is modified to include valid instructions that jump to the address 0x8157 in the binary. This address is the address of the exported _ReflectiveLoader@4 function, a function based on the RefelctiveDLLInection software that is in charge of reproducing a simple PE loader to load and map import functions before calling the entry point.

(Note that this is only optional in Cobalt Strike, many Cobalt Strike payloads do not have a PE file format but the payload directly in a shellcode-like format).

Extracting the Configuration#

The Cobalt Strike configuration is encrypted within the payload, with a different key depending on the Cobalt Strike version, either 0x23 or 0x69. Once decoded, the configuration is stored in the format type-length-value :

  • One short that represent the key of the data (the list can be found in the Cobalt Strike source code)
  • Two short bytes representing the type of data (Int, Short, String, etc.) and its length
  • The data itself

To find the start address of the configuration, we can look for the encoded value of the first key, which is 1 (DNS/SSL), 1 (Short), 2 (2 bytes), which is encoded to ihihik with the key 0x69 or ././., with the key 0x23.

We can then decode the configuration of the beacon:

dns                            False
ssl                            True
port                           443
.sleeptime                     60000
.http-get.server.output        00000004000000010000017700000001000000fa0000000200000004000000020000001c000000020000002400000002000000120000000200000004000000020000001c0000000200000024000000020000001100000002000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
.jitter                        15
.maxdns                        255
publickey                      30819f300d06092a864886f70d010101050003818d0030818902818100aef69a6fb8f21092c01a95cbdcac0f03f79738adecda36cffc6c5cf607943e72663865f8f69d84961910201ffde089b24cd4352c766414d0665537956b8ec8f4e23df0cd79e9284c16c899fde818758a22c53947e3dd52f440be86f71cdf8abb79adb3b8afaf9f80af028d823f1d70fcdbb34b0b5f5293f74dbb184a3c9109f3020301000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
.http-get.uri                  156.226.191.234,/_/scs/mail-static/_/js/,djiqowenlsakdj.com,/_/scs/mail-static/_/js/
.user-agent                    Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0; MALCJS)
.http-post.uri                 /mail/u/0/
.http-get.client               OSID=Cookie
GAccept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
DNT: 1
ui=d3244c4707ient
                hop=6928632	start=0
=Content-Type: application/x-www-form-urlencoded;charset=utf-8OSID=Cookie
.spawto
.post-ex.spawnto_x86           %windir%\syswow64\notepad.exe
.post-ex.spawnto_x64           %windir%\sysnative\notepad.exe
.pipename
.cryptoscheme                  0
.dns_idle                      134743044
.dns_sleep                     0
.http-get.verb                 GET
.http-post.verb                POST
shouldChunkPosts               0
.watermark                     305419896
.stage.cleanup                 0
CFGCaution                     0
host_header
cookieBeacon                   1
.proxy_type                    2
funk                           0
killdate                       0
text_section                   0
process-inject-start-rwx       64
process-inject-use-rwx         64
process-inject-min_alloc       0
process-inject-transform-x86
process-inject-transform-x64
process-inject-stub            a56c813864af878a4c10083ca1578e0a
process-inject-execute
process-inject-allocation-method 0

Putting Everything Together#

Now that we have decoded everything, it is quite easy to do the request, extract the beacon and the configuration directly. I have put all this in a script available on this github repository:

$ python scan.py https://103.39.18.184/
Checking https://103.39.18.184/
Unknown config command 55
Configuration of the x86 payload
dns                            False
ssl                            True
port                           443
.sleeptime                     60000
.http-get.server.output        00000004000000010000017700000001000000fa0000000200000004000000020000001c000000020000002400000002000000120000000200000004000000020000001c0000000200000024000000020000001100000002000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
.jitter                        15
.maxdns                        255
[SNIP]

x86_64: Payload not found

It is common to find the same configuration for many different samples because CS uses what they call Malleable C2 Profiles which are actually configurations for the CS beacons that can be shared easily through configuration files. For instance, Ocean Lotus uses a public profile mimicking Google Safebrowsing urls. This repository list profiles used by different APT or cybercrime groups.

One interesting value in the configuration is the watermark, which is a number generated from the license file. As it is unique to a customer, it can be used to pivot and link multiple CobaltStrike instances together (as it was done for Trickbot. As such, many cracked versions of CobaltStrike disable this watermark. This watermark is technically associated with the Cobalt Strike Customer id, so it should be possible to report this id to Cobalt Strike and identify the customer for people using paid licenses, but I have never heard anyone doing that (I guess few APT groups have a valid CS license).

Extracting Configuration of 1000 Cobalt Strike Servers#

Based on the same code, I have scanned the 3424 servers identified with JARM in Shodan, I have scanned them all using this script and found 520 serving Cobalt Strike beacons.

I have uploaded on GitHub a csv listing the IPs and configuration of these beacons, here is a short extract:

Host GET URI POST URI User Agent Watermark
54.66.253.144 54.66.253.144,/s/ref=nb_sb_noss_1/167-3294888-0262949/field-keywords=books /N4215/adj/amzn.us.sr.aps Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko 562884990
103.243.183.250 103.243.183.250,/search.js /hr Mozilla/5.0 (Linux; Android 6.0; HTC One X10 Build/MRA58K; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 305419896
185.82.126.47 185.82.126.47,/pixel /submit.php Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0; MASB) 305419896
94.156.174.121 94.156.174.121,/watch /ptracking Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) 76803050
194.36.191.118 194.36.191.118,/visit.js /submit.php Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; InfoPath.2; .NET CLR 2.0.50727) 305419896
23.106.160.198 repshd.com,/us/ky/louisville/312-s-fourth-st.html,pinglis.com,/us/ky/louisville/312-s-fourth-st.html,stargut.com,/us/ky/louisville/312-s-fourth-st.html /OrderEntryService.asmx/AddOrderLine Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) 0
23.81.246.46 contmetric.com,/s/ref=nb_sb_noss_1/167-3294888-0262949/field-keywords=books /N4215/adj/amzn.us.sr.aps Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko 0
108.174.193.11 qw.removerchangefile.monster,/media.html,as.removerchangefile.monster,/media.html,zx.removerchangefile.monster,/media.html /ak Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_2) AppleWebKit/601.3.9 (KHTML, like Gecko) Version/9.0.2 Safari/601.3.9 305419896
213.217.0.218 213.217.0.218,/visit.js /submit.php Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0; NP06) 305419896
213.252.247.31 1nubjgrcfjhjhkjftdd.com,/s/ref=nb_sb_noss_1/167-3294888-0262949/field-keywords=books /N4215/adj/amzn.us.sr.aps Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko 0

165 of them over 523 have no watermark, another watermark (305419896) is used by 160 IPs, so it is likely a default value. Then a few watermarks have more than 5 servers, such as 1580103814, 1873433027 or 16777216.

That’s All Folks#

I have uploaded all these scripts and yara rules for beacons on Github, feel free to DM me on Twitter or send me an email if you have any question.

Here are some other interesting resources on Cobalt Strike:

This blog post was mostly written while listening to Susumu Yokota.

Edit 1: adding link to Cobalt Strike JARM analysis (thanks @AZobec)