CAPE-parsers 0.1.54__py3-none-any.whl → 0.1.55__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cape_parsers/CAPE/community/KoiLoader.py +3 -2
- cape_parsers/CAPE/community/Lumma.py +5 -10
- cape_parsers/CAPE/core/NitroBunnyDownloader.py +24 -7
- cape_parsers/CAPE/core/Rhadamanthys.py +65 -61
- {cape_parsers-0.1.54.dist-info → cape_parsers-0.1.55.dist-info}/METADATA +1 -1
- {cape_parsers-0.1.54.dist-info → cape_parsers-0.1.55.dist-info}/RECORD +8 -8
- {cape_parsers-0.1.54.dist-info → cape_parsers-0.1.55.dist-info}/WHEEL +0 -0
- {cape_parsers-0.1.54.dist-info → cape_parsers-0.1.55.dist-info}/licenses/LICENSE +0 -0
|
@@ -118,8 +118,9 @@ def extract_config(data):
|
|
|
118
118
|
|
|
119
119
|
encoded_payload = remove_nulls(encoded_payload, encoded_payload_size)
|
|
120
120
|
decoded_payload = xor_data(encoded_payload, xor_key)
|
|
121
|
-
|
|
122
|
-
|
|
121
|
+
cncs = find_c2(decoded_payload)
|
|
122
|
+
if cncs and list(filter(cncs, None)):
|
|
123
|
+
config["CNCs"] = cncs
|
|
123
124
|
|
|
124
125
|
return config
|
|
125
126
|
|
|
@@ -5,7 +5,7 @@ import struct
|
|
|
5
5
|
from contextlib import suppress
|
|
6
6
|
import pefile
|
|
7
7
|
import yara
|
|
8
|
-
|
|
8
|
+
|
|
9
9
|
|
|
10
10
|
RULE_SOURCE_BUILD_ID = """rule LummaBuildId
|
|
11
11
|
{
|
|
@@ -142,7 +142,7 @@ def contains_non_printable(byte_array):
|
|
|
142
142
|
return True
|
|
143
143
|
return False
|
|
144
144
|
|
|
145
|
-
|
|
145
|
+
|
|
146
146
|
def mask32(x):
|
|
147
147
|
return x & 0xFFFFFFFF
|
|
148
148
|
|
|
@@ -242,7 +242,7 @@ def chacha20_xor(message, key, nonce, counter):
|
|
|
242
242
|
xor_key.append(message[i] ^ key_stream[i])
|
|
243
243
|
|
|
244
244
|
return xor_key
|
|
245
|
-
|
|
245
|
+
|
|
246
246
|
|
|
247
247
|
def extract_c2_domain(data):
|
|
248
248
|
pattern = rb"([\w-]+\.[\w]+)\x00"
|
|
@@ -315,9 +315,7 @@ def extract_config(data):
|
|
|
315
315
|
counter = 2
|
|
316
316
|
for i in range(12):
|
|
317
317
|
encrypted_string = data[encrypted_strings_offset : encrypted_strings_offset + 40]
|
|
318
|
-
|
|
319
|
-
chacha20_cipher.seek(counter)
|
|
320
|
-
decoded_c2 = chacha20_cipher.decrypt(encrypted_string).split(b"\x00", 1)[0]
|
|
318
|
+
decoded_c2 = chacha20_xor(encrypted_string, key, nonce, counter).split(b"\x00", 1)[0]
|
|
321
319
|
if contains_non_printable(decoded_c2):
|
|
322
320
|
break
|
|
323
321
|
config.setdefault("CNCs", []).append("https://" + decoded_c2.decode())
|
|
@@ -350,10 +348,7 @@ def extract_config(data):
|
|
|
350
348
|
c2_encrypted = data[c2_dword_offset : c2_dword_offset + 0x80]
|
|
351
349
|
counters = [0, 2, 4, 6, 8, 10, 12, 14, 16]
|
|
352
350
|
for counter in counters:
|
|
353
|
-
|
|
354
|
-
chacha20_cipher = ChaCha20.new(key=key, nonce=nonce)
|
|
355
|
-
chacha20_cipher.seek(counter)
|
|
356
|
-
decrypted = chacha20_cipher.decrypt(c2_encrypted).split(b"\x00", 1)[0]
|
|
351
|
+
decrypted = chacha20_xor(c2_encrypted, key, nonce, counter)
|
|
357
352
|
c2 = extract_c2_domain(decrypted)
|
|
358
353
|
if c2 is not None and len(c2) > 10:
|
|
359
354
|
config["CNCs"].append("https://" + c2.decode())
|
|
@@ -32,14 +32,15 @@ rule NitroBunnyDownloader
|
|
|
32
32
|
cape_type = "NitroBunnyDownloader Payload"
|
|
33
33
|
hash = "960e59200ec0a4b5fb3b44e6da763f5fec4092997975140797d4eec491de411b"
|
|
34
34
|
strings:
|
|
35
|
-
$
|
|
35
|
+
$config1 = {E8 [3] 00 41 B8 ?? ?? 00 00 48 8D 15 [3] 00 48 89 C1 48 89 ?? E8 [3] 00}
|
|
36
|
+
$config2 = {E8 [3] 00 48 8D 15 [3] 00 41 B8 ?? ?? 00 00 48 89 C1 48 89 ?? E8 [3] 00}
|
|
36
37
|
$string1 = "X-Amz-User-Agent:" wide
|
|
37
38
|
$string2 = "Amz-Security-Flag:" wide
|
|
38
39
|
$string3 = "/cart" wide
|
|
39
40
|
$string4 = "Cookie: " wide
|
|
40
41
|
$string5 = "wishlist" wide
|
|
41
42
|
condition:
|
|
42
|
-
uint16(0) == 0x5A4D and $config and 2 of ($string*)
|
|
43
|
+
uint16(0) == 0x5A4D and 1 of ($config*) and 2 of ($string*)
|
|
43
44
|
}
|
|
44
45
|
"""
|
|
45
46
|
|
|
@@ -93,25 +94,41 @@ def extract_config(filebuf):
|
|
|
93
94
|
|
|
94
95
|
cfg = {}
|
|
95
96
|
config_code_offset = None
|
|
97
|
+
config_size_offset = None
|
|
98
|
+
config_offset = None
|
|
99
|
+
rva_offset = None
|
|
100
|
+
|
|
96
101
|
for hit in yara_hit:
|
|
97
102
|
if hit.rule != "NitroBunnyDownloader":
|
|
98
103
|
continue
|
|
99
104
|
|
|
100
105
|
for item in hit.strings:
|
|
101
106
|
for instance in item.instances:
|
|
102
|
-
if "$
|
|
107
|
+
if item.identifier == "$config1":
|
|
108
|
+
config_code_offset = instance.offset
|
|
109
|
+
config_size_offset = 7
|
|
110
|
+
config_offset= 14
|
|
111
|
+
rva_offset = 18
|
|
112
|
+
break
|
|
113
|
+
elif item.identifier == "$config2":
|
|
103
114
|
config_code_offset = instance.offset
|
|
115
|
+
config_size_offset = 14
|
|
116
|
+
config_offset= 8
|
|
117
|
+
rva_offset = 12
|
|
104
118
|
break
|
|
105
119
|
|
|
120
|
+
if config_code_offset:
|
|
121
|
+
break
|
|
122
|
+
|
|
106
123
|
if config_code_offset is None:
|
|
107
124
|
return None
|
|
108
125
|
|
|
109
126
|
try:
|
|
110
127
|
pe = pefile.PE(data=filebuf, fast_load=True)
|
|
111
|
-
config_length = pe.get_dword_from_offset(config_code_offset +
|
|
112
|
-
|
|
113
|
-
rva = pe.get_rva_from_offset(config_code_offset +
|
|
114
|
-
config_rva = rva +
|
|
128
|
+
config_length = pe.get_dword_from_offset(config_code_offset + config_size_offset)
|
|
129
|
+
config = pe.get_dword_from_offset(config_code_offset + config_offset)
|
|
130
|
+
rva = pe.get_rva_from_offset(config_code_offset + rva_offset)
|
|
131
|
+
config_rva = rva + config
|
|
115
132
|
data = pe.get_data(config_rva, config_length)
|
|
116
133
|
off = 0
|
|
117
134
|
raw = cfg["raw"] = {}
|
|
@@ -7,6 +7,15 @@ import json
|
|
|
7
7
|
DESCRIPTION = "Rhadamanthys parser"
|
|
8
8
|
AUTHOR = "kevoreilly, YungBinary"
|
|
9
9
|
|
|
10
|
+
CUSTOM_ALPHABETS = [
|
|
11
|
+
b"3Fijkbc|l4NOPQRSTUVWXY567DdewxEqrstuvyz-ABC1fghop2mnGHIJKLMZ089a", # 0.9.3
|
|
12
|
+
b"4NOPQRSTUVWXY567DdeEqrstuvwxyz-ABC1fghop23Fijkbc|lmnGHIJKLMZ089a", # 0.9.2
|
|
13
|
+
b"ABC1fghijklmnop234NOPQRSTUVWXY567DEFGHIJKLMZ089abcdeqrstuvwxyz-|", # 0.9.X
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
CHACHA_KEY = b"\x52\xAB\xDF\x06\xB6\xB1\x3A\xC0\xDA\x2D\x22\xDC\x6C\xD2\xBE\x6C\x20\x17\x69\xE0\x12\xB5\xE6\xEC\x0E\xAB\x4C\x14\x73\x4A\xED\x51"
|
|
17
|
+
CHACHA_NONCE = b"\x5F\x14\xD7\x9C\xFC\xFC\x43\x9E\xC3\x40\x6B\xBA"
|
|
18
|
+
|
|
10
19
|
|
|
11
20
|
def mask32(x):
|
|
12
21
|
return x & 0xFFFFFFFF
|
|
@@ -240,76 +249,71 @@ def parse_compression_header(config: bytes):
|
|
|
240
249
|
}
|
|
241
250
|
|
|
242
251
|
|
|
252
|
+
def handle_encrypted_string(encrypted_string: str) -> list:
|
|
253
|
+
"""
|
|
254
|
+
Args:
|
|
255
|
+
encrypted_string: a str representing
|
|
256
|
+
Returns:
|
|
257
|
+
Command and Control server list, may be empty
|
|
258
|
+
"""
|
|
259
|
+
|
|
260
|
+
for alphabet in CUSTOM_ALPHABETS:
|
|
261
|
+
try:
|
|
262
|
+
custom_b64_decoded = custom_b64decode(encrypted_string, alphabet)
|
|
263
|
+
xor_key = chacha20_xor(custom_b64_decoded, CHACHA_KEY, CHACHA_NONCE)
|
|
264
|
+
# Decrypted, may still be the compressed malware configuration
|
|
265
|
+
config = decrypt_config(xor_key)
|
|
266
|
+
|
|
267
|
+
# First byte should be 0xFF
|
|
268
|
+
if config[0] != 0xFF:
|
|
269
|
+
continue
|
|
270
|
+
|
|
271
|
+
# Attempt to extract C2 url, only works in version prior to 0.9.2
|
|
272
|
+
c2_url = extract_c2_url(config)
|
|
273
|
+
if c2_url:
|
|
274
|
+
return [c2_url]
|
|
275
|
+
|
|
276
|
+
# Parse header
|
|
277
|
+
parsed = parse_compression_header(config)
|
|
278
|
+
if not parsed:
|
|
279
|
+
continue
|
|
280
|
+
|
|
281
|
+
# Decompress LZO-like compression
|
|
282
|
+
decompressed = lzo_noheader_decompress(parsed['compressed_data'], parsed['decompressed_size'])
|
|
283
|
+
pattern = re.compile(b'.' + bytes([decompressed[1]]))
|
|
284
|
+
|
|
285
|
+
cncs = [f"https://{chunk.decode()}" for chunk in pattern.split(decompressed) if chunk]
|
|
286
|
+
return cncs
|
|
287
|
+
except Exception:
|
|
288
|
+
continue
|
|
289
|
+
|
|
290
|
+
return []
|
|
291
|
+
|
|
292
|
+
|
|
243
293
|
def extract_config(data):
|
|
294
|
+
"""
|
|
295
|
+
Extract Rhadamanthys malware configuration.
|
|
296
|
+
"""
|
|
244
297
|
config_dict = {}
|
|
298
|
+
# Extract very old variant
|
|
245
299
|
magic = struct.unpack("I", data[:4])[0]
|
|
246
300
|
if magic == 0x59485221:
|
|
247
301
|
config_dict["CNCs"] = [data[24:].split(b"\0", 1)[0].decode()]
|
|
248
302
|
return config_dict
|
|
249
|
-
else:
|
|
250
|
-
key = b"\x52\xAB\xDF\x06\xB6\xB1\x3A\xC0\xDA\x2D\x22\xDC\x6C\xD2\xBE\x6C\x20\x17\x69\xE0\x12\xB5\xE6\xEC\x0E\xAB\x4C\x14\x73\x4A\xED\x51"
|
|
251
|
-
nonce = b"\x5F\x14\xD7\x9C\xFC\xFC\x43\x9E\xC3\x40\x6B\xBA"
|
|
252
|
-
|
|
253
|
-
custom_alphabets = [
|
|
254
|
-
b"ABC1fghijklmnop234NOPQRSTUVWXY567DEFGHIJKLMZ089abcdeqrstuvwxyz-|",
|
|
255
|
-
b"4NOPQRSTUVWXY567DdeEqrstuvwxyz-ABC1fghop23Fijkbc|lmnGHIJKLMZ089a", # 0.9.2
|
|
256
|
-
b"3Fijkbc|l4NOPQRSTUVWXY567DdewxEqrstuvyz-ABC1fghop2mnGHIJKLMZ089a", # 0.9.3
|
|
257
|
-
]
|
|
258
|
-
|
|
259
|
-
# Extract base64 strings
|
|
260
|
-
extracted_strings = extract_base64_strings(data, 100, 256)
|
|
261
|
-
if not extracted_strings:
|
|
262
|
-
return config_dict
|
|
263
|
-
|
|
264
|
-
pattern = re.compile(b'.\x80')
|
|
265
|
-
for string in extracted_strings:
|
|
266
|
-
try:
|
|
267
|
-
custom_b64_decoded = custom_b64decode(string, custom_alphabets[0])
|
|
268
|
-
|
|
269
|
-
xor_key = chacha20_xor(custom_b64_decoded, key, nonce)
|
|
270
|
-
|
|
271
|
-
# Decrypted, but may still be the compressed malware configuration
|
|
272
|
-
config = decrypt_config(xor_key)
|
|
273
|
-
# Attempt to extract C2 url, only works in version prior to 0.9.2
|
|
274
|
-
c2_url = extract_c2_url(config)
|
|
275
|
-
if c2_url:
|
|
276
|
-
config_dict = {"CNCs": [c2_url]}
|
|
277
|
-
return config_dict
|
|
278
|
-
else:
|
|
279
|
-
# Handle new variants that compress the Command and Control server(s)
|
|
280
|
-
custom_b64_decoded = custom_b64decode(string, custom_alphabets[2])
|
|
281
|
-
xor_key = chacha20_xor(custom_b64_decoded, key, nonce)
|
|
282
|
-
config = decrypt_config(xor_key)
|
|
283
|
-
|
|
284
|
-
parsed = parse_compression_header(config)
|
|
285
|
-
if not parsed:
|
|
286
|
-
return config_dict
|
|
287
|
-
|
|
288
|
-
decompressed = lzo_noheader_decompress(parsed['compressed_data'], parsed['decompressed_size'])
|
|
289
|
-
|
|
290
|
-
# Try old alphabet for 0.9.2
|
|
291
|
-
if not decompressed:
|
|
292
|
-
custom_b64_decoded = custom_b64decode(string, custom_alphabets[1])
|
|
293
|
-
xor_key = chacha20_xor(custom_b64_decoded, key, nonce)
|
|
294
|
-
config = decrypt_config(xor_key)
|
|
295
|
-
|
|
296
|
-
parsed = parse_compression_header(config)
|
|
297
|
-
if not parsed:
|
|
298
|
-
return config_dict
|
|
299
|
-
|
|
300
|
-
decompressed = lzo_noheader_decompress(parsed['compressed_data'], parsed['decompressed_size'])
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
cncs = [f"https://{chunk.decode()}" for chunk in pattern.split(decompressed) if chunk]
|
|
304
|
-
if cncs:
|
|
305
|
-
config_dict = {"CNCs": cncs}
|
|
306
|
-
return config_dict
|
|
307
|
-
|
|
308
|
-
except Exception:
|
|
309
|
-
continue
|
|
310
303
|
|
|
304
|
+
# New variants, extract base64 strings
|
|
305
|
+
extracted_strings = extract_base64_strings(data, 100, 256)
|
|
306
|
+
if not extracted_strings:
|
|
311
307
|
return config_dict
|
|
312
308
|
|
|
309
|
+
# Handle each encrypted string
|
|
310
|
+
for string in extracted_strings:
|
|
311
|
+
cncs = handle_encrypted_string(string)
|
|
312
|
+
if cncs:
|
|
313
|
+
return {"CNCs": cncs}
|
|
314
|
+
|
|
315
|
+
return config_dict
|
|
316
|
+
|
|
313
317
|
|
|
314
318
|
if __name__ == "__main__":
|
|
315
319
|
import sys
|
|
@@ -9,9 +9,9 @@ cape_parsers/CAPE/community/CobaltStrikeBeacon.py,sha256=U4Q0ObCrPRpiO5B5fBmkgr6
|
|
|
9
9
|
cape_parsers/CAPE/community/CobaltStrikeStager.py,sha256=HLxROBjz453uHNq1bPz0VSAhtyWDfz79ZacTPdjuWmY,7535
|
|
10
10
|
cape_parsers/CAPE/community/DCRat.py,sha256=0-FRT3d2x63KQ_cs1xmKFj7x0JRf7ID6QDc_DvBa0PM,1003
|
|
11
11
|
cape_parsers/CAPE/community/Fareit.py,sha256=OyKeZdcvyAhjxZgJqkDPJHP4Npv1ArvTHJZ5F0C1Iac,1875
|
|
12
|
-
cape_parsers/CAPE/community/KoiLoader.py,sha256=
|
|
12
|
+
cape_parsers/CAPE/community/KoiLoader.py,sha256=cWV3TcVz5Qg7kkmMYYtuh5cXhAKohGAk6d6aXuId0lc,4077
|
|
13
13
|
cape_parsers/CAPE/community/LokiBot.py,sha256=355kqLx0LNMr8XcGfPL7cxG8QZalcmE7ttVBqoWtTWE,5754
|
|
14
|
-
cape_parsers/CAPE/community/Lumma.py,sha256=
|
|
14
|
+
cape_parsers/CAPE/community/Lumma.py,sha256=Iqd9yvt3g0FeV_bYRmL1RKp4C1H92qeGg4fXivVDSxw,12206
|
|
15
15
|
cape_parsers/CAPE/community/MonsterV2.py,sha256=cFxhYxo7FruTMmFY3OtBO-E0hDyxfsC3zWX3BlcB-qI,2915
|
|
16
16
|
cape_parsers/CAPE/community/MyKings.py,sha256=bcypBMJf6Jeg0yrHZ-J-XjnhHvFbb-lpDF_zwW63gOk,1397
|
|
17
17
|
cape_parsers/CAPE/community/NanoCore.py,sha256=8QZnf1AcY9481kSfsf3SHQShwPLn97peGAf8_xEasQc,6230
|
|
@@ -44,7 +44,7 @@ cape_parsers/CAPE/core/GuLoader.py,sha256=wH6t1e7rO60Bwe0ulqFdZq12-M087zT5WQtC_W
|
|
|
44
44
|
cape_parsers/CAPE/core/IcedID.py,sha256=TEsvFq8qHz_D5kIURKWSC4lbvWaQbMriDZ3jQsVu2VA,4029
|
|
45
45
|
cape_parsers/CAPE/core/IcedIDLoader.py,sha256=YUOEILpTycO01KK4qqAxGSplsRVs2EzjscUw4T-DGWs,1602
|
|
46
46
|
cape_parsers/CAPE/core/Latrodectus.py,sha256=1K9yUUYtzRJ2c3unrYIUaA8nE--Zoqi5pjXY7t7t1qg,7751
|
|
47
|
-
cape_parsers/CAPE/core/NitroBunnyDownloader.py,sha256=
|
|
47
|
+
cape_parsers/CAPE/core/NitroBunnyDownloader.py,sha256=nxySGP7t5yv7-YL31-IiMxzkKCYKrDQgzWfMXadha2g,5265
|
|
48
48
|
cape_parsers/CAPE/core/Oyster.py,sha256=QStBScevJuLyd5d4Rw093SxTlbRG1LFkDwYgmjZx-EQ,4881
|
|
49
49
|
cape_parsers/CAPE/core/PikaBot.py,sha256=6Q8goXfMsSoU8UkdE9iuZY2KTxX_AmWhH1szke_HfWA,5280
|
|
50
50
|
cape_parsers/CAPE/core/PlugX.py,sha256=lGwr1T3mttG6CTbZCj_Cf5HnOad60A3LP264jlCsGsc,13192
|
|
@@ -53,7 +53,7 @@ cape_parsers/CAPE/core/Quickbind.py,sha256=5A077RFQQOL8dtr2Q9vmlTKsWk96JkRWuHGse
|
|
|
53
53
|
cape_parsers/CAPE/core/README.md,sha256=Zd84WEUj9NzKzGnVZV1jx6gMiEOtz01m32B7xEuS91k,17
|
|
54
54
|
cape_parsers/CAPE/core/RedLine.py,sha256=bZeKLvxaS6HDpWY4RDXtSEBt93qTNzZG5iE6FNS0dOY,5734
|
|
55
55
|
cape_parsers/CAPE/core/Remcos.py,sha256=MIpO2FwehBGIhO7hS0TT2hdDsgvxlI5ps4rAwyFwdTY,9483
|
|
56
|
-
cape_parsers/CAPE/core/Rhadamanthys.py,sha256=
|
|
56
|
+
cape_parsers/CAPE/core/Rhadamanthys.py,sha256=0vj3M1IC4oPISj1R7ELl9JZm1Uha9DTdbNJraJGdbh0,10725
|
|
57
57
|
cape_parsers/CAPE/core/SmokeLoader.py,sha256=ruQ_GDiZvqtGxUTbN2N6fajUYWkIylFTvMXijgZ8L20,3890
|
|
58
58
|
cape_parsers/CAPE/core/Socks5Systemz.py,sha256=jSt6QejL5K99dIB3qdItvUHL28w6N60xuwc8EQHM5Mk,783
|
|
59
59
|
cape_parsers/CAPE/core/SquirrelWaffle.py,sha256=UMha7l60fL64VPHxueFUnCEGaO-CXau5ftEyK-Wv__o,3308
|
|
@@ -111,7 +111,7 @@ cape_parsers/utils/blzpack_lib.so,sha256=5PJtnggw8fV5q4DlhwMJk4ZadvC3fFTsVTNZKvE
|
|
|
111
111
|
cape_parsers/utils/dotnet_utils.py,sha256=pzQGbCqccz7DRv8T_i1JURlrKDIlDT2axxViiFF9hsU,1672
|
|
112
112
|
cape_parsers/utils/lznt1.py,sha256=X-BmJtP6AwYSl0ORg5dfSt-NIuXbHrtCO5kUaaJI2C8,4066
|
|
113
113
|
cape_parsers/utils/strings.py,sha256=a-nbvP9jYST7b6t_H37Ype-fK2jEmQr-wMF5a4i04e4,3062
|
|
114
|
-
cape_parsers-0.1.
|
|
115
|
-
cape_parsers-0.1.
|
|
116
|
-
cape_parsers-0.1.
|
|
117
|
-
cape_parsers-0.1.
|
|
114
|
+
cape_parsers-0.1.55.dist-info/METADATA,sha256=eQvSMB-kC-_Hrdc78Sv-uxnNrVywaTqlLf_cjIG-gy8,1826
|
|
115
|
+
cape_parsers-0.1.55.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
|
|
116
|
+
cape_parsers-0.1.55.dist-info/licenses/LICENSE,sha256=88c01_HLG8WPj7R7aU_b-O-UoF38vrrifvcko4KDxcE,1069
|
|
117
|
+
cape_parsers-0.1.55.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|