CAPE-parsers 0.1.42__py3-none-any.whl → 0.1.54__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/AgentTesla.py +25 -10
- cape_parsers/CAPE/community/Amadey.py +199 -29
- cape_parsers/CAPE/community/Arkei.py +13 -15
- cape_parsers/CAPE/community/AsyncRAT.py +4 -2
- cape_parsers/CAPE/community/AuroraStealer.py +9 -6
- cape_parsers/CAPE/community/Carbanak.py +7 -7
- cape_parsers/CAPE/community/CobaltStrikeBeacon.py +5 -4
- cape_parsers/CAPE/community/CobaltStrikeStager.py +4 -1
- cape_parsers/CAPE/community/DCRat.py +4 -2
- cape_parsers/CAPE/community/Fareit.py +8 -9
- cape_parsers/CAPE/community/KoiLoader.py +3 -3
- cape_parsers/CAPE/community/LokiBot.py +11 -8
- cape_parsers/CAPE/community/Lumma.py +58 -40
- cape_parsers/CAPE/community/MonsterV2.py +93 -0
- cape_parsers/CAPE/community/MyKings.py +52 -0
- cape_parsers/CAPE/community/NanoCore.py +9 -9
- cape_parsers/CAPE/community/Nighthawk.py +1 -0
- cape_parsers/CAPE/community/Njrat.py +4 -4
- cape_parsers/CAPE/community/PhemedroneStealer.py +2 -0
- cape_parsers/CAPE/community/Snake.py +31 -18
- cape_parsers/CAPE/community/SparkRAT.py +3 -1
- cape_parsers/CAPE/community/Stealc.py +95 -63
- cape_parsers/CAPE/community/VenomRAT.py +4 -2
- cape_parsers/CAPE/community/WinosStager.py +75 -0
- cape_parsers/CAPE/community/XWorm.py +4 -2
- cape_parsers/CAPE/community/XenoRAT.py +4 -2
- cape_parsers/CAPE/core/AdaptixBeacon.py +7 -5
- cape_parsers/CAPE/core/AuraStealer.py +100 -0
- cape_parsers/CAPE/core/Azorult.py +5 -3
- cape_parsers/CAPE/core/BitPaymer.py +5 -2
- cape_parsers/CAPE/core/BlackDropper.py +10 -5
- cape_parsers/CAPE/core/Blister.py +12 -10
- cape_parsers/CAPE/core/BruteRatel.py +20 -7
- cape_parsers/CAPE/core/BumbleBee.py +34 -22
- cape_parsers/CAPE/core/DarkGate.py +3 -3
- cape_parsers/CAPE/core/DoppelPaymer.py +4 -2
- cape_parsers/CAPE/core/DridexLoader.py +4 -3
- cape_parsers/CAPE/core/Formbook.py +2 -2
- cape_parsers/CAPE/core/GuLoader.py +2 -5
- cape_parsers/CAPE/core/IcedID.py +5 -5
- cape_parsers/CAPE/core/IcedIDLoader.py +4 -4
- cape_parsers/CAPE/core/Latrodectus.py +14 -10
- cape_parsers/CAPE/core/NitroBunnyDownloader.py +151 -0
- cape_parsers/CAPE/core/Oyster.py +8 -6
- cape_parsers/CAPE/core/PikaBot.py +6 -6
- cape_parsers/CAPE/core/PlugX.py +3 -1
- cape_parsers/CAPE/core/QakBot.py +2 -1
- cape_parsers/CAPE/core/Quickbind.py +7 -11
- cape_parsers/CAPE/core/RedLine.py +2 -2
- cape_parsers/CAPE/core/Remcos.py +59 -51
- cape_parsers/CAPE/core/Rhadamanthys.py +175 -36
- cape_parsers/CAPE/core/SmokeLoader.py +2 -2
- cape_parsers/CAPE/core/Socks5Systemz.py +5 -5
- cape_parsers/CAPE/core/SquirrelWaffle.py +3 -3
- cape_parsers/CAPE/core/Strrat.py +1 -1
- cape_parsers/CAPE/core/WarzoneRAT.py +3 -2
- cape_parsers/CAPE/core/Zloader.py +21 -15
- cape_parsers/RATDecoders/test_rats.py +1 -0
- cape_parsers/__init__.py +14 -5
- cape_parsers/deprecated/BlackNix.py +59 -0
- cape_parsers/{CAPE/core → deprecated}/BuerLoader.py +1 -1
- cape_parsers/{CAPE/core → deprecated}/ChChes.py +3 -3
- cape_parsers/{CAPE/core → deprecated}/Enfal.py +1 -1
- cape_parsers/{CAPE/core → deprecated}/EvilGrab.py +5 -6
- cape_parsers/{CAPE/community → deprecated}/Greame.py +3 -1
- cape_parsers/{CAPE/core → deprecated}/HttpBrowser.py +7 -8
- cape_parsers/{CAPE/community → deprecated}/Pandora.py +2 -0
- cape_parsers/{CAPE/community → deprecated}/Punisher.py +2 -1
- cape_parsers/{CAPE/core → deprecated}/RCSession.py +7 -9
- cape_parsers/{CAPE/community → deprecated}/REvil.py +10 -5
- cape_parsers/{CAPE/core → deprecated}/RedLeaf.py +5 -7
- cape_parsers/{CAPE/community → deprecated}/Retefe.py +0 -2
- cape_parsers/{CAPE/community → deprecated}/Rozena.py +2 -5
- cape_parsers/{CAPE/community → deprecated}/SmallNet.py +6 -2
- {cape_parsers-0.1.42.dist-info → cape_parsers-0.1.54.dist-info}/METADATA +24 -3
- cape_parsers-0.1.54.dist-info/RECORD +117 -0
- {cape_parsers-0.1.42.dist-info → cape_parsers-0.1.54.dist-info}/WHEEL +1 -1
- cape_parsers/CAPE/community/BlackNix.py +0 -57
- cape_parsers/CAPE/core/Stealc.py +0 -21
- cape_parsers-0.1.42.dist-info/RECORD +0 -113
- /cape_parsers/{CAPE/community → deprecated}/BackOffLoader.py +0 -0
- /cape_parsers/{CAPE/community → deprecated}/BackOffPOS.py +0 -0
- /cape_parsers/{CAPE/core → deprecated}/Emotet.py +0 -0
- /cape_parsers/{CAPE/community → deprecated}/PoisonIvy.py +0 -0
- /cape_parsers/{CAPE/community → deprecated}/TSCookie.py +0 -0
- /cape_parsers/{CAPE/community → deprecated}/TrickBot.py +0 -0
- /cape_parsers/{CAPE/core → deprecated}/UrsnifV3.py +0 -0
- {cape_parsers-0.1.42.dist-info → cape_parsers-0.1.54.dist-info/licenses}/LICENSE +0 -0
|
@@ -5,7 +5,7 @@ import struct
|
|
|
5
5
|
from contextlib import suppress
|
|
6
6
|
import pefile
|
|
7
7
|
import yara
|
|
8
|
-
|
|
8
|
+
from Cryptodome.Cipher import ChaCha20
|
|
9
9
|
|
|
10
10
|
RULE_SOURCE_BUILD_ID = """rule LummaBuildId
|
|
11
11
|
{
|
|
@@ -75,7 +75,6 @@ RULE_SOURCE_LUMMA_NEW_ENCRYPTED_C2 = """rule LummaConfigNewEncryptedStrings
|
|
|
75
75
|
}"""
|
|
76
76
|
|
|
77
77
|
|
|
78
|
-
|
|
79
78
|
def yara_scan_generator(raw_data, rule_source):
|
|
80
79
|
yara_rules = yara.compile(source=rule_source)
|
|
81
80
|
matches = yara_rules.match(data=raw_data)
|
|
@@ -143,7 +142,7 @@ def contains_non_printable(byte_array):
|
|
|
143
142
|
return True
|
|
144
143
|
return False
|
|
145
144
|
|
|
146
|
-
|
|
145
|
+
"""
|
|
147
146
|
def mask32(x):
|
|
148
147
|
return x & 0xFFFFFFFF
|
|
149
148
|
|
|
@@ -198,10 +197,22 @@ def chacha20_block(key, nonce, blocknum):
|
|
|
198
197
|
nonce_words = words_from_bytes(nonce)
|
|
199
198
|
|
|
200
199
|
original_block = [
|
|
201
|
-
constant_words[0],
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
200
|
+
constant_words[0],
|
|
201
|
+
constant_words[1],
|
|
202
|
+
constant_words[2],
|
|
203
|
+
constant_words[3],
|
|
204
|
+
key_words[0],
|
|
205
|
+
key_words[1],
|
|
206
|
+
key_words[2],
|
|
207
|
+
key_words[3],
|
|
208
|
+
key_words[4],
|
|
209
|
+
key_words[5],
|
|
210
|
+
key_words[6],
|
|
211
|
+
key_words[7],
|
|
212
|
+
mask32(blocknum),
|
|
213
|
+
nonce_words[0],
|
|
214
|
+
nonce_words[1],
|
|
215
|
+
nonce_words[2],
|
|
205
216
|
]
|
|
206
217
|
|
|
207
218
|
permuted_block = list(original_block)
|
|
@@ -231,7 +242,7 @@ def chacha20_xor(message, key, nonce, counter):
|
|
|
231
242
|
xor_key.append(message[i] ^ key_stream[i])
|
|
232
243
|
|
|
233
244
|
return xor_key
|
|
234
|
-
|
|
245
|
+
"""
|
|
235
246
|
|
|
236
247
|
def extract_c2_domain(data):
|
|
237
248
|
pattern = rb"([\w-]+\.[\w]+)\x00"
|
|
@@ -241,7 +252,7 @@ def extract_c2_domain(data):
|
|
|
241
252
|
|
|
242
253
|
|
|
243
254
|
def find_encrypted_c2_blocks(data):
|
|
244
|
-
pattern = rb
|
|
255
|
+
pattern = rb"(.{128})\x00"
|
|
245
256
|
for match in re.findall(pattern, data, re.DOTALL):
|
|
246
257
|
yield match
|
|
247
258
|
|
|
@@ -251,9 +262,9 @@ def get_build_id(pe, data):
|
|
|
251
262
|
image_base = pe.OPTIONAL_HEADER.ImageBase
|
|
252
263
|
for offset in yara_scan_generator(data, RULE_SOURCE_BUILD_ID):
|
|
253
264
|
try:
|
|
254
|
-
build_id_data_rva = struct.unpack(
|
|
265
|
+
build_id_data_rva = struct.unpack("i", data[offset + 2 : offset + 6])[0]
|
|
255
266
|
build_id_dword_offset = pe.get_offset_from_rva(build_id_data_rva - image_base)
|
|
256
|
-
build_id_dword_rva = struct.unpack(
|
|
267
|
+
build_id_dword_rva = struct.unpack("i", data[build_id_dword_offset : build_id_dword_offset + 4])[0]
|
|
257
268
|
build_id_offset = pe.get_offset_from_rva(build_id_dword_rva - image_base)
|
|
258
269
|
build_id = pe.get_string_from_data(build_id_offset, data)
|
|
259
270
|
if not contains_non_printable(build_id):
|
|
@@ -263,18 +274,20 @@ def get_build_id(pe, data):
|
|
|
263
274
|
continue
|
|
264
275
|
return build_id
|
|
265
276
|
|
|
277
|
+
|
|
266
278
|
def get_build_id_new(data):
|
|
267
279
|
build_id = ""
|
|
268
|
-
pattern = b
|
|
280
|
+
pattern = b"123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ\x00"
|
|
269
281
|
offset = data.find(pattern)
|
|
270
282
|
if offset != -1:
|
|
271
|
-
build_id = data[offset + len(pattern):].split(b
|
|
283
|
+
build_id = data[offset + len(pattern) :].split(b"\x00", 1)[0]
|
|
272
284
|
build_id = build_id.decode()
|
|
273
285
|
|
|
274
286
|
return build_id
|
|
275
287
|
|
|
288
|
+
|
|
276
289
|
def extract_config(data):
|
|
277
|
-
|
|
290
|
+
config = {}
|
|
278
291
|
|
|
279
292
|
# try to load as a PE
|
|
280
293
|
pe = None
|
|
@@ -287,37 +300,38 @@ def extract_config(data):
|
|
|
287
300
|
key = None
|
|
288
301
|
nonce = None
|
|
289
302
|
for offset in yara_scan_generator(data, RULE_SOURCE_LUMMA_NEW_KEYS):
|
|
290
|
-
key_rva = struct.unpack(
|
|
303
|
+
key_rva = struct.unpack("i", data[offset + 1 : offset + 5])[0]
|
|
291
304
|
key_offset = pe.get_offset_from_rva(key_rva - image_base)
|
|
292
305
|
key = data[key_offset : key_offset + 32]
|
|
293
|
-
nonce_rva = struct.unpack(
|
|
306
|
+
nonce_rva = struct.unpack("i", data[offset + 20 : offset + 24])[0]
|
|
294
307
|
nonce_offset = pe.get_offset_from_rva(nonce_rva - image_base)
|
|
295
|
-
nonce = b
|
|
308
|
+
nonce = b"\x00\x00\x00\x00" + data[nonce_offset : nonce_offset + 8]
|
|
296
309
|
|
|
297
310
|
if key and nonce:
|
|
298
311
|
for offset in yara_scan_generator(data, RULE_SOURCE_LUMMA_NEW_ENCRYPTED_C2):
|
|
299
|
-
encrypted_strings_rva = struct.unpack(
|
|
312
|
+
encrypted_strings_rva = struct.unpack("i", data[offset + 5 : offset + 9])[0]
|
|
300
313
|
encrypted_strings_offset = pe.get_offset_from_rva(encrypted_strings_rva - image_base)
|
|
301
314
|
step_size = 0x80
|
|
302
315
|
counter = 2
|
|
303
316
|
for i in range(12):
|
|
304
|
-
encrypted_string = data[encrypted_strings_offset:encrypted_strings_offset+40]
|
|
305
|
-
|
|
317
|
+
encrypted_string = data[encrypted_strings_offset : encrypted_strings_offset + 40]
|
|
318
|
+
chacha20_cipher = ChaCha20.new(key=key, nonce=nonce)
|
|
319
|
+
chacha20_cipher.seek(counter)
|
|
320
|
+
decoded_c2 = chacha20_cipher.decrypt(encrypted_string).split(b"\x00", 1)[0]
|
|
306
321
|
if contains_non_printable(decoded_c2):
|
|
307
322
|
break
|
|
308
|
-
|
|
323
|
+
config.setdefault("CNCs", []).append("https://" + decoded_c2.decode())
|
|
309
324
|
encrypted_strings_offset = encrypted_strings_offset + step_size
|
|
310
325
|
counter += 2
|
|
311
326
|
|
|
312
|
-
if
|
|
327
|
+
if config.get("CNCs"):
|
|
313
328
|
# If found C2 servers try to find build ID
|
|
314
329
|
build_id = get_build_id_new(data)
|
|
315
330
|
if build_id:
|
|
316
|
-
|
|
317
|
-
|
|
331
|
+
config["build"] = build_id
|
|
318
332
|
|
|
319
333
|
# If no C2s try with the version after Jan 21, 2025
|
|
320
|
-
if not
|
|
334
|
+
if "CNCs" not in config:
|
|
321
335
|
offset = yara_scan(data, RULE_SOURCE_LUMMA)
|
|
322
336
|
if offset:
|
|
323
337
|
key = data[offset + 16 : offset + 48]
|
|
@@ -327,7 +341,7 @@ def extract_config(data):
|
|
|
327
341
|
try:
|
|
328
342
|
start_offset = offset + 56 + (i * 4)
|
|
329
343
|
end_offset = start_offset + 4
|
|
330
|
-
c2_dword_rva = struct.unpack(
|
|
344
|
+
c2_dword_rva = struct.unpack("i", data[start_offset:end_offset])[0]
|
|
331
345
|
if pe:
|
|
332
346
|
c2_dword_offset = pe.get_offset_from_rva(c2_dword_rva - image_base)
|
|
333
347
|
else:
|
|
@@ -336,25 +350,26 @@ def extract_config(data):
|
|
|
336
350
|
c2_encrypted = data[c2_dword_offset : c2_dword_offset + 0x80]
|
|
337
351
|
counters = [0, 2, 4, 6, 8, 10, 12, 14, 16]
|
|
338
352
|
for counter in counters:
|
|
339
|
-
decrypted = chacha20_xor(c2_encrypted, key, nonce, counter)
|
|
353
|
+
# decrypted = chacha20_xor(c2_encrypted, key, nonce, counter)
|
|
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]
|
|
340
357
|
c2 = extract_c2_domain(decrypted)
|
|
341
358
|
if c2 is not None and len(c2) > 10:
|
|
342
|
-
|
|
359
|
+
config["CNCs"].append("https://" + c2.decode())
|
|
343
360
|
break
|
|
344
361
|
|
|
345
362
|
except Exception:
|
|
346
363
|
continue
|
|
347
364
|
|
|
348
|
-
if
|
|
365
|
+
if "CNCs" in config and config["CNCs"] and pe is not None:
|
|
349
366
|
# If found C2 servers try to find build ID
|
|
350
367
|
build_id = get_build_id(pe, data)
|
|
351
368
|
if build_id:
|
|
352
|
-
|
|
353
|
-
|
|
369
|
+
config["build"] = build_id
|
|
354
370
|
|
|
355
371
|
# If no C2s try with version prior to Jan 21, 2025
|
|
356
|
-
if not
|
|
357
|
-
|
|
372
|
+
if "CNCs" not in config:
|
|
358
373
|
try:
|
|
359
374
|
if pe is not None:
|
|
360
375
|
rdata = get_rdata(pe, data)
|
|
@@ -374,21 +389,24 @@ def extract_config(data):
|
|
|
374
389
|
decoded_c2 = xor_data(encoded_c2, xor_key)
|
|
375
390
|
|
|
376
391
|
if not contains_non_printable(decoded_c2):
|
|
377
|
-
|
|
378
|
-
except Exception:
|
|
392
|
+
config.setdefault("CNCs", []).append("https://" + decoded_c2.decode())
|
|
393
|
+
except Exception as e:
|
|
394
|
+
print(e)
|
|
379
395
|
continue
|
|
380
396
|
|
|
381
|
-
except Exception:
|
|
397
|
+
except Exception as e:
|
|
398
|
+
print(e)
|
|
382
399
|
return
|
|
383
400
|
|
|
384
|
-
if
|
|
401
|
+
if "CNCs" in config and pe is not None:
|
|
385
402
|
# If found C2 servers try to find build ID
|
|
386
403
|
build_id = get_build_id(pe, data)
|
|
387
404
|
if build_id:
|
|
388
|
-
|
|
389
|
-
|
|
405
|
+
config["build"] = build_id
|
|
390
406
|
|
|
391
|
-
|
|
407
|
+
print(config)
|
|
408
|
+
if config:
|
|
409
|
+
return config
|
|
392
410
|
|
|
393
411
|
|
|
394
412
|
if __name__ == "__main__":
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
from Crypto.Cipher import ChaCha20_Poly1305
|
|
3
|
+
from contextlib import suppress
|
|
4
|
+
import zlib
|
|
5
|
+
import struct
|
|
6
|
+
import json
|
|
7
|
+
import yara
|
|
8
|
+
import pefile
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
RULE_SOURCE = """rule MonsterV2Config
|
|
12
|
+
{
|
|
13
|
+
meta:
|
|
14
|
+
author = "doomedraven,YungBinary"
|
|
15
|
+
strings:
|
|
16
|
+
$chunk_1 = {
|
|
17
|
+
41 B8 0E 04 00 00
|
|
18
|
+
48 8D 15 ?? ?? ?? 00
|
|
19
|
+
48 8B C?
|
|
20
|
+
E8 ?? ?? ?? ?? [3-17]
|
|
21
|
+
4C 8B C?
|
|
22
|
+
48 8D 54 24 28
|
|
23
|
+
48 8B CE
|
|
24
|
+
E8 ?? ?? ?? ??
|
|
25
|
+
}
|
|
26
|
+
condition:
|
|
27
|
+
$chunk_1
|
|
28
|
+
}"""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def derive_chacha_key_nonce_blake2b(seed: bytes): # -> tuple[bytes, bytes]:
|
|
32
|
+
"""
|
|
33
|
+
Derives a 32-byte ChaCha20 key and a 24-byte ChaCha20 nonce
|
|
34
|
+
using BLAKE2b from a given seed.
|
|
35
|
+
"""
|
|
36
|
+
output_length = 56 # 32 bytes for key + 24 bytes for nonce
|
|
37
|
+
h = hashlib.blake2b(digest_size=output_length)
|
|
38
|
+
h.update(seed)
|
|
39
|
+
derived_material = h.digest()
|
|
40
|
+
chacha20_key = derived_material[0:32]
|
|
41
|
+
chacha20_nonce = derived_material[32:56]
|
|
42
|
+
return chacha20_key, chacha20_nonce
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def yara_scan(raw_data, rule_source):
|
|
46
|
+
yara_rules = yara.compile(source=rule_source)
|
|
47
|
+
matches = yara_rules.match(data=raw_data)
|
|
48
|
+
|
|
49
|
+
for match in matches:
|
|
50
|
+
for block in match.strings:
|
|
51
|
+
for instance in block.instances:
|
|
52
|
+
return instance.offset
|
|
53
|
+
|
|
54
|
+
def extract_config(data: bytes) -> dict:
|
|
55
|
+
config_dict = {}
|
|
56
|
+
with suppress(Exception):
|
|
57
|
+
pe = pefile.PE(data=data)
|
|
58
|
+
offset = yara_scan(data, RULE_SOURCE)
|
|
59
|
+
|
|
60
|
+
# image_base = pe.OPTIONAL_HEADER.ImageBase
|
|
61
|
+
disp_offset = data[offset + 9 : offset + 13]
|
|
62
|
+
disp_offset = struct.unpack('i', disp_offset)[0]
|
|
63
|
+
instruction_pointer_va = pe.get_rva_from_offset(offset + 13)
|
|
64
|
+
config_offset_va = instruction_pointer_va + disp_offset
|
|
65
|
+
config_offset = pe.get_offset_from_rva(config_offset_va)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
blake_seed = data[config_offset : config_offset + 32]
|
|
69
|
+
chacha20_key, chacha20_nonce = derive_chacha_key_nonce_blake2b(blake_seed)
|
|
70
|
+
cipher_len = int.from_bytes(data[config_offset + 32 : config_offset + 40], byteorder="big")
|
|
71
|
+
cipher_text = data[config_offset + 40 : config_offset + 40 + cipher_len]
|
|
72
|
+
|
|
73
|
+
cipher = ChaCha20_Poly1305.new(key=chacha20_key, nonce=chacha20_nonce)
|
|
74
|
+
decrypted_zlib_data = cipher.decrypt(cipher_text)
|
|
75
|
+
decompressed_data = zlib.decompress(decrypted_zlib_data)
|
|
76
|
+
config_dict = json.loads(decompressed_data)
|
|
77
|
+
|
|
78
|
+
if config_dict:
|
|
79
|
+
final_config = {"raw": config_dict}
|
|
80
|
+
if "ip" in config_dict and "port" in config_dict:
|
|
81
|
+
final_config["CNCs"] = [f"tcp://{config_dict['ip']}:{config_dict['port']}"]
|
|
82
|
+
if "build_name" in config_dict:
|
|
83
|
+
final_config["build"] = config_dict["build_name"]
|
|
84
|
+
return final_config
|
|
85
|
+
|
|
86
|
+
return {}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
if __name__ == "__main__":
|
|
90
|
+
import sys
|
|
91
|
+
|
|
92
|
+
with open(sys.argv[1], "rb") as f:
|
|
93
|
+
print(extract_config(f.read()))
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Description: MyKings AKA Smominru config parser
|
|
3
|
+
Author: x.com/YungBinary
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from contextlib import suppress
|
|
7
|
+
import json
|
|
8
|
+
import re
|
|
9
|
+
import base64
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def contains_non_printable(byte_array):
|
|
13
|
+
for byte in byte_array:
|
|
14
|
+
if not chr(byte).isprintable():
|
|
15
|
+
return True
|
|
16
|
+
return False
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def extract_base64_strings(data: bytes, minchars: int, maxchars: int) -> list:
|
|
20
|
+
pattern = b"([A-Za-z0-9+/=]{" + str(minchars).encode() + b"," + str(maxchars).encode() + b"})\x00{4}"
|
|
21
|
+
strings = []
|
|
22
|
+
for string in re.findall(pattern, data):
|
|
23
|
+
decoded_string = base64_and_printable(string.decode())
|
|
24
|
+
if decoded_string:
|
|
25
|
+
strings.append(decoded_string)
|
|
26
|
+
return strings
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def base64_and_printable(b64_string: str):
|
|
30
|
+
with suppress(Exception):
|
|
31
|
+
decoded_bytes = base64.b64decode(b64_string)
|
|
32
|
+
if not contains_non_printable(decoded_bytes):
|
|
33
|
+
return decoded_bytes.decode('ascii')
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def extract_config(data: bytes) -> dict:
|
|
37
|
+
config_dict = {}
|
|
38
|
+
with suppress(Exception):
|
|
39
|
+
cncs = extract_base64_strings(data, 12, 60)
|
|
40
|
+
if cncs:
|
|
41
|
+
# as they don't have schema they going under raw
|
|
42
|
+
config_dict["raw"] = {"CNCs": cncs}
|
|
43
|
+
return config_dict
|
|
44
|
+
|
|
45
|
+
return {}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
if __name__ == "__main__":
|
|
49
|
+
import sys
|
|
50
|
+
|
|
51
|
+
with open(sys.argv[1], "rb") as f:
|
|
52
|
+
print(json.dumps(extract_config(f.read()), indent=4))
|
|
@@ -174,21 +174,21 @@ def extract_config(filebuf):
|
|
|
174
174
|
pass
|
|
175
175
|
elif DataType.DATETIME == param["type"]:
|
|
176
176
|
dt = param["value"]
|
|
177
|
-
config_dict[item_name] = dt.strftime("%Y-%m-%d %H:%M:%S.%f")
|
|
177
|
+
config_dict.setdefault("raw", {})[item_name] = dt.strftime("%Y-%m-%d %H:%M:%S.%f")
|
|
178
178
|
else:
|
|
179
|
-
config_dict[item_name] = str(param["value"])
|
|
179
|
+
config_dict.setdefault("raw", {})[item_name] = str(param["value"])
|
|
180
180
|
except Exception as e:
|
|
181
181
|
log.error("nanocore error: %s", e)
|
|
182
182
|
|
|
183
183
|
cncs = []
|
|
184
184
|
|
|
185
|
-
if config_dict.get("PrimaryConnectionHost"):
|
|
186
|
-
cncs.append(config_dict["PrimaryConnectionHost"])
|
|
187
|
-
if config_dict.get("PrimaryConnectionHost"):
|
|
188
|
-
cncs.append(config_dict["BackupConnectionHost"])
|
|
189
|
-
if config_dict.get("ConnectionPort") and cncs:
|
|
190
|
-
port = config_dict["ConnectionPort"]
|
|
191
|
-
config_dict["
|
|
185
|
+
if config_dict.get("raw", {}).get("PrimaryConnectionHost"):
|
|
186
|
+
cncs.append(config_dict["raw"]["PrimaryConnectionHost"])
|
|
187
|
+
if config_dict.get("raw", {}).get("PrimaryConnectionHost"):
|
|
188
|
+
cncs.append(config_dict["raw"]["BackupConnectionHost"])
|
|
189
|
+
if config_dict.get("raw", {}).get("ConnectionPort") and cncs:
|
|
190
|
+
port = config_dict["raw"]["ConnectionPort"]
|
|
191
|
+
config_dict["CNCs"] = [f"{cnc}:{port}" for cnc in cncs]
|
|
192
192
|
return config_dict
|
|
193
193
|
|
|
194
194
|
|
|
@@ -176,11 +176,11 @@ def extract_config(data):
|
|
|
176
176
|
config = get_clean_config(config_dict)
|
|
177
177
|
if config:
|
|
178
178
|
if config.get("domain") and config.get("port"):
|
|
179
|
-
conf["
|
|
180
|
-
|
|
179
|
+
conf["CNCs"] = [f"{config['domain']}:{config['port']}"]
|
|
180
|
+
|
|
181
181
|
if config.get("campaign_id"):
|
|
182
|
-
conf["campaign
|
|
183
|
-
|
|
182
|
+
conf["campaign"] = config["campaign_id"]
|
|
183
|
+
|
|
184
184
|
if config.get("version"):
|
|
185
185
|
conf["version"] = config["version"]
|
|
186
186
|
|
|
@@ -188,4 +188,6 @@ def extract_config(data):
|
|
|
188
188
|
value_data = inst_.split(".")[-1].strip()
|
|
189
189
|
config_field_name, str_list = check_next_inst(pe, body, DnfileParse, index)
|
|
190
190
|
config_dict[config_field_name] = value_data
|
|
191
|
+
if config_dict:
|
|
192
|
+
config_dict = {"raw": config_dict}
|
|
191
193
|
return config_dict
|
|
@@ -38,7 +38,7 @@ def handle_plain(dotnet_file, c2_type, user_strings):
|
|
|
38
38
|
if c2_type == "Telegram":
|
|
39
39
|
token = dotnet_file.net.user_strings.get(user_strings_list[15]).value.__str__()
|
|
40
40
|
chat_id = dotnet_file.net.user_strings.get(user_strings_list[16]).value.__str__()
|
|
41
|
-
return {"Type": "Telegram", "
|
|
41
|
+
return {"raw": {"Type": "Telegram"}, "CNCs": f"https://api.telegram.org/bot{token}/sendMessage?chat_id={chat_id}"}
|
|
42
42
|
elif c2_type == "SMTP":
|
|
43
43
|
smtp_from = dotnet_file.net.user_strings.get(user_strings_list[7]).value.__str__()
|
|
44
44
|
smtp_password = dotnet_file.net.user_strings.get(user_strings_list[8]).value.__str__()
|
|
@@ -46,18 +46,26 @@ def handle_plain(dotnet_file, c2_type, user_strings):
|
|
|
46
46
|
smtp_to = dotnet_file.net.user_strings.get(user_strings_list[10]).value.__str__()
|
|
47
47
|
smtp_port = dotnet_file.net.user_strings.get(user_strings_list[11]).value.__str__()
|
|
48
48
|
return {
|
|
49
|
-
"
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
49
|
+
"raw": {
|
|
50
|
+
"Type": "SMTP",
|
|
51
|
+
"Host": smtp_host,
|
|
52
|
+
"Port": smtp_port,
|
|
53
|
+
"From Address": smtp_from,
|
|
54
|
+
"To Address": smtp_to,
|
|
55
|
+
"Password": smtp_password,
|
|
56
|
+
},
|
|
57
|
+
"CNCs": [f"smtp://{smtp_host}:{smtp_port}"]
|
|
55
58
|
}
|
|
56
59
|
elif c2_type == "FTP":
|
|
57
60
|
ftp_username = dotnet_file.net.user_strings.get(user_strings_list[12]).value.__str__()
|
|
58
61
|
ftp_password = dotnet_file.net.user_strings.get(user_strings_list[13]).value.__str__()
|
|
59
62
|
ftp_host = dotnet_file.net.user_strings.get(user_strings_list[14]).value.__str__()
|
|
60
|
-
return {
|
|
63
|
+
return {
|
|
64
|
+
"raw": {
|
|
65
|
+
"Type": "FTP", "Host": ftp_host, "Username": ftp_username, "Password": ftp_password},
|
|
66
|
+
"CNCs": [f"ftp://{ftp_username}:{ftp_password}@{ftp_host}"]
|
|
67
|
+
}
|
|
68
|
+
|
|
61
69
|
|
|
62
70
|
|
|
63
71
|
def handle_encrypted(dotnet_file, data, c2_type, user_strings):
|
|
@@ -98,20 +106,25 @@ def handle_encrypted(dotnet_file, data, c2_type, user_strings):
|
|
|
98
106
|
if decrypted_strings:
|
|
99
107
|
if c2_type == "Telegram":
|
|
100
108
|
token, chat_id = decrypted_strings
|
|
101
|
-
config_dict = {"Type": "Telegram", "
|
|
109
|
+
config_dict = {"raw": {"Type": "Telegram"}, "CNCs": f"https://api.telegram.org/bot{token}/sendMessage?chat_id={chat_id}"}
|
|
102
110
|
elif c2_type == "SMTP":
|
|
103
111
|
smtp_from, smtp_password, smtp_host, smtp_to, smtp_port = decrypted_strings
|
|
104
112
|
config_dict = {
|
|
105
|
-
"
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
113
|
+
"raw": {
|
|
114
|
+
"Type": "SMTP",
|
|
115
|
+
"Host": smtp_host,
|
|
116
|
+
"Port": smtp_port,
|
|
117
|
+
"From Address": smtp_from,
|
|
118
|
+
"To Address": smtp_to,
|
|
119
|
+
"Password": smtp_password,
|
|
120
|
+
}
|
|
111
121
|
}
|
|
112
122
|
elif c2_type == "FTP":
|
|
113
123
|
ftp_username, ftp_password, ftp_host = decrypted_strings
|
|
114
|
-
config_dict = {
|
|
124
|
+
config_dict = {
|
|
125
|
+
"raw": {"Type": "FTP", "Host": ftp_host, "Username": ftp_username, "Password": ftp_password},
|
|
126
|
+
"CNCs": [f"ftp://{ftp_username}:{ftp_password}@{ftp_host}"]
|
|
127
|
+
}
|
|
115
128
|
return config_dict
|
|
116
129
|
|
|
117
130
|
|
|
@@ -120,7 +133,7 @@ def extract_config(data):
|
|
|
120
133
|
try:
|
|
121
134
|
dotnet_file = dnfile.dnPE(data=data)
|
|
122
135
|
except Exception as e:
|
|
123
|
-
log.debug(
|
|
136
|
+
log.debug("Exception when attempting to parse .NET file: %s", str(e))
|
|
124
137
|
log.debug(traceback.format_exc())
|
|
125
138
|
|
|
126
139
|
# ldstr, stsfld
|
|
@@ -152,7 +165,7 @@ def extract_config(data):
|
|
|
152
165
|
else:
|
|
153
166
|
user_strings[field_name] = string_index
|
|
154
167
|
except Exception as e:
|
|
155
|
-
log.debug(
|
|
168
|
+
log.debug("There was an exception parsing user strings: %s", str(e))
|
|
156
169
|
log.debug(traceback.format_exc())
|
|
157
170
|
|
|
158
171
|
if c2_type is None:
|
|
@@ -59,7 +59,9 @@ def extract_config(data):
|
|
|
59
59
|
key = f.read(16)
|
|
60
60
|
iv = f.read(16)
|
|
61
61
|
enc_data = f.read(data_len - 32)
|
|
62
|
-
|
|
62
|
+
config = decrypt_config(enc_data, key, iv)
|
|
63
|
+
if config:
|
|
64
|
+
return {"raw": config}
|
|
63
65
|
except Exception as e:
|
|
64
66
|
log.error("Configuration decryption failed: %s", e)
|
|
65
67
|
return {}
|