ISFB / GOZI / RM3 Config Extraction
Static config extraction for one of the longest running malwares
Overview
Sample
Sha256: 33D6C2BF629E34D4F11F3C680A3EF60501769DBDAC658E3A4A119D5AC81BFF79
MalwareBazaar
References
import pefile
import struct
import malduck
FILE_PATH = '/tmp/33d6c2bf629e34d4f11f3c680a3ef60501769dbdac658e3a4a119d5ac81bff79.bin'
file_data = open(FILE_PATH, 'rb').read()
def is_ascii(s):
return all(c < 128 or c == 0 for c in s)
def get_config_entry(data, ptr):
# Read the marker WORD
marker = None
marker_data = data[ptr:ptr + 2]
if is_ascii(marker_data):
marker = marker_data.decode('utf-8')
ptr += 2
# Skip the remaining header
ptr += 6
# Read hash value
hash_value = struct.unpack("<I",data[ptr:ptr+4])[0]
ptr += 4
# Read config RVA
rva = struct.unpack("<I",data[ptr:ptr+4])[0]
ptr += 4
# Read the config size
size = struct.unpack("<I",data[ptr:ptr+4])[0]
ptr +=4
return {"marker":marker,"hash_value":hash_value,"rva":rva,"size":size}, ptr
def get_config_entries(data, ptr):
config_entries = []
config_entry,ptr = get_config_entry(data, ptr)
config_entries.append(config_entry)
while data[ptr] != 0:
config_entry,ptr = get_config_entry(data, ptr)
config_entries.append(config_entry)
return config_entries
def get_section_table_end(pe):
end_of_last_section = None
if pe.FILE_HEADER.Machine == 0x014c:
#32bit
image_section_header_size = 0x28
#image_section_header = image_dos_header.e_lfanew + sizeof(c_uint) + sizeof(IMAGE_FILE_HEADER) + x * sizeof(IMAGE_SECTION_HEADER) + image_nt_headers.FileHeader.SizeOfOptionalHeader
end_of_last_section = pe.NT_HEADERS.FILE_HEADER.SizeOfOptionalHeader + pe.DOS_HEADER.e_lfanew + 4 + image_section_header_size * pe.FILE_HEADER.NumberOfSections
elif pe.FILE_HEADER.Machine == 0x8664:
#64bit
image_section_header_size = 0x28
end_of_last_section = pe.NT_HEADERS.FILE_HEADER.SizeOfOptionalHeader + pe.DOS_HEADER.e_lfanew + 24 + image_section_header_size * pe.FILE_HEADER.NumberOfSections
return end_of_last_section
def config_table_scan(data, scan_start, scan_end):
ptr = scan_start
while ptr < scan_end:
if data[ptr] != 0:
break
ptr += 1
if ptr == scan_end:
ptr = None
return ptr
def get_config_data(data, ptr, size):
return data[ptr:ptr+size]
def config_wordlist_handler(config_data):
ptxt_data = malduck.aplib.decompress(config_data)
hash_value = struct.unpack('<I',ptxt_data[:4])[0]
wordlist = ptxt_data[4:].decode('utf-8')
return {"type_hash":hash_value, "wordlist":wordlist.strip('\r\n').split('\r\n')}
def config_crc_client_ini_handler(config_data):
ptxt_data = malduck.aplib.decompress(config_data)
entry_data = []
for entry in ptxt_data.split(b"\x00"):
if len(entry) > 1:
if is_ascii(entry):
entry_data.append(entry.decode('utf-8'))
c2_data = max(entry_data, key=len)
return c2_data.split(' ')
pe = pefile.PE(data=file_data)
ptr_section_table_end = get_section_table_end(pe)
assert ptr_section_table_end is not None
ptr_config_table = config_table_scan(file_data, ptr_section_table_end + 1, pe.sections[0].PointerToRawData)
assert ptr_config_table is not None
# Determine the ISFB version based on the marker
marker_versions = {"FJ":"old_isfb",
"J1":"old_isfb",
"J2":"dreambot",
"J3":"isfb_v3_japan",
"JJ":"isfb_v2.14+",
"WD":"RM3"
}
marker = file_data[ptr_config_table:ptr_config_table+2].decode('utf-8')
print(f"Bot version: {marker_versions.get(marker,'Unknown')}")
# Currently we can't handle the new RM3 config entry structure
assert marker != "WD"
config_entries = get_config_entries(file_data, ptr_config_table)
assert len(config_entries) != 0
# Loop through configs and handle each type
for config_entry in config_entries:
config_type_hash = config_entry.get("hash_value")
config_rva = config_entry.get("rva")
config_offset = pe.get_offset_from_rva(config_rva)
assert config_offset < len(file_data)
config_size = config_entry.get("size")
assert config_size < 0x1000
# Get config data
config_data = get_config_data(file_data, config_offset, config_size)
# Get config type handler
if config_type_hash == 0xe1285e64:
# CRC_PUBLIC_KEY
print(f"CRC_PUBLIC_KEY at offset {hex(config_offset)}")
elif config_type_hash in [0x8fb1dde1, 0xd722afcb]:
# CRC_CLIENT_INI
print(f"CRC_CLIENT_INI at offset {hex(config_offset)}")
c2_list = config_crc_client_ini_handler(config_data)
print("C2 List")
for c2 in c2_list:
print(f"\t{c2}")
elif config_type_hash == 0x7a042a8a:
# CRC_INSTALL_INI
print(f"CRC_INSTALL_INI at offset {hex(config_offset)}")
elif config_type_hash == 0x90f8aab4:
# CRC_CLIENT64
print(f"CRC_CLIENT64 at offset {hex(config_offset)}")
elif config_type_hash in [0xda57d71a, 0x68ebb983]:
# CRC_WORDLIST
print(f"CRC_WORDLIST at offset {hex(config_offset)}")
wordlist_data = config_wordlist_handler(config_data)
print(wordlist_data)
else:
print(f"Unhandled config type {hex(config_type_hash)}")
hex(struct.unpack('<I',b'\x89j\xc0\xd8')[0])
TODO: Parse each of the config values individually