CAPE-parsers 0.1.55__tar.gz → 0.1.57__tar.gz
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-0.1.55 → cape_parsers-0.1.57}/PKG-INFO +1 -1
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/AgentTesla.py +7 -1
- cape_parsers-0.1.57/cape_parsers/CAPE/community/Amatera.py +186 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/Carbanak.py +7 -2
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/Fareit.py +3 -3
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/KoiLoader.py +1 -1
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/NanoCore.py +11 -1
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/Njrat.py +1 -1
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/Snake.py +2 -2
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/SparkRAT.py +5 -1
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/WinosStager.py +6 -3
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/AdaptixBeacon.py +13 -1
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/Blister.py +4 -1
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/DarkGate.py +30 -1
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/IcedIDLoader.py +2 -1
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/Latrodectus.py +2 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/NitroBunnyDownloader.py +33 -29
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/Oyster.py +128 -21
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/PikaBot.py +1 -2
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/QakBot.py +7 -4
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/Quickbind.py +3 -2
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/pyproject.toml +1 -1
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/LICENSE +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/README.md +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/__init__.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/Amadey.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/Arkei.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/AsyncRAT.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/AuroraStealer.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/CobaltStrikeBeacon.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/CobaltStrikeStager.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/DCRat.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/LokiBot.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/Lumma.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/MonsterV2.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/MyKings.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/Nighthawk.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/PhemedroneStealer.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/QuasarRAT.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/README.md +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/Stealc.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/VenomRAT.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/XWorm.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/XenoRAT.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/__init__.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/AuraStealer.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/Azorult.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/BitPaymer.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/BlackDropper.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/BruteRatel.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/BumbleBee.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/DoppelPaymer.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/DridexLoader.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/Formbook.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/GuLoader.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/IcedID.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/PlugX.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/README.md +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/RedLine.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/Remcos.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/Rhadamanthys.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/SmokeLoader.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/Socks5Systemz.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/SquirrelWaffle.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/Strrat.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/WarzoneRAT.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/Zloader.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/__init__.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/test_cape.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/RATDecoders/README.md +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/RATDecoders/__init__.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/RATDecoders/test_rats.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/__init__.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/deprecated/BackOffLoader.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/deprecated/BackOffPOS.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/deprecated/BlackNix.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/deprecated/BuerLoader.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/deprecated/ChChes.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/deprecated/Emotet.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/deprecated/Enfal.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/deprecated/EvilGrab.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/deprecated/Greame.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/deprecated/Hancitor.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/deprecated/HttpBrowser.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/deprecated/JavaDropper.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/deprecated/Nymaim.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/deprecated/Pandora.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/deprecated/PoisonIvy.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/deprecated/PredatorPain.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/deprecated/Punisher.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/deprecated/RCSession.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/deprecated/REvil.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/deprecated/RedLeaf.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/deprecated/Retefe.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/deprecated/Rozena.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/deprecated/SmallNet.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/deprecated/TSCookie.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/deprecated/TrickBot.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/deprecated/UrsnifV3.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/deprecated/_ShadowTech.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/deprecated/_VirusRat.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/deprecated/_jRat.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/deprecated/unrecom.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/deprecated/xRAT.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/malduck/LICENSE +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/malduck/README.md +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/malduck/__init__.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/malduck/test_malduck.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/mwcp/README.md +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/mwcp/__init__.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/mwcp/test_mwcp.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/utils/__init__.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/utils/aplib.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/utils/blzpack.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/utils/blzpack_lib.so +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/utils/dotnet_utils.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/utils/lznt1.py +0 -0
- {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/utils/strings.py +0 -0
|
@@ -83,7 +83,13 @@ def extract_config(data: bytes):
|
|
|
83
83
|
config["CNCs"] = lines[base + index + x]
|
|
84
84
|
break
|
|
85
85
|
if config or config_dict:
|
|
86
|
-
|
|
86
|
+
config.setdefault("raw", config_dict)
|
|
87
|
+
|
|
88
|
+
# If the data exfiltration is done through SMTP, then patch the extracted CNCs to include SMTP credentials
|
|
89
|
+
if config_dict.get("Protocol") == "SMTP":
|
|
90
|
+
config['CNCs'] = [f"smtp://{config_dict.get('Username')}:{config_dict.get('Password')}@{domain}:{config_dict.get('Port','587')}" for domain in config_dict.get('CNCs', [])]
|
|
91
|
+
|
|
92
|
+
return config
|
|
87
93
|
|
|
88
94
|
if __name__ == "__main__":
|
|
89
95
|
import sys
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import json
|
|
3
|
+
import pefile
|
|
4
|
+
import yara
|
|
5
|
+
import struct
|
|
6
|
+
import re
|
|
7
|
+
import ipaddress
|
|
8
|
+
from contextlib import suppress
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
DESCRIPTION = "Amatera Stealer parser"
|
|
12
|
+
AUTHOR = "YungBinary"
|
|
13
|
+
|
|
14
|
+
RULE_SOURCE = """
|
|
15
|
+
rule AmateraDecrypt
|
|
16
|
+
{
|
|
17
|
+
meta:
|
|
18
|
+
author = "YungBinary"
|
|
19
|
+
description = "Find Amatera XOR key"
|
|
20
|
+
strings:
|
|
21
|
+
$decrypt = {
|
|
22
|
+
A1 ?? ?? ?? ?? // mov eax, dword ptr ds:szXorKey ; "852149723"
|
|
23
|
+
89 45 ?? // mov dword ptr [ebp+xor_key], eax
|
|
24
|
+
8B 0D ?? ?? ?? ?? // mov ecx, dword ptr ds:szXorKey+4 ; "49723"
|
|
25
|
+
89 4D ?? // mov dword ptr [ebp+xor_key+4], ecx
|
|
26
|
+
66 8B 15 ?? ?? ?? ?? // mov dx, word ptr ds:szXorKey+8 ; "3"
|
|
27
|
+
66 89 55 ?? // mov word ptr [ebp+xor_key+8], dx
|
|
28
|
+
8D 45 ?? // lea eax, [ebp+xor_key]
|
|
29
|
+
50 // push eax
|
|
30
|
+
E8 // call
|
|
31
|
+
}
|
|
32
|
+
condition:
|
|
33
|
+
uint16(0) == 0x5A4D and $decrypt
|
|
34
|
+
}
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
RULE_SOURCE_AES_KEY = """
|
|
39
|
+
rule AmateraAESKey
|
|
40
|
+
{
|
|
41
|
+
meta:
|
|
42
|
+
author = "YungBinary"
|
|
43
|
+
description = "Find Amatera AES key"
|
|
44
|
+
strings:
|
|
45
|
+
$aes_key_on_stack = {
|
|
46
|
+
83 EC 2C // sub esp, 2Ch
|
|
47
|
+
C6 45 D4 ?? // mov byte ptr [ebp-2Ch], ??
|
|
48
|
+
C6 45 D5 ?? // mov byte ptr [ebp-2Bh], ??
|
|
49
|
+
C6 45 D6 ?? // mov byte ptr [ebp-2Ah], ??
|
|
50
|
+
C6 45 D7 ?? // mov byte ptr [ebp-29h], ??
|
|
51
|
+
C6 45 D8 ?? // mov byte ptr [ebp-28h], ??
|
|
52
|
+
C6 45 D9 ?? // mov byte ptr [ebp-27h], ??
|
|
53
|
+
C6 45 DA ?? // mov byte ptr [ebp-26h], ??
|
|
54
|
+
C6 45 DB ?? // mov byte ptr [ebp-25h], ??
|
|
55
|
+
C6 45 DC ?? // mov byte ptr [ebp-24h], ??
|
|
56
|
+
C6 45 DD ?? // mov byte ptr [ebp-23h], ??
|
|
57
|
+
C6 45 DE ?? // mov byte ptr [ebp-22h], ??
|
|
58
|
+
C6 45 DF ?? // mov byte ptr [ebp-21h], ??
|
|
59
|
+
C6 45 E0 ?? // mov byte ptr [ebp-20h], ??
|
|
60
|
+
C6 45 E1 ?? // mov byte ptr [ebp-1Fh], ??
|
|
61
|
+
C6 45 E2 ?? // mov byte ptr [ebp-1Eh], ??
|
|
62
|
+
C6 45 E3 ?? // mov byte ptr [ebp-1Dh], ??
|
|
63
|
+
C6 45 E4 ?? // mov byte ptr [ebp-1Ch], ??
|
|
64
|
+
C6 45 E5 ?? // mov byte ptr [ebp-1Bh], ??
|
|
65
|
+
C6 45 E6 ?? // mov byte ptr [ebp-1Ah], ??
|
|
66
|
+
C6 45 E7 ?? // mov byte ptr [ebp-19h], ??
|
|
67
|
+
C6 45 E8 ?? // mov byte ptr [ebp-18h], ??
|
|
68
|
+
C6 45 E9 ?? // mov byte ptr [ebp-17h], ??
|
|
69
|
+
C6 45 EA ?? // mov byte ptr [ebp-16h], ??
|
|
70
|
+
C6 45 EB ?? // mov byte ptr [ebp-15h], ??
|
|
71
|
+
C6 45 EC ?? // mov byte ptr [ebp-14h], ??
|
|
72
|
+
C6 45 ED ?? // mov byte ptr [ebp-13h], ??
|
|
73
|
+
C6 45 EE ?? // mov byte ptr [ebp-12h], ??
|
|
74
|
+
C6 45 EF ?? // mov byte ptr [ebp-11h], ??
|
|
75
|
+
C6 45 F0 ?? // mov byte ptr [ebp-10h], ??
|
|
76
|
+
C6 45 F1 ?? // mov byte ptr [ebp-0Fh], ??
|
|
77
|
+
C6 45 F2 ?? // mov byte ptr [ebp-0Eh], ??
|
|
78
|
+
C6 45 F3 ?? // mov byte ptr [ebp-0Dh], ??
|
|
79
|
+
C7 45 F4 10 00 00 00 // mov dword ptr [ebp-0Ch], 10h
|
|
80
|
+
}
|
|
81
|
+
condition:
|
|
82
|
+
uint16(0) == 0x5A4D and $aes_key_on_stack
|
|
83
|
+
}
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
DOMAIN_REGEX = r'^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$'
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def yara_scan(raw_data: bytes, rule_source: str):
|
|
90
|
+
yara_rules = yara.compile(source=rule_source)
|
|
91
|
+
matches = yara_rules.match(data=raw_data)
|
|
92
|
+
|
|
93
|
+
for match in matches:
|
|
94
|
+
for block in match.strings:
|
|
95
|
+
for instance in block.instances:
|
|
96
|
+
return instance.offset
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def extract_base64_strings(data: bytes, minchars: int, maxchars: int):
|
|
100
|
+
"""
|
|
101
|
+
Generator that returns ASCII formatted base64 strings
|
|
102
|
+
"""
|
|
103
|
+
apat = b"([A-Za-z0-9+/=]{" + str(minchars).encode() + b"," + str(maxchars).encode() + b"})\x00"
|
|
104
|
+
for s in re.findall(apat, data):
|
|
105
|
+
yield s.decode()
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def xor_data(data, key):
|
|
109
|
+
decoded = bytearray()
|
|
110
|
+
for i in range(len(data)):
|
|
111
|
+
decoded.append(key[i % len(key)] ^ data[i])
|
|
112
|
+
return decoded
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def is_public_ip(ip):
|
|
116
|
+
try:
|
|
117
|
+
# This will raise a ValueError if the IP format is incorrect
|
|
118
|
+
ip_obj = ipaddress.ip_address(ip.decode())
|
|
119
|
+
if ip_obj.is_private:
|
|
120
|
+
return False
|
|
121
|
+
return True
|
|
122
|
+
except Exception:
|
|
123
|
+
return False
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def is_valid_domain(data):
|
|
127
|
+
try:
|
|
128
|
+
if re.fullmatch(DOMAIN_REGEX, data.decode()):
|
|
129
|
+
return True
|
|
130
|
+
return False
|
|
131
|
+
except Exception:
|
|
132
|
+
return False
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def extract_config(data):
|
|
136
|
+
"""
|
|
137
|
+
Extract Amatera malware configuration.
|
|
138
|
+
"""
|
|
139
|
+
config_dict = {}
|
|
140
|
+
|
|
141
|
+
with suppress(Exception):
|
|
142
|
+
pe = pefile.PE(data=data)
|
|
143
|
+
image_base = pe.OPTIONAL_HEADER.ImageBase
|
|
144
|
+
|
|
145
|
+
# Identify XOR key decryption routine and extract key
|
|
146
|
+
offset = yara_scan(data, RULE_SOURCE)
|
|
147
|
+
if not offset:
|
|
148
|
+
return config_dict
|
|
149
|
+
key_str_va = struct.unpack('i', data[offset + 1: offset + 5])[0]
|
|
150
|
+
key_str = pe.get_string_at_rva(key_str_va - image_base, max_length=20) + b'\x00'
|
|
151
|
+
|
|
152
|
+
# Extract AES 256 key
|
|
153
|
+
aes_key_offset = yara_scan(data, RULE_SOURCE_AES_KEY)
|
|
154
|
+
aes_key = bytearray()
|
|
155
|
+
if aes_key_offset:
|
|
156
|
+
aes_block = data[aes_key_offset : aes_key_offset + 131]
|
|
157
|
+
for i in range(0, len(aes_block) - 4, 4):
|
|
158
|
+
aes_key.append(aes_block[i+6])
|
|
159
|
+
|
|
160
|
+
# Handle each base64 string -> decode -> decrypt with XOR key
|
|
161
|
+
for b64_str in extract_base64_strings(data, 8, 20):
|
|
162
|
+
try:
|
|
163
|
+
decoded = base64.b64decode(b64_str, validate=True)
|
|
164
|
+
decrypted = xor_data(decoded, key_str)
|
|
165
|
+
if not is_public_ip(decrypted) and not is_valid_domain(decrypted):
|
|
166
|
+
continue
|
|
167
|
+
|
|
168
|
+
config_dict["CNCs"] = [f"https://{decrypted.decode()}"]
|
|
169
|
+
|
|
170
|
+
if aes_key:
|
|
171
|
+
config_dict["cryptokey"] = aes_key.hex()
|
|
172
|
+
config_dict["cryptokey_type"] = "AES"
|
|
173
|
+
|
|
174
|
+
return config_dict
|
|
175
|
+
except Exception:
|
|
176
|
+
continue
|
|
177
|
+
|
|
178
|
+
return config_dict
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
if __name__ == "__main__":
|
|
182
|
+
import sys
|
|
183
|
+
|
|
184
|
+
with open(sys.argv[1], "rb") as f:
|
|
185
|
+
config_json = json.dumps(extract_config(f.read()), indent=4)
|
|
186
|
+
print(config_json)
|
|
@@ -18,7 +18,6 @@ import re
|
|
|
18
18
|
from contextlib import suppress
|
|
19
19
|
|
|
20
20
|
import pefile
|
|
21
|
-
|
|
22
21
|
import yara
|
|
23
22
|
|
|
24
23
|
log = logging.getLogger(__name__)
|
|
@@ -169,7 +168,13 @@ def extract_config(filebuf):
|
|
|
169
168
|
c2_dec = decode_string(items[10], sbox).decode("utf8")
|
|
170
169
|
if "|" in c2_dec:
|
|
171
170
|
c2_dec = c2_dec.split("|")
|
|
172
|
-
|
|
171
|
+
for c2 in c2_dec:
|
|
172
|
+
# Assign the proper scheme based on the port
|
|
173
|
+
if c2.endswith(':443'):
|
|
174
|
+
c2 = f"https://{c2}"
|
|
175
|
+
else:
|
|
176
|
+
c2 = f"http://{c2}"
|
|
177
|
+
cfg.setdefault("CNCs", []).append(c2)
|
|
173
178
|
if float(cfg["version"]) < 1.7:
|
|
174
179
|
cfg["campaign"] = decode_string(items[276], sbox).decode("utf8")
|
|
175
180
|
else:
|
|
@@ -55,11 +55,11 @@ def extract_config(memdump_path, read=False):
|
|
|
55
55
|
if gate_url.match(url):
|
|
56
56
|
config.setdefault("CNCs", []).append(url.decode())
|
|
57
57
|
elif exe_url.match(url) or dll_url.match(url):
|
|
58
|
-
artifacts_raw
|
|
58
|
+
artifacts_raw.setdefault("downloads", []).append(url.decode())
|
|
59
59
|
except Exception as e:
|
|
60
60
|
print(e, sys.exc_info(), "PONY")
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
if "downloads" in artifacts_raw:
|
|
62
|
+
config.setdefault("raw", {})["downloads"] = list(set(artifacts_raw["downloads"]))
|
|
63
63
|
return config
|
|
64
64
|
|
|
65
65
|
|
|
@@ -119,7 +119,7 @@ def extract_config(data):
|
|
|
119
119
|
encoded_payload = remove_nulls(encoded_payload, encoded_payload_size)
|
|
120
120
|
decoded_payload = xor_data(encoded_payload, xor_key)
|
|
121
121
|
cncs = find_c2(decoded_payload)
|
|
122
|
-
if cncs
|
|
122
|
+
if cncs:
|
|
123
123
|
config["CNCs"] = cncs
|
|
124
124
|
|
|
125
125
|
return config
|
|
@@ -188,7 +188,17 @@ def extract_config(filebuf):
|
|
|
188
188
|
cncs.append(config_dict["raw"]["BackupConnectionHost"])
|
|
189
189
|
if config_dict.get("raw", {}).get("ConnectionPort") and cncs:
|
|
190
190
|
port = config_dict["raw"]["ConnectionPort"]
|
|
191
|
-
config_dict["CNCs"] = [f"{cnc}:{port}" for cnc in cncs]
|
|
191
|
+
config_dict["CNCs"] = [f"tcp://{cnc}:{port}" for cnc in cncs]
|
|
192
|
+
|
|
193
|
+
if "Mutex" in config_dict.get("raw", {}):
|
|
194
|
+
config_dict["mutex"] = config_dict["raw"]["Mutex"]
|
|
195
|
+
|
|
196
|
+
if "Version" in config_dict.get("raw", {}):
|
|
197
|
+
config_dict["version"] = config_dict["raw"]["Version"]
|
|
198
|
+
|
|
199
|
+
if "DefaultGroup" in config_dict.get("raw", {}):
|
|
200
|
+
config_dict["campaign"] = config_dict["raw"]["DefaultGroup"]
|
|
201
|
+
|
|
192
202
|
return config_dict
|
|
193
203
|
|
|
194
204
|
|
|
@@ -176,7 +176,7 @@ 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["CNCs"] = [f"{config['domain']}:{config['port']}"]
|
|
179
|
+
conf["CNCs"] = [f"tcp://{config['domain']}:{config['port']}"]
|
|
180
180
|
|
|
181
181
|
if config.get("campaign_id"):
|
|
182
182
|
conf["campaign"] = config["campaign_id"]
|
|
@@ -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 {"raw": {"Type": "Telegram"}, "CNCs": f"https://api.telegram.org/bot{token}/sendMessage?chat_id={chat_id}"}
|
|
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__()
|
|
@@ -106,7 +106,7 @@ def handle_encrypted(dotnet_file, data, c2_type, user_strings):
|
|
|
106
106
|
if decrypted_strings:
|
|
107
107
|
if c2_type == "Telegram":
|
|
108
108
|
token, chat_id = decrypted_strings
|
|
109
|
-
config_dict = {"raw": {"Type": "Telegram"}, "CNCs": f"https://api.telegram.org/bot{token}/sendMessage?chat_id={chat_id}"}
|
|
109
|
+
config_dict = {"raw": {"Type": "Telegram"}, "CNCs": [f"https://api.telegram.org/bot{token}/sendMessage?chat_id={chat_id}"]}
|
|
110
110
|
elif c2_type == "SMTP":
|
|
111
111
|
smtp_from, smtp_password, smtp_host, smtp_to, smtp_port = decrypted_strings
|
|
112
112
|
config_dict = {
|
|
@@ -61,7 +61,11 @@ def extract_config(data):
|
|
|
61
61
|
enc_data = f.read(data_len - 32)
|
|
62
62
|
config = decrypt_config(enc_data, key, iv)
|
|
63
63
|
if config:
|
|
64
|
-
|
|
64
|
+
output = {"raw": config}
|
|
65
|
+
output["CNCs"] = [
|
|
66
|
+
f"{'http' if not config['secure'] else 'https'}://{config['host']}:{config['port']}{config['path']}"
|
|
67
|
+
]
|
|
68
|
+
return output
|
|
65
69
|
except Exception as e:
|
|
66
70
|
log.error("Configuration decryption failed: %s", e)
|
|
67
71
|
return {}
|
|
@@ -3,9 +3,8 @@ Description: Winos 4.0 "OnlineModule" config parser
|
|
|
3
3
|
Author: x.com/YungBinary
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
-
from contextlib import suppress
|
|
7
6
|
import re
|
|
8
|
-
|
|
7
|
+
from contextlib import suppress
|
|
9
8
|
|
|
10
9
|
CONFIG_KEY_MAP = {
|
|
11
10
|
"dd": "execution_delay_seconds",
|
|
@@ -60,7 +59,11 @@ def extract_config(data: bytes) -> dict:
|
|
|
60
59
|
|
|
61
60
|
final_config["CNCs"] = list(set(final_config["CNCs"]))
|
|
62
61
|
# Extract campaign ID
|
|
63
|
-
final_config["
|
|
62
|
+
final_config["campaign"] = "default" if config_dict["fz"] == "\u9ed8\u8ba4" else config_dict["fz"]
|
|
63
|
+
|
|
64
|
+
# Check if the version has been extracted
|
|
65
|
+
if "bb" in config_dict:
|
|
66
|
+
final_config["version"] = config_dict["bb"]
|
|
64
67
|
|
|
65
68
|
# Map keys, e.g. dd -> execution_delay_seconds
|
|
66
69
|
final_config["raw"] = {v: config_dict[k] for k, v in CONFIG_KEY_MAP.items() if k in config_dict}
|
|
@@ -59,7 +59,19 @@ def parse_http_config(rc4_key: bytes, data: bytes) -> dict:
|
|
|
59
59
|
config["sleep_delay"] = read("<I")
|
|
60
60
|
config["jitter_delay"] = read("<I")
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
output = {"raw": config}
|
|
63
|
+
|
|
64
|
+
# Map some fields to CAPE's output format, where possible
|
|
65
|
+
output['cryptokey'] = config['cryptokey']
|
|
66
|
+
output['cryptokey_type'] = config['cryptokey_type']
|
|
67
|
+
output['user_agent'] = config['user_agent']
|
|
68
|
+
output['CNCs'] = [f"{'https' if config['use_ssl'] else 'http'}://{server}:{ports[i]}{config['uri']}"
|
|
69
|
+
for i, server in enumerate(servers)]
|
|
70
|
+
|
|
71
|
+
# TODO: Does agent_type map to version or build?
|
|
72
|
+
# output['version'] = output['raw']['agent_type']
|
|
73
|
+
|
|
74
|
+
return output
|
|
63
75
|
|
|
64
76
|
|
|
65
77
|
def extract_config(filebuf: bytes) -> dict:
|
|
@@ -15,8 +15,8 @@ from pathlib import Path
|
|
|
15
15
|
from struct import pack, unpack
|
|
16
16
|
|
|
17
17
|
import pefile
|
|
18
|
-
|
|
19
18
|
import yara
|
|
19
|
+
|
|
20
20
|
from cape_parsers.utils.lznt1 import lznt1
|
|
21
21
|
|
|
22
22
|
log = logging.getLogger(__name__)
|
|
@@ -556,4 +556,7 @@ def extract_config(data):
|
|
|
556
556
|
}
|
|
557
557
|
}
|
|
558
558
|
|
|
559
|
+
config["cryptokey"] = config["raw"]["Rabbit key"]
|
|
560
|
+
config["cryptokey_type"] = "Rabbit"
|
|
561
|
+
|
|
559
562
|
return config
|
|
@@ -92,11 +92,14 @@ def decode(data):
|
|
|
92
92
|
|
|
93
93
|
|
|
94
94
|
def extract_config(data):
|
|
95
|
+
config_extracted = False
|
|
95
96
|
with suppress(pefile.PEFormatError):
|
|
96
97
|
pe = pefile.PE(data=data, fast_load=True)
|
|
97
98
|
for section in pe.sections:
|
|
98
99
|
if b"CODE" in section.Name:
|
|
99
|
-
|
|
100
|
+
config = decode(section.get_data())
|
|
101
|
+
config_extracted = True
|
|
102
|
+
break
|
|
100
103
|
|
|
101
104
|
if b"1=Yes" in data or b"1=No" in data:
|
|
102
105
|
config = {}
|
|
@@ -105,6 +108,32 @@ def extract_config(data):
|
|
|
105
108
|
config["CNCs"] = [x for x in item[2:].decode("utf-8").split("|") if x.strip() != ""]
|
|
106
109
|
else:
|
|
107
110
|
config.update(parse_config(item, config_map_2))
|
|
111
|
+
config_extracted = True
|
|
112
|
+
|
|
113
|
+
if config_extracted:
|
|
114
|
+
# Map information to CAPE's format, if possible
|
|
115
|
+
if "c2_port" in config["raw"] and "CNCs" in config:
|
|
116
|
+
# We're making the assumpton the same port is used for all CNCs
|
|
117
|
+
config["CNCs"] = [f"{url}:{config['raw']['c2_port']}" for url in config["CNCs"]]
|
|
118
|
+
|
|
119
|
+
if "internal_mutex" in config["raw"]:
|
|
120
|
+
# Bring mutex to top level
|
|
121
|
+
config["mutex"] = config["raw"]["internal_mutex"]
|
|
122
|
+
|
|
123
|
+
if "crypto_key" in config["raw"]:
|
|
124
|
+
# Bring crypto_key to top level
|
|
125
|
+
config["cryptokey"] = config["raw"]["crypto_key"]
|
|
126
|
+
|
|
127
|
+
if "campaign_id" in config["raw"]:
|
|
128
|
+
# Bring campaign_id to top level
|
|
129
|
+
config["campaign"] = config["raw"]["campaign_id"]
|
|
130
|
+
|
|
131
|
+
if "xor_key" in config["raw"]:
|
|
132
|
+
# Bring xor_key to top level
|
|
133
|
+
config["cryptokey"] = config["raw"]["xor_key"]
|
|
134
|
+
config["cruptokey_type"] = "XOR"
|
|
135
|
+
|
|
136
|
+
|
|
108
137
|
return config
|
|
109
138
|
|
|
110
139
|
return ""
|
|
@@ -35,7 +35,8 @@ def extract_config(filebuf):
|
|
|
35
35
|
if n > 32:
|
|
36
36
|
break
|
|
37
37
|
campaign, c2 = struct.unpack("I30s", bytes(dec))
|
|
38
|
-
|
|
38
|
+
c2 = c2.split(b"\00", 1)[0].decode()
|
|
39
|
+
config["CNCs"] = f"http://{c2}"
|
|
39
40
|
config["campaign"] = campaign
|
|
40
41
|
return config
|
|
41
42
|
|
|
@@ -154,6 +154,7 @@ def extract_config(filebuf):
|
|
|
154
154
|
minor = int.from_bytes(data[18:19], byteorder="big")
|
|
155
155
|
release = int.from_bytes(data[26:27], byteorder="big")
|
|
156
156
|
version = f"{major}.{minor}.{release}"
|
|
157
|
+
|
|
157
158
|
if "$key" in item.identifier:
|
|
158
159
|
key = instance.matched_data[4::5]
|
|
159
160
|
try:
|
|
@@ -161,6 +162,7 @@ def extract_config(filebuf):
|
|
|
161
162
|
data_sections = [s for s in pe.sections if s.Name.find(b".data") != -1]
|
|
162
163
|
if not data_sections:
|
|
163
164
|
return
|
|
165
|
+
|
|
164
166
|
data = data_sections[0].get_data()
|
|
165
167
|
str_vals = []
|
|
166
168
|
c2 = []
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
import logging
|
|
16
16
|
import struct
|
|
17
|
+
from typing import List
|
|
17
18
|
|
|
18
19
|
import pefile
|
|
19
20
|
import yara
|
|
@@ -32,8 +33,8 @@ rule NitroBunnyDownloader
|
|
|
32
33
|
cape_type = "NitroBunnyDownloader Payload"
|
|
33
34
|
hash = "960e59200ec0a4b5fb3b44e6da763f5fec4092997975140797d4eec491de411b"
|
|
34
35
|
strings:
|
|
35
|
-
$config1 = {E8 [3] 00 41 B8 ?? ?? 00 00 48 8D 15 [3] 00 48 89 C1
|
|
36
|
-
$config2 = {E8 [3] 00 48 8D 15 [3] 00 41 B8 ?? ?? 00 00 48 89 C1
|
|
36
|
+
$config1 = {E8 [3] 00 41 B8 ?? ?? 00 00 48 8D 15 [3] 00 48 89 C1 4? 89 ?? E8 [3] 00}
|
|
37
|
+
$config2 = {E8 [3] 00 48 8D 15 [3] 00 41 B8 ?? ?? 00 00 48 89 C1 4? 89 ?? E8 [3] 00}
|
|
37
38
|
$string1 = "X-Amz-User-Agent:" wide
|
|
38
39
|
$string2 = "Amz-Security-Flag:" wide
|
|
39
40
|
$string3 = "/cart" wide
|
|
@@ -87,6 +88,20 @@ def read_string_list(data, off, count):
|
|
|
87
88
|
return items, off
|
|
88
89
|
|
|
89
90
|
|
|
91
|
+
def make_endpoints(cncs: List[str], port: int, uris: List[str]) -> List[str]:
|
|
92
|
+
endpoints = []
|
|
93
|
+
schema = {80: "http", 443: "https"}.get(port, "tcp")
|
|
94
|
+
for cnc in cncs:
|
|
95
|
+
base_url = f"{schema}://{cnc}"
|
|
96
|
+
if port not in (80, 443):
|
|
97
|
+
base_url += f":{port}"
|
|
98
|
+
|
|
99
|
+
for uri in uris:
|
|
100
|
+
endpoints.append(f"{base_url}/{uri.lstrip('/')}")
|
|
101
|
+
|
|
102
|
+
return endpoints
|
|
103
|
+
|
|
104
|
+
|
|
90
105
|
def extract_config(filebuf):
|
|
91
106
|
yara_hit = yara_scan(filebuf)
|
|
92
107
|
if not yara_hit:
|
|
@@ -98,24 +113,20 @@ def extract_config(filebuf):
|
|
|
98
113
|
config_offset = None
|
|
99
114
|
rva_offset = None
|
|
100
115
|
|
|
116
|
+
CONFIG_OFFSETS = {
|
|
117
|
+
"$config1": (7, 14, 18),
|
|
118
|
+
"$config2": (14, 8, 12),
|
|
119
|
+
}
|
|
120
|
+
|
|
101
121
|
for hit in yara_hit:
|
|
102
122
|
if hit.rule != "NitroBunnyDownloader":
|
|
103
123
|
continue
|
|
104
124
|
|
|
105
125
|
for item in hit.strings:
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
config_offset= 14
|
|
111
|
-
rva_offset = 18
|
|
112
|
-
break
|
|
113
|
-
elif item.identifier == "$config2":
|
|
114
|
-
config_code_offset = instance.offset
|
|
115
|
-
config_size_offset = 14
|
|
116
|
-
config_offset= 8
|
|
117
|
-
rva_offset = 12
|
|
118
|
-
break
|
|
126
|
+
if item.identifier in CONFIG_OFFSETS and item.instances:
|
|
127
|
+
config_code_offset = item.instances[0].offset
|
|
128
|
+
config_size_offset, config_offset, rva_offset = CONFIG_OFFSETS[item.identifier]
|
|
129
|
+
break
|
|
119
130
|
|
|
120
131
|
if config_code_offset:
|
|
121
132
|
break
|
|
@@ -126,33 +137,26 @@ def extract_config(filebuf):
|
|
|
126
137
|
try:
|
|
127
138
|
pe = pefile.PE(data=filebuf, fast_load=True)
|
|
128
139
|
config_length = pe.get_dword_from_offset(config_code_offset + config_size_offset)
|
|
129
|
-
|
|
140
|
+
config_data_offset = pe.get_dword_from_offset(config_code_offset + config_offset)
|
|
130
141
|
rva = pe.get_rva_from_offset(config_code_offset + rva_offset)
|
|
131
|
-
config_rva = rva +
|
|
142
|
+
config_rva = rva + config_data_offset
|
|
132
143
|
data = pe.get_data(config_rva, config_length)
|
|
133
144
|
off = 0
|
|
134
|
-
|
|
145
|
+
cfg["CNCs"] = []
|
|
135
146
|
port, off = read_dword(data, off)
|
|
136
147
|
num, off = read_dword(data, off)
|
|
137
148
|
cncs, off = read_string_list(data, off, num)
|
|
138
149
|
num, off = read_qword(data, off)
|
|
139
|
-
|
|
150
|
+
cfg["user_agent"], off = read_utf16le_string(data, off, num)
|
|
140
151
|
num, off = read_dword(data, off)
|
|
152
|
+
raw = cfg["raw"] = {}
|
|
141
153
|
raw["http_header_items"], off = read_string_list(data, off, num)
|
|
142
154
|
num, off = read_dword(data, off)
|
|
143
|
-
|
|
155
|
+
uris, off = read_string_list(data, off, num)
|
|
144
156
|
raw["unknown_1"], off = read_dword(data, off)
|
|
145
157
|
raw["unknown_2"], off = read_dword(data, off)
|
|
146
|
-
|
|
147
158
|
if cncs:
|
|
148
|
-
cfg["CNCs"] =
|
|
149
|
-
schema = {80: "http", 443: "https"}.get(port, "tcp")
|
|
150
|
-
for cnc in cncs:
|
|
151
|
-
cnc = f"{schema}://{cnc}"
|
|
152
|
-
if port not in (80, 443):
|
|
153
|
-
cnc += f":{port}"
|
|
154
|
-
|
|
155
|
-
cfg["CNCs"].append(cnc)
|
|
159
|
+
cfg["CNCs"] = make_endpoints(cncs, port, uris)
|
|
156
160
|
|
|
157
161
|
except Exception as e:
|
|
158
162
|
log.error("Error: %s", e)
|