Rhadamanthys
What is this thing are we just looking at a downloader
Overview
Sample
dca16a0e7bdc4968f1988c2d38db133a0e742edf702c923b4f4a3c2f3bdaacf5
Malware Bazaar
This malware name is crazy, we will refer to it as "ramen noodls" for simplicity.
References
- Triage Run
-
Dancing With Shellcodes: Analyzing Rhadamanthys Stealer
- Downloader
af04ee03d69a7962fa5350d0df00fafc4ae85a07dff32f99f0d8d63900a47466
- Downloader
Stage 1
The first stage is a C++ loader that is used to decrypt, load, and execute the 2nd stage shellcode. The program flow of the loader is difficult to follow due to the C++ and some nested structures and callbacks. It is not clear if this was intentional or note.
The shellcode appears to be located in what appears to be a giant Base64 encoded string (not confirmed). This first stage may have alos implmented some PAGE_GUARD exception handling progrem flow redirection for anti-analysis (not confrimed). Dispite this it is trivial to execute the stage until it jumps to the decrypted loaded shellcode (as long as the PAGE_GUARD exceptions are passed to the process).
Stage 2
For analysis we just dumped the entire memory region that contained the second stage shellcode. The region is based at 0x00760000
but the shellcode entry is at 0x00780A68
. The memory region dump is available on Malshare d4f37699c4b283418d1c73416436826e95858cf07f3c29e6af76e91db98e0fc0
.
The first task of the shellcode is to walk the PEB to locate Kernel32.
PEB Walk _LDR_DATA_TABLE_ENTRY and Shifted Pointers in IDA
The process of "walking the PEB" to access the modules loaded in a process as been a shellcode meme since the beginning of shellcode. It's everywhere, and we all mostly understand how it works.
The issue comes when we try to represent this cleanly in IDA's pseudocode view... accessing a _LDR_DATA_TABLE_ENTRY
via its LIST_ENTRY
in the _PEB_LDR_DATA
structure creates a very messy IDB. Instead of the _LDR_DATA_TABLE_ENTRY
members we see unhelpful offsets relative to the Flink and Blink member of the LIST_ENTRY
.
The reason this happens is that the InMemoryOrderModuleList
is defined as a LIST_ENTRY
in the _PEB_LDR_DATA
. The LIST_ENTRY
struct simply describes a linked list as follows.
typedef struct _LIST_ENTRY {
struct _LIST_ENTRY *Flink;
struct _LIST_ENTRY *Blink;
}
The problem comes from where that struct is actually located in the _LDR_DATA_TABLE_ENTRY
...
typedef struct _LDR_DATA_TABLE_ENTRY {
PVOID Reserved1[2];
LIST_ENTRY InMemoryOrderLinks; //<---- Offset into the struct!
PVOID Reserved2[2];
PVOID DllBase;
PVOID EntryPoint;
PVOID Reserved3;
UNICODE_STRING FullDllName;
BYTE Reserved4[8];
PVOID Reserved5[3];
union {
ULONG CheckSum;
PVOID Reserved6;
};
ULONG TimeDateStamp;
}
Because the LIST_ENTRY
is offset into the _LDR_DATA_TABLE_ENTRY
it means that when we attempt to cast a pointer to a LIST_ENTRY
as a _LDR_DATA_TABLE_ENTRY
we are off by some amount (in this case 2 * PVOID = 8 bytes). The following diagram attempts to explain the issue.
One hack might be to create our own custom struct that starts at the offset, but this would be madness when dealing with more than a few types... instead we have... shifted pointers!
IDA Shifted Pointers
IDA has a simple concept (with some insane syntax) to deal with this issue called a shifted pointer. When assigning a type to LIST_ENTRY
the shifted pointer syntax can be used to tell IDA that it is actually a pointer inside a larger struct with an offset.
_LIST_ENTRY *__shifted(_LDR_DATA_TABLE_ENTRY,8) pListEntry
References
- PE Header notes yates
- Geoff Chappell LDR_DATA_TABLE_ENTRY
- [Understanding LIST_ENTRY Lists and Its Importance in Operating Systems] (https://www.codeproject.com/Articles/800404/Understanding-LIST-ENTRY-Lists-and-Its-Importance)
- MSDN PEB_LDR_DATA
- IDA Tricks CONTAINING_RECORD macro
- IDA docs Shifted Pointers
Thanks
Thanks to everyone who helped me figure this out for once and for all!
Analysis
This stage only serves one purpose; unpack and execute the final stage. The unpacking algorithm is currently unknown!
Thanks Mishap
Full analysis of Stage 2 (with unpacker)
Emulation Attempt
Instead of analyzing this intermediate stage we are going to try and emulate passed it.
from dumpulator import Dumpulator
dp = Dumpulator("/tmp/stage2.dmp", quiet=True)
shellcode_start = dp.regs.eip
print(hex(shellcode_start))
shell_base = 0x00810000
shell_end = 0x0082CFF5
dp.start(shellcode_start, end=shell_end)
stage3_start = dp.regs.ebx
print(hex(stage3_start))
shell_page = dp.memory.find_region(stage3_start)
print(shell_page.size)
print(shell_page.start)
print(f"Shellcode entrypoint: {hex(stage3_start - shell_page.start)}")
print(f"Shellcode base: {hex(shell_page.start)}")
shell_page_data = dp.read(shell_page.start, shell_page.size)
open('/tmp/dump_stage3.bin','wb').write(shell_page_data)
We don't yet have a way to add exception hooks to dumpulator so if we want to do this without knowing the address of the end of stage 2 then we need to modify the dumpulator source code.
Update! @mishap has a public implementation of a quick and dirty approach to modifying dumpulator for a generic approach to this Dirty exception hook
print(hex(dp.read_long(dp.regs.esp)))
print(hex(dp.read_long(dp.regs.esp-4)))
dp.memory.find_region(0x19fefc)
dp.exports.get(0x7656f530)
hex(dp.read_long(0x21d0000))
dp.read(0x13BAC, 10)
dp.start(dp.regs.eip, end=0x021D5D88 )
dp.read(0x13BAC, 100)
dp.start(dp.regs.eip, end=0x021D6098)
dp.modules.find(dp.regs.eax)
dp = Dumpulator("/tmp/stage2.dmp", quiet=True)
shellcode_start = dp.regs.eip
print(hex(shellcode_start))
shell_base = 0x00810000
shell_end = 0x0082CFF5
dp.start(shellcode_start, end=shell_end)
print(f"EIP: {hex(dp.regs.eip)}")
dp.start(dp.regs.eip, end=0x021D5D88)
dp.regs.esi
print(hex(dp.regs.esi))
dp.read(dp.regs.esi, 100)
Rename IAT Hashes in IDA
### apis = {dict with hashdb enum of all imports}
api_nums = dict((v,k) for k,v in apis.items())
ptr = 0x0001B114
while ptr < 0x0001BC9E:
api_hash = ida_bytes.get_dword(ptr)
api_name = api_nums.get(api_hash,'')
if api_name != '':
print("ptr_"+api_name)
idc.set_name(ptr, "ptr_"+api_name, 0x800) #SN_FORCE
ptr += 4
else:
ptr += 1
from dumpulator import Dumpulator
dp = Dumpulator("/tmp/stage2.dmp", quiet=True)
shellcode_start = dp.regs.eip
print(hex(shellcode_start))
shell_base = 0x00810000
shell_end = 0x0082CFF5
dp.start(shellcode_start, end=shell_end)
print(f"EIP: {hex(dp.regs.eip)}")
hex(dp.read_long(dp.regs.esp+4))
init_data_offset_0 = dp.read_long(0x19ff54)
dp.read_long(init_data_offset_0)
hex(dp.read_long(0x19ff54+4))
config_data = dp.read(0x426008, 152)
print(config_data.hex())
key = bytes.fromhex('52abdf06b6b13ac0da2d22dc6cd2be6c201769e012b5e6ec0eab4c14734aed51')
out = bytes.fromhex('215248590e000000ef4456b59328427fba4377a1d192aa92687474703a2f2f3131362e3230322e31382e3133322f626c6f622f71336b36746b2e7869386f008e8a1f771e17b45183cfda277723418924c7936eb3cb4ef3541c60510f1dd542c28d53aa06cf6cf4e16e3ebcbc8c4c702a9a31f99e44e93688702587843fe3834d0ca21df074ae3aabfe9b480a09fbd2df9b8ae88b40ad5ce6')
out
len('\xefDV\xb5\x93(B\x7f\xbaCw\xa1\xd1\x92\xaa\x92')
c2 = out[24:].split(b'\x00')[0].decode('utf-8')
c2
from dumpulator import Dumpulator
dp = Dumpulator("/tmp/stage2.dmp", quiet=True)
shellcode_start = dp.regs.eip
print(hex(shellcode_start))
shell_base = 0x00810000
shell_end = 0x0082CFF5
dp.start(shellcode_start, end=shell_end)
# Extract stage3
stage3_base = dp.read_long(dp.regs.esp)
stage3_size = dp.memory.query(stage3_base).region_size
stage3_data = dp.read(stage3_base, stage3_size)
# Extract encrypted config
ptr_config_data = dp.read_long(0x19ff54+4)
config_data = dp.read(ptr_config_data, 152)
import re
import struct
from malduck import rc4
# 6A 20 push 20h ; ' ' ; a3
# 8D 85 F8 FE FF FF lea eax, [ebp+arg_rc4_key_stream]
# 68 A4 2C 01 00 push offset g_key ; a2
# 8B 77 04 mov esi, [edi+4]
# 50 push eax ; a1
# E8 03 E6 FF FF call mw_rc4_ksa
# 56 push esi ; arg_out_buff
# 56 push esi ; arg_in_buff
# 8D 85 F8 FE FF FF lea eax, [ebp+arg_rc4_key_stream]
# 68 98 00 00 00 push 98h ; arg_size
key_egg = rb'\x6A\x20(?:(?!\x6A\x20).)*?\x98\x00\x00\x00'
key_candidates = []
for match in re.finditer(key_egg, stage3_data):
data_target = match.group()
key_address = struct.unpack('<I', data_target.split(b'\x68')[1][:4])[0]
# print(hex(key_address))
key_offset = key_address
key_data = stage3_data[key_offset:key_offset+32]
key_candidates.append(key_data)
assert len(key_candidates) != 0
# Test each key and look for decrypted magic byte
magic_bytes = b'!RHY'
config_out = None
for key in key_candidates:
out = rc4(key, config_data)
#print(out)
if out[:4] == magic_bytes:
config_out = out
break
assert config_out != None
c2 = config_out[24:].split(b'\x00')[0].decode('utf-8')
print(f'C2: {c2}')
Hat tip to @printup for this as an alternative to our regex
index: int = len(data)
while True:
end_index: int = data.rfind(end_token, 0, index)
if end_index == -1:
break
begin_index: int = data.rfind(begin_token, 0, end_index)
if begin_index == -1:
break
print(f"Found a match: {data[begin_index:end_index + len(end_token)]}")
index = begin_index