Matanbuchus Triage Notes
Matanbuchus loader triage and yara rule
Overview
Matanbuchus is a malware downloader that has been observed as the final loading stage in multiple phishing campaigns. There are two components, a Matanbuchus loader intented to load the Matanbuchus downloader. Once the downloader is executed it makes an HTTP request to the C2 with some victim info, and downloads the final payload.
According to DCSO CyTec...
Matanbuchus is the name given to a Malware-as-a-Service sold on Russian-speaking cybercriminal forums. Starting at a rental price of $2,500, the malware consists of an obfuscated two-stage loader which has been deployed in conjunction with Qakbot and Cobalt Strike payloads.
References
- Malspam pushes Matanbuchus malware, leads to Cobalt Strike
- Not Packed (UnpacMe)
- f8cc2cf36e193774f13c9c5f23ab777496dcd7ca588f4f73b45a7a5ffa96145e
- Matanbuchus: Malware-as-a-Service with Demonic Intentions
- A deal with the devil: Analysis of a recent Matanbuchus sample
- MATANBUCHUS: Another Loader As A Service Malware
- List of known versions of Windows DLLs
- Free Yara Scan Service
- MalwareBazaar Client
Yara Rule
This yara rule is very brittle and needs lots of testing/refining
rule matanbuchus {
meta:
description = "Identifies matanbuchus"
strings:
// recursive fnv1 hash
// 69 C2 93 01 00 01 imul eax, edx, 1000193h
// 50 push eax
// B9 01 00 00 00 mov ecx, 1
// C1 E1 00 shl ecx, 0
$x1 = { 69 c2 93 01 00 01 50 b9 01 00 00 00 c1 e1 00 }
// string decryption
// 0F BE 1C 01 movsx ebx, byte ptr [ecx+eax]
// 33 DE xor ebx, esi
// 6A 00 push 0
// 6A 01 push 1
$x2 = { 0f be 1c 01 33 de 6a 00 6a 01 }
condition:
all of ($x*)
}
Yara Rule Revised
rule matanbuchus {
meta:
description = "Identifies matanbuchus"
strings:
// fnv1 hash
// 69 C2 93 01 00 01 imul eax, edx, 1000193h
//
// 69 C0 93 01 00 01 imul eax, 1000193h
$x1 = { 69 ?? 93 01 00 01 }
//pe loader
$x2 = { B8 4D 5A 00 00 }
$x3 = { 81 38 50 45 00 00 }
//InternetCloseHandle
$x5 = { 66 E9 DD 4D }
//InternetOpenUrlA
$x6 = { BC 8B CF F4 }
//InternetCheckConnectionA
$x7 = { 3F 82 58 52 }
//InternetReadFile
$x8 = { 66 E9 DD 4D }
condition:
all of ($x*)
}
String Decryption
Simple Decryption
Some strings are built as stack strings then copied into a buffer and returned from a function. The returned buffer is then decrypted directly using a simple XOR decryption routine where the first byte is the key.
import struct
data = b'jl8|tt8Py{s[p}{s'
key = struct.pack('<I',0x796C6B18)
data = key[1:] + data
out = []
for i in data:
out.append(i ^ key[0])
bytes(out)
def unhex(hex_string):
import binascii
if type(hex_string) == str:
return binascii.unhexlify(hex_string.encode('utf-8'))
else:
return binascii.unhexlify(hex_string)
def tohex(data):
import binascii
if type(data) == str:
return binascii.hexlify(data.encode('utf-8'))
else:
return binascii.hexlify(data)
Complex Decryption
Other strings are also build from stack strings in a function and returned in a buffer, but these strings are decrypted with a second function call to a decryption function. To handle these more complex strings, we will use dumpulator
from dumpulator import Dumpulator
import struct
import re
import pefile
file_data = open('/tmp/mat.bin','rb').read()
pe = pefile.PE(data=file_data)
dp = Dumpulator("/tmp/mat.dmp", quiet=True)
fn_get_string = 0x708641C0
ss_start = 0x708631B2
ss_end = 0x708632AA
dp.start(ss_start, end=ss_end)
ss = dp.read(dp.regs.ebp-0x50, 0x48)
ss = bytes(ss)[:0x3e]
buff = dp.allocate(0x3e)
dp.write(buff, ss)
fn_dec_str = 0x7086F3D0
dp.call(fn_dec_str, [buff, 0x3e, 0x0, 0x7ebbfa3, 0x1010101])
dp.read(buff,0x3e)
Dumpulator Notes
The DLL uses thread safe functions that require a call into EnterCriticalSection
and LeaveCriticalSection
. Because our dump was taken before the DLL was initialized the critical section object was not initialized and this causes Dumpulator to crash.
First we tried just calling the initialization function in the DLL to setup this object (also tried calling the wrapper functions for InitializeCriticalSectionEx
) but these all led to crashes (we could implement some syscalls and try again but we are lazy!
Our solution is to just patch out the EnterCriticalSection
and LeaveCriticalSection
calls in the loaded ntdll
using a simple return 0.
33 C0 xor eax,eax
C2 04 00 ret 4
Dumpulator Patching Out Functions
dp = Dumpulator("/tmp/mat.dmp", quiet=True)
ntdll_start = 0x77a10000
patch_bytes = b'\x33\xC0\xC2\x04\x00'
ptr_RtlLeaveCriticalSection = 0x77A5FFF0
ptr_RtlEnterCriticalSection = 0x77A5FDF0
get_str_fn = 0x70861000
#tohex(dp.read(ntdll_init_crit_sec,10))
dp.write(ptr_RtlLeaveCriticalSection, patch_bytes)
dp.write(ptr_RtlEnterCriticalSection, patch_bytes)
dp.call(get_str_fn)
from dumpulator import Dumpulator, syscall
from dumpulator.native import *
@syscall
def ZwQueryVolumeInformationFile(dp: Dumpulator,
FileHandle: HANDLE,
IoStatusBlock: P(IO_STATUS_BLOCK),
FsInformation: PVOID,
Length: ULONG,
FsInformationClass: FSINFOCLASS
):
return STATUS_SUCCESS
dp = Dumpulator("/tmp/mat2.dmp", quiet=True)
dll_base_addr = 0x71950000
dp.start(dp.regs.eip, end=dp.read_ptr(dp.regs.esp))
get_str_fn = 0x71951000
dp.read_str(dp.call(get_str_fn))
egg = rb'(\xC6\x45..){4}'
egg = rb'\x55\x8b\xec\x83\xec\x08\x33\xc0\x88\x45\xff'
stack_string_offsets = []
for m in re.finditer(egg, file_data):
fn_offset = m.start()
fn_addr = pe.get_rva_from_offset(fn_offset) + dll_base_addr
tmp_str = dp.read_str(dp.call(fn_addr))
print(tmp_str)
Revising Our Config Extractor
Now that we have a base config extractor we need to test this across multiple samples/versions of the malware to make sure it is robust enough.
Our initial sample was f8cc2cf36e193774f13c9c5f23ab777496dcd7ca588f4f73b45a7a5ffa96145e
.
We have collected the following samples to triage:
b9b399dbb5d901c16d97b7c30cc182736cd83a7c53313194a1798d61f9c7501e
-
c41f7b7ec0909d5c21107ddebc2fe84dbc137f701b42943c1a5e69f5d50e05ab
-
b4e7710488c2b7aaa71688b8bd546410af07a215c2e835e8dfbe24887186bd4f
-
60f030597c75f9df0f7a494cb5432b600d41775cfe5cf13006c1448fa3a68d8d
-
58a673023bbc7f2726e3b7ac917a43d9306692dc87b638ee21b98705a3262ccd
-
4b87f95c4477fc66c58b8e16a74f9c47217913cb4a78dc69f27a364a099acd90
4eb85a5532b98cbc4a6db1697cf46b9e2b7e28e89d6bbfc137b36c0736cd80e2
More samples from Rattle (these work with our existing tools)
67a9e8599ab71865a97e75dae9be438c24d015a93e6a12fb5b450ec558528290
075c904c5e18cd49f7d48f0fd1fc67d0921d51c7effb9233a3c92fbfa4f218ed
60f030597c75f9df0f7a494cb5432b600d41775cfe5cf13006c1448fa3a68d8d
These samples don't work with our first version of the config extractor:
e58b9bbb7bcdf3e901453b7b9c9e514fed1e53565e3280353dccc77cde26a98e.bin
b9b399dbb5d901c16d97b7c30cc182736cd83a7c53313194a1798d61f9c7501e.bin
def unhex(hex_string):
import binascii
if type(hex_string) == str:
return binascii.unhexlify(hex_string.encode('utf-8'))
else:
return binascii.unhexlify(hex_string)
def tohex(data):
import binascii
if type(data) == str:
return binascii.hexlify(data.encode('utf-8'))
else:
return binascii.hexlify(data)
import re
import struct
import pefile
FILE_PATH = '/tmp/samples/e58b9bbb7bcdf3e901453b7b9c9e514fed1e53565e3280353dccc77cde26a98e.bin'
FILE_PATH = '/tmp/samples/orig.bin'
FILE_PATH = '/tmp/samples/bd_rony.bin'
file_data = open(FILE_PATH,'rb').read()
def xor_decrypt(data, key):
out = []
for i in range(len(data)):
out.append(data[i] ^ key[i%len(key)])
return bytes(out)
def is_ascii(data):
return re.match(B"^[\s!-~]+\0*$", data) is not None
def filter(data):
return re.match(rb'[\f\v\t]', data) is not None
stack_strings = []
#string_egg = rb'(\xC6\x45..){2,}'
string_egg = rb'(?P<a>(?:\xC6\x85.{5}){0,})(?P<b>(?:\xC6\x45..){1,})'
for m in re.finditer(string_egg, file_data):
match_data = m.group(0)
#stack_strings.append({"data":match_data.replace(b'\xC6\x45',b'')[1::2], "match":match_data})
stack_strings.append({"data":m['a'][6::7] + m['b'][3::4], "match":match_data})
keys = []
key_byte_len_egg = rb'\x68(....)\x68(....)\x6a\x00\x6a(.)'
for m in re.finditer(key_byte_len_egg, file_data):
keys.append( {'key':m.group(2) + m.group(1), 'length':ord(m.group(3))})
key_dw_len_egg = rb'\x68(....)\x68(....)\x6a\x00\x68(....)'
for m in re.finditer(key_dw_len_egg, file_data):
keys.append( {'key':m.group(2) + m.group(1), 'length':struct.unpack('<I',m.group(3))[0]})
for sj in stack_strings:
s = sj.get('data')
str_len = len(s)
if str_len < 5:
continue
#print(f"\n\n{str_len}")
flag_found = False
#print('\n')
#print(f'{tohex(sj.get("match"))}')
tmp_strings = []
for k in keys:
if k.get('length') == str_len:
#print(f"found key: {tohex(k.get('key'))}")
out = xor_decrypt(s, k.get('key'))
if is_ascii(out) and not filter(out):
tmp_strings.append(out)
flag_found = True
if not flag_found:
tmp_xoe_dec = xor_decrypt(s[1:-1], bytes([s[0]]))
if is_ascii(tmp_xoe_dec) and not filter(tmp_xoe_dec):
print(tmp_xoe_dec)
else:
#print(f"\n{tmp_strings}")
spec_char_egg = rb'[^A-Za-z0-9\s\./\\\%]{1}'
best_match = min( (len(re.findall(spec_char_egg, s)),s) for s in tmp_strings )[1]
print(best_match)
import re
import struct
import pefile
def xor_decrypt(data, key):
out = []
for i in range(len(data)):
out.append(data[i] ^ key[i%len(key)])
return bytes(out)
def is_ascii(data):
return re.match(B"^[\s!-~]+\0*$", data) is not None
def get_strings(file_path):
file_data = open(file_path,'rb').read()
stack_strings = []
#string_egg = rb'(\xC6\x45..){2,}'
string_egg = rb'(?P<a>(?:\xC6\x85.{5}){0,})(?P<b>(?:\xC6\x45..){3,})'
for m in re.finditer(string_egg, file_data):
match_data = m.group(0)
#stack_strings.append({"data":match_data.replace(b'\xC6\x45',b'')[1::2], "match":match_data})
stack_strings.append({"data":m['a'][6::7] + m['b'][3::4], "match":match_data})
keys = []
key_byte_len_egg = rb'\x68(....)\x68(....)\x6a\x00\x6a(.)'
for m in re.finditer(key_byte_len_egg, file_data):
keys.append( {'key':m.group(2) + m.group(1), 'length':ord(m.group(3))})
key_dw_len_egg = rb'\x68(....)\x68(....)\x6a\x00\x68(....)'
for m in re.finditer(key_dw_len_egg, file_data):
keys.append( {'key':m.group(2) + m.group(1), 'length':struct.unpack('<I',m.group(3))[0]})
out_strings = []
for sj in stack_strings:
s = sj.get('data')
str_len = len(s)
flag_found = False
tmp_out = []
for k in keys:
if k.get('length') == str_len:
out = xor_decrypt(s, k.get('key'))
if is_ascii(out):
tmp_out.append(out)
if len(tmp_out) == 0:
out = xor_decrypt(s[1:-1], bytes([s[0]]))
if is_ascii(out):
out_strings.append(out)
elif len(tmp_out) == 1:
out_strings.append(tmp_out[0])
else:
# This is a hack
# We have strings that will give valid ascii for multiple keys
# So far these have only been dll names
for out in tmp_out:
if out[:-3].lower() == b'dll':
out_strings.append(out)
break
return list(set(out_strings))
get_strings(FILE_PATH)
import os
for filename in os.listdir('/tmp/samples'):
if filename.endswith(".bin"):
file_path = os.path.join('/tmp/samples', filename)
print(f"\n{file_path}")
print(get_strings(file_path))
Sample Using ADV String Obfuscation
A newer sample uses some type of ADV string obfuscation b9b399dbb5d901c16d97b7c30cc182736cd83a7c53313194a1798d61f9c7501e
. We can probably use our old dumpulator tricks for this.
The start of the .text
section contains a vtable with all of the string decryption functions.
from dumpulator import Dumpulator, syscall
from dumpulator.native import *
@syscall
def ZwQueryVolumeInformationFile(dp: Dumpulator,
FileHandle: HANDLE,
IoStatusBlock: P(IO_STATUS_BLOCK),
FsInformation: PVOID,
Length: ULONG,
FsInformationClass: FSINFOCLASS
):
return STATUS_SUCCESS
dp = Dumpulator("/tmp/b9b.dmp", quiet=True)
dp.start(dp.regs.eip, end=dp.read_ptr(dp.regs.esp))
functs = [0x74040976,0x740409A4,0x740409BA,0x74040998,0x7404098C,0x7403F910,0x7403F91C,0x7403F967,0x7403F9B2,0x7403F9FD,0x7403FA48,0x7403FA93,0x7403FADE,0x7403FB44,0x7403FBAE,0x7403FC20,0x7403FC98,0x7403FD06,0x7403FD74,0x7403FDDD,0x7403FE43,0x7403FEBF,0x7403FF46,0x7403FFD5,0x74040065,0x740400DC,0x74040146,0x740401A2,0x740401FE,0x7404025A,0x740402B6,0x74040312,0x74040378,0x740403D4,0x74040438,0x7404048B,0x740404DE,0x74040542,0x7404059E,0x740405F1,0x74040644,0x740406A0,0x74040706,0x7404074F,0x740407AB,0x74040807,0x7404085A,0x740408B6,0x7404091A]
for fn in functs:
dp.call(fn,[])
str_tbl_start = 0x7407FDB4
str_tbl_end = 0x7407FE7C
str_tbl_start = 0x7407E000
str_tbl_end = 0x74080224
for ptr in range(str_tbl_start,str_tbl_end,4):
try:
ss = dp.read_str(dp.read_ptr(ptr))
if len(ss) > 4:
print(ss)
except:
continue
Different Sample From Same Family
This is clearly a different sample based on the decrypted strings, but it seems to be part of the matanbuchus family... maybe this is a payload instead of a loader? The sample matches analysis from this blog: Introduction of a PE file extractor for various situations>
We need to figure out why these samples are so different...
Taking a Closer Look At Obfuscated Samples
Obfuscated sample
-
b9b399dbb5d901c16d97b7c30cc182736cd83a7c53313194a1798d61f9c7501e
- does match yara (2021-11-12 11:47:44 UTC)
Rony
-
bd68ecd681b844232f050c21c1ea914590351ef64e889d8ef37ea63bd9e2a2ec
- doesn't match yara (2022-06-14 10:30:32 UTC)
These samples appear to be the same (they are likely the "payload" portion of Matanbuchus) but the earlier sample uses obfuscated string encryption, while the newer sample uses the simpler stack based string encryption.
Fixing our Yara Rule to Match the Stack Based String Encryption Payload (as well as loaders)
This was a simple fix! The payload does not contain the murmur hash code, once we removed that the yara rule matched all samples.
b'Content-Length: \x00'
b'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe \x00'
b'collectiontelemetrysystem.com\x00'
b'DllRegisterServer\x00'
b'097f5m\x00'
b'Running dll in memory #3 (DllInstall(Unstall))\x00'
b'runas\x00'
b'.exe\x00'
b'timeout /t 3 && move /Y \x00'
b'Running dll in memory #3 (DllInstall(Install))\x00'
b'.exe\x00'
b'.exe\x00'
b'TiC7\x00'
b'.nls\x00'
b'Run PS in memory\x00'
b'Admin\x00'
b'%LOCALAPPDATA%\\\x00'
b'DllInstall\x00'
b'cmd.exe /c \x00'
b'collectiontelemetrysystem.com\x00'
b'Not in domain\x00'
b'regsvr32.exe \x00'
b'%PROCESSOR_REVISION%\\\x00'
b'%APPDATA%\\\x00'
b'41.4.0\x00'
b'8QN04\x00'
b'64 Bit\x00'
b'8S2x\x00'
b'Starting the exe with parameters\x00'
b'C:\\Windows\\System32\\cmd.exe /c \x00'
b'cmd.exe /c \x00'
b'High start exe\x00'
b'Running exe\x00'
b'User-Agent: \x00'
b'%PROCESSOR_REVISION%\\\x00'
b'Content-Type: application/x-www-form-urlencoded\x00'
b'%APPDATA%\\\x00'
b'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/\x00'
b'\\explorer.exe\x00'
b'MemLoadDllMain || MemLoadExe\x00'
b'Run CMD in memory\x00'
b'3CEk\x00'
b'3m7x\x00'
b'.nls\x00'
b'%APPDATA%\\\x00'
b'NCST \x00'
b'BCha\x00'
b"/c ECHO 'You must restart the program to resolve a critical error!' && start \x00"
b'7eriel\x00'
b'%APPDATA%\\\x00'
b'.nls\x00'
b'%PROCESSOR_REVISION%\\\x00'
b"\\'z{VIS\rA6\rb\x00"
b'%LOCALAPPDATA%\\\x00'
b'IsWow64Process\x00'
b'rundll32.exe \x00'
b'%PROCESSOR_REVISION%\\\x00'
b'kernel32\x00'
b'%PROCESSOR_REVISION%\\\x00'
b'.nls\x00'
b'User\x00'
b'%PROCESSOR_REVISION%\\\x00'
b'.nls\x00'
b'\r\n\r\n\x00'
b'\rXOGONSEzBER%\x00'
b'%PROCESSOR_REVISION%\\\x00'
b'\nost: \x00'
b'MemLoadShellCode\x00'
b'Q6X6\x00'
b'timeout /t 3 && del \x00'
b'cmd.exe\x00'
b'%02X-%02X-%02X-%02X-%02X-%02X\x00'
b'Running dll in memory #2 (DllRegisterServer)\x00'
b'Crypt update & Bots upgrade\x00'
b'timeout /t 3 && del \x00'
b'3fe11\x00'
b'%PROCESSOR_ARCHITECTURE%\x00'
b'%PROCESSOR_ARCHITECTURE%\x00'
b'NSeyDX\x00'
b'%APPDATA%\\\x00'
b'4nes\x00'
b'jpofxs\x00'
b'Not in domain\x00'
b'DllInstall\x00'
b' && rd /s /q \x00'
b' && regsvr32.exe -e "\x00'
b'32 Bit\x00'
b'%USERDOMAIN%\x00'
from dumpulator import Dumpulator, syscall
from dumpulator.native import *
import pefile
@syscall
def ZwQueryVolumeInformationFile(dp: Dumpulator,
FileHandle: HANDLE,
IoStatusBlock: P(IO_STATUS_BLOCK),
FsInformation: PVOID,
Length: ULONG,
FsInformationClass: FSINFOCLASS
):
return STATUS_SUCCESS
dp = Dumpulator("/tmp/b9b.dmp", quiet=True)
dp.start(dp.regs.eip, end=dp.read_ptr(dp.regs.esp))
functs = [0x74040976,0x740409A4,0x740409BA,0x74040998,0x7404098C,0x7403F910,0x7403F91C,0x7403F967,0x7403F9B2,0x7403F9FD,0x7403FA48,0x7403FA93,0x7403FADE,0x7403FB44,0x7403FBAE,0x7403FC20,0x7403FC98,0x7403FD06,0x7403FD74,0x7403FDDD,0x7403FE43,0x7403FEBF,0x7403FF46,0x7403FFD5,0x74040065,0x740400DC,0x74040146,0x740401A2,0x740401FE,0x7404025A,0x740402B6,0x74040312,0x74040378,0x740403D4,0x74040438,0x7404048B,0x740404DE,0x74040542,0x7404059E,0x740405F1,0x74040644,0x740406A0,0x74040706,0x7404074F,0x740407AB,0x74040807,0x7404085A,0x740408B6,0x7404091A]
functs = [0x7403FC98]
for fn in functs:
dp.call(fn,[])
data_start = 0x7407E000
data_end = 0x74080228
for ptr in range(data_start,data_end,4):
try:
ss = dp.read_str(dp.read_ptr(ptr))
if len(ss) >= 4:
print(ss)
except:
continue
print("\n\n\n ===\n")
print(dp.read_str(dp.read_ptr(0x7407FDFC )))
FILE_PATH = '/tmp/samples/b9b399dbb5d901c16d97b7c30cc182736cd83a7c53313194a1798d61f9c7501e.bin'
file_data = open(FILE_PATH,'rb').read()
def xor_decrypt(data, key):
out = []
for i in range(len(data)):
out.append(data[i] ^ key[i%len(key)])
return bytes(out)
def is_ascii(data):
return re.match(B"^[\s!-~]+\0*$", data) is not None
# .text:74049A7D C6 45 B8 0D mov byte ptr [ebp-48h], 13
# .text:74049A81 C7 45 E0 6E 60 69 23 mov dword ptr [ebp-20h], 2369606Eh
# .text:74049A88 C7 45 E4 68 75 68 2D mov dword ptr [ebp-1Ch], 2D687568h
# .text:74049A8F C7 45 E8 22 6E 2D 00 mov dword ptr [ebp-18h], 2D6E22h
# .text:74049A96 C7 45 EC DA 4E 1E 00 mov dword ptr [ebp-14h], 1E4EDAh
test_data = unhex('895db4c645b80dc745e06e606923c745e46875682dc745e8226e2d00c745ecda4e1e0052')
stack_strings = []
string_egg = rb'(?P<a>(?:\xC6\x45..){1})(?P<b>(?:\xC7\x45.....){2,})'
for m in re.finditer(string_egg, file_data):
match_data = m.group(0)
#print(tohex(match_data))
key = m['a'][3]
raw_data = m['b']
str_data = b''
for i in range(0,len(raw_data),7):
str_data += raw_data[i:i+7][-4:]
print(xor_decrypt(str_data, bytes([key])))
# .text:7404406B C7 45 AC 71 5D 48 5D mov dword ptr [ebp-54h], 5D485D71h
# .text:74044072 C7 45 B0 52 5E 49 5F mov dword ptr [ebp-50h], 5F495E52h
# .text:74044079 C7 45 B4 54 49 4F 0A mov dword ptr [ebp-4Ch], 0A4F4954h
# .text:74044080 66 C7 45 B8 0A 0A mov word ptr [ebp-48h], 0A0Ah
# .text:74048CF5 C6 45 B8 31 mov byte ptr [ebp-48h], 31h ; '1'
# .text:74048CF9 C7 45 E0 75 5D 5D 78 mov dword ptr [ebp-20h], 785D5D75h
# .text:74048D00 C7 45 E4 5F 42 45 50 mov dword ptr [ebp-1Ch], 5045425Fh
# .text:74048D07 66 C7 45 E8 5D 5D mov word ptr [ebp-18h], 5D5Dh
# .text:74048D0D 88 5D EA mov [ebp-16h], bl
# .text:74048D10 C7 45 EC DA 4E 1E 00 mov dword ptr [ebp-14h], 1E4EDAh
# .text:740454A9 30 14 08 xor [eax+ecx], dl
# .text:740454AC 41 inc ecx
# .text:740454AD 83 F9 1D cmp ecx, 1Dh
from dumpulator import Dumpulator, syscall
from dumpulator.native import *
import pefile
@syscall
def ZwQueryVolumeInformationFile(dp: Dumpulator,
FileHandle: HANDLE,
IoStatusBlock: P(IO_STATUS_BLOCK),
FsInformation: PVOID,
Length: ULONG,
FsInformationClass: FSINFOCLASS
):
return STATUS_SUCCESS
dp = Dumpulator("/tmp/b9b.dmp", quiet=True)
dp.start(dp.regs.eip, end=dp.read_ptr(dp.regs.esp))
start_addr = 0x74044390
end_addr = 0x740443C1
dp.start(start_addr, end=end_addr)
dp.read(dp.regs.ebx, 14)
String Decryption Recap
With these older samples, there are 3 different string decryption methods used
- Stack strings build with DWORDs that are decrypted using a single byte XOR, the byte is the first byte pushed onto the stack string (this is the same method used for small strings in the new samples)
- Global strings that are generated using constructors which have some light obfuscation. To deal with these we simply emulate all of the constructors and scrape the global strings from the
.data
section of the PE. - The third and most complex method relies on two calls to functions used to build the encrypted string and then a simple single-byte XOR to decrypt the string.
Complex Third String Decryption Method
c6 45 c4 53 57 6a 08
740458C7
.text:740436A5 BE DA 4E 1E 00 mov esi, 1E4EDAh
.text:740436AA 89 9D 28 FF FF FF mov [ebp+var_D8], ebx
.text:740436B0 89 9D 2C FF FF FF mov [ebp+var_D4], ebx
BE DA ?? ?? ?? 89 ?? ?? ?? ?? ?? 89
.text:740480FF 89 5D B0 mov [ebp-50h], ebx
.text:74048102 8B D3 mov edx, ebx
.text:74048104 89 5D B4 mov [ebp-4Ch], ebx
.text:74048107 89 5D B8 mov [ebp-48h], ebx
.text:7404810A C6 45 BC 1A mov byte ptr [ebp-44h], 1Ah
89 ?? ?? 8b ?? 89 ?? ?? 89 ?? ?? c6 45
.text:740431A2 72 F7 jb short loc_7404319B
.text:740431A4 88 59 0C mov [ecx+12], bl
.text:740438E2 72 F7 jb short loc_740438DB
.text:740438E4 88 59 0B mov [ecx+0Bh], bl
from dumpulator import Dumpulator, syscall
from dumpulator.native import *
import pefile
@syscall
def ZwQueryVolumeInformationFile(dp: Dumpulator,
FileHandle: HANDLE,
IoStatusBlock: P(IO_STATUS_BLOCK),
FsInformation: PVOID,
Length: ULONG,
FsInformationClass: FSINFOCLASS
):
return STATUS_SUCCESS
def emulate(start_addr, end_addr, ret_reg, str_len):
dp = Dumpulator("/tmp/b9b.dmp", quiet=True)
dp.start(dp.regs.eip, end=dp.read_ptr(dp.regs.esp))
dp.start(start_addr, end=end_addr)
return dp.read(dp.regs.__getitem__(ret_reg), str_len)
pe = pefile.PE(data=file_data)
em_egg = rb'\xBE\xDA...\x89.....\x89.+?(?=\x72\xf7\x88)'
for m in re.finditer(em_egg, file_data):
start_offset = m.start()
end_offset = m.end() + 2
start_addr = pe.get_rva_from_offset(start_offset) + 0x74030000
end_addr = pe.get_rva_from_offset(end_offset) + 0x74030000
str_len = file_data[end_offset + 2]
if file_data[end_offset + 1] == 0x5f:
reg_name = 'edi'
elif file_data[end_offset + 1] == 0x59:
reg_name = 'ecx'
print(f"Testing: {hex(start_addr)}")
try:
print(emulate(start_addr, end_addr, reg_name, str_len ))
except:
print("fail")
continue