CAPE-parsers 0.1.44__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 +49 -36
- 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.44.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.44.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.44.dist-info → cape_parsers-0.1.46.dist-info}/LICENSE +0 -0
- {cape_parsers-0.1.44.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__":
|
|
@@ -75,7 +75,6 @@ RULE_SOURCE_LUMMA_NEW_ENCRYPTED_C2 = """rule LummaConfigNewEncryptedStrings
|
|
|
75
75
|
}"""
|
|
76
76
|
|
|
77
77
|
|
|
78
|
-
|
|
79
78
|
def yara_scan_generator(raw_data, rule_source):
|
|
80
79
|
yara_rules = yara.compile(source=rule_source)
|
|
81
80
|
matches = yara_rules.match(data=raw_data)
|
|
@@ -198,10 +197,22 @@ def chacha20_block(key, nonce, blocknum):
|
|
|
198
197
|
nonce_words = words_from_bytes(nonce)
|
|
199
198
|
|
|
200
199
|
original_block = [
|
|
201
|
-
constant_words[0],
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
200
|
+
constant_words[0],
|
|
201
|
+
constant_words[1],
|
|
202
|
+
constant_words[2],
|
|
203
|
+
constant_words[3],
|
|
204
|
+
key_words[0],
|
|
205
|
+
key_words[1],
|
|
206
|
+
key_words[2],
|
|
207
|
+
key_words[3],
|
|
208
|
+
key_words[4],
|
|
209
|
+
key_words[5],
|
|
210
|
+
key_words[6],
|
|
211
|
+
key_words[7],
|
|
212
|
+
mask32(blocknum),
|
|
213
|
+
nonce_words[0],
|
|
214
|
+
nonce_words[1],
|
|
215
|
+
nonce_words[2],
|
|
205
216
|
]
|
|
206
217
|
|
|
207
218
|
permuted_block = list(original_block)
|
|
@@ -241,7 +252,7 @@ def extract_c2_domain(data):
|
|
|
241
252
|
|
|
242
253
|
|
|
243
254
|
def find_encrypted_c2_blocks(data):
|
|
244
|
-
pattern = rb
|
|
255
|
+
pattern = rb"(.{128})\x00"
|
|
245
256
|
for match in re.findall(pattern, data, re.DOTALL):
|
|
246
257
|
yield match
|
|
247
258
|
|
|
@@ -251,9 +262,9 @@ def get_build_id(pe, data):
|
|
|
251
262
|
image_base = pe.OPTIONAL_HEADER.ImageBase
|
|
252
263
|
for offset in yara_scan_generator(data, RULE_SOURCE_BUILD_ID):
|
|
253
264
|
try:
|
|
254
|
-
build_id_data_rva = struct.unpack(
|
|
265
|
+
build_id_data_rva = struct.unpack("i", data[offset + 2 : offset + 6])[0]
|
|
255
266
|
build_id_dword_offset = pe.get_offset_from_rva(build_id_data_rva - image_base)
|
|
256
|
-
build_id_dword_rva = struct.unpack(
|
|
267
|
+
build_id_dword_rva = struct.unpack("i", data[build_id_dword_offset : build_id_dword_offset + 4])[0]
|
|
257
268
|
build_id_offset = pe.get_offset_from_rva(build_id_dword_rva - image_base)
|
|
258
269
|
build_id = pe.get_string_from_data(build_id_offset, data)
|
|
259
270
|
if not contains_non_printable(build_id):
|
|
@@ -263,18 +274,20 @@ def get_build_id(pe, data):
|
|
|
263
274
|
continue
|
|
264
275
|
return build_id
|
|
265
276
|
|
|
277
|
+
|
|
266
278
|
def get_build_id_new(data):
|
|
267
279
|
build_id = ""
|
|
268
|
-
pattern = b
|
|
280
|
+
pattern = b"123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ\x00"
|
|
269
281
|
offset = data.find(pattern)
|
|
270
282
|
if offset != -1:
|
|
271
|
-
build_id = data[offset + len(pattern):].split(b
|
|
283
|
+
build_id = data[offset + len(pattern) :].split(b"\x00", 1)[0]
|
|
272
284
|
build_id = build_id.decode()
|
|
273
285
|
|
|
274
286
|
return build_id
|
|
275
287
|
|
|
288
|
+
|
|
276
289
|
def extract_config(data):
|
|
277
|
-
|
|
290
|
+
config = {}
|
|
278
291
|
|
|
279
292
|
# try to load as a PE
|
|
280
293
|
pe = None
|
|
@@ -287,37 +300,36 @@ def extract_config(data):
|
|
|
287
300
|
key = None
|
|
288
301
|
nonce = None
|
|
289
302
|
for offset in yara_scan_generator(data, RULE_SOURCE_LUMMA_NEW_KEYS):
|
|
290
|
-
key_rva = struct.unpack(
|
|
303
|
+
key_rva = struct.unpack("i", data[offset + 1 : offset + 5])[0]
|
|
291
304
|
key_offset = pe.get_offset_from_rva(key_rva - image_base)
|
|
292
305
|
key = data[key_offset : key_offset + 32]
|
|
293
|
-
nonce_rva = struct.unpack(
|
|
306
|
+
nonce_rva = struct.unpack("i", data[offset + 20 : offset + 24])[0]
|
|
294
307
|
nonce_offset = pe.get_offset_from_rva(nonce_rva - image_base)
|
|
295
|
-
nonce = b
|
|
308
|
+
nonce = b"\x00\x00\x00\x00" + data[nonce_offset : nonce_offset + 8]
|
|
296
309
|
|
|
297
310
|
if key and nonce:
|
|
298
311
|
for offset in yara_scan_generator(data, RULE_SOURCE_LUMMA_NEW_ENCRYPTED_C2):
|
|
299
|
-
encrypted_strings_rva = struct.unpack(
|
|
312
|
+
encrypted_strings_rva = struct.unpack("i", data[offset + 5 : offset + 9])[0]
|
|
300
313
|
encrypted_strings_offset = pe.get_offset_from_rva(encrypted_strings_rva - image_base)
|
|
301
314
|
step_size = 0x80
|
|
302
315
|
counter = 2
|
|
303
316
|
for i in range(12):
|
|
304
|
-
encrypted_string = data[encrypted_strings_offset:encrypted_strings_offset+40]
|
|
305
|
-
decoded_c2 = chacha20_xor(encrypted_string, key, nonce, counter).split(b
|
|
317
|
+
encrypted_string = data[encrypted_strings_offset : encrypted_strings_offset + 40]
|
|
318
|
+
decoded_c2 = chacha20_xor(encrypted_string, key, nonce, counter).split(b"\x00", 1)[0]
|
|
306
319
|
if contains_non_printable(decoded_c2):
|
|
307
320
|
break
|
|
308
|
-
|
|
321
|
+
config.setdefault("CNCs", []).append(decoded_c2.decode())
|
|
309
322
|
encrypted_strings_offset = encrypted_strings_offset + step_size
|
|
310
323
|
counter += 2
|
|
311
324
|
|
|
312
|
-
if
|
|
325
|
+
if config.get("CNCs"):
|
|
313
326
|
# If found C2 servers try to find build ID
|
|
314
327
|
build_id = get_build_id_new(data)
|
|
315
328
|
if build_id:
|
|
316
|
-
|
|
317
|
-
|
|
329
|
+
config["build"] = build_id
|
|
318
330
|
|
|
319
331
|
# If no C2s try with the version after Jan 21, 2025
|
|
320
|
-
if not
|
|
332
|
+
if "CNCs" not in config:
|
|
321
333
|
offset = yara_scan(data, RULE_SOURCE_LUMMA)
|
|
322
334
|
if offset:
|
|
323
335
|
key = data[offset + 16 : offset + 48]
|
|
@@ -327,7 +339,7 @@ def extract_config(data):
|
|
|
327
339
|
try:
|
|
328
340
|
start_offset = offset + 56 + (i * 4)
|
|
329
341
|
end_offset = start_offset + 4
|
|
330
|
-
c2_dword_rva = struct.unpack(
|
|
342
|
+
c2_dword_rva = struct.unpack("i", data[start_offset:end_offset])[0]
|
|
331
343
|
if pe:
|
|
332
344
|
c2_dword_offset = pe.get_offset_from_rva(c2_dword_rva - image_base)
|
|
333
345
|
else:
|
|
@@ -339,22 +351,20 @@ def extract_config(data):
|
|
|
339
351
|
decrypted = chacha20_xor(c2_encrypted, key, nonce, counter)
|
|
340
352
|
c2 = extract_c2_domain(decrypted)
|
|
341
353
|
if c2 is not None and len(c2) > 10:
|
|
342
|
-
|
|
354
|
+
config["CNCs"].append(c2.decode())
|
|
343
355
|
break
|
|
344
356
|
|
|
345
357
|
except Exception:
|
|
346
358
|
continue
|
|
347
359
|
|
|
348
|
-
if
|
|
360
|
+
if "CNCs" in config and config["CNCs"] and pe is not None:
|
|
349
361
|
# If found C2 servers try to find build ID
|
|
350
362
|
build_id = get_build_id(pe, data)
|
|
351
363
|
if build_id:
|
|
352
|
-
|
|
353
|
-
|
|
364
|
+
config["build"] = build_id
|
|
354
365
|
|
|
355
366
|
# If no C2s try with version prior to Jan 21, 2025
|
|
356
|
-
if not
|
|
357
|
-
|
|
367
|
+
if "CNCs" not in config:
|
|
358
368
|
try:
|
|
359
369
|
if pe is not None:
|
|
360
370
|
rdata = get_rdata(pe, data)
|
|
@@ -374,21 +384,24 @@ def extract_config(data):
|
|
|
374
384
|
decoded_c2 = xor_data(encoded_c2, xor_key)
|
|
375
385
|
|
|
376
386
|
if not contains_non_printable(decoded_c2):
|
|
377
|
-
|
|
378
|
-
except Exception:
|
|
387
|
+
config.setdefault("CNCs", []).append(decoded_c2.decode())
|
|
388
|
+
except Exception as e:
|
|
389
|
+
print(e)
|
|
379
390
|
continue
|
|
380
391
|
|
|
381
|
-
except Exception:
|
|
392
|
+
except Exception as e:
|
|
393
|
+
print(e)
|
|
382
394
|
return
|
|
383
395
|
|
|
384
|
-
if
|
|
396
|
+
if "CNCs" in config and pe is not None:
|
|
385
397
|
# If found C2 servers try to find build ID
|
|
386
398
|
build_id = get_build_id(pe, data)
|
|
387
399
|
if build_id:
|
|
388
|
-
|
|
389
|
-
|
|
400
|
+
config["build"] = build_id
|
|
390
401
|
|
|
391
|
-
|
|
402
|
+
print(config)
|
|
403
|
+
if config:
|
|
404
|
+
return config
|
|
392
405
|
|
|
393
406
|
|
|
394
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
|