CAPE-parsers 0.1.45__py3-none-any.whl → 0.1.46__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 +18 -9
- 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 +2 -1
- 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 +1 -1
- cape_parsers/CAPE/community/Lumma.py +19 -15
- 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 +29 -16
- cape_parsers/CAPE/community/SparkRAT.py +3 -1
- cape_parsers/CAPE/community/Stealc.py +86 -64
- cape_parsers/CAPE/community/VenomRAT.py +4 -2
- cape_parsers/CAPE/community/XWorm.py +4 -2
- cape_parsers/CAPE/community/XenoRAT.py +4 -2
- cape_parsers/CAPE/community/monsterv2.py +96 -0
- cape_parsers/CAPE/core/AdaptixBeacon.py +7 -5
- 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 +29 -17
- 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 +10 -7
- 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 +58 -50
- cape_parsers/CAPE/core/Rhadamanthys.py +18 -8
- 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 +13 -4
- 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.45.dist-info → cape_parsers-0.1.46.dist-info}/METADATA +20 -1
- cape_parsers-0.1.46.dist-info/RECORD +112 -0
- cape_parsers/CAPE/community/BlackNix.py +0 -57
- cape_parsers/CAPE/core/Stealc.py +0 -21
- cape_parsers-0.1.45.dist-info/RECORD +0 -112
- /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.45.dist-info → cape_parsers-0.1.46.dist-info}/LICENSE +0 -0
- {cape_parsers-0.1.45.dist-info → cape_parsers-0.1.46.dist-info}/WHEEL +0 -0
|
@@ -6,7 +6,8 @@ except ImportError as e:
|
|
|
6
6
|
print(f"Problem to import extract_strings: {e}")
|
|
7
7
|
|
|
8
8
|
|
|
9
|
-
def extract_config(data):
|
|
9
|
+
def extract_config(data: bytes):
|
|
10
|
+
config = {}
|
|
10
11
|
config_dict = {}
|
|
11
12
|
with suppress(Exception):
|
|
12
13
|
if data[:2] == b"MZ":
|
|
@@ -22,20 +23,21 @@ def extract_config(data):
|
|
|
22
23
|
# Data Exfiltration via Telegram
|
|
23
24
|
if "api.telegram.org" in lines[base + x]:
|
|
24
25
|
config_dict["Protocol"] = "Telegram"
|
|
25
|
-
|
|
26
|
+
config["CNCs"] = lines[base + x]
|
|
26
27
|
config_dict["Password"] = lines[base + x + 1]
|
|
27
28
|
break
|
|
28
29
|
# Data Exfiltration via Discord
|
|
29
30
|
elif "discord" in lines[base + x]:
|
|
30
31
|
config_dict["Protocol"] = "Discord"
|
|
31
|
-
|
|
32
|
+
config["CNCs"] = [lines[base + x]]
|
|
32
33
|
break
|
|
33
34
|
# Data Exfiltration via FTP
|
|
34
35
|
elif "ftp:" in lines[base + x]:
|
|
35
36
|
config_dict["Protocol"] = "FTP"
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
hostname = lines[base + x]
|
|
38
|
+
username = lines[base + x + 1]
|
|
39
|
+
password = lines[base + x + 2]
|
|
40
|
+
config["CNCs"] = [f"ftp://{username}:{password}@{hostname}"]
|
|
39
41
|
break
|
|
40
42
|
# Data Exfiltration via SMTP
|
|
41
43
|
elif "@" in lines[base + x]:
|
|
@@ -45,7 +47,7 @@ def extract_config(data):
|
|
|
45
47
|
config_dict["Port"] = lines[base + x - 2]
|
|
46
48
|
elif lines[base + x - 2] in {"true", "false"} and lines[base + x - 3].isdigit() and len(lines[base + x - 3]) <= 5:
|
|
47
49
|
config_dict["Port"] = lines[base + x - 3]
|
|
48
|
-
config_dict["
|
|
50
|
+
config_dict["CNCs"] = [lines[base + +x - 1]]
|
|
49
51
|
config_dict["Username"] = lines[base + x]
|
|
50
52
|
config_dict["Password"] = lines[base + x + 1]
|
|
51
53
|
if "@" in lines[base + x + 2]:
|
|
@@ -72,6 +74,13 @@ def extract_config(data):
|
|
|
72
74
|
for x in range(1, 8):
|
|
73
75
|
if any(s in lines[base + index + x] for s in temp_match):
|
|
74
76
|
config_dict["Protocol"] = "HTTP(S)"
|
|
75
|
-
|
|
77
|
+
config["CNCs"] = lines[base + index + x]
|
|
76
78
|
break
|
|
77
|
-
|
|
79
|
+
if config or config_dict:
|
|
80
|
+
return config.setdefault("raw", config_dict)
|
|
81
|
+
|
|
82
|
+
if __name__ == "__main__":
|
|
83
|
+
import sys
|
|
84
|
+
|
|
85
|
+
with open(sys.argv[1], "rb") as f:
|
|
86
|
+
print(extract_config(f.read()))
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import struct
|
|
2
2
|
import pefile
|
|
3
3
|
import yara
|
|
4
|
-
|
|
4
|
+
from contextlib import suppress
|
|
5
5
|
|
|
6
6
|
# Hash = 69ba4e2995d6b11bb319d7373d150560ea295c02773fe5aa9c729bfd2c334e1e
|
|
7
7
|
|
|
@@ -46,10 +46,10 @@ def xor_data(data, key):
|
|
|
46
46
|
|
|
47
47
|
|
|
48
48
|
def extract_config(data):
|
|
49
|
-
|
|
49
|
+
config = {}
|
|
50
50
|
|
|
51
51
|
# Attempt to extract via old method
|
|
52
|
-
|
|
52
|
+
with suppress(Exception):
|
|
53
53
|
domain = ""
|
|
54
54
|
uri = ""
|
|
55
55
|
lines = data.decode().split("\n")
|
|
@@ -59,14 +59,12 @@ def extract_config(data):
|
|
|
59
59
|
if line.startswith("/") and line[-4] == ".":
|
|
60
60
|
uri = line
|
|
61
61
|
if domain and uri:
|
|
62
|
-
|
|
63
|
-
return
|
|
64
|
-
except Exception:
|
|
65
|
-
pass
|
|
62
|
+
config.setdefault("CNCs", []).append(f"{domain}{uri}")
|
|
63
|
+
return config
|
|
66
64
|
|
|
67
65
|
# Try with new method
|
|
68
66
|
|
|
69
|
-
#config_dict["Strings"] = []
|
|
67
|
+
# config_dict["Strings"] = []
|
|
70
68
|
pe = pefile.PE(data=data, fast_load=True)
|
|
71
69
|
image_base = pe.OPTIONAL_HEADER.ImageBase
|
|
72
70
|
domain = ""
|
|
@@ -84,17 +82,17 @@ def extract_config(data):
|
|
|
84
82
|
if rule_str_name.startswith("$decode"):
|
|
85
83
|
key_rva = data[str_decode_offset + 3 : str_decode_offset + 7]
|
|
86
84
|
encoded_str_rva = data[str_decode_offset + 8 : str_decode_offset + 12]
|
|
87
|
-
#dword_rva = data[str_decode_offset + 21 : str_decode_offset + 25]
|
|
85
|
+
# dword_rva = data[str_decode_offset + 21 : str_decode_offset + 25]
|
|
88
86
|
|
|
89
87
|
key_offset = pe.get_offset_from_rva(struct.unpack("i", key_rva)[0] - image_base)
|
|
90
88
|
encoded_str_offset = pe.get_offset_from_rva(struct.unpack("i", encoded_str_rva)[0] - image_base)
|
|
91
|
-
#dword_offset = struct.unpack("i", dword_rva)[0]
|
|
92
|
-
#dword_name = f"dword_{hex(dword_offset)[2:]}"
|
|
89
|
+
# dword_offset = struct.unpack("i", dword_rva)[0]
|
|
90
|
+
# dword_name = f"dword_{hex(dword_offset)[2:]}"
|
|
93
91
|
|
|
94
92
|
key = data[key_offset : key_offset + str_size]
|
|
95
93
|
encoded_str = data[encoded_str_offset : encoded_str_offset + str_size]
|
|
96
94
|
decoded_str = xor_data(encoded_str, key).decode()
|
|
97
|
-
#config_dict["Strings"].append({dword_name : decoded_str})
|
|
95
|
+
# config_dict["Strings"].append({dword_name : decoded_str})
|
|
98
96
|
|
|
99
97
|
if last_str in ("http://", "https://"):
|
|
100
98
|
domain += decoded_str
|
|
@@ -114,12 +112,12 @@ def extract_config(data):
|
|
|
114
112
|
continue
|
|
115
113
|
|
|
116
114
|
if domain and uri:
|
|
117
|
-
|
|
115
|
+
config.setdefault("CNCs", []).append(f"{domain}{uri}")
|
|
118
116
|
|
|
119
117
|
if botnet_id:
|
|
120
|
-
|
|
118
|
+
config.setdefault("botnet", botnet_id)
|
|
121
119
|
|
|
122
|
-
return
|
|
120
|
+
return config
|
|
123
121
|
|
|
124
122
|
|
|
125
123
|
if __name__ == "__main__":
|
|
@@ -5,10 +5,10 @@ import os
|
|
|
5
5
|
from rat_king_parser.rkp import RATConfigParser
|
|
6
6
|
|
|
7
7
|
HAVE_ASYNCRAT_COMMON = False
|
|
8
|
-
module_file_path =
|
|
8
|
+
module_file_path = "/opt/CAPEv2/data/asyncrat_common.py"
|
|
9
9
|
if os.path.exists(module_file_path):
|
|
10
10
|
try:
|
|
11
|
-
module_name = os.path.basename(module_file_path).replace(
|
|
11
|
+
module_name = os.path.basename(module_file_path).replace(".py", "")
|
|
12
12
|
spec = importlib.util.spec_from_file_location(module_name, module_file_path)
|
|
13
13
|
asyncrat_common = importlib.util.module_from_spec(spec)
|
|
14
14
|
sys.modules[module_name] = asyncrat_common
|
|
@@ -17,6 +17,7 @@ if os.path.exists(module_file_path):
|
|
|
17
17
|
except Exception as e:
|
|
18
18
|
print("Error loading asyncrat_common.py", e)
|
|
19
19
|
|
|
20
|
+
|
|
20
21
|
def extract_config(data: bytes):
|
|
21
22
|
config = RATConfigParser(data=data, remap_config=True).report.get("config", {})
|
|
22
23
|
if config and HAVE_ASYNCRAT_COMMON:
|
|
@@ -24,6 +25,7 @@ def extract_config(data: bytes):
|
|
|
24
25
|
|
|
25
26
|
return config
|
|
26
27
|
|
|
28
|
+
|
|
27
29
|
if __name__ == "__main__":
|
|
28
30
|
data = open(sys.argv[1], "rb").read()
|
|
29
31
|
print(extract_config(data))
|
|
@@ -32,9 +32,12 @@ 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
|
-
|
|
36
|
-
|
|
37
|
-
config_dict[
|
|
35
|
+
config_dict["CNCs"] = [value]
|
|
36
|
+
elif key == "BuildID":
|
|
37
|
+
config_dict["build"] = value
|
|
38
|
+
else:
|
|
39
|
+
if value:
|
|
40
|
+
config_dict.setdefault("raw", {})[key] = value
|
|
38
41
|
|
|
39
42
|
grabber_found = False
|
|
40
43
|
|
|
@@ -47,13 +50,13 @@ def extract_config(data):
|
|
|
47
50
|
data_dict = json.loads(decoded_str)
|
|
48
51
|
for elem in data_dict:
|
|
49
52
|
if elem["Method"] == "DW":
|
|
50
|
-
config_dict["Loader module"] = elem
|
|
53
|
+
config_dict.setdefault("raw", {})["Loader module"] = elem
|
|
51
54
|
|
|
52
55
|
if b"PS" in decoded_str:
|
|
53
56
|
data_dict = json.loads(decoded_str)
|
|
54
57
|
for elem in data_dict:
|
|
55
58
|
if elem["Method"] == "PS":
|
|
56
|
-
config_dict["PowerShell module"] = elem
|
|
59
|
+
config_dict.setdefault("raw", {})["PowerShell module"] = elem
|
|
57
60
|
|
|
58
61
|
if b"Path" in decoded_str:
|
|
59
62
|
grabber_found = True
|
|
@@ -68,6 +71,6 @@ def extract_config(data):
|
|
|
68
71
|
|
|
69
72
|
if not grabber_found:
|
|
70
73
|
grabber_found = True
|
|
71
|
-
config_dict["Grabber"] = cleanup_str
|
|
74
|
+
config_dict.setdefault("raw", {})["Grabber"] = cleanup_str
|
|
72
75
|
|
|
73
76
|
return config_dict
|
|
@@ -158,22 +158,22 @@ def extract_config(filebuf):
|
|
|
158
158
|
if dec:
|
|
159
159
|
ver = re.findall(r"^(\d+\.\d+)$", dec)
|
|
160
160
|
if ver:
|
|
161
|
-
cfg["
|
|
161
|
+
cfg["version"] = ver[0]
|
|
162
162
|
|
|
163
163
|
data = data_sections[0].get_data()
|
|
164
164
|
items = data.split(b"\x00")
|
|
165
165
|
|
|
166
166
|
with suppress(IndexError, UnicodeDecodeError, ValueError):
|
|
167
|
-
cfg["Unknown 1"] = decode_string(items[0], sbox).decode("utf8")
|
|
168
|
-
cfg["Unknown 2"] = decode_string(items[8], sbox).decode("utf8")
|
|
167
|
+
cfg.setdefault("raw", {})["Unknown 1"] = decode_string(items[0], sbox).decode("utf8")
|
|
168
|
+
cfg.setdefault("raw", {})["Unknown 2"] = decode_string(items[8], sbox).decode("utf8")
|
|
169
169
|
c2_dec = decode_string(items[10], sbox).decode("utf8")
|
|
170
170
|
if "|" in c2_dec:
|
|
171
171
|
c2_dec = c2_dec.split("|")
|
|
172
|
-
cfg["
|
|
173
|
-
if float(cfg["
|
|
174
|
-
cfg["
|
|
172
|
+
cfg["CNCs"] = c2_dec
|
|
173
|
+
if float(cfg["version"]) < 1.7:
|
|
174
|
+
cfg["campaign"] = decode_string(items[276], sbox).decode("utf8")
|
|
175
175
|
else:
|
|
176
|
-
cfg["
|
|
176
|
+
cfg["campaign"] = decode_string(items[25], sbox).decode("utf8")
|
|
177
177
|
|
|
178
178
|
return cfg
|
|
179
179
|
|
|
@@ -5,10 +5,10 @@ import os
|
|
|
5
5
|
from rat_king_parser.rkp import RATConfigParser
|
|
6
6
|
|
|
7
7
|
HAVE_ASYNCRAT_COMMON = False
|
|
8
|
-
module_file_path =
|
|
8
|
+
module_file_path = "/opt/CAPEv2/data/asyncrat_common.py"
|
|
9
9
|
if os.path.exists(module_file_path):
|
|
10
10
|
try:
|
|
11
|
-
module_name = os.path.basename(module_file_path).replace(
|
|
11
|
+
module_name = os.path.basename(module_file_path).replace(".py", "")
|
|
12
12
|
spec = importlib.util.spec_from_file_location(module_name, module_file_path)
|
|
13
13
|
asyncrat_common = importlib.util.module_from_spec(spec)
|
|
14
14
|
sys.modules[module_name] = asyncrat_common
|
|
@@ -17,6 +17,7 @@ if os.path.exists(module_file_path):
|
|
|
17
17
|
except Exception as e:
|
|
18
18
|
print("Error loading asyncrat_common.py", e)
|
|
19
19
|
|
|
20
|
+
|
|
20
21
|
def extract_config(data: bytes):
|
|
21
22
|
config = RATConfigParser(data=data, remap_config=True).report.get("config", {})
|
|
22
23
|
if config and HAVE_ASYNCRAT_COMMON:
|
|
@@ -24,6 +25,7 @@ def extract_config(data: bytes):
|
|
|
24
25
|
|
|
25
26
|
return config
|
|
26
27
|
|
|
28
|
+
|
|
27
29
|
if __name__ == "__main__":
|
|
28
30
|
data = open(sys.argv[1], "rb").read()
|
|
29
31
|
print(extract_config(data))
|
|
@@ -35,10 +35,8 @@ def extract_config(memdump_path, read=False):
|
|
|
35
35
|
if buf and len(buf[0]) > 200:
|
|
36
36
|
cData = buf[0][200:]
|
|
37
37
|
"""
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
"downloads": [],
|
|
41
|
-
}
|
|
38
|
+
config = {}
|
|
39
|
+
artifacts_raw = {}
|
|
42
40
|
|
|
43
41
|
start = F.find(b"YUIPWDFILE0YUIPKDFILE0YUICRYPTED0YUI1.0")
|
|
44
42
|
if start:
|
|
@@ -53,15 +51,16 @@ def extract_config(memdump_path, read=False):
|
|
|
53
51
|
# url = self._check_valid_url(url)
|
|
54
52
|
if url is None:
|
|
55
53
|
continue
|
|
54
|
+
url = url.lower()
|
|
56
55
|
if gate_url.match(url):
|
|
57
|
-
|
|
56
|
+
config.setdefault("CNCs", []).append(url.decode())
|
|
58
57
|
elif exe_url.match(url) or dll_url.match(url):
|
|
59
|
-
artifacts_raw["downloads"].append(url.
|
|
58
|
+
artifacts_raw["downloads"].append(url.decode())
|
|
60
59
|
except Exception as e:
|
|
61
60
|
print(e, sys.exc_info(), "PONY")
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
return
|
|
61
|
+
config["CNCs"] = list(set(config["controllers"]))
|
|
62
|
+
config.setdefault("raw", {})["downloads"] = list(set(artifacts_raw["downloads"]))
|
|
63
|
+
return config
|
|
65
64
|
|
|
66
65
|
|
|
67
66
|
if __name__ == "__main__":
|
|
@@ -89,7 +89,7 @@ def xor_data(data, key):
|
|
|
89
89
|
|
|
90
90
|
|
|
91
91
|
def extract_config(data):
|
|
92
|
-
|
|
92
|
+
config = {}
|
|
93
93
|
|
|
94
94
|
xor_key = b""
|
|
95
95
|
encoded_payload = b""
|
|
@@ -119,9 +119,9 @@ 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
|
|
|
122
|
-
|
|
122
|
+
config["CNCs"] = find_c2(decoded_payload)
|
|
123
123
|
|
|
124
|
-
return
|
|
124
|
+
return config
|
|
125
125
|
|
|
126
126
|
|
|
127
127
|
if __name__ == "__main__":
|
|
@@ -287,7 +287,7 @@ def get_build_id_new(data):
|
|
|
287
287
|
|
|
288
288
|
|
|
289
289
|
def extract_config(data):
|
|
290
|
-
|
|
290
|
+
config = {}
|
|
291
291
|
|
|
292
292
|
# try to load as a PE
|
|
293
293
|
pe = None
|
|
@@ -318,18 +318,18 @@ def extract_config(data):
|
|
|
318
318
|
decoded_c2 = chacha20_xor(encrypted_string, key, nonce, counter).split(b"\x00", 1)[0]
|
|
319
319
|
if contains_non_printable(decoded_c2):
|
|
320
320
|
break
|
|
321
|
-
|
|
321
|
+
config.setdefault("CNCs", []).append(decoded_c2.decode())
|
|
322
322
|
encrypted_strings_offset = encrypted_strings_offset + step_size
|
|
323
323
|
counter += 2
|
|
324
324
|
|
|
325
|
-
if
|
|
325
|
+
if config.get("CNCs"):
|
|
326
326
|
# If found C2 servers try to find build ID
|
|
327
327
|
build_id = get_build_id_new(data)
|
|
328
328
|
if build_id:
|
|
329
|
-
|
|
329
|
+
config["build"] = build_id
|
|
330
330
|
|
|
331
331
|
# If no C2s try with the version after Jan 21, 2025
|
|
332
|
-
if "
|
|
332
|
+
if "CNCs" not in config:
|
|
333
333
|
offset = yara_scan(data, RULE_SOURCE_LUMMA)
|
|
334
334
|
if offset:
|
|
335
335
|
key = data[offset + 16 : offset + 48]
|
|
@@ -351,20 +351,20 @@ def extract_config(data):
|
|
|
351
351
|
decrypted = chacha20_xor(c2_encrypted, key, nonce, counter)
|
|
352
352
|
c2 = extract_c2_domain(decrypted)
|
|
353
353
|
if c2 is not None and len(c2) > 10:
|
|
354
|
-
|
|
354
|
+
config["CNCs"].append(c2.decode())
|
|
355
355
|
break
|
|
356
356
|
|
|
357
357
|
except Exception:
|
|
358
358
|
continue
|
|
359
359
|
|
|
360
|
-
if "
|
|
360
|
+
if "CNCs" in config and config["CNCs"] and pe is not None:
|
|
361
361
|
# If found C2 servers try to find build ID
|
|
362
362
|
build_id = get_build_id(pe, data)
|
|
363
363
|
if build_id:
|
|
364
|
-
|
|
364
|
+
config["build"] = build_id
|
|
365
365
|
|
|
366
366
|
# If no C2s try with version prior to Jan 21, 2025
|
|
367
|
-
if "
|
|
367
|
+
if "CNCs" not in config:
|
|
368
368
|
try:
|
|
369
369
|
if pe is not None:
|
|
370
370
|
rdata = get_rdata(pe, data)
|
|
@@ -384,20 +384,24 @@ def extract_config(data):
|
|
|
384
384
|
decoded_c2 = xor_data(encoded_c2, xor_key)
|
|
385
385
|
|
|
386
386
|
if not contains_non_printable(decoded_c2):
|
|
387
|
-
|
|
388
|
-
except Exception:
|
|
387
|
+
config.setdefault("CNCs", []).append(decoded_c2.decode())
|
|
388
|
+
except Exception as e:
|
|
389
|
+
print(e)
|
|
389
390
|
continue
|
|
390
391
|
|
|
391
|
-
except Exception:
|
|
392
|
+
except Exception as e:
|
|
393
|
+
print(e)
|
|
392
394
|
return
|
|
393
395
|
|
|
394
|
-
if "
|
|
396
|
+
if "CNCs" in config and pe is not None:
|
|
395
397
|
# If found C2 servers try to find build ID
|
|
396
398
|
build_id = get_build_id(pe, data)
|
|
397
399
|
if build_id:
|
|
398
|
-
|
|
400
|
+
config["build"] = build_id
|
|
399
401
|
|
|
400
|
-
|
|
402
|
+
print(config)
|
|
403
|
+
if config:
|
|
404
|
+
return config
|
|
401
405
|
|
|
402
406
|
|
|
403
407
|
if __name__ == "__main__":
|
|
@@ -174,21 +174,21 @@ def extract_config(filebuf):
|
|
|
174
174
|
pass
|
|
175
175
|
elif DataType.DATETIME == param["type"]:
|
|
176
176
|
dt = param["value"]
|
|
177
|
-
config_dict[item_name] = dt.strftime("%Y-%m-%d %H:%M:%S.%f")
|
|
177
|
+
config_dict.setdefault("raw", {})[item_name] = dt.strftime("%Y-%m-%d %H:%M:%S.%f")
|
|
178
178
|
else:
|
|
179
|
-
config_dict[item_name] = str(param["value"])
|
|
179
|
+
config_dict.setdefault("raw", {})[item_name] = str(param["value"])
|
|
180
180
|
except Exception as e:
|
|
181
181
|
log.error("nanocore error: %s", e)
|
|
182
182
|
|
|
183
183
|
cncs = []
|
|
184
184
|
|
|
185
|
-
if config_dict.get("PrimaryConnectionHost"):
|
|
186
|
-
cncs.append(config_dict["PrimaryConnectionHost"])
|
|
187
|
-
if config_dict.get("PrimaryConnectionHost"):
|
|
188
|
-
cncs.append(config_dict["BackupConnectionHost"])
|
|
189
|
-
if config_dict.get("ConnectionPort") and cncs:
|
|
190
|
-
port = config_dict["ConnectionPort"]
|
|
191
|
-
config_dict["
|
|
185
|
+
if config_dict.get("raw", {}).get("PrimaryConnectionHost"):
|
|
186
|
+
cncs.append(config_dict["raw"]["PrimaryConnectionHost"])
|
|
187
|
+
if config_dict.get("raw", {}).get("PrimaryConnectionHost"):
|
|
188
|
+
cncs.append(config_dict["raw"]["BackupConnectionHost"])
|
|
189
|
+
if config_dict.get("raw", {}).get("ConnectionPort") and cncs:
|
|
190
|
+
port = config_dict["raw"]["ConnectionPort"]
|
|
191
|
+
config_dict["CNCs"] = [f"{cnc}:{port}" for cnc in cncs]
|
|
192
192
|
return config_dict
|
|
193
193
|
|
|
194
194
|
|
|
@@ -176,11 +176,11 @@ def extract_config(data):
|
|
|
176
176
|
config = get_clean_config(config_dict)
|
|
177
177
|
if config:
|
|
178
178
|
if config.get("domain") and config.get("port"):
|
|
179
|
-
conf["
|
|
180
|
-
|
|
179
|
+
conf["CNCs"] = [f"{config['domain']}:{config['port']}"]
|
|
180
|
+
|
|
181
181
|
if config.get("campaign_id"):
|
|
182
|
-
conf["campaign
|
|
183
|
-
|
|
182
|
+
conf["campaign"] = config["campaign_id"]
|
|
183
|
+
|
|
184
184
|
if config.get("version"):
|
|
185
185
|
conf["version"] = config["version"]
|
|
186
186
|
|
|
@@ -188,4 +188,6 @@ def extract_config(data):
|
|
|
188
188
|
value_data = inst_.split(".")[-1].strip()
|
|
189
189
|
config_field_name, str_list = check_next_inst(pe, body, DnfileParse, index)
|
|
190
190
|
config_dict[config_field_name] = value_data
|
|
191
|
+
if config_dict:
|
|
192
|
+
config_dict = {"raw": config_dict}
|
|
191
193
|
return config_dict
|
|
@@ -38,7 +38,7 @@ def handle_plain(dotnet_file, c2_type, user_strings):
|
|
|
38
38
|
if c2_type == "Telegram":
|
|
39
39
|
token = dotnet_file.net.user_strings.get(user_strings_list[15]).value.__str__()
|
|
40
40
|
chat_id = dotnet_file.net.user_strings.get(user_strings_list[16]).value.__str__()
|
|
41
|
-
return {"Type": "Telegram", "
|
|
41
|
+
return {"raw": {"Type": "Telegram"}, "CNCs": f"https://api.telegram.org/bot{token}/sendMessage?chat_id={chat_id}"}
|
|
42
42
|
elif c2_type == "SMTP":
|
|
43
43
|
smtp_from = dotnet_file.net.user_strings.get(user_strings_list[7]).value.__str__()
|
|
44
44
|
smtp_password = dotnet_file.net.user_strings.get(user_strings_list[8]).value.__str__()
|
|
@@ -46,18 +46,26 @@ def handle_plain(dotnet_file, c2_type, user_strings):
|
|
|
46
46
|
smtp_to = dotnet_file.net.user_strings.get(user_strings_list[10]).value.__str__()
|
|
47
47
|
smtp_port = dotnet_file.net.user_strings.get(user_strings_list[11]).value.__str__()
|
|
48
48
|
return {
|
|
49
|
-
"
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
49
|
+
"raw": {
|
|
50
|
+
"Type": "SMTP",
|
|
51
|
+
"Host": smtp_host,
|
|
52
|
+
"Port": smtp_port,
|
|
53
|
+
"From Address": smtp_from,
|
|
54
|
+
"To Address": smtp_to,
|
|
55
|
+
"Password": smtp_password,
|
|
56
|
+
},
|
|
57
|
+
"CNCs": [f"smtp://{smtp_host}:{smtp_port}"]
|
|
55
58
|
}
|
|
56
59
|
elif c2_type == "FTP":
|
|
57
60
|
ftp_username = dotnet_file.net.user_strings.get(user_strings_list[12]).value.__str__()
|
|
58
61
|
ftp_password = dotnet_file.net.user_strings.get(user_strings_list[13]).value.__str__()
|
|
59
62
|
ftp_host = dotnet_file.net.user_strings.get(user_strings_list[14]).value.__str__()
|
|
60
|
-
return {
|
|
63
|
+
return {
|
|
64
|
+
"raw": {
|
|
65
|
+
"Type": "FTP", "Host": ftp_host, "Username": ftp_username, "Password": ftp_password},
|
|
66
|
+
"CNCs": [f"ftp://{ftp_username}:{ftp_password}@{ftp_host}"]
|
|
67
|
+
}
|
|
68
|
+
|
|
61
69
|
|
|
62
70
|
|
|
63
71
|
def handle_encrypted(dotnet_file, data, c2_type, user_strings):
|
|
@@ -98,20 +106,25 @@ def handle_encrypted(dotnet_file, data, c2_type, user_strings):
|
|
|
98
106
|
if decrypted_strings:
|
|
99
107
|
if c2_type == "Telegram":
|
|
100
108
|
token, chat_id = decrypted_strings
|
|
101
|
-
config_dict = {"Type": "Telegram", "
|
|
109
|
+
config_dict = {"raw": {"Type": "Telegram"}, "CNCs": f"https://api.telegram.org/bot{token}/sendMessage?chat_id={chat_id}"}
|
|
102
110
|
elif c2_type == "SMTP":
|
|
103
111
|
smtp_from, smtp_password, smtp_host, smtp_to, smtp_port = decrypted_strings
|
|
104
112
|
config_dict = {
|
|
105
|
-
"
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
113
|
+
"raw": {
|
|
114
|
+
"Type": "SMTP",
|
|
115
|
+
"Host": smtp_host,
|
|
116
|
+
"Port": smtp_port,
|
|
117
|
+
"From Address": smtp_from,
|
|
118
|
+
"To Address": smtp_to,
|
|
119
|
+
"Password": smtp_password,
|
|
120
|
+
}
|
|
111
121
|
}
|
|
112
122
|
elif c2_type == "FTP":
|
|
113
123
|
ftp_username, ftp_password, ftp_host = decrypted_strings
|
|
114
|
-
config_dict = {
|
|
124
|
+
config_dict = {
|
|
125
|
+
"raw": {"Type": "FTP", "Host": ftp_host, "Username": ftp_username, "Password": ftp_password},
|
|
126
|
+
"CNCs": [f"ftp://{ftp_username}:{ftp_password}@{ftp_host}"]
|
|
127
|
+
}
|
|
115
128
|
return config_dict
|
|
116
129
|
|
|
117
130
|
|
|
@@ -59,7 +59,9 @@ def extract_config(data):
|
|
|
59
59
|
key = f.read(16)
|
|
60
60
|
iv = f.read(16)
|
|
61
61
|
enc_data = f.read(data_len - 32)
|
|
62
|
-
|
|
62
|
+
config = decrypt_config(enc_data, key, iv)
|
|
63
|
+
if config:
|
|
64
|
+
return {"raw": config}
|
|
63
65
|
except Exception as e:
|
|
64
66
|
log.error("Configuration decryption failed: %s", e)
|
|
65
67
|
return {}
|