Brute Ratel
Some notes on this dual purpose RAT
Collab with @BoymoderRE
This is part of an ongoing collaboration with BoymoderRE she has been streaming her work over on her twitch channel, and we have been sharing IDBs using the free open source IDA collaboration tool IDArling.
Overview
Brute Ratel is a pentesting framework what was recently leaked and has been showing up in the hands of ransomware operators. We are going to focus on the releases before 1.3 where many of the weaknesses in the implant were fixed. To date we have only seen the older versions used by ransomware operators.
Some of the weaknesses in the older version.
- Default Rc4 key used
bYXJm/3#M?:XyMBF
- Using ror13 string hashes
- Strings in the badger's memory
- Config is base64 encoded in stage 1
Sample
The sample we are analyzing in not public.
References
- When Pentest Tools Go Brutal: Red-Teaming Tool Being Abused by Malicious Actors
- Black Basta Ransomware Gang Infiltrates Networks via QAKBOT, Brute Ratel, and Cobalt Strike
- Brute Ratel Config Decoding update
- Immersive-Labs-Sec/BruteRatel-DetectionTools (github)
- Brute Ratel release notes
- Blobrunner shellcode debugging tool
b64_data = bytes.fromhex('42 53 4B 50 36 52 5A 38 61 66 62 4F 6E 48 47 38 4E 54 76 50 66 6B 59 42 51 35 67 42 42 67 67 68 43 6B 71 6E 2F 43 35 51 62 68 72 55 51 53 39 75 4B 4C 4C 37 33 57 50 31 4A 68 46 77 51 66 35 6E 55 39 38 50 4E 53 70 36 70 6D 2B 69 55 55 63 46 70 64 45 4C 58 79 38 79 64 6A 67 71 49 4C 4C 78 61 6F 55 4A 57 33 49 6D 49 6C 48 74 64 31 48 34 51 70 4F 37 6E 2B 56 4D 62 56 45 77 36 77 75 32 47 7A 68 6B 4B 68 51 77 72 4B 31 32 51 35 4F 78 61 6F 6A 57 31 30 2F 42 70 39 31 72 77 68 49 4B 4C 37 4E 51 73 62 38 75 57 66 34 46 62 52 42 69 70 6D 73 37 33 37 65 4E 41 71 47 4E 51 58 78 44 34 59 41 6E 51 41 65 68 47 71 49 47 39 6A 4A 36 2B 7A 4F 78 34 6F 6A 63 30 70 77 57 70 42 48 53 79 63 37 2F 57 73 46 53 46 69 77 37 36 43 53 6C 6D 38 43 43 6E 4D 45 6F 54 56 30 56 41 57 6E 6A 41 4F 31 33 75 72 79 43 6C 77 4E 38 77 4C 2F 73 63 45 62 5A 37 30 73 77 67 67 73 54 45 42 32 34 64 73 6B 78 6A 6F 41 55 55 6C 7A 70 6E 6F 31 5A 6F 57 57 4B 5A 67 4E 6E 7A 72 65 67 64 41 4E 2B 6B 49 53 71 31 4F 2F 48 43 44 70 47 62 54 42 42 43 67 58 34 36 48 34 4A 38 47 4F 59 42 70 43 55 53 66 66 56 58 6A 68 53 49 31 32 65 55 55 65 48 41 37 79 57 38 63 46 5A 00'.replace(' ',''))
b64_data
Stage 1 Unpacker
-
The shellcode payload contains an RC4 encrypted PE with the "badger" payload. The RC4 key is 8 bytes which is appended directly to the end of the encrypted payload.
-
This encrypted blob is moved on the the stack in blocks of 8 bytes. The stack blob and size are then copied on the heap.
-
The same approach is used for the (config?) but this is base64 encoded.
-
The heap allocations containing the encrypted payload, the base64 encoded config, and the lenght of both data allocations are passed to the initialization function in the shellcode.
def rc4crypt(data, key):
#If the input is a string convert to byte arrays
if type(data) == str:
data = data.encode('utf-8')
if type(key) == str:
key = key.encode('utf-8')
x = 0
box = list(range(256))
for i in range(256):
x = (x + box[i] + key[i % len(key)]) % 256
box[i], box[x] = box[x], box[i]
x = 0
y = 0
out = []
for c in data:
x = (x + 1) % 256
y = (y + box[x]) % 256
box[x], box[y] = box[y], box[x]
out.append(c ^ box[(box[x] + box[y]) % 256])
return bytes(out)
payload_enc = open('/tmp/brute_enc_shellcode.bin','rb').read()
payload_key = payload_enc[-8:]
out = rc4crypt(payload_enc[:-8],payload_key)
out[:0x400]
stage1_data = open('/tmp/stage1.bin','rb').read()
from ctypes import *
import unicorn as uc
stage1_data = open('/tmp/stage1.bin','rb').read()
call_state = 0
def hook_call(uc_engine, mem_type, address, size):
global call_state
ptr_data = uc_engine.reg_read(uc.x86_const.UC_X86_REG_RCX)
data_size = uc_engine.reg_read(uc.x86_const.UC_X86_REG_RDX)
print(f"Hook: blob ptr: {hex(ptr_data)} size:{hex(data_size)}")
buf = uc_engine.mem_read(ptr_data, data_size)
if call_state == 0:
print("Found first blob")
print(buf[:100])
name = "first.bin"
else:
print("Found second blob")
print(buf[:100])
name = "second.bin"
call_state += 1
rip = uc_engine.reg_read(uc.x86_const.UC_X86_REG_RIP)
print(f"RIP: {hex(rip)}")
rip += 5
uc_engine.reg_write(uc.x86_const.UC_X86_REG_RIP, rip)
open(f'/tmp/{name}', 'wb').write(buf)
if call_state == 2:
print("End emulation")
uc_engine.reg_write(uc.x86_const.UC_X86_REG_RIP, 0x99999999999)
return True
def main(buf):
uc_engine = uc.Uc(uc.UC_ARCH_X86, uc.UC_MODE_64)
STACK_ADDR = 0x4400000
CODE_ADDR = 0x1400000
uc_engine.mem_map(CODE_ADDR, 0x100000, uc.UC_PROT_ALL)
uc_engine.mem_map(STACK_ADDR - 0x100000, 0x200000, uc.UC_PROT_ALL)
uc_engine.mem_write(CODE_ADDR, buf)
uc_engine.reg_write(uc.x86_const.UC_X86_REG_RIP, CODE_ADDR)
uc_engine.reg_write(uc.x86_const.UC_X86_REG_RSP, STACK_ADDR)
hook1 = uc_engine.hook_add(uc.UC_HOOK_CODE, hook_call, None, CODE_ADDR + 0x0536B1, CODE_ADDR + 0x0536B2)
hook2 = uc_engine.hook_add(uc.UC_HOOK_CODE, hook_call, None, CODE_ADDR + 0x536CB, CODE_ADDR + 0x536CC)
uc_engine.emu_start(CODE_ADDR, CODE_ADDR + 0x10000, 0, 0)
out_rip = uc_engine.reg_read(uc.x86_const.UC_X86_REG_RIP)
main(stage1_data)
from ctypes import *
import unicorn as uc
stage1_data = open('/tmp/stage1.bin','rb').read()
call_state = 0
def hook_call(uc_engine, mem_type, address, size):
global call_state
rip = uc_engine.reg_read(uc.x86_const.UC_X86_REG_RIP)
rip_byte = uc_engine.mem_read(rip, 1)
if rip_byte == b'\xe8':
print(f"Call hook at RIP: {hex(rip)}")
if call_state == 0:
call_state += 1
return True
ptr_data = uc_engine.reg_read(uc.x86_const.UC_X86_REG_RCX)
data_size = uc_engine.reg_read(uc.x86_const.UC_X86_REG_RDX)
print(f"Hook: blob ptr: {hex(ptr_data)} size:{hex(data_size)}")
buf = uc_engine.mem_read(ptr_data, data_size)
if call_state == 1:
print("Found first blob")
print(buf[:100])
name = "first.bin"
else:
print("Found second blob")
print(buf[:100])
name = "second.bin"
call_state += 1
rip += 5
uc_engine.reg_write(uc.x86_const.UC_X86_REG_RIP, rip)
open(f'/tmp/{name}', 'wb').write(buf)
if call_state == 3:
print("End emulation")
uc_engine.reg_write(uc.x86_const.UC_X86_REG_RIP, 0x99999999999)
return True
def main(buf):
uc_engine = uc.Uc(uc.UC_ARCH_X86, uc.UC_MODE_64)
STACK_ADDR = 0x4400000
CODE_ADDR = 0x1400000
uc_engine.mem_map(CODE_ADDR, 0x100000, uc.UC_PROT_ALL)
uc_engine.mem_map(STACK_ADDR - 0x100000, 0x200000, uc.UC_PROT_ALL)
uc_engine.mem_write(CODE_ADDR, buf)
uc_engine.reg_write(uc.x86_const.UC_X86_REG_RIP, CODE_ADDR)
uc_engine.reg_write(uc.x86_const.UC_X86_REG_RSP, STACK_ADDR)
hook1 = uc_engine.hook_add(uc.UC_HOOK_CODE, hook_call, None)
uc_engine.emu_start(CODE_ADDR, CODE_ADDR + 0x10000, 0, 0)
out_rip = uc_engine.reg_read(uc.x86_const.UC_X86_REG_RIP)
main(stage1_data)