CAPE-parsers 0.1.48__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 +7 -1
- cape_parsers/CAPE/community/Amadey.py +4 -9
- cape_parsers/CAPE/community/AuroraStealer.py +1 -1
- cape_parsers/CAPE/community/Lumma.py +10 -5
- cape_parsers/CAPE/community/{monsterv2.py → MonsterV2.py} +6 -9
- cape_parsers/CAPE/community/MyKings.py +52 -0
- cape_parsers/CAPE/community/Stealc.py +11 -1
- cape_parsers/CAPE/community/WinosStager.py +75 -0
- cape_parsers/CAPE/core/AuraStealer.py +100 -0
- cape_parsers/CAPE/core/Latrodectus.py +4 -3
- cape_parsers/CAPE/core/NitroBunnyDownloader.py +151 -0
- cape_parsers/CAPE/core/Rhadamanthys.py +161 -32
- {cape_parsers-0.1.48.dist-info → cape_parsers-0.1.54.dist-info}/METADATA +4 -2
- {cape_parsers-0.1.48.dist-info → cape_parsers-0.1.54.dist-info}/RECORD +16 -12
- {cape_parsers-0.1.48.dist-info → cape_parsers-0.1.54.dist-info}/WHEEL +1 -1
- {cape_parsers-0.1.48.dist-info → cape_parsers-0.1.54.dist-info/licenses}/LICENSE +0 -0
|
@@ -9,6 +9,7 @@ except ImportError as e:
|
|
|
9
9
|
def extract_config(data: bytes):
|
|
10
10
|
config = {}
|
|
11
11
|
config_dict = {}
|
|
12
|
+
is_c2_found = False
|
|
12
13
|
with suppress(Exception):
|
|
13
14
|
if data[:2] == b"MZ":
|
|
14
15
|
lines = extract_strings(data=data, on_demand=True, minchars=3)
|
|
@@ -25,11 +26,13 @@ def extract_config(data: bytes):
|
|
|
25
26
|
config_dict["Protocol"] = "Telegram"
|
|
26
27
|
config["CNCs"] = lines[base + x]
|
|
27
28
|
config_dict["Password"] = lines[base + x + 1]
|
|
29
|
+
is_c2_found = True
|
|
28
30
|
break
|
|
29
31
|
# Data Exfiltration via Discord
|
|
30
32
|
elif "discord" in lines[base + x]:
|
|
31
33
|
config_dict["Protocol"] = "Discord"
|
|
32
34
|
config["CNCs"] = [lines[base + x]]
|
|
35
|
+
is_c2_found = True
|
|
33
36
|
break
|
|
34
37
|
# Data Exfiltration via FTP
|
|
35
38
|
elif "ftp:" in lines[base + x]:
|
|
@@ -38,6 +41,7 @@ def extract_config(data: bytes):
|
|
|
38
41
|
username = lines[base + x + 1]
|
|
39
42
|
password = lines[base + x + 2]
|
|
40
43
|
config["CNCs"] = [f"ftp://{username}:{password}@{hostname}"]
|
|
44
|
+
is_c2_found = True
|
|
41
45
|
break
|
|
42
46
|
# Data Exfiltration via SMTP
|
|
43
47
|
elif "@" in lines[base + x]:
|
|
@@ -52,10 +56,12 @@ def extract_config(data: bytes):
|
|
|
52
56
|
config_dict["Password"] = lines[base + x + 1]
|
|
53
57
|
if "@" in lines[base + x + 2]:
|
|
54
58
|
config_dict["EmailTo"] = lines[base + x + 2]
|
|
59
|
+
is_c2_found = True
|
|
55
60
|
break
|
|
56
61
|
# Get Persistence Payload Filename
|
|
57
62
|
for x in range(2, 22):
|
|
58
|
-
|
|
63
|
+
# Only extract Persistence Filename when a C2 is detected.
|
|
64
|
+
if ".exe" in lines[base + x] and is_c2_found:
|
|
59
65
|
config_dict["Persistence_Filename"] = lines[base + x]
|
|
60
66
|
break
|
|
61
67
|
# Get External IP Check Services
|
|
@@ -173,25 +173,20 @@ def extract_config(data):
|
|
|
173
173
|
version = ""
|
|
174
174
|
install_dir = ""
|
|
175
175
|
install_file = ""
|
|
176
|
-
ip_pattern = r"^(?:\d{1,3}\.){3}\d{1,3}$"
|
|
177
176
|
version_pattern = r"^\d+\.\d{1,2}$"
|
|
178
177
|
install_dir_pattern = r"^[0-9a-f]{10}$"
|
|
179
178
|
|
|
180
|
-
i
|
|
181
|
-
while i < len(decoded_strings):
|
|
179
|
+
for i in range(len(decoded_strings)):
|
|
182
180
|
s = decoded_strings[i]
|
|
183
|
-
if
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
final_config.setdefault("CNCs", []).append(f"http://{s}{path}")
|
|
187
|
-
i += 1 # Skip next element as it has been processed
|
|
181
|
+
if s.endswith(".php"):
|
|
182
|
+
c2 = decoded_strings[i-1]
|
|
183
|
+
final_config.setdefault("CNCs", []).append(f"http://{c2}{s}")
|
|
188
184
|
elif re.match(version_pattern, s):
|
|
189
185
|
version = s
|
|
190
186
|
elif re.match(install_dir_pattern, s):
|
|
191
187
|
install_dir = s
|
|
192
188
|
elif s.endswith(".exe"):
|
|
193
189
|
install_file = s
|
|
194
|
-
i += 1
|
|
195
190
|
|
|
196
191
|
if version:
|
|
197
192
|
final_config["version"] = version
|
|
@@ -32,7 +32,7 @@ def extract_config(data):
|
|
|
32
32
|
key = item.split(":")[0].strip("{").strip('"')
|
|
33
33
|
value = item.split(":")[1].strip('"')
|
|
34
34
|
if key == "IP":
|
|
35
|
-
config_dict["CNCs"] = [value]
|
|
35
|
+
config_dict["CNCs"] = [f"tcp://{value}"]
|
|
36
36
|
elif key == "BuildID":
|
|
37
37
|
config_dict["build"] = value
|
|
38
38
|
else:
|
|
@@ -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
|
{
|
|
@@ -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,7 +315,9 @@ 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
|
-
|
|
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]
|
|
319
321
|
if contains_non_printable(decoded_c2):
|
|
320
322
|
break
|
|
321
323
|
config.setdefault("CNCs", []).append("https://" + decoded_c2.decode())
|
|
@@ -348,7 +350,10 @@ def extract_config(data):
|
|
|
348
350
|
c2_encrypted = data[c2_dword_offset : c2_dword_offset + 0x80]
|
|
349
351
|
counters = [0, 2, 4, 6, 8, 10, 12, 14, 16]
|
|
350
352
|
for counter in counters:
|
|
351
|
-
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]
|
|
352
357
|
c2 = extract_c2_domain(decrypted)
|
|
353
358
|
if c2 is not None and len(c2) > 10:
|
|
354
359
|
config["CNCs"].append("https://" + c2.decode())
|
|
@@ -14,15 +14,12 @@ RULE_SOURCE = """rule MonsterV2Config
|
|
|
14
14
|
author = "doomedraven,YungBinary"
|
|
15
15
|
strings:
|
|
16
16
|
$chunk_1 = {
|
|
17
|
-
41 B8
|
|
18
|
-
48 8D 15 ?? ?? ??
|
|
19
|
-
48 8B
|
|
20
|
-
E8 ?? ?? ?? ??
|
|
21
|
-
|
|
22
|
-
48
|
|
23
|
-
48 89 6C 24 ??
|
|
24
|
-
4C 8B C7
|
|
25
|
-
48 8D 54 24 ??
|
|
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
|
|
26
23
|
48 8B CE
|
|
27
24
|
E8 ?? ?? ?? ??
|
|
28
25
|
}
|
|
@@ -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))
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import struct
|
|
2
2
|
import pefile
|
|
3
3
|
import yara
|
|
4
|
+
import ipaddress
|
|
4
5
|
from contextlib import suppress
|
|
5
6
|
|
|
6
7
|
|
|
@@ -42,6 +43,13 @@ def yara_scan(raw_data):
|
|
|
42
43
|
yield block.identifier, instance.offset
|
|
43
44
|
|
|
44
45
|
|
|
46
|
+
def _is_ip(ip):
|
|
47
|
+
try:
|
|
48
|
+
ipaddress.ip_address(ip)
|
|
49
|
+
return True
|
|
50
|
+
except Exception:
|
|
51
|
+
return False
|
|
52
|
+
|
|
45
53
|
def xor_data(data, key):
|
|
46
54
|
decoded = bytearray()
|
|
47
55
|
for i in range(len(data)):
|
|
@@ -67,7 +75,9 @@ def parse_text(data):
|
|
|
67
75
|
for line in lines:
|
|
68
76
|
if line.startswith("http") and "://" in line:
|
|
69
77
|
domain = line
|
|
70
|
-
|
|
78
|
+
elif _is_ip(line):
|
|
79
|
+
domain = line
|
|
80
|
+
if line.startswith("/") and len(line) >= 4 and line[-4] == ".":
|
|
71
81
|
uri = line
|
|
72
82
|
|
|
73
83
|
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Description: Winos 4.0 "OnlineModule" config parser
|
|
3
|
+
Author: x.com/YungBinary
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from contextlib import suppress
|
|
7
|
+
import re
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
CONFIG_KEY_MAP = {
|
|
11
|
+
"dd": "execution_delay_seconds",
|
|
12
|
+
"cl": "communication_interval_seconds",
|
|
13
|
+
"bb": "version",
|
|
14
|
+
"bz": "comment",
|
|
15
|
+
"jp": "keylogger",
|
|
16
|
+
"bh": "end_bluescreen",
|
|
17
|
+
"ll": "anti_traffic_monitoring",
|
|
18
|
+
"dl": "entrypoint",
|
|
19
|
+
"sh": "process_daemon",
|
|
20
|
+
"kl": "process_hollowing"
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def find_config(data):
|
|
25
|
+
start = ":db|".encode("utf-16le")
|
|
26
|
+
end = ":1p|".encode("utf-16le")
|
|
27
|
+
pattern = re.compile(re.escape(start) + b".*?" + re.escape(end), re.DOTALL)
|
|
28
|
+
match = pattern.search(data)
|
|
29
|
+
if match:
|
|
30
|
+
return match.group(0).decode("utf-16le")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def extract_config(data: bytes) -> dict:
|
|
34
|
+
config_dict = {}
|
|
35
|
+
final_config = {}
|
|
36
|
+
|
|
37
|
+
with suppress(Exception):
|
|
38
|
+
config = find_config(data)
|
|
39
|
+
if not config:
|
|
40
|
+
return config_dict
|
|
41
|
+
|
|
42
|
+
# Reverse the config string, which is delimited by '|'
|
|
43
|
+
config = config[::-1]
|
|
44
|
+
# Remove leading/trailing pipes and split into key/value pairs
|
|
45
|
+
elements = [element for element in config.strip('|').split('|') if ':' in element]
|
|
46
|
+
# Split each element for key : value in a dictionary
|
|
47
|
+
config_dict = dict(element.split(':', 1) for element in elements)
|
|
48
|
+
if config_dict:
|
|
49
|
+
# Handle extraction and formatting of CNCs
|
|
50
|
+
for i in range(1, 4):
|
|
51
|
+
p, o, t = config_dict.get(f"p{i}"), config_dict.get(f"o{i}"), config_dict.get(f"t{i}")
|
|
52
|
+
if p and p != "127.0.0.1" and o:
|
|
53
|
+
protocol = {"0": "udp", "1": "tcp"}.get(t)
|
|
54
|
+
if protocol:
|
|
55
|
+
cnc = f"{protocol}://{p}:{o}"
|
|
56
|
+
final_config.setdefault("CNCs", []).append(cnc)
|
|
57
|
+
|
|
58
|
+
if "CNCs" not in final_config:
|
|
59
|
+
return {}
|
|
60
|
+
|
|
61
|
+
final_config["CNCs"] = list(set(final_config["CNCs"]))
|
|
62
|
+
# Extract campaign ID
|
|
63
|
+
final_config["campaign_id"] = "default" if config_dict["fz"] == "\u9ed8\u8ba4" else config_dict["fz"]
|
|
64
|
+
|
|
65
|
+
# Map keys, e.g. dd -> execution_delay_seconds
|
|
66
|
+
final_config["raw"] = {v: config_dict[k] for k, v in CONFIG_KEY_MAP.items() if k in config_dict}
|
|
67
|
+
|
|
68
|
+
return final_config
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
if __name__ == "__main__":
|
|
72
|
+
import sys
|
|
73
|
+
|
|
74
|
+
with open(sys.argv[1], "rb") as f:
|
|
75
|
+
print(extract_config(f.read()))
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import struct
|
|
3
|
+
from contextlib import suppress
|
|
4
|
+
from typing import Any, Dict, Tuple
|
|
5
|
+
|
|
6
|
+
import pefile
|
|
7
|
+
from Cryptodome.Cipher import AES
|
|
8
|
+
from Cryptodome.Util.Padding import unpad
|
|
9
|
+
|
|
10
|
+
# Define the format for the fixed-size header part.
|
|
11
|
+
# < : little-endian
|
|
12
|
+
# 32s : 32-byte string (for aes_key)
|
|
13
|
+
# 16s : 16-byte string (for iv)
|
|
14
|
+
# I : 4-byte unsigned int (for dword1)
|
|
15
|
+
# I : 4-byte unsigned int (for dword2)
|
|
16
|
+
HEADER_FORMAT = "<32s16sII"
|
|
17
|
+
HEADER_SIZE = struct.calcsize(HEADER_FORMAT) # This will be 32 + 16 + 4 + 4 = 56 bytes
|
|
18
|
+
|
|
19
|
+
def parse_blob(data: bytes):
|
|
20
|
+
"""
|
|
21
|
+
Parse the blob according to the scheme:
|
|
22
|
+
- 32 bytes = AES key
|
|
23
|
+
- Next 16 bytes = IV
|
|
24
|
+
- Next 2 DWORDs (8 bytes total) = XOR to get cipher data size
|
|
25
|
+
- Remaining bytes = cipher data of that size
|
|
26
|
+
"""
|
|
27
|
+
aes_key, iv, dword1, dword2 = struct.unpack_from(HEADER_FORMAT, data, 0)
|
|
28
|
+
ciphertext_size = dword1 ^ dword2
|
|
29
|
+
cipher_data = data[HEADER_SIZE : HEADER_SIZE + ciphertext_size]
|
|
30
|
+
return aes_key, iv, cipher_data
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def decrypt(data: bytes) -> Tuple[bytes, bytes, bytes]:
|
|
34
|
+
aes_key, iv, cipher_data = parse_blob(data)
|
|
35
|
+
cipher = AES.new(aes_key, AES.MODE_CBC, iv)
|
|
36
|
+
plaintext_padded = cipher.decrypt(cipher_data)
|
|
37
|
+
return aes_key, iv, unpad(plaintext_padded, AES.block_size)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def extract_config(data: bytes) -> Dict[str, Any]:
|
|
41
|
+
cfg: Dict[str, Any] = {}
|
|
42
|
+
plaintext = b""
|
|
43
|
+
|
|
44
|
+
pe = pefile.PE(data=data, fast_load=True)
|
|
45
|
+
try:
|
|
46
|
+
data_section = [s for s in pe.sections if s.Name.find(b".data") != -1][0]
|
|
47
|
+
except IndexError:
|
|
48
|
+
return cfg
|
|
49
|
+
|
|
50
|
+
if not data_section:
|
|
51
|
+
return cfg
|
|
52
|
+
|
|
53
|
+
data = data_section.get_data()
|
|
54
|
+
block_size = 4096
|
|
55
|
+
zeros = b"\x00" * block_size
|
|
56
|
+
offset = data.find(zeros)
|
|
57
|
+
if offset == -1:
|
|
58
|
+
return cfg
|
|
59
|
+
|
|
60
|
+
while offset > 0:
|
|
61
|
+
with suppress(Exception):
|
|
62
|
+
aes_key, iv, plaintext = decrypt(data[offset : offset + block_size])
|
|
63
|
+
if plaintext and b"conf" in plaintext:
|
|
64
|
+
break
|
|
65
|
+
|
|
66
|
+
offset -= 1
|
|
67
|
+
|
|
68
|
+
if plaintext:
|
|
69
|
+
try:
|
|
70
|
+
parsed = json.loads(plaintext.decode("utf-8", errors="ignore").rstrip("\x00"))
|
|
71
|
+
except json.JSONDecodeError:
|
|
72
|
+
return cfg
|
|
73
|
+
|
|
74
|
+
conf = parsed.get("conf", {})
|
|
75
|
+
build = parsed.get("build", {})
|
|
76
|
+
if conf:
|
|
77
|
+
cfg = {
|
|
78
|
+
"CNCs": conf.get("hosts"),
|
|
79
|
+
"user_agent": conf.get("useragents"),
|
|
80
|
+
"version": build.get("ver"),
|
|
81
|
+
"build": build.get("build_id"),
|
|
82
|
+
"cryptokey": aes_key.hex(),
|
|
83
|
+
"cryptokey_type": "AES",
|
|
84
|
+
"raw": {
|
|
85
|
+
"iv": iv.hex(),
|
|
86
|
+
"anti_vm": conf.get("anti_vm"),
|
|
87
|
+
"anti_dbg": conf.get("anti_dbg"),
|
|
88
|
+
"self_del": conf.get("self_del"),
|
|
89
|
+
"run_delay": conf.get("run_delay"),
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return cfg
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
if __name__ == "__main__":
|
|
97
|
+
import sys
|
|
98
|
+
|
|
99
|
+
with open(sys.argv[1], "rb") as f:
|
|
100
|
+
print(extract_config(f.read()))
|
|
@@ -41,7 +41,7 @@ rule Latrodectus
|
|
|
41
41
|
$fnvhash2 = {8B 0C 24 33 C8 8B C1 89 04 24 69 04 24 93 01 00 01}
|
|
42
42
|
$procchk1 = {E8 [3] FF 85 C0 74 [2] FF FF FF FF E9 [4] E8 [4] 89 44 24 ?? E8 [4] 83 F8 4B 73 ?? 83 [3] 06}
|
|
43
43
|
$procchk2 = {72 [2] FF FF FF FF E9 [4] E8 [4] 83 F8 32 73 ?? 83 [3] 06}
|
|
44
|
-
$version = {C7 44 2? ?? ?? 00 00 00 C7 44 2? ?? ?? 00 00 00 8B 05 [4] 89}
|
|
44
|
+
$version = {C7 44 2? ?? ?? 00 00 00 C7 44 2? ?? ?? 00 00 00 C7 44 2? ?? ?? 00 00 00 8B 05 [4] 89}
|
|
45
45
|
condition:
|
|
46
46
|
all of them
|
|
47
47
|
}
|
|
@@ -59,7 +59,7 @@ rule Latrodectus_AES
|
|
|
59
59
|
$key = {C6 44 2? ?? ?? [150] C6 44 2? ?? ?? B8 02}
|
|
60
60
|
$aes_ctr_1 = {8B 44 24 ?? FF C8 89 44 24 ?? 83 7C 24 ?? 00 7C ?? 4? 63 44 24 ?? 4? 8B 4C 24 ?? 0F B6 84 01 F0 00 00 00 3D FF 00 00 00}
|
|
61
61
|
$aes_ctr_2 = {48 03 C8 48 8B C1 0F B6 ?? 48 63 4C 24 ?? 0F B6 4C 0C ?? 33 C1 48 8B 4C 24 ?? 48 8B 54 24 ?? 48 03 D1 48 8B CA 88 01}
|
|
62
|
-
$version = {C7 44 2? ?? ?? 00 00 00 C7 44 2? ?? ?? 00 00 00 8B 05 [4] 89}
|
|
62
|
+
$version = {C7 44 2? ?? ?? 00 00 00 C7 44 2? ?? ?? 00 00 00 C7 44 2? ?? ?? 00 00 00 8B 05 [4] 89}
|
|
63
63
|
condition:
|
|
64
64
|
all of them
|
|
65
65
|
}
|
|
@@ -152,7 +152,8 @@ def extract_config(filebuf):
|
|
|
152
152
|
data = instance.matched_data[::-1]
|
|
153
153
|
major = int.from_bytes(data[10:11], byteorder="big")
|
|
154
154
|
minor = int.from_bytes(data[18:19], byteorder="big")
|
|
155
|
-
|
|
155
|
+
release = int.from_bytes(data[26:27], byteorder="big")
|
|
156
|
+
version = f"{major}.{minor}.{release}"
|
|
156
157
|
if "$key" in item.identifier:
|
|
157
158
|
key = instance.matched_data[4::5]
|
|
158
159
|
try:
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# Copyright (C) 2024 enzok
|
|
2
|
+
# This program is free software: you can redistribute it and/or modify
|
|
3
|
+
# it under the terms of the GNU General Public License as published by
|
|
4
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
5
|
+
# (at your option) any later version.
|
|
6
|
+
#
|
|
7
|
+
# This program is distributed in the hope that it will be useful,
|
|
8
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
9
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
10
|
+
# GNU General Public License for more details.
|
|
11
|
+
#
|
|
12
|
+
# You should have received a copy of the GNU General Public License
|
|
13
|
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
14
|
+
|
|
15
|
+
import logging
|
|
16
|
+
import struct
|
|
17
|
+
|
|
18
|
+
import pefile
|
|
19
|
+
import yara
|
|
20
|
+
|
|
21
|
+
log = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
DESCRIPTION = "NitroBunnyDownloader configuration parser."
|
|
24
|
+
AUTHOR = "enzok"
|
|
25
|
+
|
|
26
|
+
yara_rule = """
|
|
27
|
+
rule NitroBunnyDownloader
|
|
28
|
+
{
|
|
29
|
+
meta:
|
|
30
|
+
author = "enzok"
|
|
31
|
+
description = "NitroBunnyDownloader Payload"
|
|
32
|
+
cape_type = "NitroBunnyDownloader Payload"
|
|
33
|
+
hash = "960e59200ec0a4b5fb3b44e6da763f5fec4092997975140797d4eec491de411b"
|
|
34
|
+
strings:
|
|
35
|
+
$config = {E8 [3] 00 41 B8 ?? ?? 00 00 48 8D 15 [3] 00 48 89 C1 48 89 ?? E8 [3] 00}
|
|
36
|
+
$string1 = "X-Amz-User-Agent:" wide
|
|
37
|
+
$string2 = "Amz-Security-Flag:" wide
|
|
38
|
+
$string3 = "/cart" wide
|
|
39
|
+
$string4 = "Cookie: " wide
|
|
40
|
+
$string5 = "wishlist" wide
|
|
41
|
+
condition:
|
|
42
|
+
uint16(0) == 0x5A4D and $config and 2 of ($string*)
|
|
43
|
+
}
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
yara_rules = yara.compile(source=yara_rule)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def yara_scan(raw_data):
|
|
50
|
+
try:
|
|
51
|
+
return yara_rules.match(data=raw_data)
|
|
52
|
+
except Exception as e:
|
|
53
|
+
print(e)
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def read_dword(data, off):
|
|
58
|
+
if off + 4 > len(data):
|
|
59
|
+
raise ValueError(f"EOF reading dword at {off}")
|
|
60
|
+
val = struct.unpack_from("<I", data, off)[0]
|
|
61
|
+
return val, off + 4
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def read_qword(data, off):
|
|
65
|
+
"""Read a 64-bit unsigned little-endian value."""
|
|
66
|
+
if off + 8 > len(data):
|
|
67
|
+
raise ValueError(f"EOF reading qword at {off}")
|
|
68
|
+
val = struct.unpack_from("<Q", data, off)[0]
|
|
69
|
+
return val, off + 8
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def read_utf16le_string(data, off, length):
|
|
73
|
+
if off + length > len(data):
|
|
74
|
+
raise ValueError(f"EOF reading string at {off} len={length}")
|
|
75
|
+
raw = data[off:off + length]
|
|
76
|
+
s = raw.decode("utf-16le", errors="replace").rstrip("\x00")
|
|
77
|
+
return s, off + length
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def read_string_list(data, off, count):
|
|
81
|
+
items = []
|
|
82
|
+
for i in range(count):
|
|
83
|
+
length_words, off = read_qword(data, off)
|
|
84
|
+
s, off = read_utf16le_string(data, off, length_words)
|
|
85
|
+
items.append(s)
|
|
86
|
+
return items, off
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def extract_config(filebuf):
|
|
90
|
+
yara_hit = yara_scan(filebuf)
|
|
91
|
+
if not yara_hit:
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
cfg = {}
|
|
95
|
+
config_code_offset = None
|
|
96
|
+
for hit in yara_hit:
|
|
97
|
+
if hit.rule != "NitroBunnyDownloader":
|
|
98
|
+
continue
|
|
99
|
+
|
|
100
|
+
for item in hit.strings:
|
|
101
|
+
for instance in item.instances:
|
|
102
|
+
if "$config" in item.identifier:
|
|
103
|
+
config_code_offset = instance.offset
|
|
104
|
+
break
|
|
105
|
+
|
|
106
|
+
if config_code_offset is None:
|
|
107
|
+
return None
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
pe = pefile.PE(data=filebuf, fast_load=True)
|
|
111
|
+
config_length = pe.get_dword_from_offset(config_code_offset + 7)
|
|
112
|
+
config_offset = pe.get_dword_from_offset(config_code_offset + 14)
|
|
113
|
+
rva = pe.get_rva_from_offset(config_code_offset + 18)
|
|
114
|
+
config_rva = rva + config_offset
|
|
115
|
+
data = pe.get_data(config_rva, config_length)
|
|
116
|
+
off = 0
|
|
117
|
+
raw = cfg["raw"] = {}
|
|
118
|
+
port, off = read_dword(data, off)
|
|
119
|
+
num, off = read_dword(data, off)
|
|
120
|
+
cncs, off = read_string_list(data, off, num)
|
|
121
|
+
num, off = read_qword(data, off)
|
|
122
|
+
raw["user_agent"], off = read_utf16le_string(data, off, num)
|
|
123
|
+
num, off = read_dword(data, off)
|
|
124
|
+
raw["http_header_items"], off = read_string_list(data, off, num)
|
|
125
|
+
num, off = read_dword(data, off)
|
|
126
|
+
raw["uri_list"], off = read_string_list(data, off, num)
|
|
127
|
+
raw["unknown_1"], off = read_dword(data, off)
|
|
128
|
+
raw["unknown_2"], off = read_dword(data, off)
|
|
129
|
+
|
|
130
|
+
if cncs:
|
|
131
|
+
cfg["CNCs"] = []
|
|
132
|
+
schema = {80: "http", 443: "https"}.get(port, "tcp")
|
|
133
|
+
for cnc in cncs:
|
|
134
|
+
cnc = f"{schema}://{cnc}"
|
|
135
|
+
if port not in (80, 443):
|
|
136
|
+
cnc += f":{port}"
|
|
137
|
+
|
|
138
|
+
cfg["CNCs"].append(cnc)
|
|
139
|
+
|
|
140
|
+
except Exception as e:
|
|
141
|
+
log.error("Error: %s", e)
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
return cfg
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
if __name__ == "__main__":
|
|
148
|
+
import sys
|
|
149
|
+
|
|
150
|
+
with open(sys.argv[1], "rb") as f:
|
|
151
|
+
print(extract_config(f.read()))
|
|
@@ -91,7 +91,7 @@ def chacha20_stream(key, nonce, length, blocknum):
|
|
|
91
91
|
|
|
92
92
|
|
|
93
93
|
def decrypt_config(data):
|
|
94
|
-
decrypted_config = b"
|
|
94
|
+
decrypted_config = b""
|
|
95
95
|
data_len = len(data)
|
|
96
96
|
v3 = 0
|
|
97
97
|
while True:
|
|
@@ -118,40 +118,128 @@ def chacha20_xor(custom_b64_decoded, key, nonce):
|
|
|
118
118
|
return xor_key
|
|
119
119
|
|
|
120
120
|
|
|
121
|
-
def
|
|
122
|
-
apat = b"([
|
|
123
|
-
strings = [
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
return None
|
|
127
|
-
upat = b"((?:[\x20-\x7e][\x00]){" + str(minchars).encode() + b"," + str(maxchars).encode() + b"})\x00\x00"
|
|
128
|
-
strings.extend(str(ws.decode("utf-16le")) for ws in re.findall(upat, data))
|
|
121
|
+
def extract_base64_strings(data, minchars, maxchars):
|
|
122
|
+
apat = b"([A-Za-z0-9-|]{" + str(minchars).encode() + b"," + str(maxchars).encode() + b"})\x00"
|
|
123
|
+
strings = [s.decode() for s in re.findall(apat, data)]
|
|
124
|
+
upat = b"((?:[A-Za-z0-9-|]\x00){" + str(minchars).encode() + b"," + str(maxchars).encode() + b"})\x00\x00"
|
|
125
|
+
strings.extend(ws.decode("utf-16le") for ws in re.findall(upat, data))
|
|
129
126
|
return strings
|
|
130
127
|
|
|
131
128
|
|
|
132
129
|
def extract_c2_url(data):
|
|
133
130
|
pattern = b"(http[\x20-\x7e]+)\x00"
|
|
134
131
|
match = re.search(pattern, data)
|
|
135
|
-
|
|
132
|
+
if match:
|
|
133
|
+
return match.group(1).decode()
|
|
136
134
|
|
|
137
135
|
|
|
138
|
-
def
|
|
139
|
-
custom_alphabet = "ABC1fghijklmnop234NOPQRSTUVWXY567DEFGHIJKLMZ089abcdeqrstuvwxyz-|"
|
|
140
|
-
for c in string:
|
|
141
|
-
if c not in custom_alphabet:
|
|
142
|
-
return False
|
|
143
|
-
return True
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
def custom_b64decode(data):
|
|
136
|
+
def custom_b64decode(data: bytes, custom_alphabet: bytes):
|
|
147
137
|
"""Decodes base64 data using a custom alphabet."""
|
|
148
138
|
standard_alphabet = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
|
149
|
-
custom_alphabet = b"ABC1fghijklmnop234NOPQRSTUVWXY567DEFGHIJKLMZ089abcdeqrstuvwxyz-|"
|
|
150
139
|
# Translate the data back to the standard alphabet before decoding
|
|
151
140
|
table = bytes.maketrans(custom_alphabet, standard_alphabet)
|
|
152
141
|
return base64.b64decode(data.translate(table), validate=True)
|
|
153
142
|
|
|
154
143
|
|
|
144
|
+
def lzo_noheader_decompress(data: bytes, decompressed_size: int):
|
|
145
|
+
src = 0
|
|
146
|
+
dst = bytearray()
|
|
147
|
+
length = len(data)
|
|
148
|
+
|
|
149
|
+
while src < length:
|
|
150
|
+
ctrl = data[src]
|
|
151
|
+
src += 1
|
|
152
|
+
|
|
153
|
+
# Special short match
|
|
154
|
+
# Copies exactly 3 bytes from dst starting match_len + 1 bytes back.
|
|
155
|
+
if ctrl == 0x20:
|
|
156
|
+
match_len = data[src]
|
|
157
|
+
src += 1
|
|
158
|
+
start = len(dst) - match_len - 1
|
|
159
|
+
end = start + 3
|
|
160
|
+
#print(f"Control code: {hex(ctrl)}, Offset backtrack length: {hex(match_len)}, Current offset: {hex(len(dst))}, New offset: {hex(start)}")
|
|
161
|
+
dst.extend(dst[start:end])
|
|
162
|
+
|
|
163
|
+
elif ctrl >= 0xE0 or ctrl == 0x40:
|
|
164
|
+
# Compute base copy length from the upper bits of ctrl
|
|
165
|
+
base_len = ((ctrl >> 5) - 1) + 3
|
|
166
|
+
|
|
167
|
+
if ctrl >= 0xE0:
|
|
168
|
+
# Long copy: extra length byte follows
|
|
169
|
+
copy_len = base_len + data[src]
|
|
170
|
+
# Offset is byte after
|
|
171
|
+
start = data[src + 1]
|
|
172
|
+
src += 2
|
|
173
|
+
elif ctrl == 0x40:
|
|
174
|
+
# Short copy: offset byte after control code
|
|
175
|
+
copy_len = base_len
|
|
176
|
+
start = data[src]
|
|
177
|
+
src += 1
|
|
178
|
+
|
|
179
|
+
# Calculate offset in output buffer
|
|
180
|
+
offset = len(dst) - start - 1
|
|
181
|
+
|
|
182
|
+
#print(f"Control code: {hex(ctrl)}, Offset backtrack length: {hex(start)}, Current offset: {hex(len(dst))}, New offset: {hex(len(dst) - start)}, Length to copy: {hex(copy_len)}")
|
|
183
|
+
|
|
184
|
+
# Copy from previously decompressed data
|
|
185
|
+
dst.extend(dst[offset:offset + copy_len])
|
|
186
|
+
|
|
187
|
+
else:
|
|
188
|
+
# Literal run
|
|
189
|
+
literal_len = (ctrl & 0x1F) + 1
|
|
190
|
+
#print(f"Control code: {hex(ctrl)}, Literal length: {hex(literal_len)}")
|
|
191
|
+
dst.extend(data[src:src+literal_len])
|
|
192
|
+
src += literal_len
|
|
193
|
+
|
|
194
|
+
if len(dst) == decompressed_size:
|
|
195
|
+
return bytes(dst)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def parse_compression_header(config: bytes):
|
|
199
|
+
"""Parse compressed size, decompressed size, and data offset from config"""
|
|
200
|
+
|
|
201
|
+
# 0x2A when looking at the config in memory
|
|
202
|
+
base_offset = 0x26
|
|
203
|
+
|
|
204
|
+
# Compressed data offset field, for calculating the offset to the compressed buffer
|
|
205
|
+
comp_offset_field = config[base_offset]
|
|
206
|
+
# Number of bytes the field spans
|
|
207
|
+
comp_offset_size_len = (comp_offset_field & 3) + 1
|
|
208
|
+
for i in range(1, comp_offset_size_len):
|
|
209
|
+
comp_offset_field |= config[base_offset + i] << (8 * i)
|
|
210
|
+
|
|
211
|
+
comp_size_offset = comp_offset_field >> 2
|
|
212
|
+
|
|
213
|
+
# Compressed size field, for finding the size of the compressed buffer
|
|
214
|
+
comp_offset = base_offset + comp_offset_size_len
|
|
215
|
+
comp_size_field = config[comp_offset]
|
|
216
|
+
# Number of bytes the field spans
|
|
217
|
+
comp_size_len = (comp_size_field & 3) + 1
|
|
218
|
+
for i in range(1, comp_size_len):
|
|
219
|
+
comp_size_field |= config[comp_offset + i] << (8 * i)
|
|
220
|
+
|
|
221
|
+
# Decompressed size field
|
|
222
|
+
decomp_field_offset = base_offset + comp_offset_size_len + comp_size_len
|
|
223
|
+
decomp_size_field = config[decomp_field_offset]
|
|
224
|
+
# Number of bytes the field spans
|
|
225
|
+
decomp_field_len = (decomp_size_field & 3) + 1
|
|
226
|
+
for i in range(1, decomp_field_len):
|
|
227
|
+
decomp_size_field |= config[decomp_field_offset + i] << (8 * i)
|
|
228
|
+
|
|
229
|
+
# Calculate return values
|
|
230
|
+
decompressed_size = decomp_size_field >> 2
|
|
231
|
+
compressed_data_offset = decomp_field_offset + decomp_field_len + comp_size_offset
|
|
232
|
+
compressed_size_key = config[0x28] << 8
|
|
233
|
+
compressed_size = (compressed_size_key | comp_size_field) >> 2
|
|
234
|
+
compressed_data = config[compressed_data_offset : compressed_data_offset + compressed_size]
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
"compressed_size": compressed_size,
|
|
238
|
+
"decompressed_size": decompressed_size,
|
|
239
|
+
"compressed_data": compressed_data
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
|
|
155
243
|
def extract_config(data):
|
|
156
244
|
config_dict = {}
|
|
157
245
|
magic = struct.unpack("I", data[:4])[0]
|
|
@@ -162,25 +250,66 @@ def extract_config(data):
|
|
|
162
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"
|
|
163
251
|
nonce = b"\x5F\x14\xD7\x9C\xFC\xFC\x43\x9E\xC3\x40\x6B\xBA"
|
|
164
252
|
|
|
165
|
-
|
|
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')
|
|
166
265
|
for string in extracted_strings:
|
|
167
266
|
try:
|
|
168
|
-
|
|
169
|
-
continue
|
|
267
|
+
custom_b64_decoded = custom_b64decode(string, custom_alphabets[0])
|
|
170
268
|
|
|
171
|
-
custom_b64_decoded = custom_b64decode(string)
|
|
172
269
|
xor_key = chacha20_xor(custom_b64_decoded, key, nonce)
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
+
|
|
181
308
|
except Exception:
|
|
182
309
|
continue
|
|
183
310
|
|
|
311
|
+
return config_dict
|
|
312
|
+
|
|
184
313
|
|
|
185
314
|
if __name__ == "__main__":
|
|
186
315
|
import sys
|
|
@@ -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)
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
cape_parsers/CAPE/__init__.py,sha256=JcY8WPKzUFYgexwV1eyKIuT1JyNZzMJjBynlPSzxY_I,7
|
|
2
|
-
cape_parsers/CAPE/community/AgentTesla.py,sha256=
|
|
3
|
-
cape_parsers/CAPE/community/Amadey.py,sha256=
|
|
2
|
+
cape_parsers/CAPE/community/AgentTesla.py,sha256=ln5MqFXkTb7WrlDrUHNTnMWBYRHDSqyK4VHeq0ZldtA,4047
|
|
3
|
+
cape_parsers/CAPE/community/Amadey.py,sha256=IUyt909q9IDQPPip6UW9uD16rJMD_gvkwvNZ8NHTW-k,5577
|
|
4
4
|
cape_parsers/CAPE/community/Arkei.py,sha256=k36qHxdo5yPa9V1cg7EImSWP06kMog0rBda4KXqLKCY,3783
|
|
5
5
|
cape_parsers/CAPE/community/AsyncRAT.py,sha256=0-FRT3d2x63KQ_cs1xmKFj7x0JRf7ID6QDc_DvBa0PM,1003
|
|
6
|
-
cape_parsers/CAPE/community/AuroraStealer.py,sha256=
|
|
6
|
+
cape_parsers/CAPE/community/AuroraStealer.py,sha256=LRu2QFBYkGhRGDJBw3GlcKub4E0_TBWmjdR2PnobDZM,2643
|
|
7
7
|
cape_parsers/CAPE/community/Carbanak.py,sha256=Smi_vTWDfWxYBQa661ZIy0624IYJA22LMHAJEQbstpk,5607
|
|
8
8
|
cape_parsers/CAPE/community/CobaltStrikeBeacon.py,sha256=U4Q0ObCrPRpiO5B5fBmkgr63jXdizujNth8v6kUPnEQ,19466
|
|
9
9
|
cape_parsers/CAPE/community/CobaltStrikeStager.py,sha256=HLxROBjz453uHNq1bPz0VSAhtyWDfz79ZacTPdjuWmY,7535
|
|
@@ -11,7 +11,9 @@ cape_parsers/CAPE/community/DCRat.py,sha256=0-FRT3d2x63KQ_cs1xmKFj7x0JRf7ID6QDc_
|
|
|
11
11
|
cape_parsers/CAPE/community/Fareit.py,sha256=OyKeZdcvyAhjxZgJqkDPJHP4Npv1ArvTHJZ5F0C1Iac,1875
|
|
12
12
|
cape_parsers/CAPE/community/KoiLoader.py,sha256=F2gsgCvrVuwxY1bg8rlexsjCjikAP5HIGGOqU8zhT8E,4008
|
|
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=0xwX6kOWR3ezrHxN1y1A5RD2-n8XRKx5hj3ynQjgRKU,12583
|
|
15
|
+
cape_parsers/CAPE/community/MonsterV2.py,sha256=cFxhYxo7FruTMmFY3OtBO-E0hDyxfsC3zWX3BlcB-qI,2915
|
|
16
|
+
cape_parsers/CAPE/community/MyKings.py,sha256=bcypBMJf6Jeg0yrHZ-J-XjnhHvFbb-lpDF_zwW63gOk,1397
|
|
15
17
|
cape_parsers/CAPE/community/NanoCore.py,sha256=8QZnf1AcY9481kSfsf3SHQShwPLn97peGAf8_xEasQc,6230
|
|
16
18
|
cape_parsers/CAPE/community/Nighthawk.py,sha256=8ss8yvslrwUt53zV6U0xuwGKU3hgYfOt13S5lkOVpNo,12105
|
|
17
19
|
cape_parsers/CAPE/community/Njrat.py,sha256=GiwSENBB43RUqyJ7zT7ZPkPUYqo8Ew4kd5MJUj0jzdc,4702
|
|
@@ -20,13 +22,14 @@ cape_parsers/CAPE/community/QuasarRAT.py,sha256=dzVInOc-BPVRdArk92oEY4PKq1AEW04N
|
|
|
20
22
|
cape_parsers/CAPE/community/README.md,sha256=SHgVQraCdp033IQjM4Cm6t70U4kULn1MfSwTq3rsZv8,22
|
|
21
23
|
cape_parsers/CAPE/community/Snake.py,sha256=v_MAPmg86ZdgGOkzc9GVHbi-lu4nLa1_0Lp90qiCg8s,6650
|
|
22
24
|
cape_parsers/CAPE/community/SparkRAT.py,sha256=OVDty_1i9PTGuEumT0BHoDn0bD2UtdhHVNjThah80pg,2140
|
|
23
|
-
cape_parsers/CAPE/community/Stealc.py,sha256=
|
|
25
|
+
cape_parsers/CAPE/community/Stealc.py,sha256=18EkQ-lMMAreKV5vA9xLBmOK5B4JtYcBwVqNfof4K2A,5321
|
|
24
26
|
cape_parsers/CAPE/community/VenomRAT.py,sha256=0-FRT3d2x63KQ_cs1xmKFj7x0JRf7ID6QDc_DvBa0PM,1003
|
|
27
|
+
cape_parsers/CAPE/community/WinosStager.py,sha256=6jkfTJiVdz6oyyqTjh3I4qunYgXbD4qXS_bd-uogwGA,2407
|
|
25
28
|
cape_parsers/CAPE/community/XWorm.py,sha256=0-FRT3d2x63KQ_cs1xmKFj7x0JRf7ID6QDc_DvBa0PM,1003
|
|
26
29
|
cape_parsers/CAPE/community/XenoRAT.py,sha256=0-FRT3d2x63KQ_cs1xmKFj7x0JRf7ID6QDc_DvBa0PM,1003
|
|
27
30
|
cape_parsers/CAPE/community/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
28
|
-
cape_parsers/CAPE/community/monsterv2.py,sha256=eVEs4VIeS3PiZtRjNb69itmDq2Zkbrpn5k3M68GujiI,2995
|
|
29
31
|
cape_parsers/CAPE/core/AdaptixBeacon.py,sha256=40wMfrXt-7UG30WsLC5GxUtG6tSUaaP1OT-ntWzPZn0,2956
|
|
32
|
+
cape_parsers/CAPE/core/AuraStealer.py,sha256=RSiclflsvcrcNLHpRokc_qF2cdQKXGBKg8Ti-Q-XmaM,3021
|
|
30
33
|
cape_parsers/CAPE/core/Azorult.py,sha256=YkMIhC6zRTxEkLVMUdr2MMsbV9iAnZ8hUS8be9GZ5N4,2150
|
|
31
34
|
cape_parsers/CAPE/core/BitPaymer.py,sha256=HQwoE0o7HMiXItxE08vBenf2ZWMxZp84-Hf_1eZ8QdE,3050
|
|
32
35
|
cape_parsers/CAPE/core/BlackDropper.py,sha256=sCSu2T5oPvcFHlSAzSsLj_gCv2Tldl0UPguwy0MVg6A,3282
|
|
@@ -40,7 +43,8 @@ cape_parsers/CAPE/core/Formbook.py,sha256=rvf0BRuRl_v8K9SJuSSfbVVMWLSTEemIgP3NtP
|
|
|
40
43
|
cape_parsers/CAPE/core/GuLoader.py,sha256=wH6t1e7rO60Bwe0ulqFdZq12-M087zT5WQtC_Wn2biU,354
|
|
41
44
|
cape_parsers/CAPE/core/IcedID.py,sha256=TEsvFq8qHz_D5kIURKWSC4lbvWaQbMriDZ3jQsVu2VA,4029
|
|
42
45
|
cape_parsers/CAPE/core/IcedIDLoader.py,sha256=YUOEILpTycO01KK4qqAxGSplsRVs2EzjscUw4T-DGWs,1602
|
|
43
|
-
cape_parsers/CAPE/core/Latrodectus.py,sha256=
|
|
46
|
+
cape_parsers/CAPE/core/Latrodectus.py,sha256=1K9yUUYtzRJ2c3unrYIUaA8nE--Zoqi5pjXY7t7t1qg,7751
|
|
47
|
+
cape_parsers/CAPE/core/NitroBunnyDownloader.py,sha256=2HD7UOqZ8gtVxwR84OMCB4NSv0VqPAEMONuDSEgtDqk,4627
|
|
44
48
|
cape_parsers/CAPE/core/Oyster.py,sha256=QStBScevJuLyd5d4Rw093SxTlbRG1LFkDwYgmjZx-EQ,4881
|
|
45
49
|
cape_parsers/CAPE/core/PikaBot.py,sha256=6Q8goXfMsSoU8UkdE9iuZY2KTxX_AmWhH1szke_HfWA,5280
|
|
46
50
|
cape_parsers/CAPE/core/PlugX.py,sha256=lGwr1T3mttG6CTbZCj_Cf5HnOad60A3LP264jlCsGsc,13192
|
|
@@ -49,7 +53,7 @@ cape_parsers/CAPE/core/Quickbind.py,sha256=5A077RFQQOL8dtr2Q9vmlTKsWk96JkRWuHGse
|
|
|
49
53
|
cape_parsers/CAPE/core/README.md,sha256=Zd84WEUj9NzKzGnVZV1jx6gMiEOtz01m32B7xEuS91k,17
|
|
50
54
|
cape_parsers/CAPE/core/RedLine.py,sha256=bZeKLvxaS6HDpWY4RDXtSEBt93qTNzZG5iE6FNS0dOY,5734
|
|
51
55
|
cape_parsers/CAPE/core/Remcos.py,sha256=MIpO2FwehBGIhO7hS0TT2hdDsgvxlI5ps4rAwyFwdTY,9483
|
|
52
|
-
cape_parsers/CAPE/core/Rhadamanthys.py,sha256=
|
|
56
|
+
cape_parsers/CAPE/core/Rhadamanthys.py,sha256=kvkNj_Ydv_ewjctBNyBvJk31nRnXLl6pU18Ihq9bTXE,11259
|
|
53
57
|
cape_parsers/CAPE/core/SmokeLoader.py,sha256=ruQ_GDiZvqtGxUTbN2N6fajUYWkIylFTvMXijgZ8L20,3890
|
|
54
58
|
cape_parsers/CAPE/core/Socks5Systemz.py,sha256=jSt6QejL5K99dIB3qdItvUHL28w6N60xuwc8EQHM5Mk,783
|
|
55
59
|
cape_parsers/CAPE/core/SquirrelWaffle.py,sha256=UMha7l60fL64VPHxueFUnCEGaO-CXau5ftEyK-Wv__o,3308
|
|
@@ -107,7 +111,7 @@ cape_parsers/utils/blzpack_lib.so,sha256=5PJtnggw8fV5q4DlhwMJk4ZadvC3fFTsVTNZKvE
|
|
|
107
111
|
cape_parsers/utils/dotnet_utils.py,sha256=pzQGbCqccz7DRv8T_i1JURlrKDIlDT2axxViiFF9hsU,1672
|
|
108
112
|
cape_parsers/utils/lznt1.py,sha256=X-BmJtP6AwYSl0ORg5dfSt-NIuXbHrtCO5kUaaJI2C8,4066
|
|
109
113
|
cape_parsers/utils/strings.py,sha256=a-nbvP9jYST7b6t_H37Ype-fK2jEmQr-wMF5a4i04e4,3062
|
|
110
|
-
cape_parsers-0.1.
|
|
111
|
-
cape_parsers-0.1.
|
|
112
|
-
cape_parsers-0.1.
|
|
113
|
-
cape_parsers-0.1.
|
|
114
|
+
cape_parsers-0.1.54.dist-info/METADATA,sha256=LH-LzvpGEEy_jgRIxzLAAvpmyqZAbL6f934-B4aEkGI,1826
|
|
115
|
+
cape_parsers-0.1.54.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
|
|
116
|
+
cape_parsers-0.1.54.dist-info/licenses/LICENSE,sha256=88c01_HLG8WPj7R7aU_b-O-UoF38vrrifvcko4KDxcE,1069
|
|
117
|
+
cape_parsers-0.1.54.dist-info/RECORD,,
|
|
File without changes
|