diff --git a/modules/processing/parsers/CAPE/AsyncRat.py b/modules/processing/parsers/CAPE/AsyncRat.py index 12a5bcb2a60..a498e1fdc0d 100644 --- a/modules/processing/parsers/CAPE/AsyncRat.py +++ b/modules/processing/parsers/CAPE/AsyncRat.py @@ -2,8 +2,10 @@ import base64 import logging +import os import string import struct +from urllib.parse import urlparse import yara from Cryptodome.Cipher import AES @@ -14,6 +16,8 @@ DESCRIPTION = "AsyncRat configuration parser." AUTHOR = "Based on work of c3rb3ru5" +IP_REGEX = r"(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)" + rule_source = """ rule asyncrat { meta: @@ -101,17 +105,50 @@ def extract_config(filebuf): key = base64.b64decode(get_string(data, 7)) log.debug("extracted key: " + str(key)) try: + family = "asyncrat" + hosts = decrypt_config_item_list(key, data, 2) + ports = decrypt_config_item_list(key, data, 1) + version = decrypt_config_item_printable(key, data, 3) + install_folder = get_wide_string(data, 5) + install_file = get_wide_string(data, 6) + install = decrypt_config_item_printable(key, data, 4) + mutex = decrypt_config_item_printable(key, data, 8) + pastebin = decrypt(key, base64.b64decode(data[12][1:])).encode("ascii").replace(b"\x0f", b"") + config = { - "family": "asyncrat", - "hosts": decrypt_config_item_list(key, data, 2), - "ports": decrypt_config_item_list(key, data, 1), - "version": decrypt_config_item_printable(key, data, 3), - "install_folder": get_wide_string(data, 5), - "install_file": get_wide_string(data, 6), - "install": decrypt_config_item_printable(key, data, 4), - "mutex": decrypt_config_item_printable(key, data, 8), - "pastebin": decrypt(key, base64.b64decode(data[12][1:])).encode("ascii").replace(b"\x0f", b""), + "family": family, + "version": version, + "category": "rat", + "mutex": mutex, + "paths": [{"path": os.path.join(install_folder, install_file), "usage": "install" if install else "other"}], + "other": { + # No context around how these are used + "hosts": hosts, + "ports": ports, + }, } + + if pastebin != b"null": + parsed_url = urlparse(pastebin).decode() + port = parsed_url.port + if not port: + port = 443 if parsed_url.scheme == "https" else 80 + + config.update( + { + "http": [ + { + "uri": parsed_url.geturl(), + "protocol": parsed_url.scheme, + "hostname": parsed_url.netloc, + "port": port, + "path": parsed_url.path, + "method": "GET", + "usage": "c2", + } + ] + } + ) except Exception as e: print(e) return {} diff --git a/modules/processing/parsers/CAPE/Azorult.py b/modules/processing/parsers/CAPE/Azorult.py index aed7457f865..fc1641c35aa 100644 --- a/modules/processing/parsers/CAPE/Azorult.py +++ b/modules/processing/parsers/CAPE/Azorult.py @@ -54,10 +54,11 @@ def string_from_offset(data, offset): def extract_config(filebuf): pe = pefile.PE(data=filebuf, fast_load=False) image_base = pe.OPTIONAL_HEADER.ImageBase + config = {} ref_c2 = yara_scan(filebuf, "$ref_c2") if ref_c2 is None: - return + return config ref_c2_offset = int(ref_c2["$ref_c2"]) @@ -71,6 +72,6 @@ def extract_config(filebuf): c2_domain = string_from_offset(filebuf, c2_list_offset) if c2_domain: - return {"address": c2_domain.decode()} + config["tcp"] = [{"server_domain": c2_domain.decode(), "usage": "c2"}] - return {} + return config diff --git a/modules/processing/parsers/CAPE/BackOffLoader.py b/modules/processing/parsers/CAPE/BackOffLoader.py index f62fd29461d..7c6615a530d 100644 --- a/modules/processing/parsers/CAPE/BackOffLoader.py +++ b/modules/processing/parsers/CAPE/BackOffLoader.py @@ -7,6 +7,8 @@ from Cryptodome.Cipher import ARC4 CFG_START = "1020304050607080" +AUTHOR = "CAPE" +DESCRIPTION = "BackOffLoader configuration parser." def RC4(key, data): @@ -16,24 +18,35 @@ def RC4(key, data): def extract_config(data): config_data = {} - pe = pefile.PE(data=data) - for section in pe.sections: - if b".data" in section.Name: - data = section.get_data() - if CFG_START != hexlify(unpack_from(">8s", data, offset=8)[0]): - return None - rc4_seed = bytes(bytearray(unpack_from(">8B", data, offset=24))) - key = md5(rc4_seed).digest()[:5] - enc_data = bytes(bytearray(unpack_from(">8192B", data, offset=32))) - dec_data = RC4(key, enc_data) - config_data = { - "Version": unpack_from(">5s", data, offset=16)[0], - "RC4Seed": hexlify(rc4_seed), - "EncryptionKey": hexlify(key), - "OnDiskConfigKey": unpack_from("20s", data, offset=8224)[0], - "Build": dec_data[:16].strip("\x00"), - "URLs": [url.strip("\x00") for url in dec_data[16:].split("|")], - } + try: + pe = pefile.PE(data=data) + for section in pe.sections: + if b".data" in section.Name: + data = section.get_data() + if CFG_START != hexlify(unpack_from(">8s", data, offset=8)[0]): + return None + rc4_seed = bytes(bytearray(unpack_from(">8B", data, offset=24))) + key = md5(rc4_seed).digest()[:5] + enc_data = bytes(bytearray(unpack_from(">8192B", data, offset=32))) + dec_data = RC4(key, enc_data) + config_data = { + "version": unpack_from(">5s", data, offset=16)[0], + "encryption": [ + { + "algorithm": "RC4", + "key": hexlify(key), + "seed": hexlify(rc4_seed), + "binaries": [{"data": dec_data[:16].strip("\x00")}], + "http": [{"uri": url} for url in [url.strip("\x00") for url in dec_data[16:].split("|")]], + "other": { + "OnDiskConfigKey": unpack_from("20s", data, offset=8224)[0], + }, + } + ], + } + except pefile.PEFormatError: + # This isn't a PE file, therefore unlikely to extract a configuration + pass return config_data diff --git a/modules/processing/parsers/CAPE/BackOffPOS.py b/modules/processing/parsers/CAPE/BackOffPOS.py index c3bc9693af0..a7070ec01fc 100644 --- a/modules/processing/parsers/CAPE/BackOffPOS.py +++ b/modules/processing/parsers/CAPE/BackOffPOS.py @@ -7,6 +7,8 @@ from Cryptodome.Cipher import ARC4 header_ptrn = b"Content-Type: application/x-www-form-urlencoded" +AUTHOR = "CAPE" +DESCRIPTION = "BackOffPOS configuration parser." def RC4(key, data): @@ -16,25 +18,34 @@ def RC4(key, data): def extract_config(data): config_data = {} - pe = pefile.PE(data=data) - for section in pe.sections: - if b".data" in section.Name: - data = section.get_data() - cfg_start = data.find(header_ptrn) - if not cfg_start or cfg_start == -1: - return None - start_offset = cfg_start + len(header_ptrn) + 1 - rc4_seed = bytes(bytearray(unpack_from(">8B", data, offset=start_offset))) - key = md5(rc4_seed).digest()[:5] - enc_data = bytes(bytearray(unpack_from(">8192B", data, offset=start_offset + 8))) - dec_data = RC4(key, enc_data) - config_data = { - "RC4Seed": hexlify(rc4_seed), - "EncryptionKey": hexlify(key), - "Build": dec_data[:16].strip("\x00"), - "URLs": [url.strip("\x00") for url in dec_data[16:].split("|")], - "Version": unpack_from(">5s", data, offset=start_offset + 16 + 8192)[0], - } + try: + pe = pefile.PE(data=data) + for section in pe.sections: + if b".data" in section.Name: + data = section.get_data() + cfg_start = data.find(header_ptrn) + if not cfg_start or cfg_start == -1: + return None + start_offset = cfg_start + len(header_ptrn) + 1 + rc4_seed = bytes(bytearray(unpack_from(">8B", data, offset=start_offset))) + key = md5(rc4_seed).digest()[:5] + enc_data = bytes(bytearray(unpack_from(">8192B", data, offset=start_offset + 8))) + dec_data = RC4(key, enc_data) + config_data = { + "version": unpack_from(">5s", data, offset=start_offset + 16 + 8192)[0], + "encryption": [ + { + "algorithm": "RC4", + "key": hexlify(key), + "seed": hexlify(rc4_seed), + "binaries": [{"data": dec_data[:16].strip("\x00")}], + "http": [{"uri": url} for url in [url.strip("\x00") for url in dec_data[16:].split("|")]], + } + ], + } + except pefile.PEFormatError: + # This isn't a PE file, therefore unlikely to extract a configuration + pass return config_data diff --git a/modules/processing/parsers/CAPE/BitPaymer.py b/modules/processing/parsers/CAPE/BitPaymer.py index 89be091e276..55369af2093 100644 --- a/modules/processing/parsers/CAPE/BitPaymer.py +++ b/modules/processing/parsers/CAPE/BitPaymer.py @@ -12,15 +12,16 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -DESCRIPTION = "BitPaymer configuration parser." -AUTHOR = "kevoreilly" - import string import pefile import yara from Cryptodome.Cipher import ARC4 +DESCRIPTION = "BitPaymer configuration parser." +AUTHOR = "kevoreilly" + + rule_source = """ rule BitPaymer { @@ -83,7 +84,7 @@ def extract_config(file_data): for item in raw.split(b"\x00"): data = "".join(convert_char(c) for c in item) if len(data) == 760: - config["RSA public key"] = data + config["encryption"] = [{"algorithm": "RSA", "public_key": data}] elif len(data) > 1 and "\\x" not in data: - config["strings"] = data + config["decoded_strings"] = [data] return config diff --git a/modules/processing/parsers/CAPE/BlackNix.py b/modules/processing/parsers/CAPE/BlackNix.py index 56dec06c9be..f9eb3fff0ed 100644 --- a/modules/processing/parsers/CAPE/BlackNix.py +++ b/modules/processing/parsers/CAPE/BlackNix.py @@ -1,19 +1,19 @@ import pefile +AUTHOR = "CAPE" +DESCRIPTION = "BlackNix configuration parser." + def extract_raw_config(raw_data): - try: - pe = pefile.PE(data=raw_data) - rt_string_idx = [entry.id for entry in pe.DIRECTORY_ENTRY_RESOURCE.entries].index(pefile.RESOURCE_TYPE["RT_RCDATA"]) - rt_string_directory = pe.DIRECTORY_ENTRY_RESOURCE.entries[rt_string_idx] - for entry in rt_string_directory.directory.entries: - if str(entry.name) == "SETTINGS": - data_rva = entry.directory.entries[0].data.struct.OffsetToData - size = entry.directory.entries[0].data.struct.Size - data = pe.get_memory_mapped_image()[data_rva : data_rva + size] - return data.split("}") - except Exception: - return None + pe = pefile.PE(data=raw_data) + rt_string_idx = [entry.id for entry in pe.DIRECTORY_ENTRY_RESOURCE.entries].index(pefile.RESOURCE_TYPE["RT_RCDATA"]) + rt_string_directory = pe.DIRECTORY_ENTRY_RESOURCE.entries[rt_string_idx] + for entry in rt_string_directory.directory.entries: + if str(entry.name) == "SETTINGS": + data_rva = entry.directory.entries[0].data.struct.OffsetToData + size = entry.directory.entries[0].data.struct.Size + data = pe.get_memory_mapped_image()[data_rva : data_rva + size] + return data.split("}") def decode(line): @@ -28,30 +28,34 @@ def extract_config(data): try: config_raw = extract_raw_config(data) if config_raw: - return { - "Mutex": decode(config_raw[1])[::-1], - "Anti Sandboxie": decode(config_raw[2])[::-1], - "Max Folder Size": decode(config_raw[3])[::-1], - "Delay Time": decode(config_raw[4])[::-1], - "Password": decode(config_raw[5])[::-1], - "Kernel Mode Unhooking": decode(config_raw[6])[::-1], - "User More Unhooking": decode(config_raw[7])[::-1], - "Melt Server": decode(config_raw[8])[::-1], - "Offline Screen Capture": decode(config_raw[9])[::-1], - "Offline Keylogger": decode(config_raw[10])[::-1], - "Copy To ADS": decode(config_raw[11])[::-1], - "Domain": decode(config_raw[12])[::-1], - "Persistence Thread": decode(config_raw[13])[::-1], - "Active X Key": decode(config_raw[14])[::-1], - "Registry Key": decode(config_raw[15])[::-1], - "Active X Run": decode(config_raw[16])[::-1], - "Registry Run": decode(config_raw[17])[::-1], - "Safe Mode Startup": decode(config_raw[18])[::-1], - "Inject winlogon.exe": decode(config_raw[19])[::-1], - "Install Name": decode(config_raw[20])[::-1], - "Install Path": decode(config_raw[21])[::-1], - "Campaign Name": decode(config_raw[22])[::-1], - "Campaign Group": decode(config_raw[23])[::-1], + config = { + "campaign_id": [config_raw["Campaign Name"], config_raw["Campaign Group"]], + "category": ["keylogger", "apt"], + "password": [config_raw["Password"]], + "mutex": [config_raw["Mutex"]], + "sleep_delay": config_raw["Delay Time"], + "paths": [{"path": config_raw["Install Path"], "usage": "install"}], + "registry": [{"key": config_raw["Registry Key"], "usage": "other"}], + "other": { + "Anti Sandboxie": config_raw["Anti Sandboxie"], + "Max Folder Size": config_raw["Max Folder Size"], + "Kernel Mode Unhooking": config_raw["Kernel Mode Unhooking"], + "User More Unhooking": config_raw["User More Unhooking"], + "Melt Server": config_raw["Melt Server"], + "Offline Screen Capture": config_raw["Offline Screen Capture"], + "Offline Keylogger": config_raw["Offline Keylogger"], + "Copy To ADS": config_raw["Copy To ADS"], + "Domain": config_raw["Domain"], + "Persistence Thread": config_raw["Persistence Thread"], + "Active X Key": config_raw["Active X Key"], + "Active X Run": config_raw["Active X Run"], + "Registry Run": config_raw["Registry Run"], + "Safe Mode Startup": config_raw["Safe Mode Startup"], + "Inject winlogon.exe": config_raw["Inject winlogon.exe"], + }, } + + return config + except Exception: - return None + return {} diff --git a/modules/processing/parsers/CAPE/Blister.py b/modules/processing/parsers/CAPE/Blister.py index 9c758117157..c3aeacb0798 100644 --- a/modules/processing/parsers/CAPE/Blister.py +++ b/modules/processing/parsers/CAPE/Blister.py @@ -20,6 +20,10 @@ log = logging.getLogger(__name__) # https://github.com/Robin-Pwner/Rabbit-Cipher/ +AUTHOR = "Based on work of soolidsnake" +DESCRIPTION = "Blister configuration parser." + + def ROTL8(v, n): return ((v << n) & 0xFF) | ((v >> (8 - n)) & 0xFF) @@ -436,12 +440,14 @@ def main(): main() # CAPE: Derived from decrypt_memory() + + def extract_config(data): try: pe = pefile.PE(data=data) except Exception: log.info("Not a PE file") - return -1 + return {} if pe.FILE_HEADER.Machine == 0x8664: arch_size = 8 @@ -468,7 +474,7 @@ def extract_config(data): if not key_offset or not tag_offset: log.info("Error: signature not found") - return -1 + return {} key_offset = key_offset[0].strings[0][0] tag_offset = tag_offset[0].strings[0][0] @@ -516,7 +522,7 @@ def extract_config(data): key_pattern = decrypted_memory[key_pattern_offset + 12 : key_pattern_offset + 12 + 4] else: log.info("key_pattern_rule: Error signature not found") - return 0 + return {} config_tag = (u32(key)) ^ (u32(key_pattern)) @@ -524,7 +530,7 @@ def extract_config(data): if encrypted_config_offset == -1: log.info("Encrypted config not found") - return -1 + return {} config_size = 0x644 @@ -556,7 +562,7 @@ def extract_config(data): elif (flag & 0x10) != 0: injection_method = "Process hollowing IE or Werfault" - config = { + config_raw = { "Flag": hex(flag), "Payload export hash": hex(u32(payload_export_hash)), "Payload filename": w_payload_filename_and_cmdline, @@ -569,4 +575,24 @@ def extract_config(data): "Injection method": injection_method, } + config = { + "sleep_delay": config_raw["Sleep after injection"], + "binaries": [ + { + "datatype": "payload", + "other": { + "Payload filename": config_raw["Payload filename"], + "Payload export hash": config_raw["Payload export hash"], + }, + } + ], + "encryptions": {"algorithm": "rabbit", "key": config_raw["Rabbit key"], "seed": config_raw["Rabbit IV"]}, + "other": { + "Compressed data size": config_raw["Compressed data size"], + "Uncompressed data size": config_raw["Uncompressed data size"], + "Persistence": config_raw["Persistence"], + "Injection method": config_raw["Injection method"], + }, + } + return config diff --git a/modules/processing/parsers/CAPE/BuerLoader.py b/modules/processing/parsers/CAPE/BuerLoader.py index b0dde9968d1..bdf706a8ee3 100644 --- a/modules/processing/parsers/CAPE/BuerLoader.py +++ b/modules/processing/parsers/CAPE/BuerLoader.py @@ -16,6 +16,20 @@ DESCRIPTION = "BuerLoader configuration parser." AUTHOR = "kevoreilly" +rule_source = """ +rule BuerLoader +{ + meta: + author = "kevoreilly & Rony (@r0ny_123)" + cape_type = "BuerLoader Payload" + strings: + $trap = {0F 31 89 45 ?? 6A 00 8D 45 ?? 8B CB 50 E8 [4] 0F 31} + $decode = {8A 0E 84 C9 74 0E 8B D0 2A 0F 46 88 0A 42 8A 0E 84 C9 75 F4 5F 5E 5D C2 04 00} + $op = {33 C0 85 D2 7E 1? 3B C7 7D [0-15] 40 3B C2 7C ?? EB 02} + condition: + uint16(0) == 0x5A4D and 2 of them +} +""" def decrypt_string(string): @@ -32,8 +46,10 @@ def extract_config(filebuf): for item in data.split(b"\x00\x00"): try: dec = decrypt_string(item.lstrip(b"\x00").rstrip(b"\x00").decode()) + if "dll" not in dec and " " not in dec and ";" not in dec and "." in dec: + cfg["other"] = {"address": dec} except Exception: pass - if "dll" not in dec and " " not in dec and ";" not in dec and "." in dec: - cfg.setdefault("address", []).append(dec) - return cfg + if cfg: + cfg["family"] = "BuerLoader" + return cfg diff --git a/modules/processing/parsers/CAPE/BumbleBee.py b/modules/processing/parsers/CAPE/BumbleBee.py index dc166cb3d9b..6015cf205d2 100644 --- a/modules/processing/parsers/CAPE/BumbleBee.py +++ b/modules/processing/parsers/CAPE/BumbleBee.py @@ -128,6 +128,8 @@ def extract_config(data): # Extract config ciphertext config_match = regex.search(data) + if not config_match: + return cfg campaign_id, botnet_id, c2s = extract_config_data(data, pe, config_match) # RC4 Decrypt diff --git a/modules/processing/parsers/CAPE/ChChes.py b/modules/processing/parsers/CAPE/ChChes.py index f6f7619710d..508fac9db0e 100644 --- a/modules/processing/parsers/CAPE/ChChes.py +++ b/modules/processing/parsers/CAPE/ChChes.py @@ -11,10 +11,11 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import yara + DESCRIPTION = "ChChes configuration parser." AUTHOR = "kevoreilly" -import yara rule_source = """ rule ChChes @@ -63,10 +64,7 @@ def extract_config(filebuf): if yara_matches.get("$payload3"): c2_offsets.append(0xE2B9) # no c2 for type4 - - for c2_offset in c2_offsets: - c2_url = string_from_offset(filebuf, c2_offset) - if c2_url: - tmp_config.setdefault("c2_url", []).append(c2_url) + c2_urls = [string_from_offset(filebuf, c2_offset) for c2_offset in c2_offsets if string_from_offset(filebuf, c2_offset)] + tmp_config = {"http": [{"uri": url, "usage": "c2"} for url in c2_urls]} return tmp_config diff --git a/modules/processing/parsers/CAPE/CobaltStrikeBeacon.py b/modules/processing/parsers/CAPE/CobaltStrikeBeacon.py index ced0988ab0d..58405f228ae 100644 --- a/modules/processing/parsers/CAPE/CobaltStrikeBeacon.py +++ b/modules/processing/parsers/CAPE/CobaltStrikeBeacon.py @@ -23,6 +23,40 @@ from netstruct import unpack as netunpack log = logging.getLogger(__name__) +AUTHOR = "Gal Kristal from SentinelOne" +DESCRIPTION = "Parses CobaltStrike Beacon's configuration from PE file or memory dump." +rule_source = """ +rule CobaltStrikeBeacon +{ + meta: + author = "ditekshen, enzo & Elastic" + description = "Cobalt Strike Beacon Payload" + cape_type = "CobaltStrikeBeacon Payload" + strings: + $s1 = "%%IMPORT%/%" fullword ascii + $s2 = "www6.%%x%%x.%%s" fullword ascii + $s3 = "cdn.%%x%%x.%%s" fullword ascii + $s4 = "api.%%x%%x.%%s" fullword ascii + $s5 = "%%s (admin)" fullword ascii + $s6 = "could not spawn %%s: %%d" fullword ascii + $s7 = "Could not kill %%d: %%d" fullword ascii + $s8 = "Could not connect to pipe (%%s): %%d" fullword ascii + $s9 = /%%s\\.\\d[(%%08x).]+\\.%%x%%x\\.%%s/ ascii + $pwsh1 = "IEX (New-Object Net.Webclient).DownloadString('http" ascii + $pwsh2 = "powershell -nop -exec bypass -EncodedCommand \\"%%s\\"" fullword ascii + $ver3a = {69 68 69 68 69 6b ?? ?? 69} + $ver3b = {69 69 69 69} + $ver4a = {2e 2f 2e 2f 2e 2c ?? ?? 2e} + $ver4b = {2e 2e 2e 2e} + $a1 = "%%02d/%%02d/%%02d %%02d:%%02d:%%02d" xor(0x00-0xff) + $a2 = "Started service %%s on %%s" xor(0x00-0xff) + $a3 = "%%s as %%s\\\\%%s: %%d" xor(0x00-0xff) + $b_x64 = {4C 8B 53 08 45 8B 0A 45 8B 5A 04 4D 8D 52 08 45 85 C9 75 05 45 85 DB 74 33 45 3B CB 73 E6 49 8B F9 4C 8B 03} + $b_x86 = {8B 46 04 8B 08 8B 50 04 83 C0 08 89 55 08 89 45 0C 85 C9 75 04 85 D2 74 23 3B CA 73 E6 8B 06 8D 3C 08 33 D2} + condition: + all of ($ver3*) or all of ($ver4*) or 2 of ($a*) or any of ($b*) or 5 of ($s*) or (all of ($pwsh*) and 2 of ($s*)) or (#s9 > 6 and 4 of them) +} +""" COLUMN_WIDTH = 35 SUPPORTED_VERSIONS = (3, 4) @@ -110,7 +144,7 @@ def binary_repr(self): def pretty_repr(self, full_config_data): data_offset = full_config_data.find(self.binary_repr()) if data_offset < 0: - return "Not Found" + return None repr_len = len(self.binary_repr()) conf_data = full_config_data[data_offset + repr_len : data_offset + repr_len + self.length] @@ -343,6 +377,10 @@ def _parse_config(self, version, quiet=False, as_json=False): for conf_name, packed_conf in settings: parsed_setting = packed_conf.pretty_repr(full_config_data) + if parsed_setting == None: + # Nothing of value + continue + if as_json: parsed_config[conf_name] = parsed_setting continue @@ -443,9 +481,69 @@ def parse_encrypted_config(self, version=None, quiet=False, as_json=False): print(json.dumps(parsed_config, cls=Base64Encoder)) +def beacon_settings_to_maco(output: dict): + if not output: + return + config = {"family": "Cobalt StrikeBeacon"} + + # SSH details + ssh = { + "hostname": output.pop("SSH_Host", None), + "port": output.pop("SSH_Port", None), + "username": output.pop("SSH_Username", None), + "password": output.pop("SSH_Password_Plaintext", None), + "public_key": output.pop("SSH_Password_Pubkey", None), + "usage": "c2", + } + [ssh.pop(k) for k in list(ssh.keys()) if not ssh[k]] + if len(ssh.keys()) > 1: + config["ssh"] = [ssh] + + # HTTP details + http = [] + c2_domain, c2_get_path = output.pop("C2Server", ",").split(",") + c2_post_path = output.pop("HttpPostUri", None) + if c2_domain: + protocol = output.get("BeaconType")[0] + if protocol in ["HTTPS", "HTTP"]: + port = output.pop("Port", None) + if not port: + port = 443 if protocol == "HTTPS" else 80 + user_agent = output.pop("UserAgent", None) + http_get = { + "uri": f"{protocol.lower()}://{c2_domain}{c2_get_path}", + "protocol": protocol.lower(), + "hostname": c2_domain, + "port": port, + "path": c2_get_path, + "method": output.pop("HttpGet_Verb", "GET"), + } + http_get.update({"user_agent": user_agent}) if user_agent else None + http.append(http_get) + if c2_post_path: + http_post = { + "uri": f"{protocol.lower()}://{c2_domain}{c2_get_path}", + "protocol": protocol.lower(), + "hostname": c2_domain, + "port": port, + "path": c2_post_path, + "method": output.pop("HttpPost_Verb", "POST"), + } + http_post.update({"user_agent": user_agent}) if user_agent else None + http.append(http_post) + config["http"] = http + + config.update({"pipe": [output.pop("PipeName")]}) if output.get("PipeName") else None + config.update({"sleep_delay": output.pop("SleepTime")}) if output.get("SleepTime") else None + # Other + config["other"] = output + return config + + # CAPE def extract_config(data): output = cobaltstrikeConfig(data).parse_config(quiet=False, as_json=True) if output is None: output = cobaltstrikeConfig(data).parse_encrypted_config(quiet=False, as_json=True) - return output + + return beacon_settings_to_maco(output) diff --git a/modules/processing/parsers/CAPE/DoppelPaymer.py b/modules/processing/parsers/CAPE/DoppelPaymer.py index e70da9d0299..cb8b7939594 100644 --- a/modules/processing/parsers/CAPE/DoppelPaymer.py +++ b/modules/processing/parsers/CAPE/DoppelPaymer.py @@ -12,15 +12,15 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -DESCRIPTION = "DoppelPaymer configuration parser." -AUTHOR = "kevoreilly" - import string import pefile -import yara from Cryptodome.Cipher import ARC4 +DESCRIPTION = "DoppelPaymer configuration parser." +AUTHOR = "kevoreilly" + + rule_source = """ rule DoppelPaymer { @@ -63,7 +63,9 @@ def extract_rdata(pe): def extract_config(filebuf): pe = pefile.PE(data=filebuf, fast_load=False) - config = {} + config = { + "family": "DoppelPaymer", + } blobs = filter(None, [x.strip(b"\x00\x00\x00\x00") for x in extract_rdata(pe).split(b"\x00\x00\x00\x00")]) for blob in blobs: if len(blob) < LEN_BLOB_KEY: @@ -74,7 +76,8 @@ def extract_config(filebuf): for item in raw.split(b"\x00"): data = "".join(convert_char(c) for c in item) if len(data) == 406: - config["RSA public key"] = data + config["encryption"] = [{"algorithm": "RSA", "public_key": data, "usage": "ransom"}] elif len(data) > 1 and "\\x" not in data: - config["strings"] = data + config.setdefault("decoded_strings", []) + config["decoded_strings"].append(data) return config diff --git a/modules/processing/parsers/CAPE/DridexLoader.py b/modules/processing/parsers/CAPE/DridexLoader.py index 3a0ee1e0553..1642d9186de 100644 --- a/modules/processing/parsers/CAPE/DridexLoader.py +++ b/modules/processing/parsers/CAPE/DridexLoader.py @@ -65,7 +65,7 @@ def extract_rdata(pe): def extract_config(filebuf): - cfg = {} + cfg = {"family": "Dridex"} pe = pefile.PE(data=filebuf, fast_load=False) image_base = pe.OPTIONAL_HEADER.ImageBase line, c2va_offset, delta = 0, 0, 0 @@ -121,7 +121,8 @@ def extract_config(filebuf): port = str(struct.unpack("H", filebuf[c2_offset + 4 : c2_offset + 6])[0]) if c2_address and port: - cfg.setdefault("address", []).append(f"{c2_address}:{port}") + cfg.setdefault("tcp", []) + cfg.append({"server_ip": c2_address, "server_port": port, "usage": "c2"}) c2_offset += 6 + delta @@ -144,14 +145,14 @@ def extract_config(filebuf): ) for item in raw.split(b"\x00"): if len(item) == LEN_BLOB_KEY - 1: - cfg["RC4 key"] = item.split(b";", 1)[0].decode() + cfg["encryption"] = [{"algorithm": "RSA", "public_key": item.split(b";", 1)[0].decode(), "usage": "ransom"}] if botnet_code: botnet_rva = struct.unpack("i", filebuf[botnet_code + 23 : botnet_code + 27])[0] - image_base if botnet_rva: botnet_offset = pe.get_offset_from_rva(botnet_rva) botnet_id = struct.unpack("H", filebuf[botnet_offset : botnet_offset + 2])[0] - cfg["Botnet ID"] = str(botnet_id) + cfg["other"] = {"Botnet ID": str(botnet_id)} # Might fall under identifier? return cfg diff --git a/modules/processing/parsers/CAPE/Emotet.py b/modules/processing/parsers/CAPE/Emotet.py index 8e8388dea00..96bc5d43b0a 100644 --- a/modules/processing/parsers/CAPE/Emotet.py +++ b/modules/processing/parsers/CAPE/Emotet.py @@ -27,6 +27,7 @@ log.setLevel(logging.INFO) AUTHOR = "kevoreilly" +DESCRIPTION = "Emotet configuration parser." rule_source = """ rule Emotet @@ -186,7 +187,7 @@ def extract_emotet_rsakey(pe): def extract_config(filebuf): - conf_dict = {} + conf_dict = {"family": "Emotet", "tcp": []} pe = None try: pe = pefile.PE(data=filebuf, fast_load=False) @@ -223,7 +224,7 @@ def extract_config(filebuf): port = str(struct.unpack("H", filebuf[c2_list_offset + 4 : c2_list_offset + 6])[0]) if not c2_address or not port: return - conf_dict.setdefault("address", []).append(f"{c2_address}:{port}") + conf_dict["tcp"].append({"server_ip": c2_address, "server_port": port, "usage": "c2"}) c2_list_offset += 8 elif yara_matches.get("$snippet4"): c2list_va_offset = int(yara_matches["$snippet4"]) @@ -244,7 +245,7 @@ def extract_config(filebuf): port = str(struct.unpack("H", filebuf[c2_list_offset + 4 : c2_list_offset + 6])[0]) if not c2_address or not port: return - conf_dict.setdefault("address", []).append(f"{c2_address}:{port}") + conf_dict["tcp"].append({"server_ip": c2_address, "server_port": port, "usage": "c2"}) c2_list_offset += 8 elif any( yara_matches.get(name, False) @@ -297,7 +298,7 @@ def extract_config(filebuf): port = str(struct.unpack("H", filebuf[c2_list_offset + 4 : c2_list_offset + 6])[0]) if not c2_address or not port: break - conf_dict.setdefault("address", []).append(f"{c2_address}:{port}") + conf_dict["tcp"].append({"server_ip": c2_address, "server_port": port, "usage": "c2"}) c2found = True c2_list_offset += 8 elif yara_matches.get("$snippet6"): @@ -323,7 +324,7 @@ def extract_config(filebuf): port = str(struct.unpack("H", filebuf[c2_list_offset + 4 : c2_list_offset + 6])[0]) if not c2_address or not port: break - conf_dict.setdefault("address", []).append(f"{c2_address}:{port}") + conf_dict["tcp"].append({"server_ip": c2_address, "server_port": port, "usage": "c2"}) c2found = True c2_list_offset += 8 elif yara_matches.get("$snippet7"): @@ -349,7 +350,7 @@ def extract_config(filebuf): port = str(struct.unpack("H", filebuf[c2_list_offset + 4 : c2_list_offset + 6])[0]) if not c2_address or not port: break - conf_dict.setdefault("address", []).append(f"{c2_address}:{port}") + conf_dict["tcp"].append({"server_ip": c2_address, "server_port": port, "usage": "c2"}) c2found = True c2_list_offset += 8 elif yara_matches.get("$snippetA"): @@ -371,7 +372,7 @@ def extract_config(filebuf): port = str(struct.unpack("H", filebuf[c2_list_offset + 4 : c2_list_offset + 6])[0]) if not c2_address or not port: break - conf_dict.setdefault("address", []).append(f"{c2_address}:{port}") + conf_dict["tcp"].append({"server_ip": c2_address, "server_port": port, "usage": "c2"}) c2found = True c2_list_offset += 8 elif yara_matches.get("$snippetD"): @@ -472,7 +473,7 @@ def extract_config(filebuf): port = str(struct.unpack(">H", c2_list[offset + 4 : offset + 6])[0]) if not c2_address or not port: break - conf_dict.setdefault("address", []).append(f"{c2_address}:{port}") + conf_dict["tcp"].append({"server_ip": c2_address, "server_port": port, "usage": "c2"}) c2found = True offset += 8 @@ -485,7 +486,7 @@ def extract_config(filebuf): log.error(e) if pem_key: # self.reporter.add_metadata("other", {"RSA public key": pem_key.exportKey().decode()}) - conf_dict.setdefault("RSA public key", pem_key.exportKey().decode()) + conf_dict.setdefault("encryption", []).append({"algorithm": "RSA", "public_key": pem_key.exportKey().decode()}) else: if yara_matches.get("$ref_rsa"): ref_rsa_offset = int(yara_matches["$ref_rsa"]) @@ -518,7 +519,9 @@ def extract_config(filebuf): seq = asn1.DerSequence() seq.decode(rsa_key) # self.reporter.add_metadata("other", {"RSA public key": RSA.construct((seq[0], seq[1])).exportKey()}) - conf_dict.setdefault("RSA public key", RSA.construct((seq[0], seq[1])).exportKey()) + conf_dict.setdefault("encryption", []).append( + {"algorithm": "RSA", "public_key": RSA.construct((seq[0], seq[1])).exportKey()} + ) else: ref_ecc_offset = 0 delta1 = 0 @@ -645,26 +648,32 @@ def extract_config(filebuf): eck_offset += 8 eck_key = xor_data(filebuf[eck_offset : eck_offset + size], key) key_len = struct.unpack(". +import yara + DESCRIPTION = "Enfal configuration parser." AUTHOR = "kevoreilly" -import yara rule_source = """ rule Enfal @@ -58,17 +59,20 @@ def list_from_offset(data, offset): def extract_config(filebuf): config = yara_scan(filebuf, "$config") - return_conf = {} + return_conf = {"family": "Enfal"} if config: yara_offset = int(config["$config"]) + http = dict() c2_address = string_from_offset(filebuf, yara_offset + 0x2E8) if c2_address: - return_conf["c2_address"] = c2_address + # Based on other extractors, this is the domain? + http["hostname"] = c2_address + # Assuming c2_url is related to c2_address c2_url = string_from_offset(filebuf, yara_offset + 0xE8) if c2_url: - return_conf["c2_url"] = c2_url + http["uri"] = c2_url if filebuf[yara_offset + 0x13B0 : yara_offset + 0x13B1] == "S": registrypath = string_from_offset(filebuf, yara_offset + 0x13B0) @@ -80,7 +84,7 @@ def extract_config(filebuf): registrypath = "" if registrypath: - return_conf["registrypath"] = registrypath + return_conf["registry"] = [{"key": registrypath, "usage": "c2"}] if filebuf[yara_offset + 0x14A2 : yara_offset + 0x14A3] == "C": servicename = "" @@ -100,7 +104,11 @@ def extract_config(filebuf): filepaths = [] if servicename: - return_conf["servicename"] = servicename + return_conf["service"] = [{"name": servicename}] if filepaths: for path in filepaths: - return_conf.setdefault("filepath", []).append(path) + return_conf.setdefault("paths", []).append({"path": path, "usage": "c2"}) + if http: + return_conf["http"] = [http] + + return return_conf diff --git a/modules/processing/parsers/CAPE/EvilGrab.py b/modules/processing/parsers/CAPE/EvilGrab.py index 2e4cae619dc..beace6fc34f 100644 --- a/modules/processing/parsers/CAPE/EvilGrab.py +++ b/modules/processing/parsers/CAPE/EvilGrab.py @@ -12,14 +12,15 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -DESCRIPTION = "EvilGrab configuration parser." -AUTHOR = "kevoreilly" - import struct import pefile import yara +DESCRIPTION = "EvilGrab configuration parser." +AUTHOR = "kevoreilly" + + rule_source = """ rule EvilGrab { @@ -80,31 +81,34 @@ def extract_config(filebuf): pe = pefile.PE(data=filebuf, fast_load=False) # image_base = pe.OPTIONAL_HEADER.ImageBase yara_matches = yara_scan(filebuf) - end_config = {} + end_config = {"family": "EvilGrab"} for key, values in map_offset.keys(): if not yara_matches.get(key): continue yara_offset = int(yara_matches[key]) + c2_tcp = dict() c2_address = string_from_va(pe, yara_offset + values[0]) if c2_address: - end_config["c2_address"] = c2_address + c2_tcp["server_ip"] = c2_address port = str(struct.unpack("h", filebuf[yara_offset + values[1] : yara_offset + values[1] + 2])[0]) if port: - end_config["port"] = [port, "tcp"] + c2_tcp["server_port"] = port missionid = string_from_va(pe, yara_offset + values[3]) if missionid: - end_config["missionid"] = missionid + end_config.setdefault("campaign_id", []).append(missionid) version = string_from_va(pe, yara_offset + values[4]) if version: end_config["version"] = version injectionprocess = string_from_va(pe, yara_offset + values[5]) if injectionprocess: - end_config["injectionprocess"] = injectionprocess + end_config.setdefault("inject_exe", []).append(injectionprocess) if key != "$configure3": mutex = string_from_va(pe, yara_offset - values[6]) if mutex: - end_config["mutex"] = mutex + end_config.setdefault("mutex", []).append(mutex) + if c2_tcp: + end_config.setdefault("tcp", []).append(c2_tcp) return end_config diff --git a/modules/processing/parsers/CAPE/Fareit.py b/modules/processing/parsers/CAPE/Fareit.py index 93bc8023e6c..a8f6b67223e 100644 --- a/modules/processing/parsers/CAPE/Fareit.py +++ b/modules/processing/parsers/CAPE/Fareit.py @@ -1,7 +1,10 @@ import re import sys -""" +AUTHOR = "CAPE" +DESCRIPTION = "Fareit configuration parser." + +rule_source = """ rule pony { meta: author = "adam" @@ -35,10 +38,7 @@ def extract_config(memdump_path, read=False): if buf and len(buf[0]) > 200: cData = buf[0][200:] """ - artifacts_raw = { - "controllers": [], - "downloads": [], - } + controllers, downloads = set(), set() start = F.find(b"YUIPWDFILE0YUIPKDFILE0YUICRYPTED0YUI1.0") if start: @@ -54,14 +54,18 @@ def extract_config(memdump_path, read=False): if url is None: continue if gate_url.match(url): - artifacts_raw["controllers"].append(url.lower().decode()) + controllers.add(url.lower().decode()) elif exe_url.match(url) or dll_url.match(url): - artifacts_raw["downloads"].append(url.lower().decode()) + downloads.add(url.lower().decode()) except Exception as e: print(e, sys.exc_info(), "PONY") - artifacts_raw["controllers"] = list(set(artifacts_raw["controllers"])) - artifacts_raw["downloads"] = list(set(artifacts_raw["downloads"])) - return artifacts_raw if len(artifacts_raw["controllers"]) != 0 or len(artifacts_raw["downloads"]) != 0 else False + + config = { + "family": "Fareit", + "http": [{"uri": c, "usage": "c2"} for c in controllers] + [{"uri": d, "usage": "download"} for d in downloads], + } + + return config if __name__ == "__main__": diff --git a/modules/processing/parsers/CAPE/Greame.py b/modules/processing/parsers/CAPE/Greame.py index 0a7aeb1c128..7610b3f42cd 100644 --- a/modules/processing/parsers/CAPE/Greame.py +++ b/modules/processing/parsers/CAPE/Greame.py @@ -1,3 +1,4 @@ +import os import string import pefile @@ -27,61 +28,67 @@ def xor_decode(data): def parse_config(raw_config): + config = {"family": "Greame"} if len(raw_config) <= 20: return None - domains = "" - ports = "" # Config sections 0 - 19 contain a list of Domains and Ports for x in range(19): if len(raw_config[x]) > 1: - domains += xor_decode(raw_config[x]).split(":", 1)[0] - domains += "|" - ports += xor_decode(raw_config[x]).split(":", 2)[1] - ports += "|" - config_dict = { - "Domain": domains[:-1], - "Port": ports[:-1], - "ServerID": xor_decode(raw_config[20]), - "Password": xor_decode(raw_config[21]), + domain, port = xor_decode(raw_config[x]).split(":", 2) + config.setdefault("tcp", []).append({"server_domain": domain, "server_port": port}) + config["identifier"] = xor_decode(raw_config[20]) # Server ID + config["passwords"] = [xor_decode(raw_config[n]) for n in [21, 73]] # Password, Google Chrome Passwords + config["ftp"] = ( + [ + { + "username": xor_decode(raw_config[41]), + "password": xor_decode(raw_config[42]), + "hostname": xor_decode(raw_config[38]), + "port": xor_decode(raw_config[43]), + "path": xor_decode(raw_config[39]), + } + ], + ) + config["paths"] = [{"path": os.path.join(xor_decode(raw_config[25]), xor_decode(raw_config[26])), "usage": "install"}] + config["mutex"] = [xor_decode(raw_config[62])] + config["registry"] = [ + {"key": xor_decode(raw_config[28])}, # "REG Key HKLM" + {"key": xor_decode(raw_config[29])}, # "REG Key HKLU" + ] + config["binary"] = [ + {"data": xor_decode(raw_config[31])}, # "Message Box Icon" + {"data": xor_decode(raw_config[32])}, # "Message Box Button" + ] + + # Below sound like capabilities but unsure of values.. + config["other"] = { "Install Flag": xor_decode(raw_config[22]), - "Install Directory": xor_decode(raw_config[25]), - "Install File Name": xor_decode(raw_config[26]), "Active X Startup": xor_decode(raw_config[27]), - "REG Key HKLM": xor_decode(raw_config[28]), - "REG Key HKCU": xor_decode(raw_config[29]), "Enable Message Box": xor_decode(raw_config[30]), - "Message Box Icon": xor_decode(raw_config[31]), - "Message Box Button": xor_decode(raw_config[32]), "Install Message Title": xor_decode(raw_config[33]), "Install Message Box": xor_decode(raw_config[34]).replace("\r\n", " "), "Activate Keylogger": xor_decode(raw_config[35]), "Keylogger Backspace = Delete": xor_decode(raw_config[36]), "Keylogger Enable FTP": xor_decode(raw_config[37]), - "FTP Address": xor_decode(raw_config[38]), - "FTP Directory": xor_decode(raw_config[39]), - "FTP UserName": xor_decode(raw_config[41]), - "FTP Password": xor_decode(raw_config[42]), - "FTP Port": xor_decode(raw_config[43]), "FTP Interval": xor_decode(raw_config[44]), "Persistance": xor_decode(raw_config[59]), "Hide File": xor_decode(raw_config[60]), "Change Creation Date": xor_decode(raw_config[61]), - "Mutex": xor_decode(raw_config[62]), "Melt File": xor_decode(raw_config[63]), "Startup Policies": xor_decode(raw_config[69]), "USB Spread": xor_decode(raw_config[70]), "P2P Spread": xor_decode(raw_config[71]), - "Google Chrome Passwords": xor_decode(raw_config[73]), } if xor_decode(raw_config[57]) == 0: - config_dict["Process Injection"] = "Disabled" + config["other"]["Process Injection"] = "Disabled" elif xor_decode(raw_config[57]) == 1: - config_dict["Process Injection"] = "Default Browser" + config["other"]["Process Injection"] = "Default Browser" elif xor_decode(raw_config[57]) == 2: - config_dict["Process Injection"] = xor_decode(raw_config[58]) + config["other"]["Process Injection"] = xor_decode(raw_config[58]) else: - config_dict["Process Injection"] = "None" - return config_dict + config["other"]["Process Injection"] = "None" + + return config def extract_config(data): diff --git a/modules/processing/parsers/CAPE/GuLoader.py b/modules/processing/parsers/CAPE/GuLoader.py index 1d858ac7d6d..4063a136757 100644 --- a/modules/processing/parsers/CAPE/GuLoader.py +++ b/modules/processing/parsers/CAPE/GuLoader.py @@ -5,12 +5,33 @@ url_regex = re.compile(rb"http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+") +DESCRIPTION = "GuLoader config extractor." +AUTHOR = "CAPE" +rule_source = """ +rule GuLoader +{ + meta: + author = "kevoreilly" + description = "Shellcode injector and downloader" + cape_type = "GuLoader Payload" + strings: + $trap0 = {0F 85 [2] FF FF 81 BD ?? 00 00 00 [2] 00 00 0F 8F [2] FF FF 39 D2 83 FF 00} + $trap1 = {49 83 F9 00 75 [1-20] 83 FF 00 [2-6] 81 FF} + $trap2 = {39 CB 59 01 D7 49 85 C8 83 F9 00 75 B3} + $trap3 = {61 0F AE E8 0F 31 0F AE E8 C1 E2 20 09 C2 29 F2 83 FA 00 7E CE C3} + $antihook = {FF 34 08 [0-48] 8F 04 0B [0-80] 83 C1 04 83 F9 18 75 [0-128] FF E3} + $cape_string = "cape_options" + condition: + 2 of them and not $cape_string +} +""" + def extract_config(data): try: urls = [url.lower().decode() for url in url_regex.findall(data)] if urls: - return {"URLs": urls} + return {"family": "GuLoader", "http": [{"uri": uri, "usage": "download"} for uri in urls]} except Exception as e: print(e) diff --git a/modules/processing/parsers/CAPE/Hancitor.py b/modules/processing/parsers/CAPE/Hancitor.py index b1b6560490f..d1dca0a26bc 100644 --- a/modules/processing/parsers/CAPE/Hancitor.py +++ b/modules/processing/parsers/CAPE/Hancitor.py @@ -24,13 +24,15 @@ def extract_config(filebuf): ENCRYPT_DATA = DATA_SECTION[24:2000] DECRYPTED_DATA = ARC4.new(key).decrypt(ENCRYPT_DATA) build_id, controllers = list(filter(None, DECRYPTED_DATA.split(b"\x00"))) - cfg.setdefault("Build ID", build_id.decode()) + cfg.setdefault("version", build_id.decode()) controllers = list(filter(None, controllers.split(b"|"))) if controllers: - cfg.setdefault("address", [url.decode() for url in controllers]) + cfg.setdefault("http", []).extend([{"uri": url.decode(), "usage": "c2"} for url in controllers]) except Exception as e: log.warning(e) + if cfg: + cfg["family"] = "Hancitor" return cfg diff --git a/modules/processing/parsers/CAPE/HttpBrowser.py b/modules/processing/parsers/CAPE/HttpBrowser.py index 293ef590b7e..3fbcb9abc58 100644 --- a/modules/processing/parsers/CAPE/HttpBrowser.py +++ b/modules/processing/parsers/CAPE/HttpBrowser.py @@ -12,15 +12,15 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -DESCRIPTION = "HttpBrowser configuration parser." -AUTHOR = "kevoreilly" - - import struct import pefile import yara +DESCRIPTION = "HttpBrowser configuration parser." +AUTHOR = "kevoreilly" + + rule_source = """ rule HttpBrowser { @@ -90,35 +90,42 @@ def extract_config(filebuf): # image_base = pe.OPTIONAL_HEADER.ImageBase yara_matches = yara_scan(filebuf) - tmp_config = {} + tmp_config = {"family": "HTTPBrowser"} + tcp_connections = [] for key, values in match_map.keys(): if yara_matches.get(key): yara_offset = int(yara_matches[key]) - if key in ("$connect_1", "$connect_2", "$connect_3"): port = ascii_from_va(pe, yara_offset + values[0]) - if port: - tmp_config["port"] = [port, "tcp"] c2_address = unicode_from_va(pe, yara_offset + values[1]) if c2_address: - tmp_config.setdefault("c2_address", []).append(c2_address) + tcp_conn = {"server_ip": c2_address, "usage": "c2"} + if port: + tcp_conn["server_port"] = port + tcp_connections.append(tcp_conn) if key == "$connect_3": c2_address = unicode_from_va(pe, yara_offset + values[2]) if c2_address: - tmp_config.setdefault("c2_address", []).append(c2_address) + tcp_conn = {"server_ip": c2_address, "usage": "c2"} + if port: + tcp_conn["server_port"] = port + tcp_connections.append(tcp_conn) else: c2_address = unicode_from_va(pe, yara_offset + values[0]) if c2_address: - tmp_config["c2_address"] = c2_address + tcp_connections.append({"server_ip": c2_address, "usage": "c2"}) filepath = unicode_from_va(pe, yara_offset + values[1]) if filepath: - tmp_config["filepath"] = filepath + tmp_config["paths"] = [{"path": filepath, "usage": "c2"}] injectionprocess = unicode_from_va(pe, yara_offset - values[2]) if injectionprocess: - tmp_config["injectionprocess"] = injectionprocess + tmp_config["inject_exe"] = [injectionprocess] + + if tcp_connections: + tmp_config["tcp"] = tcp_connections return tmp_config diff --git a/modules/processing/parsers/CAPE/IcedID.py b/modules/processing/parsers/CAPE/IcedID.py index d041acc8b95..d6e3ba1cf3e 100644 --- a/modules/processing/parsers/CAPE/IcedID.py +++ b/modules/processing/parsers/CAPE/IcedID.py @@ -60,10 +60,13 @@ def extract_config(filebuf): decrypted_data = ARC4.new(key).decrypt(enc_config) config = list(filter(None, decrypted_data.split(b"\x00"))) return { - "Bot ID": str(struct.unpack("I", decrypted_data[:4])[0]), - "Minor Version": str(struct.unpack("I", decrypted_data[4:8])[0]), - "Path": config[1], - "address": [controller[1:] for controller in config[2:]], + "family": "IcedID", + "version": str(struct.unpack("I", decrypted_data[4:8])[0]), + "paths": [{"path": config[1], "usage": "other"}], + "http": [{"uri": controller[1:]} for controller in config[2:]], + "other": { + "Bot ID": str(struct.unpack("I", decrypted_data[:4])[0]), + }, } except Exception as e: log.error("Error: %s", e) diff --git a/modules/processing/parsers/CAPE/IcedIDLoader.py b/modules/processing/parsers/CAPE/IcedIDLoader.py index b0cf2288ff1..bfe628b98c6 100644 --- a/modules/processing/parsers/CAPE/IcedIDLoader.py +++ b/modules/processing/parsers/CAPE/IcedIDLoader.py @@ -12,7 +12,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import os import struct import pefile @@ -37,8 +36,9 @@ def extract_config(filebuf): if n > 32: break campaign, c2 = struct.unpack("I30s", bytes(dec)) - cfg["C2"] = c2.split(b"\00", 1)[0].decode() - cfg["Campaign"] = campaign + cfg["family"] = "IcedIDLoader" + cfg["tcp"] = [{"server_domain": c2.split(b"\00", 1)[0].decode(), "usage": "c2"}] + cfg["campaign_id"] = campaign return cfg diff --git a/modules/processing/parsers/CAPE/LokiBot.py b/modules/processing/parsers/CAPE/LokiBot.py index 20fed82cfe8..a28e9411b23 100644 --- a/modules/processing/parsers/CAPE/LokiBot.py +++ b/modules/processing/parsers/CAPE/LokiBot.py @@ -30,6 +30,20 @@ DESCRIPTION = "LokiBot configuration parser." AUTHOR = "sysopfb" +rule_source = """ +rule LokiBot +{ + meta: + author = "kevoreilly" + description = "LokiBot Payload" + cape_type = "LokiBot Payload" + strings: + $a1 = "DlRycq1tP2vSeaogj5bEUFzQiHT9dmKCn6uf7xsOY0hpwr43VINX8JGBAkLMZW" + $a2 = "last_compatible_version" + condition: + uint16(0) == 0x5A4D and (all of ($a*)) +} +""" def find_iv(img): @@ -41,54 +55,43 @@ def find_iv(img): return iv -def find_conf(img): - ret = [] - - num_addr_re1 = re.compile( - rb""" - \x6A(?P.) # 6A 08 push 8 - \x59 # 59 pop ecx - \xBE(?P.{4}) # BE D0 88 41 00 mov esi, offset encrypted_data1 - \x8D\xBD.{4} # 8D BD 68 FE FF FF lea edi, [ebp+encrypted_data_list] - \xF3\xA5 # F3 A5 rep movsd - \x6A. # 6A 43 push 43h ; 'C' - \x5B # 5B pop ebx - \x53 # 53 push ebx - \x8D\x85.{4} # 8D 85 89 FE FF FF lea eax, [ebp+var_177] - \xA4 # A4 movsb - \x6A\x00 # 6A 00 push 0 - \x50 # 50 push eax - \xE8.{4} # E8 78 E9 FE FF call about_memset - """, - re.DOTALL | re.VERBOSE, - ) - num_addr_re2 = re.compile( - rb""" - \x6A(?P.) # 6A 08 push 8 - \x59 # 59 pop ecx - \xBE(?P.{4}) # BE F4 88 41 00 mov esi, offset encrypted_data2 - \x8D.{2,5} # 8D BD CC FE FF FF lea edi, [ebp+var_134] - \xF3\xA5 # F3 A5 rep movsd - \x53 # 53 push ebx - \x8D.{2,5} # 8D 85 ED FE FF FF lea eax, [ebp+var_113] - \x6A\x00 # 6A 00 push 0 - \x50 # 50 push eax - \xA4 # A4 movsb - \xE8.{4} # E8 58 E9 FE FF call about_memset - """, - re.DOTALL | re.VERBOSE, - ) - - num_addr_list = re.findall(num_addr_re1, img) - num_addr_list.extend(re.findall(num_addr_re2, img)) - - for num, addr in num_addr_list: - dlen = ord(num) * 4 - (addr,) = struct.unpack_from(". -DESCRIPTION = "RCSession configuration parser." -AUTHOR = "kevoreilly" - import struct import pefile import yara +DESCRIPTION = "RCSession configuration parser." +AUTHOR = "kevoreilly" + + rule_source = """ rule RCSession { @@ -93,29 +94,33 @@ def extract_config(filebuf): config_offset = pe.get_offset_from_rva(config_rva) size = struct.unpack("i", filebuf[yara_offset + 88 : yara_offset + 92])[0] key = struct.unpack("i", filebuf[config_offset + 128 : config_offset + 132])[0] - end_config = {} + end_config = {"family": "RCSession"} tmp_config = decode(filebuf[config_offset : config_offset + size], size, key) c2_address = str(tmp_config[156 : 156 + MAX_IP_STRING_SIZE]) if c2_address: - end_config.setdefault("c2_address", []).append(c2_address) + end_config.setdefault("tcp", []).append({"server_ip": c2_address, "usage": "c2"}) c2_address = str(tmp_config[224 : 224 + MAX_IP_STRING_SIZE]) if c2_address: - end_config.setdefault("c2_address", []).append(c2_address) + end_config.setdefault("tcp", []).append({"server_ip": c2_address, "usage": "c2"}) installdir = unicode_string_from_offset(bytes(tmp_config), 0x2A8, 128) if installdir: - end_config["directory"] = installdir + end_config.setdefault("paths", []).append({"path": installdir, "usage": "install"}) executable = unicode_string_from_offset(tmp_config, 0x4B0, 128) if executable: - end_config["filename"] = executable + end_config.setdefault("paths", []).append({"path": executable, "usage": "install"}) servicename = unicode_string_from_offset(tmp_config, 0x530, 128) + service = {} if servicename: - end_config["servicename"] = servicename + service["name"] = servicename displayname = unicode_string_from_offset(tmp_config, 0x738, 128) if displayname: - end_config["servicedisplayname"] = displayname + service["display_name"] = displayname description = unicode_string_from_offset(tmp_config, 0x940, 512) if description: - end_config["servicedescription"] = description + service["description"] = description + + if service: + end_config["services"] = [service] return end_config diff --git a/modules/processing/parsers/CAPE/REvil.py b/modules/processing/parsers/CAPE/REvil.py index c5c20c48439..340a7e47151 100644 --- a/modules/processing/parsers/CAPE/REvil.py +++ b/modules/processing/parsers/CAPE/REvil.py @@ -19,6 +19,9 @@ import pefile +AUTHOR = "R3MRUM" +DESCRIPTION = "REvil configuration parser." + def getSectionNames(sections): return [section.Name.partition(b"\0")[0] for section in sections] @@ -44,10 +47,14 @@ def decodeREvilConfig(config_key, config_data): # print(f"Key:\t{key}") + if not encoded_config: + # No config to decode + return + ECX = EAX = ESI = 0 for char in init255: - ESI = ((char & 0xFF) + (ord(key[EAX % len(key)]) + ESI)) & 0xFF + ESI = ((char & 0xFF) + (key[EAX % len(key)] + ESI)) & 0xFF init255[EAX] = init255[ESI] & 0xFF EAX += 1 init255[ESI] = char & 0xFF @@ -61,7 +68,7 @@ def decodeREvilConfig(config_key, config_data): ESI = (ESI + DL) & 0xFF init255[ECX] = init255[ESI] init255[ESI] = DL - decoded_config.append((init255[((init255[ECX] + DL) & 0xFF)]) ^ ord(char)) + decoded_config.append((init255[((init255[ECX] + DL) & 0xFF)]) ^ char) EAX = LOCAL1 return json.loads("".join(map(chr, decoded_config))) @@ -70,16 +77,23 @@ def decodeREvilConfig(config_key, config_data): def extract_config(data): config_data = "" config_key = "" - pe = pefile.PE(data=data) - - if len(pe.sections) == 5: - section_names = getSectionNames(pe.sections) - required_sections = (".text", ".rdata", ".data", ".reloc") - - # print section_names - if all(sections in section_names for sections in required_sections): - # print("all required section names found") - config_section_name = [resource for resource in section_names if resource not in required_sections][0] - config_key, config_data = getREvilKeyAndConfig(pe.sections, config_section_name) - if config_key and config_data: - return decodeREvilConfig(config_key, config_data) + try: + pe = pefile.PE(data=data) + + if len(pe.sections) == 5: + section_names = getSectionNames(pe.sections) + required_sections = (b".text", b".rdata", b".data", b".reloc") + + # print section_names + if all(sections in section_names for sections in required_sections): + # print("all required section names found") + config_section_name = [resource for resource in section_names if resource not in required_sections][0] + config_key, config_data = getREvilKeyAndConfig(pe.sections, config_section_name) + if config_key and config_data: + config = decodeREvilConfig(config_key, config_data) + if config: + return {"family": "REvil", "other": config} + except pefile.PEFormatError: + # This isn't a PE file, therefore unlikely to extract a configuration + pass + return {} diff --git a/modules/processing/parsers/CAPE/RedLeaf.py b/modules/processing/parsers/CAPE/RedLeaf.py index 3bffb4cbb72..bbe845a40ce 100644 --- a/modules/processing/parsers/CAPE/RedLeaf.py +++ b/modules/processing/parsers/CAPE/RedLeaf.py @@ -12,14 +12,15 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -DESCRIPTION = "RedLeaf configuration parser." -AUTHOR = "kevoreilly" - import struct import pefile import yara +DESCRIPTION = "RedLeaf configuration parser." +AUTHOR = "kevoreilly" + + rule_source = """ rule RedLeaf { @@ -89,21 +90,21 @@ def extract_config(filebuf): end_config = {} c2_address = tmp_config[8 : 8 + MAX_IP_STRING_SIZE] if c2_address: - end_config.setdefault("c2_address", []).append(c2_address) + end_config.setdefault("tcp", []).append({"server_ip": c2_address, "usage": "c2"}) c2_address = tmp_config[0x48 : 0x48 + MAX_IP_STRING_SIZE] if c2_address: - end_config.setdefault("c2_address", []).append(c2_address) + end_config.setdefault("tcp", []).append({"server_ip": c2_address, "usage": "c2"}) c2_address = tmp_config[0x88 : 0x88 + MAX_IP_STRING_SIZE] if c2_address: - end_config.setdefault("c2_address", []).append(c2_address) + end_config.setdefault("tcp", []).append({"server_ip": c2_address, "usage": "c2"}) missionid = string_from_offset(tmp_config, 0x1EC) if missionid: - end_config["missionid"] = missionid + end_config["campaign_id"] = missionid mutex = unicode_string_from_offset(tmp_config, 0x508) if mutex: - end_config["mutex"] = mutex + end_config["mutex"] = [mutex] key = string_from_offset(tmp_config, 0x832) if key: - end_config["key"] = key + end_config["encryption"] = [{"algorithm": "RC4", "key": key}] return end_config diff --git a/modules/processing/parsers/CAPE/Remcos.py b/modules/processing/parsers/CAPE/Remcos.py index 34cbc071758..f10fa464cae 100644 --- a/modules/processing/parsers/CAPE/Remcos.py +++ b/modules/processing/parsers/CAPE/Remcos.py @@ -6,11 +6,9 @@ # By Talos July 2018 - https://github.com/Cisco-Talos/remcos-decoder # Updates based on work presented here https://gist.github.com/sysopfb/11e6fb8c1377f13ebab09ab717026c87 -DESCRIPTION = "Remcos config extractor." -AUTHOR = "threathive,sysopfb,kevoreilly" - import base64 import logging +import os import re import string from collections import OrderedDict @@ -18,8 +16,12 @@ import pefile from Cryptodome.Cipher import ARC4 +DESCRIPTION = "Remcos config extractor." +AUTHOR = "threathive,sysopfb,kevoreilly" + + # From JPCERT -FLAG = {b"\x00": "Disable", b"\x01": "Enable"} +FLAG = {b"\x00": "Disabled", b"\x01": "Enabled"} # From JPCERT idx_list = { @@ -151,7 +153,6 @@ def check_version(filedata): def extract_config(filebuf): config = {} - try: pe = pefile.PE(data=filebuf) blob = False @@ -162,32 +163,61 @@ def extract_config(filebuf): break if blob: + config = {"family": "Remcos", "category": ["rat"]} keylen = blob[0] key = blob[1 : keylen + 1] decrypted_data = ARC4.new(key).decrypt(blob[keylen + 1 :]) p_data = OrderedDict() - p_data["Version"] = check_version(filebuf) + version = check_version(filebuf) + if version: + config["version"] = version configs = re.split(rb"\|\x1e\x1e\x1f\|", decrypted_data) for i, cont in enumerate(configs): if cont in (b"\x00", b"\x01"): - p_data[idx_list[i]] = FLAG[cont] + # Flag capabilities that are enabled/disabled whether known or not + config.setdefault(f"capability_{FLAG[cont].lower()}", []).append(idx_list[i]) elif i in (9, 16, 25, 37): p_data[idx_list[i]] = setup_list[int(cont)] elif i in (56, 57, 58): - p_data[idx_list[i]] = base64.b64encode(cont) + config.setdefault("other", {})[idx_list[i]] = base64.b64encode(cont) elif i == 0: - host, port, password = cont.split(b"|", 1)[0].split(b":") - p_data["Control"] = f"tcp://{host.decode()}:{port.decode()}:{password.decode()}" + host, port, password = cont.split(b"|", 1)[0].decode().split(":") + config.setdefault("tcp", []).append({"server_ip": host, "server_port": port, "usage": "other"}) + config.setdefault("password", []).append(password) else: - p_data[idx_list[i]] = cont + p_data[idx_list[i]] = cont.decode() + + # Flag paths + for path_key in [k for k in p_data.keys() if k.endswith("path")]: + prefix = path_key.split(" ")[0] + usage = "other" + if prefix == "Install": + usage = "install" + elif prefix == "Keylog": + usage = "logs" + + def get_string(key) -> str: + value = p_data.pop(key, None) + if key in utf_16_string_list: + if isinstance(value, str): + value = value.encode() + value = value.decode("utf16").strip("\00") + return value + + path_parts = [get_string(f"{prefix} {path_part}") for path_part in ["path", "folder", "file"]] + full_path = os.path.join(*[p for p in path_parts if p]) + config.setdefault("paths", []).append({"path": full_path, "usage": usage}) for k, v in p_data.items(): if k in utf_16_string_list: v = v.decode("utf16").strip("\00") - config[k] = v + config.setdefault("other", {})[k] = v + except pefile.PEFormatError: + # Not a PE file + pass except Exception as e: logger.error(f"Caught an exception: {e}") diff --git a/modules/processing/parsers/CAPE/Retefe.py b/modules/processing/parsers/CAPE/Retefe.py index 2f22b039501..318a8e25db9 100644 --- a/modules/processing/parsers/CAPE/Retefe.py +++ b/modules/processing/parsers/CAPE/Retefe.py @@ -3,14 +3,15 @@ # http://tomasuh.github.io/2018/12/28/retefe-unpack.html # Many thanks to Tomasuh -DESCRIPTION = "Retefe configuration parser." -AUTHOR = "Tomasuh" - import struct import pefile import yara +DESCRIPTION = "Retefe configuration parser." +AUTHOR = "Tomasuh" + + rule_source = """ rule Retefe { @@ -34,7 +35,7 @@ def yara_scan(raw_data): yara_rules = yara.compile(source=rule_source) matches = yara_rules.match(data=raw_data) for match in matches: - if match.rule == "Emotet": + if match.rule == "Retefe": for item in match.strings: addresses[item[1]] = item[0] return addresses @@ -117,10 +118,10 @@ def extract_config(filebuf): n = 0 result = "" for ch in buffer: - result += chr((ord(ch) ^ xor_arr[n % 4])) + result += chr(ch ^ xor_arr[n % 4]) n += 1 - return {"Script": result} + return {"family": "Retefe", "decoded_strings": [result]} # Some logical reasoning left behind.... diff --git a/modules/processing/parsers/CAPE/SmallNet.py b/modules/processing/parsers/CAPE/SmallNet.py index 58aaee142bd..9343264f8b1 100644 --- a/modules/processing/parsers/CAPE/SmallNet.py +++ b/modules/processing/parsers/CAPE/SmallNet.py @@ -1,14 +1,23 @@ +import os +from gettext import install + + def ver_52(data): config_parts = data.split("!!<3SAFIA<3!!") + config = { + "family": "SmallNet", + "category": ["rat"], + "version": "5.2", + "tcp": [ + {"server_domain": config_parts[1], "server_port": config_parts[2]}, + {"server_domain": config_parts[5], "usage": "c2"}, # Install Server + ], + "registry": [{"key": config_parts[8]}], + } config_dict = { - "Domain": config_parts[1], - "Port": config_parts[2], - "Disbale Registry": config_parts[3], - "Disbale TaskManager": config_parts[4], - "Install Server": config_parts[5], - "Registry Key": config_parts[8], - "Install Name": config_parts[9], - "Disbale UAC": config_parts[10], + "Disable Registry": config_parts[3], + "Disable TaskManager": config_parts[4], + "Disable UAC": config_parts[10], "Anti-Sandboxie": config_parts[13], "Anti-Anubis": config_parts[14], "Anti-VirtualBox": config_parts[15], @@ -21,7 +30,7 @@ def ver_52(data): "MSN Spread": config_parts[22], "Yahoo Spread": config_parts[23], "LAN Spread": config_parts[24], - "Disbale Firewall": config_parts[25], + "Disable Firewall": config_parts[25], "Delay Execution MiliSeconds": config_parts[26], "Attribute Read Only": config_parts[27], "Attribute System File": config_parts[28], @@ -46,28 +55,37 @@ def ver_52(data): "MessageBox Title": config_parts[47], } + install_path = config_parts[9] if config_parts[6] == 1: - config_dict["Install Path"] = "Temp" + install_path = os.path.join("Temp", config_parts[9]) if config_parts[7] == 1: - config_dict["Install Path"] = "Windows" + install_path = os.path.join("Windows", config_parts[9]) if config_parts[11] == 1: - config_dict["Install Path"] = "System32" + install_path = os.path.join("System32", config_parts[9]) if config_parts[12] == 1: - config_dict["Install Path"] = "Program Files" - return config_dict + install_path = os.path.join("Program Files", config_parts[9]) + + config["paths"] = [{"path": install_path, "usage": "install"}] + config["other"] = config_dict # Placing in other for now + return config def ver_5(data): config_parts = data.split("!!ElMattadorDz!!") + config = { + "family": "SmallNet", + "category": ["rat"], + "version": "5", + "tcp": [ + {"server_domain": config_parts[1], "server_port": config_parts[2]}, + {"server_domain": config_parts[5], "usage": "c2"}, # Install Server + ], + "registry": [{"key": config_parts[8]}], + } config_dict = { - "Domain": config_parts[1], - "Port": config_parts[2], "Disable Registry": config_parts[3], - "Disbale TaskManager": config_parts[4], - "Install Server": config_parts[5], - "Registry Key": config_parts[8], - "Install Name": config_parts[9], - "Disbale UAC": config_parts[10], + "Disable TaskManager": config_parts[4], + "Disable UAC": config_parts[10], "Anti-Sandboxie": config_parts[13], "Anti-Anubis": config_parts[14], "Anti-VirtualBox": config_parts[15], @@ -80,22 +98,32 @@ def ver_5(data): "MSN Spread": config_parts[22], "Yahoo Spread": config_parts[23], "LAN Spread": config_parts[24], - "Disbale Firewall": config_parts[25], + "Disable Firewall": config_parts[25], "Delay Execution MiliSeconds": config_parts[26], } + install_path = config_parts[9] if config_parts[6] == 1: - config_dict["Install Path"] = "Temp" + install_path = os.path.join("Temp", config_parts[9]) if config_parts[7] == 1: - config_dict["Install Path"] = "Windows" + install_path = os.path.join("Windows", config_parts[9]) if config_parts[11] == 1: - config_dict["Install Path"] = "System32" + install_path = os.path.join("System32", config_parts[9]) if config_parts[12] == 1: - config_dict["Install Path"] = "Program Files" - return [config_dict, [config_dict["Domain"]]] + install_path = os.path.join("Program Files", config_parts[9]) + config["paths"] = [{"path": install_path, "usage": "install"}] + config["other"] = config_dict # Placing in other for now + + return config def extract_config(data): + try: + if isinstance(data, bytes): + data = data.decode() + except: + return + if "!!<3SAFIA<3!!" in data: return ver_52(data) diff --git a/modules/processing/parsers/CAPE/SquirrelWaffle.py b/modules/processing/parsers/CAPE/SquirrelWaffle.py index 3e6d46c5619..65dd431e54a 100644 --- a/modules/processing/parsers/CAPE/SquirrelWaffle.py +++ b/modules/processing/parsers/CAPE/SquirrelWaffle.py @@ -17,6 +17,9 @@ import pefile import yara +AUTHOR = "kevoreilly" +DESCRIPTION = "SquirrelWaffle configuration parser." + rule_source = """ rule SquirrelWaffle { @@ -69,12 +72,13 @@ def extract_config(data): if "\r\n" in decrypted and "|" not in decrypted: config["IP Blocklist"] = list(filter(None, decrypted.split("\r\n"))) elif "|" in decrypted and "." in decrypted and "\r\n" not in decrypted: - config["URLs"] = list(filter(None, decrypted.split("|"))) + config.setdefault("http", []).extend([{"uri": uri} for uri in list(filter(None, decrypted.split("|")))]) except Exception: continue matches = yara_rules.match(data=data) if not matches: return config + config["family"] = "SquirrelWaffle" for match in matches: if match.rule != "SquirrelWaffle": continue @@ -83,5 +87,5 @@ def extract_config(data): c2key_offset = int(item[0]) key_rva = struct.unpack("i", data[c2key_offset + 28 : c2key_offset + 32])[0] - pe.OPTIONAL_HEADER.ImageBase key_offset = pe.get_offset_from_rva(key_rva) - config["C2 key"] = string_from_offset(data, key_offset).decode() + config.setdefault("other", {})["C2 key"] = string_from_offset(data, key_offset).decode() return config diff --git a/modules/processing/parsers/CAPE/Strrat.py b/modules/processing/parsers/CAPE/Strrat.py index c6d5390185c..d446dc7cd7c 100644 --- a/modules/processing/parsers/CAPE/Strrat.py +++ b/modules/processing/parsers/CAPE/Strrat.py @@ -75,6 +75,27 @@ def extract_config(data): configdata = unzip_config(tmpzip) if configdata: - raw_config["config"] = decode(configdata) + c2_1, mutex_1, dl_url, c2_2, mutex_2, startup_persist, secondary_persist, skype_persist, license = decode(configdata).split( + "|" + ) + raw_config["tcp"] = [{"server_domain": c2_1, "usage": "c2"}, {"server_domain": c2_2, "usage": "c2"}] + raw_config["http"] = [{"uri": dl_url, "usage": "download"}] + raw_config["mutex"] = [mutex_1, mutex_2] + raw_config["other"]["license"] = license + + if startup_persist == "true": + raw_config.setdefault("capability_enabled", []).append("Setup Startup Folder Persistence") + else: + raw_config.setdefault("capability_disabled", []).append("Setup Startup Folder Persistence") + + if secondary_persist == "true": + raw_config.setdefault("capability_enabled", []).append("Secondary Startup Folder Persistence") + else: + raw_config.setdefault("capability_disabled", []).append("Secondary Startup Folder Persistence") + + if skype_persist == "true": + raw_config.setdefault("capability_enabled", []).append("Setup Skype Scheduled Task Persistence") + else: + raw_config.setdefault("capability_disabled", []).append("Setup Skype Scheduled Task Persistence") return raw_config diff --git a/modules/processing/parsers/CAPE/TSCookie.py b/modules/processing/parsers/CAPE/TSCookie.py index 4575ea30d9d..1565a819e61 100644 --- a/modules/processing/parsers/CAPE/TSCookie.py +++ b/modules/processing/parsers/CAPE/TSCookie.py @@ -30,8 +30,8 @@ # Config pattern CONFIG_PATTERNS = [ - re.compile("\xC3\x90\x68(....)\xE8(....)\x59\x6A\x01\x58\xC3", re.DOTALL), - re.compile("\x6A\x04\x68(....)\x8D(.....)\x56\x50\xE8", re.DOTALL), + re.compile("\xC3\x90\x68(....)\xE8(....)\x59\x6A\x01\x58\xC3".encode(), re.DOTALL), + re.compile("\x6A\x04\x68(....)\x8D(.....)\x56\x50\xE8".encode(), re.DOTALL), ] CONFIG_SIZE = 0x8D4 @@ -65,17 +65,22 @@ def parse_config(config): config_dict = collections.OrderedDict() for i in range(4): if config[0x10 + 0x100 * i] != "\x00": - config_dict[f"Server name #{i + 1}"] = __format_string( - unpack_from("<240s", config, 0x10 + 0x100 * i)[0].decode("utf-16") + server_name = (__format_string(unpack_from("<240s", config, 0x10 + 0x100 * i)[0].decode("utf-16")),) + main_port = unpack_from("I', config, 0x604)[0]:X}" - config_dict["Sleep time"] = unpack_from("I', config, 0x604)[0]:X}"}] + config_dict["sleep_delay"] = [unpack_from(". -DESCRIPTION = "Zloader configuration parser" -AUTHOR = "kevoreilly" - import logging import struct @@ -22,6 +19,10 @@ import yara from Cryptodome.Cipher import ARC4 +DESCRIPTION = "Zloader configuration parser" +AUTHOR = "kevoreilly" + + log = logging.getLogger(__name__) rule_source = """ @@ -65,18 +66,19 @@ def extract_config(filebuf): for item in match.strings: if "$decrypt_conf" in item[1]: decrypt_conf = int(item[0]) + 21 + end_config["family"] = "Zloader" va = struct.unpack("I", filebuf[decrypt_conf : decrypt_conf + 4])[0] key = string_from_offset(filebuf, pe.get_offset_from_rva(va - image_base)) data_offset = pe.get_offset_from_rva(struct.unpack("I", filebuf[decrypt_conf + 5 : decrypt_conf + 9])[0] - image_base) enc_data = filebuf[data_offset:].split(b"\0\0", 1)[0] raw = decrypt_rc4(key, enc_data) items = list(filter(None, raw.split(b"\x00\x00"))) - end_config["Botnet name"] = items[1].lstrip(b"\x00") - end_config["Campaign ID"] = items[2] + end_config.setdefault("other", {})["Botnet name"] = items[1].lstrip(b"\x00") + end_config["campaign_id"] = items[2] for item in items: item = item.lstrip(b"\x00") if item.startswith(b"http"): - end_config.setdefault("address", []).append(item) + end_config.setdefault("http", []).append({"uri": item}) elif len(item) == 16: - end_config["RC4 key"] = item + end_config["encryption"] = [{"algorithm": "RC4", "key": item}] return end_config diff --git a/modules/processing/parsers/CAPE/deprecated/SmokeLoader.py b/modules/processing/parsers/CAPE/deprecated/SmokeLoader.py index a43c1d9bdcb..b346e9dc87b 100644 --- a/modules/processing/parsers/CAPE/deprecated/SmokeLoader.py +++ b/modules/processing/parsers/CAPE/deprecated/SmokeLoader.py @@ -12,14 +12,14 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -DESCRIPTION = "SmokeLoader configuration parser." -AUTHOR = "kevoreilly" - import struct import pefile import yara +AUTHOR = "kevoreilly" +DESCRIPTION = "SmokeLoader configuration parser." + rule_source = """ rule SmokeLoader { @@ -66,10 +66,10 @@ def extract_config(filebuf): except Exception: image_base = 0 - end_config = {} - matches = yara_scan(filebuf, "$") - if matches["$ref64_1"]: - table_ref_offset = int(matches["$ref64_1"]) + end_config = {"family": "SmokeLoader"} + table_ref = yara_scan(filebuf, "$ref64_1") + if table_ref: + table_ref_offset = int(table_ref["$ref64_1"]) table_delta = struct.unpack("i", filebuf[table_ref_offset + 62 : table_ref_offset + 66])[0] table_offset = table_ref_offset + table_delta + 66 @@ -94,15 +94,17 @@ def extract_config(filebuf): c2_key = struct.unpack("I", filebuf[c2_offset + c2_size + 1 : c2_offset + c2_size + 5])[0] c2_url = xor_decode(filebuf[c2_offset + 1 : c2_offset + c2_size + 1], c2_key).decode("ascii") if c2_url: - end_config.setdefault("address", []).append(c2_url) + end_config.setdefault("http", []).append({"uri": c2_url, "usage": "c2"}) except Exception: table_loop = False else: table_loop = False table_offset += 8 return end_config - elif matches["$ref64_2"]: - table_ref_offset = int(matches["$ref64_1"]) + else: + table_ref = yara_scan(filebuf, "$ref64_2") + if table_ref: + table_ref_offset = int(table_ref["$ref64_2"]) table_delta = struct.unpack("i", filebuf[table_ref_offset + 26 : table_ref_offset + 30])[0] table_offset = table_ref_offset + table_delta + 30 @@ -117,13 +119,15 @@ def extract_config(filebuf): try: c2_url = xor_decode(filebuf[c2_offset + 1 : c2_offset + c2_size + 1], c2_key).decode("ascii") if c2_url: - end_config.setdefault("address", []).append(c2_url) + end_config.setdefault("http", []).append({"uri": c2_url, "usage": "c2"}) except Exception: pass table_offset += 8 return end_config - elif matches["$ref32_1"]: - table_ref_offset = int(matches["$ref32_1"]) + else: + table_ref = yara_scan(filebuf, "$ref32_1") + if table_ref: + table_ref_offset = int(table_ref["$ref32_1"]) table_rva = struct.unpack("i", filebuf[table_ref_offset + 55 : table_ref_offset + 59])[0] - image_base table_offset = pe.get_offset_from_rva(table_rva) @@ -148,7 +152,7 @@ def extract_config(filebuf): c2_key = struct.unpack("I", filebuf[c2_offset + c2_size + 1 : c2_offset + c2_size + 5])[0] c2_url = xor_decode(filebuf[c2_offset + 1 : c2_offset + c2_size + 1], c2_key).decode("ascii") if c2_url: - end_config.setdefault("address", []).append(c2_url) + end_config.setdefault("http", []).append({"uri": c2_url, "usage": "c2"}) except Exception: table_loop = False else: diff --git a/modules/processing/parsers/CAPE/test_cape.py b/modules/processing/parsers/CAPE/test_cape.py index f9190c3c306..b30b0476a2b 100644 --- a/modules/processing/parsers/CAPE/test_cape.py +++ b/modules/processing/parsers/CAPE/test_cape.py @@ -1,2 +1,2 @@ -def extract_config(): +def extract_config(data: bytes): pass diff --git a/modules/processing/parsers/CAPE/unrecom.py b/modules/processing/parsers/CAPE/unrecom.py index 172369bc4ce..51a1f08d534 100644 --- a/modules/processing/parsers/CAPE/unrecom.py +++ b/modules/processing/parsers/CAPE/unrecom.py @@ -1,21 +1,25 @@ import string import xml.etree.ElementTree as ET -from io import StringIO -from zipfile import ZipFile +from io import BytesIO, StringIO +from zipfile import BadZipFile, ZipFile from Cryptodome.Cipher import ARC4 def extract_embedded(zip_data): raw_embedded = None - archive = StringIO(zip_data) - with ZipFile(archive) as zip: - for name in zip.namelist(): # get all the file names - if name == "load/ID": # contains first part of key - partial_key = zip.read(name) - enckey = f"{partial_key}DESW7OWKEJRU4P2K" # complete key - if name == "load/MANIFEST.MF": # this is the embedded jar - raw_embedded = zip.read(name) + archive = BytesIO(zip_data) if isinstance(zip_data, bytes) else StringIO(zip_data) + try: + with ZipFile(archive) as zip: + for name in zip.namelist(): # get all the file names + if name == "load/ID": # contains first part of key + partial_key = zip.read(name) + enckey = f"{partial_key}DESW7OWKEJRU4P2K" # complete key + if name == "load/MANIFEST.MF": # this is the embedded jar + raw_embedded = zip.read(name) + except BadZipFile: + # File is not a zip + pass if raw_embedded is None: return None # Decrypt the raw file @@ -47,16 +51,22 @@ def parse_config(config): else: raw_config[child.attrib["key"]] = child.text return { - "Version": raw_config["Version"], - "Delay": raw_config["delay"], - "Domain": raw_config["dns"], - "Extension": raw_config["extensionname"], - "Install": raw_config["install"], - "Port1": raw_config["p1"], - "Port2": raw_config["p2"], - "Password": raw_config["password"], - "PluginFolder": raw_config["pluginfoldername"], - "Prefix": raw_config["prefix"], + "family": "unrecom", + "version": raw_config["Version"], + "sleep_delay": [raw_config["delay"]], + "password": [raw_config["password"]], + "paths": [ + {"path": raw_config["pluginfoldername"], "usage": "plugins"}, + {"path": raw_config["install"], "usage": "install"}, + ], + "other": { + # Need context around how these are used TCP/HTTP connections + "Prefix": raw_config["prefix"], + "Domain": raw_config["dns"], + "Extension": raw_config["extensionname"], + "Port1": raw_config["p1"], + "Port2": raw_config["p2"], + }, }