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
|
@@ -76,8 +76,8 @@ def string_from_offset(data, offset):
|
|
|
76
76
|
|
|
77
77
|
def parse_config(data):
|
|
78
78
|
parsed = {}
|
|
79
|
-
parsed["
|
|
80
|
-
parsed["
|
|
79
|
+
parsed["botnet"] = data[4:].split(b"\x00", 1)[0].decode("utf-8")
|
|
80
|
+
parsed["campaign"] = data[25:].split(b"\x00", 1)[0].decode("utf-8")
|
|
81
81
|
c2s = []
|
|
82
82
|
c2_data = data[46:686]
|
|
83
83
|
for i in range(10):
|
|
@@ -85,16 +85,17 @@ def parse_config(data):
|
|
|
85
85
|
chunk = chunk.rstrip(b"\x00")
|
|
86
86
|
if chunk:
|
|
87
87
|
c2s.append(chunk.decode("utf-8"))
|
|
88
|
-
parsed["
|
|
89
|
-
parsed["
|
|
88
|
+
parsed["CNCs"] = c2s
|
|
89
|
+
parsed["cryptokey"] = data[704:].split(b"\x00", 1)[0]
|
|
90
|
+
parsed["cryptokey_type"] = "RSA Public Key"
|
|
90
91
|
dns_data = data[1004:].split(b"\x00", 1)[0]
|
|
91
92
|
parsed["TLS SNI"] = dns_data.split(b"~")[0].decode("utf-8").rstrip()
|
|
92
93
|
parsed["DNS C2"] = dns_data.split(b"~")[1].decode("utf-8").strip()
|
|
93
94
|
dns_ips = []
|
|
94
|
-
dns_ips.append(socket.inet_ntoa(data[1204:1208].split(b"\x00",1)[0]))
|
|
95
|
+
dns_ips.append(socket.inet_ntoa(data[1204:1208].split(b"\x00", 1)[0]))
|
|
95
96
|
dns_ip_data = data[1208:1248]
|
|
96
97
|
for i in range(10):
|
|
97
|
-
chunk = dns_ip_data[i * 4:(i + 1) * 4]
|
|
98
|
+
chunk = dns_ip_data[i * 4 : (i + 1) * 4]
|
|
98
99
|
chunk = chunk.rstrip(b"\x00")
|
|
99
100
|
if chunk:
|
|
100
101
|
dns_ips.append(socket.inet_ntoa(chunk))
|
|
@@ -103,6 +104,7 @@ def parse_config(data):
|
|
|
103
104
|
|
|
104
105
|
|
|
105
106
|
def extract_config(filebuf):
|
|
107
|
+
config = {}
|
|
106
108
|
end_config = {}
|
|
107
109
|
pe = pefile.PE(data=filebuf, fast_load=False)
|
|
108
110
|
image_base = pe.OPTIONAL_HEADER.ImageBase
|
|
@@ -167,14 +169,15 @@ def extract_config(filebuf):
|
|
|
167
169
|
enc_data = filebuf[data_offset:].split(b"\0\0", 1)[0]
|
|
168
170
|
raw = decrypt_rc4(key, enc_data)
|
|
169
171
|
items = list(filter(None, raw.split(b"\x00\x00")))
|
|
170
|
-
|
|
171
|
-
|
|
172
|
+
config["botnet"] = items[1].lstrip(b"\x00")
|
|
173
|
+
config["campaign"] = items[2]
|
|
172
174
|
for item in items:
|
|
173
175
|
item = item.lstrip(b"\x00")
|
|
174
176
|
if item.startswith(b"http"):
|
|
175
|
-
|
|
177
|
+
config.setdefault("CNCs", []).append(item)
|
|
176
178
|
elif len(item) == 16:
|
|
177
|
-
|
|
179
|
+
config["cryptokey"] = item
|
|
180
|
+
config["cryptokey_type"] = "RC4"
|
|
178
181
|
elif conf_type == "2" and decrypt_key:
|
|
179
182
|
conf_va = struct.unpack("I", filebuf[decrypt_conf + cva : decrypt_conf + cva + 4])[0]
|
|
180
183
|
conf_offset = pe.get_offset_from_rva(conf_va + pe.get_rva_from_offset(decrypt_conf) + cva + 4)
|
|
@@ -186,14 +189,15 @@ def extract_config(filebuf):
|
|
|
186
189
|
conf_data = filebuf[conf_offset : conf_offset + conf_size]
|
|
187
190
|
raw = decrypt_rc4(key, conf_data)
|
|
188
191
|
items = list(filter(None, raw.split(b"\x00\x00")))
|
|
189
|
-
|
|
190
|
-
|
|
192
|
+
config["botnet"] = items[0].decode("utf-8")
|
|
193
|
+
config["campaign"] = items[1].decode("utf-8")
|
|
191
194
|
for item in items:
|
|
192
195
|
item = item.lstrip(b"\x00")
|
|
193
196
|
if item.startswith(b"http"):
|
|
194
|
-
|
|
197
|
+
config.setdefault("CNCs", []).append(item.decode("utf-8"))
|
|
195
198
|
elif b"PUBLIC KEY" in item:
|
|
196
|
-
|
|
199
|
+
config["cryptokey"] = item.decode("utf-8").replace("\n", "")
|
|
200
|
+
config["cryptokey_type"] = "RSA Public key"
|
|
197
201
|
elif conf_type == "3" and rc4_chunk1 and rc4_chunk2:
|
|
198
202
|
conf_va = struct.unpack("I", filebuf[decrypt_conf : decrypt_conf + 4])[0]
|
|
199
203
|
conf_offset = pe.get_offset_from_rva(conf_va + pe.get_rva_from_offset(decrypt_conf) + 4)
|
|
@@ -208,8 +212,10 @@ def extract_config(filebuf):
|
|
|
208
212
|
conf = decrypt_rc4(decrypt_key, conf_data)
|
|
209
213
|
end_config = parse_config(conf)
|
|
210
214
|
|
|
211
|
-
|
|
215
|
+
if config and end_config:
|
|
216
|
+
config = config.update({"raw": end_config})
|
|
212
217
|
|
|
218
|
+
return config
|
|
213
219
|
|
|
214
220
|
if __name__ == "__main__":
|
|
215
221
|
import sys
|
cape_parsers/__init__.py
CHANGED
|
@@ -12,10 +12,11 @@ from typing import Dict, Tuple
|
|
|
12
12
|
PARSERS_ROOT = Path(__file__).absolute().parent
|
|
13
13
|
log = logging.getLogger()
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
|
|
16
|
+
def load_cape_parsers(load: str = "all", exclude_parsers: list = []):
|
|
16
17
|
"""
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
load: all, core, community
|
|
19
|
+
exclude_parsers: [names of parsers that will be ignored]
|
|
19
20
|
"""
|
|
20
21
|
versions = {
|
|
21
22
|
"cape": "core",
|
|
@@ -25,7 +26,9 @@ def load_cape_parsers(load: str="all", exclude_parsers: list = []):
|
|
|
25
26
|
cape_parsers = {}
|
|
26
27
|
CAPE_DECODERS = {
|
|
27
28
|
"cape": [os.path.basename(decoder)[:-3] for decoder in glob.glob(os.path.join(PARSERS_ROOT, "CAPE", "core", "[!_]*.py"))],
|
|
28
|
-
"community": [
|
|
29
|
+
"community": [
|
|
30
|
+
os.path.basename(decoder)[:-3] for decoder in glob.glob(os.path.join(PARSERS_ROOT, "CAPE", "community", "[!_]*.py"))
|
|
31
|
+
],
|
|
29
32
|
}
|
|
30
33
|
|
|
31
34
|
for version, names in CAPE_DECODERS.items():
|
|
@@ -54,6 +57,7 @@ def load_mwcp_parsers() -> Tuple[Dict[str, str], ModuleType]:
|
|
|
54
57
|
with suppress(ImportError):
|
|
55
58
|
# We do not install this by default
|
|
56
59
|
import mwcp
|
|
60
|
+
|
|
57
61
|
HAVE_MWCP = True
|
|
58
62
|
|
|
59
63
|
if not HAVE_MWCP:
|
|
@@ -66,6 +70,7 @@ def load_mwcp_parsers() -> Tuple[Dict[str, str], ModuleType]:
|
|
|
66
70
|
return {}, mwcp
|
|
67
71
|
return _malware_parsers, mwcp
|
|
68
72
|
|
|
73
|
+
|
|
69
74
|
def _malduck_load_decoders():
|
|
70
75
|
|
|
71
76
|
malduck_modules = {}
|
|
@@ -80,6 +85,7 @@ def _malduck_load_decoders():
|
|
|
80
85
|
|
|
81
86
|
return malduck_modules
|
|
82
87
|
|
|
88
|
+
|
|
83
89
|
"""
|
|
84
90
|
def load_malduck_parsers():
|
|
85
91
|
HAVE_MALDUCK = False
|
|
@@ -108,6 +114,7 @@ def load_malduck_parsers():
|
|
|
108
114
|
return malduck_modules
|
|
109
115
|
"""
|
|
110
116
|
|
|
117
|
+
|
|
111
118
|
def load_malwareconfig_parsers() -> Tuple[bool, dict, ModuleType]:
|
|
112
119
|
try:
|
|
113
120
|
from malwareconfig import fileparser
|
|
@@ -122,15 +129,17 @@ def load_malwareconfig_parsers() -> Tuple[bool, dict, ModuleType]:
|
|
|
122
129
|
except ImportError:
|
|
123
130
|
log.info("Missed RATDecoders -> poetry run pip install malwareconfig")
|
|
124
131
|
except Exception as e:
|
|
125
|
-
log.
|
|
132
|
+
log.exception(e)
|
|
126
133
|
return False, False, False
|
|
127
134
|
|
|
135
|
+
|
|
128
136
|
def load_ratdecoders_parsers():
|
|
129
137
|
dec_modules = {}
|
|
130
138
|
HAVE_MLW_CONFIGS = False
|
|
131
139
|
with suppress(ImportError):
|
|
132
140
|
# We do not install this by default as is outdated now, but if installed will be imported
|
|
133
141
|
from malwareconfig.common import Decoder
|
|
142
|
+
|
|
134
143
|
HAVE_MLW_CONFIGS = True
|
|
135
144
|
|
|
136
145
|
if not HAVE_MLW_CONFIGS:
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import pefile
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def extract_raw_config(raw_data):
|
|
5
|
+
try:
|
|
6
|
+
pe = pefile.PE(data=raw_data)
|
|
7
|
+
rt_string_idx = [entry.id for entry in pe.DIRECTORY_ENTRY_RESOURCE.entries].index(pefile.RESOURCE_TYPE["RT_RCDATA"])
|
|
8
|
+
rt_string_directory = pe.DIRECTORY_ENTRY_RESOURCE.entries[rt_string_idx]
|
|
9
|
+
for entry in rt_string_directory.directory.entries:
|
|
10
|
+
if str(entry.name) == "SETTINGS":
|
|
11
|
+
data_rva = entry.directory.entries[0].data.struct.OffsetToData
|
|
12
|
+
size = entry.directory.entries[0].data.struct.Size
|
|
13
|
+
data = pe.get_memory_mapped_image()[data_rva : data_rva + size]
|
|
14
|
+
return data.split("}")
|
|
15
|
+
except Exception:
|
|
16
|
+
return None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def decode(line):
|
|
20
|
+
return "".join(chr(ord(char) - 1) for char in line)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def domain_parse(config):
|
|
24
|
+
return [domain.split(":", 1)[0] for domain in config["Domains"].split(";")]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def extract_config(data):
|
|
28
|
+
try:
|
|
29
|
+
config_raw = extract_raw_config(data)
|
|
30
|
+
if config_raw:
|
|
31
|
+
return {
|
|
32
|
+
"mutex": decode(config_raw[1])[::-1],
|
|
33
|
+
"raw": {
|
|
34
|
+
"Anti Sandboxie": decode(config_raw[2])[::-1],
|
|
35
|
+
"Max Folder Size": decode(config_raw[3])[::-1],
|
|
36
|
+
"Delay Time": decode(config_raw[4])[::-1],
|
|
37
|
+
"Password": decode(config_raw[5])[::-1],
|
|
38
|
+
"Kernel Mode Unhooking": decode(config_raw[6])[::-1],
|
|
39
|
+
"User More Unhooking": decode(config_raw[7])[::-1],
|
|
40
|
+
"Melt Server": decode(config_raw[8])[::-1],
|
|
41
|
+
"Offline Screen Capture": decode(config_raw[9])[::-1],
|
|
42
|
+
"Offline Keylogger": decode(config_raw[10])[::-1],
|
|
43
|
+
"Copy To ADS": decode(config_raw[11])[::-1],
|
|
44
|
+
"Domain": decode(config_raw[12])[::-1],
|
|
45
|
+
"Persistence Thread": decode(config_raw[13])[::-1],
|
|
46
|
+
"Active X Key": decode(config_raw[14])[::-1],
|
|
47
|
+
"Registry Key": decode(config_raw[15])[::-1],
|
|
48
|
+
"Active X Run": decode(config_raw[16])[::-1],
|
|
49
|
+
"Registry Run": decode(config_raw[17])[::-1],
|
|
50
|
+
"Safe Mode Startup": decode(config_raw[18])[::-1],
|
|
51
|
+
"Inject winlogon.exe": decode(config_raw[19])[::-1],
|
|
52
|
+
"Install Name": decode(config_raw[20])[::-1],
|
|
53
|
+
"Install Path": decode(config_raw[21])[::-1],
|
|
54
|
+
"Campaign Name": decode(config_raw[22])[::-1],
|
|
55
|
+
"Campaign Group": decode(config_raw[23])[::-1],
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
except Exception:
|
|
59
|
+
return None
|
|
@@ -35,5 +35,5 @@ def extract_config(filebuf):
|
|
|
35
35
|
with suppress(Exception):
|
|
36
36
|
dec = decrypt_string(item.lstrip(b"\x00").rstrip(b"\x00").decode())
|
|
37
37
|
if "dll" not in dec and " " not in dec and ";" not in dec and "." in dec:
|
|
38
|
-
cfg.setdefault("
|
|
38
|
+
cfg.setdefault("CNCs", []).append(dec)
|
|
39
39
|
return cfg
|
|
@@ -55,7 +55,7 @@ def string_from_offset(data, offset):
|
|
|
55
55
|
|
|
56
56
|
|
|
57
57
|
def extract_config(filebuf):
|
|
58
|
-
|
|
58
|
+
config = {}
|
|
59
59
|
yara_matches = yara_scan(filebuf)
|
|
60
60
|
|
|
61
61
|
c2_offsets = []
|
|
@@ -70,6 +70,6 @@ def extract_config(filebuf):
|
|
|
70
70
|
for c2_offset in c2_offsets:
|
|
71
71
|
c2_url = string_from_offset(filebuf, c2_offset)
|
|
72
72
|
if c2_url:
|
|
73
|
-
|
|
73
|
+
config.setdefault("CNCs", []).append(c2_url)
|
|
74
74
|
|
|
75
|
-
return
|
|
75
|
+
return config
|
|
@@ -64,7 +64,7 @@ def extract_config(filebuf):
|
|
|
64
64
|
|
|
65
65
|
c2_address = string_from_offset(filebuf, yara_offset + 0x2E8)
|
|
66
66
|
if c2_address:
|
|
67
|
-
return_conf["
|
|
67
|
+
return_conf["CNCs"] = c2_address
|
|
68
68
|
|
|
69
69
|
c2_url = string_from_offset(filebuf, yara_offset + 0xE8)
|
|
70
70
|
if c2_url:
|
|
@@ -16,9 +16,7 @@ DESCRIPTION = "EvilGrab configuration parser."
|
|
|
16
16
|
AUTHOR = "kevoreilly"
|
|
17
17
|
|
|
18
18
|
import struct
|
|
19
|
-
|
|
20
19
|
import pefile
|
|
21
|
-
|
|
22
20
|
import yara
|
|
23
21
|
|
|
24
22
|
rule_source = """
|
|
@@ -88,21 +86,22 @@ def extract_config(filebuf):
|
|
|
88
86
|
|
|
89
87
|
yara_offset = int(yara_matches[key])
|
|
90
88
|
|
|
89
|
+
# ToDo missed schema
|
|
91
90
|
c2_address = string_from_va(pe, yara_offset + values[0])
|
|
92
91
|
if c2_address:
|
|
93
|
-
end_config["
|
|
92
|
+
end_config["CNCs"] = c2_address
|
|
94
93
|
port = str(struct.unpack("h", filebuf[yara_offset + values[1] : yara_offset + values[1] + 2])[0])
|
|
95
94
|
if port:
|
|
96
|
-
end_config["port"] = [port, "tcp"]
|
|
95
|
+
end_config.setdefault("raw", {})["port"] = [port, "tcp"]
|
|
97
96
|
missionid = string_from_va(pe, yara_offset + values[3])
|
|
98
97
|
if missionid:
|
|
99
|
-
end_config["missionid"] = missionid
|
|
98
|
+
end_config.setdefault("raw", {})["missionid"] = missionid
|
|
100
99
|
version = string_from_va(pe, yara_offset + values[4])
|
|
101
100
|
if version:
|
|
102
101
|
end_config["version"] = version
|
|
103
102
|
injectionprocess = string_from_va(pe, yara_offset + values[5])
|
|
104
103
|
if injectionprocess:
|
|
105
|
-
end_config["injectionprocess"] = injectionprocess
|
|
104
|
+
end_config.setdefault("raw", {})["injectionprocess"] = injectionprocess
|
|
106
105
|
if key != "$configure3":
|
|
107
106
|
mutex = string_from_va(pe, yara_offset - values[6])
|
|
108
107
|
if mutex:
|
|
@@ -17,9 +17,7 @@ AUTHOR = "kevoreilly"
|
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
import struct
|
|
20
|
-
|
|
21
20
|
import pefile
|
|
22
|
-
|
|
23
21
|
import yara
|
|
24
22
|
|
|
25
23
|
rule_source = """
|
|
@@ -91,6 +89,7 @@ def extract_config(filebuf):
|
|
|
91
89
|
# image_base = pe.OPTIONAL_HEADER.ImageBase
|
|
92
90
|
|
|
93
91
|
yara_matches = yara_scan(filebuf)
|
|
92
|
+
config = {}
|
|
94
93
|
tmp_config = {}
|
|
95
94
|
for key, values in match_map.keys():
|
|
96
95
|
if yara_matches.get(key):
|
|
@@ -103,23 +102,23 @@ def extract_config(filebuf):
|
|
|
103
102
|
|
|
104
103
|
c2_address = unicode_from_va(pe, yara_offset + values[1])
|
|
105
104
|
if c2_address:
|
|
106
|
-
|
|
105
|
+
config.setdefault("CNCs", []).append(c2_address)
|
|
107
106
|
|
|
108
107
|
if key == "$connect_3":
|
|
109
108
|
c2_address = unicode_from_va(pe, yara_offset + values[2])
|
|
110
109
|
if c2_address:
|
|
111
|
-
|
|
110
|
+
config.setdefault("CNCs", []).append(c2_address)
|
|
112
111
|
else:
|
|
113
112
|
c2_address = unicode_from_va(pe, yara_offset + values[0])
|
|
114
113
|
if c2_address:
|
|
115
|
-
|
|
114
|
+
config["CNCs"] = [c2_address]
|
|
116
115
|
|
|
117
116
|
filepath = unicode_from_va(pe, yara_offset + values[1])
|
|
118
117
|
if filepath:
|
|
119
|
-
|
|
118
|
+
config["filepath"] = filepath
|
|
120
119
|
|
|
121
120
|
injectionprocess = unicode_from_va(pe, yara_offset - values[2])
|
|
122
121
|
if injectionprocess:
|
|
123
|
-
|
|
122
|
+
config["injectionprocess"] = injectionprocess
|
|
124
123
|
|
|
125
|
-
return tmp_config
|
|
124
|
+
return {"raw": tmp_config}.update(config)
|
|
@@ -16,9 +16,7 @@ DESCRIPTION = "RCSession configuration parser."
|
|
|
16
16
|
AUTHOR = "kevoreilly"
|
|
17
17
|
|
|
18
18
|
import struct
|
|
19
|
-
|
|
20
19
|
import pefile
|
|
21
|
-
|
|
22
20
|
import yara
|
|
23
21
|
|
|
24
22
|
rule_source = """
|
|
@@ -99,24 +97,24 @@ def extract_config(filebuf):
|
|
|
99
97
|
|
|
100
98
|
c2_address = str(tmp_config[156 : 156 + MAX_IP_STRING_SIZE])
|
|
101
99
|
if c2_address:
|
|
102
|
-
end_config.setdefault("
|
|
100
|
+
end_config.setdefault("CNCs", []).append(c2_address)
|
|
103
101
|
c2_address = str(tmp_config[224 : 224 + MAX_IP_STRING_SIZE])
|
|
104
102
|
if c2_address:
|
|
105
|
-
end_config.setdefault("
|
|
103
|
+
end_config.setdefault("CNCs", []).append(c2_address)
|
|
106
104
|
installdir = unicode_string_from_offset(bytes(tmp_config), 0x2A8, 128)
|
|
107
105
|
if installdir:
|
|
108
|
-
end_config["directory"] = installdir
|
|
106
|
+
end_config.setdefault("raw", {})["directory"] = installdir
|
|
109
107
|
executable = unicode_string_from_offset(tmp_config, 0x4B0, 128)
|
|
110
108
|
if executable:
|
|
111
|
-
end_config["filename"] = executable
|
|
109
|
+
end_config.setdefault("raw", {})["filename"] = executable
|
|
112
110
|
servicename = unicode_string_from_offset(tmp_config, 0x530, 128)
|
|
113
111
|
if servicename:
|
|
114
|
-
end_config["servicename"] = servicename
|
|
112
|
+
end_config.setdefault("raw", {})["servicename"] = servicename
|
|
115
113
|
displayname = unicode_string_from_offset(tmp_config, 0x738, 128)
|
|
116
114
|
if displayname:
|
|
117
|
-
end_config["servicedisplayname"] = displayname
|
|
115
|
+
end_config.setdefault("raw", {})["servicedisplayname"] = displayname
|
|
118
116
|
description = unicode_string_from_offset(tmp_config, 0x940, 512)
|
|
119
117
|
if description:
|
|
120
|
-
end_config["servicedescription"] = description
|
|
118
|
+
end_config.setdefault("raw", {})["servicedescription"] = description
|
|
121
119
|
|
|
122
120
|
return end_config
|
|
@@ -47,7 +47,7 @@ def decodeREvilConfig(config_key, config_data):
|
|
|
47
47
|
ECX = EAX = ESI = 0
|
|
48
48
|
|
|
49
49
|
for char in init255:
|
|
50
|
-
ESI = ((char & 0xFF) + (
|
|
50
|
+
ESI = ((char & 0xFF) + (key[EAX % len(key)] + ESI)) & 0xFF
|
|
51
51
|
init255[EAX] = init255[ESI] & 0xFF
|
|
52
52
|
EAX += 1
|
|
53
53
|
init255[ESI] = char & 0xFF
|
|
@@ -61,7 +61,7 @@ def decodeREvilConfig(config_key, config_data):
|
|
|
61
61
|
ESI = (ESI + DL) & 0xFF
|
|
62
62
|
init255[ECX] = init255[ESI]
|
|
63
63
|
init255[ESI] = DL
|
|
64
|
-
decoded_config.append((init255[((init255[ECX] + DL) & 0xFF)]) ^
|
|
64
|
+
decoded_config.append((init255[((init255[ECX] + DL) & 0xFF)]) ^ char)
|
|
65
65
|
EAX = LOCAL1
|
|
66
66
|
|
|
67
67
|
return json.loads("".join(map(chr, decoded_config)))
|
|
@@ -74,12 +74,17 @@ def extract_config(data):
|
|
|
74
74
|
|
|
75
75
|
if len(pe.sections) == 5:
|
|
76
76
|
section_names = getSectionNames(pe.sections)
|
|
77
|
-
required_sections = (".text", ".rdata", ".data", ".reloc")
|
|
77
|
+
required_sections = (b".text", b".rdata", b".data", b".reloc")
|
|
78
78
|
|
|
79
|
-
# print section_names
|
|
80
79
|
if all(sections in section_names for sections in required_sections):
|
|
81
80
|
# print("all required section names found")
|
|
82
|
-
|
|
81
|
+
section_names_set = set(section_names)
|
|
82
|
+
required_sections_set = set(required_sections)
|
|
83
|
+
config_section_names = section_names_set - required_sections_set
|
|
84
|
+
if len(config_section_names) == 1:
|
|
85
|
+
config_section_name = config_section_names.pop()
|
|
86
|
+
else:
|
|
87
|
+
return None # Or raise an exception, depending on desired behavior
|
|
83
88
|
config_key, config_data = getREvilKeyAndConfig(pe.sections, config_section_name)
|
|
84
89
|
if config_key and config_data:
|
|
85
90
|
return decodeREvilConfig(config_key, config_data)
|
|
@@ -16,9 +16,7 @@ DESCRIPTION = "RedLeaf configuration parser."
|
|
|
16
16
|
AUTHOR = "kevoreilly"
|
|
17
17
|
|
|
18
18
|
import struct
|
|
19
|
-
|
|
20
19
|
import pefile
|
|
21
|
-
|
|
22
20
|
import yara
|
|
23
21
|
|
|
24
22
|
rule_source = """
|
|
@@ -90,21 +88,21 @@ def extract_config(filebuf):
|
|
|
90
88
|
end_config = {}
|
|
91
89
|
c2_address = tmp_config[8 : 8 + MAX_IP_STRING_SIZE]
|
|
92
90
|
if c2_address:
|
|
93
|
-
end_config.setdefault("
|
|
91
|
+
end_config.setdefault("CNCs", []).append(c2_address)
|
|
94
92
|
c2_address = tmp_config[0x48 : 0x48 + MAX_IP_STRING_SIZE]
|
|
95
93
|
if c2_address:
|
|
96
|
-
end_config.setdefault("
|
|
94
|
+
end_config.setdefault("CNCs", []).append(c2_address)
|
|
97
95
|
c2_address = tmp_config[0x88 : 0x88 + MAX_IP_STRING_SIZE]
|
|
98
96
|
if c2_address:
|
|
99
|
-
end_config.setdefault("
|
|
97
|
+
end_config.setdefault("CNCs", []).append(c2_address)
|
|
100
98
|
missionid = string_from_offset(tmp_config, 0x1EC)
|
|
101
99
|
if missionid:
|
|
102
|
-
end_config["missionid"] = missionid
|
|
100
|
+
end_config.setdefault("raw", {})["missionid"] = missionid
|
|
103
101
|
mutex = unicode_string_from_offset(tmp_config, 0x508)
|
|
104
102
|
if mutex:
|
|
105
103
|
end_config["mutex"] = mutex
|
|
106
104
|
key = string_from_offset(tmp_config, 0x832)
|
|
107
105
|
if key:
|
|
108
|
-
end_config["
|
|
106
|
+
end_config["cryptokey"] = key
|
|
109
107
|
|
|
110
108
|
return end_config
|
|
@@ -9,8 +9,5 @@ def extract_config(data: bytes):
|
|
|
9
9
|
if matches:
|
|
10
10
|
ip = "".join(".".join(f"{c}" for c in matches[0][0]))
|
|
11
11
|
port = int.from_bytes(matches[0][1], byteorder="big")
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
config_dict["Port"] = port
|
|
15
|
-
|
|
16
|
-
return config_dict
|
|
12
|
+
config_dict["CNCs"] = f"{ip}:{port}"
|
|
13
|
+
return {}
|
|
@@ -96,8 +96,12 @@ def ver_5(data):
|
|
|
96
96
|
|
|
97
97
|
|
|
98
98
|
def extract_config(data):
|
|
99
|
+
config = {}
|
|
99
100
|
if "!!<3SAFIA<3!!" in data:
|
|
100
|
-
|
|
101
|
+
config = ver_52(data)
|
|
101
102
|
|
|
102
103
|
elif "!!ElMattadorDz!!" in data:
|
|
103
|
-
|
|
104
|
+
config = ver_5(data)
|
|
105
|
+
|
|
106
|
+
if config:
|
|
107
|
+
return {"raw": config}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: CAPE-parsers
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.54
|
|
4
4
|
Summary: CAPE: Malware Configuration Extraction
|
|
5
5
|
License: MIT
|
|
6
|
+
License-File: LICENSE
|
|
6
7
|
Keywords: cape,parsers,malware,configuration
|
|
7
8
|
Author: Kevin O'Reilly
|
|
8
9
|
Author-email: kev@capesandbox.com
|
|
@@ -13,6 +14,7 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
13
14
|
Classifier: Programming Language :: Python :: 3.11
|
|
14
15
|
Classifier: Programming Language :: Python :: 3.12
|
|
15
16
|
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
16
18
|
Provides-Extra: maco
|
|
17
19
|
Requires-Dist: capstone (>=4.0.2)
|
|
18
20
|
Requires-Dist: dncil (>=1.0.2)
|
|
@@ -23,7 +25,7 @@ Requires-Dist: pefile
|
|
|
23
25
|
Requires-Dist: pycryptodomex (>=3.20.0)
|
|
24
26
|
Requires-Dist: rat-king-parser (>=4.1.0)
|
|
25
27
|
Requires-Dist: ruff (>=0.7.2)
|
|
26
|
-
Requires-Dist: unicorn (
|
|
28
|
+
Requires-Dist: unicorn (>=2.1.1)
|
|
27
29
|
Requires-Dist: yara-python (>=4.5.1)
|
|
28
30
|
Description-Content-Type: text/markdown
|
|
29
31
|
|
|
@@ -32,3 +34,22 @@ CAPE core and community parsers
|
|
|
32
34
|
|
|
33
35
|
[](https://pypi.org/project/CAPE-parsers/)
|
|
34
36
|
|
|
37
|
+
### Configs structure
|
|
38
|
+
```
|
|
39
|
+
CNCs: []
|
|
40
|
+
campaign: str
|
|
41
|
+
botnet: str
|
|
42
|
+
dga_seed: hex str
|
|
43
|
+
version: str
|
|
44
|
+
mutex: str
|
|
45
|
+
user_agent: str
|
|
46
|
+
build: str
|
|
47
|
+
cryptokey: str
|
|
48
|
+
cryptokey_type: str (algorithm). Ex: RC4, RSA public key. salsa20, (x)chacha20
|
|
49
|
+
raw: {any other data goes here}
|
|
50
|
+
```
|
|
51
|
+
* All CNC entries should be in URL format. aka `<schema>://<hostname>:<port>/<uri>`
|
|
52
|
+
* Schema examples: `tcp://`, `ftp://`, `udp://`, `http(s)`, etc.
|
|
53
|
+
* Old CAPE configs still have lack of this structures as most of them are dead families.
|
|
54
|
+
* This CNC simplification make it easier to parse with tools like `tldextract` or `urlparse`
|
|
55
|
+
|