Quick analysis CobaltStrike loader and shellcode

Posted: September 6, 2021 in My Tutorials, Quick analysis CobaltStrike loader and shellcode
Tags: , , , ,

I saw this hash 2569cc660d2ae0102aa74c98d78bb9409ded24101a0eeec15af29d59917265f3 shared at malwareresearchgroup.slack.com. It was submitted to VT at 2021-09-01 19:47:50 and 37 security vendors flagged this file as malicious.

1. Analyze loader

This loader is 64-bit Dll, compiled by MinGW and has one exported function:

With the help of IDA, we can see the ServiceMain function will spawn a new thread (I renamed to f_spawn_shellcode_thread):

The f_spawn_shellcode_thread function does the following tasks:

  • Init xor_key is “jKfXmEkWYshKkZdPhJYS
  • Allocate heap buffer for storing encrypted shellcode bytes and assign values to this buffer based on the global byte array has been declared from the beginning.
  • Peform loop to decode the shellcode.
  • Spawn new thread to execute the decoded shellcode.

I wrote a short script to do shellcode extraction for later analysis:

import sys
import pefile

xor_key = "jKfXmEkWYshKkZdPhJYS"

def decode_sc(data, key):
    key_len = len(key)
    data_len = len(data)
    decrypted = bytearray(data_len) 

    for i in range(0, data_len):
        decrypted[i] = data[i] ^ key[i%key_len]
        
    print("Decode Done!")
    return decrypted


def extract_sc(input_file):
    encrypted_sc = []
    try:
        print("\r\nFile: " + input_file)
        pe = pefile.PE(input_file)

        for section in pe.sections:
            if b'.rdata\x00\x00' in section.Name:
                rdata_section = bytearray(section.get_data())
        
        size = 0        
        for i in rdata_section:
            if rdata_section[size] == 0x00 and rdata_section[size+1] == 0x00:
                break
            else:
                size += 1
        print("Encrypted bytes size: " + str(size - 24) + " bytes")

        encrypted_bytes = rdata_section[24:size+1]
        for i in range(len(encrypted_bytes)):
            if ((i & 1) == 0):
                encrypted_sc.append(encrypted_bytes[i])

        key = xor_key.encode('ascii')
        decrypted_sc = decode_sc(encrypted_sc, key)

        with open(sys.argv[1]+"-decrypted", "wb") as out_file:
            out_file.write(decrypted_sc)
        print("Shellcode extracted at " + sys.argv[1]+"-decrypted!\r\n")        

        print("Extract Shellcode Done!")
    except Exception as e:
        print("Error: " + str(e))


if __name__ == '__main__':
    if len(sys.argv) == 2:
        extract_sc(sys.argv[1])
    else:
        print("Usage: cobalt_extract_sc.py <cobalt_loader_dll>")

After run script, I got the shellcode like the figure bellow:

2. Analyze shellcode

If we load the raw shellcode into IDA and convert to asm code, it will look like the figure bellow. At the first beginning of this code, we can see the pattern code that shellcode use to locate the fields of PEB structure. This makes me think that it will use PEB to looking up the addresses of the API functions in the Dll used by shelllcode.

Go into sub_D2, the first statement assigns the return address to the rbp register. And we know that this address is 0xA (push r9). Then we see the string value ‘wininet’ is load to r14 register at 0xD5. We see a value is assigned to the r10 (726774Ch; 726774Ch) register and following is a call to the address pointed by the rbp register. At that time, I think these are hash values related to api functions, shellcode will perform calculations to compare with these values from which to get the related API address.

For the convenience of analysis and debugging, I converted the shellcode to an exe. Finally, I got the following pseudocode related to finding the address of the API function and calling API through jmp rax command:

Based on the above pseudocode, we can see that the shellcode will calculate two hash values, the first value is based on the name of the Dll, the second value is based on the name of the API function of that Dll. These two values are added together and compared with the pre-computed hash value.

You can write scripts to recover API functions or to save time, I always use shellcode_hashes_search_plugin.py of FLARE Team. Details can be found in this article. Final result after using the plugin:

shellcode_hash: Starting up
[INFO] Starting up	(shellcode_hash_search:run)
shellcode_hash: Processing current segment only: 0x140001000 - 0x140003000
[INFO] Processing current segment only: 0x140001000 - 0x140003000	(shellcode_hash_search:processCode)
shellcode_hash: 0x1400020e7: ror13AddHash32AddDll:0x0726774c kernel32.dll!LoadLibraryA
[INFO] 0x1400020e7: ror13AddHash32AddDll:0x0726774c kernel32.dll!LoadLibraryA	(shellcode_hash_search:lookForOpArgs)
shellcode_hash: 0x1400020ff: ror13AddHash32AddDll:0xa779563a wininet.dll!InternetOpenA
[INFO] 0x1400020ff: ror13AddHash32AddDll:0xa779563a wininet.dll!InternetOpenA	(shellcode_hash_search:lookForOpArgs)
shellcode_hash: 0x140002121: ror13AddHash32AddDll:0xc69f8957 wininet.dll!InternetConnectA
[INFO] 0x140002121: ror13AddHash32AddDll:0xc69f8957 wininet.dll!InternetConnectA	(shellcode_hash_search:lookForOpArgs)
shellcode_hash: 0x140002140: ror13AddHash32AddDll:0x3b2e55eb wininet.dll!HttpOpenRequestA
[INFO] 0x140002140: ror13AddHash32AddDll:0x3b2e55eb wininet.dll!HttpOpenRequestA	(shellcode_hash_search:lookForOpArgs)
shellcode_hash: 0x14000216a: ror13AddHash32AddDll:0x869e4675 wininet.dll!InternetSetOptionA
[INFO] 0x14000216a: ror13AddHash32AddDll:0x869e4675 wininet.dll!InternetSetOptionA	(shellcode_hash_search:lookForOpArgs)
shellcode_hash: 0x140002184: ror13AddHash32AddDll:0x7b18062d wininet.dll!HttpSendRequestA
[INFO] 0x140002184: ror13AddHash32AddDll:0x7b18062d wininet.dll!HttpSendRequestA	(shellcode_hash_search:lookForOpArgs)
shellcode_hash: 0x140002329: ror13AddHash32AddDll:0x56a2b5f0 kernel32.dll!ExitProcess
[INFO] 0x140002329: ror13AddHash32AddDll:0x56a2b5f0 kernel32.dll!ExitProcess	(shellcode_hash_search:lookForOpArgs)
shellcode_hash: 0x140002345: ror13AddHash32AddDll:0xe553a458 kernel32.dll!VirtualAlloc
[INFO] 0x140002345: ror13AddHash32AddDll:0xe553a458 kernel32.dll!VirtualAlloc	(shellcode_hash_search:lookForOpArgs)
shellcode_hash: 0x140002363: ror13AddHash32AddDll:0xe2899612 wininet.dll!InternetReadFile
[INFO] 0x140002363: ror13AddHash32AddDll:0xe2899612 wininet.dll!InternetReadFile	(shellcode_hash_search:lookForOpArgs)
shellcode_hash: Done
[INFO] Done	(shellcode_hash_search:run)

At this point, we can do debugging for further analysis, however, for quickly I use hasherezade’s tiny_tracer tool to trace the shellcode:

End!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.