CAPE-parsers 0.1.45__py3-none-any.whl → 0.1.47__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.47.dist-info}/METADATA +20 -1
- cape_parsers-0.1.47.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.47.dist-info}/LICENSE +0 -0
- {cape_parsers-0.1.45.dist-info → cape_parsers-0.1.47.dist-info}/WHEEL +0 -0
|
@@ -1,31 +1,35 @@
|
|
|
1
1
|
import struct
|
|
2
2
|
import pefile
|
|
3
3
|
import yara
|
|
4
|
+
from contextlib import suppress
|
|
4
5
|
|
|
5
6
|
|
|
6
|
-
#
|
|
7
|
+
# V1 hash = 619751f5ed0a9716318092998f2e4561f27f7f429fe6103406ecf16e33837470
|
|
8
|
+
# V2 hash = 2f42dcf05dd87e6352491ff9d4ea3dc3f854df53d548a8da0c323be42df797b6 (32-bit payload)
|
|
9
|
+
# V2 hash = 8301936f439f43579cffe98e11e3224051e2fb890ffe9df680bbbd8db0729387 (64-bit payload)
|
|
7
10
|
|
|
8
|
-
RULE_SOURCE = """
|
|
11
|
+
RULE_SOURCE = """
|
|
12
|
+
rule StealC
|
|
9
13
|
{
|
|
10
14
|
meta:
|
|
11
15
|
author = "Yung Binary"
|
|
12
16
|
strings:
|
|
13
|
-
$decode_1 = {
|
|
14
|
-
|
|
15
|
-
68 ?? ?? ?? ??
|
|
16
|
-
68 ?? ?? ?? ??
|
|
17
|
-
E8 ?? ?? ?? ??
|
|
18
|
-
}
|
|
19
|
-
$decode_2 = {
|
|
20
|
-
6A ??
|
|
21
|
-
68 ?? ?? ?? ??
|
|
22
|
-
68 ?? ?? ?? ??
|
|
23
|
-
[0-5]
|
|
24
|
-
E8 ?? ?? ?? ??
|
|
25
|
-
}
|
|
17
|
+
$decode_1 = {6A ?? 68 [4] 68 [4] E8}
|
|
18
|
+
$decode_2 = {6A ?? 68 [4] 68 [4] [0-5] E8}
|
|
26
19
|
condition:
|
|
27
20
|
any of them
|
|
28
|
-
}
|
|
21
|
+
}
|
|
22
|
+
rule StealcV2
|
|
23
|
+
{
|
|
24
|
+
meta:
|
|
25
|
+
author = "kevoreilly"
|
|
26
|
+
strings:
|
|
27
|
+
$botnet32 = {AB AB AB AB 89 4B ?? C7 43 ?? 0F 00 00 00 88 0B A0 [4] EB 12 3C 20 74 0B 0F B6 06 8B CB 50 E8}
|
|
28
|
+
$botnet64 = {0F 11 01 48 C7 41 ?? 00 00 00 00 48 8B D9 48 C7 41 ?? 0F 00 00 00 C6 01 00 8A 05 [4] EB ?? 3C 20 74 ?? 48 8B 4B ?? 44 8A 0F}
|
|
29
|
+
condition:
|
|
30
|
+
any of them
|
|
31
|
+
}
|
|
32
|
+
"""
|
|
29
33
|
|
|
30
34
|
|
|
31
35
|
def yara_scan(raw_data):
|
|
@@ -45,78 +49,96 @@ def xor_data(data, key):
|
|
|
45
49
|
return decoded
|
|
46
50
|
|
|
47
51
|
|
|
48
|
-
def
|
|
49
|
-
|
|
52
|
+
def extract_ascii_string(data: bytes, offset: int, max_length=4096) -> str:
|
|
53
|
+
if offset >= len(data):
|
|
54
|
+
raise ValueError("Offset beyond data bounds")
|
|
55
|
+
end = data.find(b'\x00', offset, offset + max_length)
|
|
56
|
+
if end == -1:
|
|
57
|
+
end = offset + max_length
|
|
58
|
+
return data[offset:end].decode('ascii', errors='replace')
|
|
50
59
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
60
|
+
|
|
61
|
+
def parse_text(data):
|
|
62
|
+
global domain, uri
|
|
63
|
+
with suppress(Exception):
|
|
55
64
|
lines = data.decode().split("\n")
|
|
65
|
+
if not lines:
|
|
66
|
+
return
|
|
56
67
|
for line in lines:
|
|
57
68
|
if line.startswith("http") and "://" in line:
|
|
58
69
|
domain = line
|
|
59
70
|
if line.startswith("/") and line[-4] == ".":
|
|
60
71
|
uri = line
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
# Try with new method
|
|
68
|
-
|
|
69
|
-
#config_dict["Strings"] = []
|
|
70
|
-
pe = pefile.PE(data=data, fast_load=True)
|
|
71
|
-
image_base = pe.OPTIONAL_HEADER.ImageBase
|
|
72
|
-
domain = ""
|
|
73
|
-
uri = ""
|
|
74
|
-
botnet_id = ""
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def parse_pe(data):
|
|
75
|
+
global domain, uri, botnet_id
|
|
76
|
+
pe = None
|
|
77
|
+
image_base = 0
|
|
75
78
|
last_str = ""
|
|
79
|
+
with suppress(Exception):
|
|
80
|
+
pe = pefile.PE(data=data, fast_load=True)
|
|
81
|
+
if not pe:
|
|
82
|
+
return
|
|
83
|
+
image_base = pe.OPTIONAL_HEADER.ImageBase
|
|
84
|
+
if not image_base:
|
|
85
|
+
return
|
|
76
86
|
for match in yara_scan(data):
|
|
77
87
|
try:
|
|
78
88
|
rule_str_name, str_decode_offset = match
|
|
89
|
+
if rule_str_name.startswith("$botnet"):
|
|
90
|
+
botnet_var = struct.unpack("I", data[str_decode_offset - 4 : str_decode_offset])[0]
|
|
91
|
+
if hasattr(pe, 'OPTIONAL_HEADER'):
|
|
92
|
+
magic = pe.OPTIONAL_HEADER.Magic
|
|
93
|
+
if magic == 0x10b: # 32-bit
|
|
94
|
+
botnet_offset = pe.get_offset_from_rva(botnet_var - image_base)
|
|
95
|
+
elif magic == 0x20b: # 64-bit
|
|
96
|
+
botnet_offset = pe.get_offset_from_rva(pe.get_rva_from_offset(str_decode_offset) + botnet_var)
|
|
97
|
+
if botnet_offset:
|
|
98
|
+
botnet_id = extract_ascii_string(data, botnet_offset)
|
|
79
99
|
str_size = int(data[str_decode_offset + 1])
|
|
80
100
|
# Ignore size 0 strings
|
|
81
101
|
if not str_size:
|
|
82
102
|
continue
|
|
83
|
-
|
|
84
103
|
if rule_str_name.startswith("$decode"):
|
|
85
104
|
key_rva = data[str_decode_offset + 3 : str_decode_offset + 7]
|
|
86
105
|
encoded_str_rva = data[str_decode_offset + 8 : str_decode_offset + 12]
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
elif "http" in decoded_str and "://" in decoded_str:
|
|
104
|
-
domain = decoded_str
|
|
105
|
-
elif uri == "" and decoded_str.startswith("/") and decoded_str[-4] == ".":
|
|
106
|
-
uri = decoded_str
|
|
107
|
-
elif last_str[0] == '/' and last_str[-1] == '/':
|
|
108
|
-
botnet_id = decoded_str
|
|
109
|
-
|
|
110
|
-
last_str = decoded_str
|
|
111
|
-
|
|
106
|
+
key_offset = pe.get_offset_from_rva(struct.unpack("i", key_rva)[0] - image_base)
|
|
107
|
+
encoded_str_offset = pe.get_offset_from_rva(struct.unpack("i", encoded_str_rva)[0] - image_base)
|
|
108
|
+
key = data[key_offset : key_offset + str_size]
|
|
109
|
+
encoded_str = data[encoded_str_offset : encoded_str_offset + str_size]
|
|
110
|
+
decoded_str = xor_data(encoded_str, key).decode()
|
|
111
|
+
if last_str in ("http://", "https://"):
|
|
112
|
+
domain += decoded_str
|
|
113
|
+
elif decoded_str in ("http://", "https://"):
|
|
114
|
+
domain = decoded_str
|
|
115
|
+
elif "http" in decoded_str and "://" in decoded_str:
|
|
116
|
+
domain = decoded_str
|
|
117
|
+
elif uri is None and decoded_str.startswith("/") and decoded_str[-4] == ".":
|
|
118
|
+
uri = decoded_str
|
|
119
|
+
elif last_str[0] == "/" and last_str[-1] == "/":
|
|
120
|
+
botnet_id = decoded_str
|
|
121
|
+
last_str = decoded_str
|
|
112
122
|
except Exception:
|
|
113
123
|
continue
|
|
124
|
+
return
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def extract_config(data):
|
|
128
|
+
global domain, uri, botnet_id
|
|
129
|
+
domain = uri = botnet_id = None
|
|
130
|
+
config_dict = {}
|
|
131
|
+
|
|
132
|
+
if data[:2] == b'MZ':
|
|
133
|
+
parse_pe(data)
|
|
134
|
+
else:
|
|
135
|
+
parse_text(data)
|
|
114
136
|
|
|
115
137
|
if domain and uri:
|
|
116
|
-
config_dict.setdefault("
|
|
138
|
+
config_dict.setdefault("CNCs", []).append(f"{domain}{uri}")
|
|
117
139
|
|
|
118
140
|
if botnet_id:
|
|
119
|
-
config_dict.setdefault("
|
|
141
|
+
config_dict.setdefault("botnet", botnet_id)
|
|
120
142
|
|
|
121
143
|
return config_dict
|
|
122
144
|
|
|
@@ -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))
|
|
@@ -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))
|
|
@@ -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))
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
from Crypto.Cipher import ChaCha20_Poly1305
|
|
3
|
+
from contextlib import suppress
|
|
4
|
+
import zlib
|
|
5
|
+
import struct
|
|
6
|
+
import json
|
|
7
|
+
import yara
|
|
8
|
+
import pefile
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
RULE_SOURCE = """rule MonsterV2Config
|
|
12
|
+
{
|
|
13
|
+
meta:
|
|
14
|
+
author = "doomedraven,YungBinary"
|
|
15
|
+
strings:
|
|
16
|
+
$chunk_1 = {
|
|
17
|
+
41 B8 ?? ?? ?? ??
|
|
18
|
+
48 8D 15 ?? ?? ?? ??
|
|
19
|
+
48 8B CB
|
|
20
|
+
E8 ?? ?? ?? ??
|
|
21
|
+
48 8D 83 ?? ?? ?? ??
|
|
22
|
+
48 89 44 24 ??
|
|
23
|
+
48 89 6C 24 ??
|
|
24
|
+
4C 8B C7
|
|
25
|
+
48 8D 54 24 ??
|
|
26
|
+
48 8B CE
|
|
27
|
+
E8 ?? ?? ?? ??
|
|
28
|
+
}
|
|
29
|
+
condition:
|
|
30
|
+
$chunk_1
|
|
31
|
+
}"""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def derive_chacha_key_nonce_blake2b(seed: bytes): # -> tuple[bytes, bytes]:
|
|
35
|
+
"""
|
|
36
|
+
Derives a 32-byte ChaCha20 key and a 24-byte ChaCha20 nonce
|
|
37
|
+
using BLAKE2b from a given seed.
|
|
38
|
+
"""
|
|
39
|
+
output_length = 56 # 32 bytes for key + 24 bytes for nonce
|
|
40
|
+
h = hashlib.blake2b(digest_size=output_length)
|
|
41
|
+
h.update(seed)
|
|
42
|
+
derived_material = h.digest()
|
|
43
|
+
chacha20_key = derived_material[0:32]
|
|
44
|
+
chacha20_nonce = derived_material[32:56]
|
|
45
|
+
return chacha20_key, chacha20_nonce
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def yara_scan(raw_data, rule_source):
|
|
49
|
+
yara_rules = yara.compile(source=rule_source)
|
|
50
|
+
matches = yara_rules.match(data=raw_data)
|
|
51
|
+
|
|
52
|
+
for match in matches:
|
|
53
|
+
for block in match.strings:
|
|
54
|
+
for instance in block.instances:
|
|
55
|
+
return instance.offset
|
|
56
|
+
|
|
57
|
+
def extract_config(data: bytes) -> dict:
|
|
58
|
+
config_dict = {}
|
|
59
|
+
with suppress(Exception):
|
|
60
|
+
pe = pefile.PE(data=data)
|
|
61
|
+
offset = yara_scan(data, RULE_SOURCE)
|
|
62
|
+
|
|
63
|
+
# image_base = pe.OPTIONAL_HEADER.ImageBase
|
|
64
|
+
disp_offset = data[offset + 9 : offset + 13]
|
|
65
|
+
disp_offset = struct.unpack('i', disp_offset)[0]
|
|
66
|
+
instruction_pointer_va = pe.get_rva_from_offset(offset + 13)
|
|
67
|
+
config_offset_va = instruction_pointer_va + disp_offset
|
|
68
|
+
config_offset = pe.get_offset_from_rva(config_offset_va)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
blake_seed = data[config_offset : config_offset + 32]
|
|
72
|
+
chacha20_key, chacha20_nonce = derive_chacha_key_nonce_blake2b(blake_seed)
|
|
73
|
+
cipher_len = int.from_bytes(data[config_offset + 32 : config_offset + 40], byteorder="big")
|
|
74
|
+
cipher_text = data[config_offset + 40 : config_offset + 40 + cipher_len]
|
|
75
|
+
|
|
76
|
+
cipher = ChaCha20_Poly1305.new(key=chacha20_key, nonce=chacha20_nonce)
|
|
77
|
+
decrypted_zlib_data = cipher.decrypt(cipher_text)
|
|
78
|
+
decompressed_data = zlib.decompress(decrypted_zlib_data)
|
|
79
|
+
config_dict = json.loads(decompressed_data)
|
|
80
|
+
|
|
81
|
+
if config_dict:
|
|
82
|
+
final_config = {"raw": config_dict}
|
|
83
|
+
if "ip" in config_dict and "port" in config_dict:
|
|
84
|
+
final_config["CNCs"] = [f"tcp://{config_dict['ip']}:{config_dict['port']}"]
|
|
85
|
+
if "build_name" in config_dict:
|
|
86
|
+
final_config["build"] = config_dict["build_name"]
|
|
87
|
+
return final_config
|
|
88
|
+
|
|
89
|
+
return {}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
if __name__ == "__main__":
|
|
93
|
+
import sys
|
|
94
|
+
|
|
95
|
+
with open(sys.argv[1], "rb") as f:
|
|
96
|
+
print(extract_config(f.read()))
|
|
@@ -26,11 +26,12 @@ def parse_http_config(rc4_key: bytes, data: bytes) -> dict:
|
|
|
26
26
|
|
|
27
27
|
def read_str(length: int):
|
|
28
28
|
nonlocal offset
|
|
29
|
-
value = data[offset:offset + length].decode("utf-8", errors="replace")
|
|
29
|
+
value = data[offset : offset + length].decode("utf-8", errors="replace")
|
|
30
30
|
offset += length
|
|
31
31
|
return value
|
|
32
32
|
|
|
33
|
-
config["
|
|
33
|
+
config["cryptokey"] = rc4_key.hex()
|
|
34
|
+
config["cryptokey_type"] = "RC4"
|
|
34
35
|
config["agent_type"] = f"{read('<I'):8X}"
|
|
35
36
|
config["use_ssl"] = read("<B")
|
|
36
37
|
host_count = read("<I")
|
|
@@ -58,7 +59,8 @@ def parse_http_config(rc4_key: bytes, data: bytes) -> dict:
|
|
|
58
59
|
config["sleep_delay"] = read("<I")
|
|
59
60
|
config["jitter_delay"] = read("<I")
|
|
60
61
|
|
|
61
|
-
return config
|
|
62
|
+
return {"raw": config}
|
|
63
|
+
|
|
62
64
|
|
|
63
65
|
def extract_config(filebuf: bytes) -> dict:
|
|
64
66
|
pe = pefile.PE(data=filebuf, fast_load=True)
|
|
@@ -78,9 +80,9 @@ def extract_config(filebuf: bytes) -> dict:
|
|
|
78
80
|
pos = start_offset + 1
|
|
79
81
|
continue
|
|
80
82
|
|
|
81
|
-
encrypted_data = data[pos:pos + key_offset]
|
|
83
|
+
encrypted_data = data[pos : pos + key_offset]
|
|
82
84
|
pos += key_offset
|
|
83
|
-
rc4_key = data[pos:pos + 16]
|
|
85
|
+
rc4_key = data[pos : pos + 16]
|
|
84
86
|
|
|
85
87
|
if key_offset == 787:
|
|
86
88
|
pass
|
|
@@ -30,7 +30,7 @@ rule Azorult
|
|
|
30
30
|
cape_type = "Azorult Payload"
|
|
31
31
|
strings:
|
|
32
32
|
$ref_c2 = {6A 00 6A 00 6A 00 6A 00 68 ?? ?? ?? ?? FF 55 F0 8B D8 C7 47 10 ?? ?? ?? ?? 90 C7 45 B0 C0 C6 2D 00 6A 04 8D 45 B0 50 6A 06 53 FF 55 D4}
|
|
33
|
-
|
|
33
|
+
condition:
|
|
34
34
|
uint16(0) == 0x5A4D and all of them
|
|
35
35
|
}
|
|
36
36
|
"""
|
|
@@ -48,16 +48,18 @@ def extract_config(filebuf):
|
|
|
48
48
|
for instance in block.instances:
|
|
49
49
|
try:
|
|
50
50
|
cnc_offset = struct.unpack("i", instance.matched_data[21:25])[0]
|
|
51
|
-
cnc = pe.get_data(cnc_offset-image_base, 32).split(b"\x00")[0]
|
|
51
|
+
cnc = pe.get_data(cnc_offset - image_base, 32).split(b"\x00")[0]
|
|
52
52
|
if cnc:
|
|
53
53
|
if not cnc.startswith(b"http"):
|
|
54
54
|
cnc = b"http://" + cnc
|
|
55
|
-
return {"
|
|
55
|
+
return {"CNCs": [cnc.decode()]}
|
|
56
56
|
except Exception as e:
|
|
57
57
|
log.error("Error parsing Azorult config: %s", e)
|
|
58
58
|
return {}
|
|
59
59
|
|
|
60
|
+
|
|
60
61
|
if __name__ == "__main__":
|
|
61
62
|
import sys
|
|
63
|
+
|
|
62
64
|
with open(sys.argv[1], "rb") as f:
|
|
63
65
|
print(extract_config(f.read()))
|
|
@@ -86,7 +86,10 @@ def extract_config(file_data):
|
|
|
86
86
|
for item in raw.split(b"\x00"):
|
|
87
87
|
data = "".join(convert_char(c) for c in item)
|
|
88
88
|
if len(data) == 760:
|
|
89
|
-
config
|
|
89
|
+
config.setdefault("cryptokey", data)
|
|
90
|
+
# ToDO proper naming here
|
|
91
|
+
config.setdefault("raw", {})["cryptokey_type"] = "RSA public key"
|
|
92
|
+
|
|
90
93
|
elif len(data) > 1 and "\\x" not in data:
|
|
91
|
-
config["strings"] = data
|
|
94
|
+
config.setdefault("raw", {})["strings"] = data
|
|
92
95
|
return config
|
|
@@ -55,7 +55,7 @@ def extract_config(data: bytes) -> dict:
|
|
|
55
55
|
return {}
|
|
56
56
|
|
|
57
57
|
rdata_data = rdata_section.get_data()
|
|
58
|
-
patterns = [
|
|
58
|
+
patterns = [rb"Builder\.dll\x00", rb"Builder\.exe\x00"]
|
|
59
59
|
matches = []
|
|
60
60
|
for pattern in patterns:
|
|
61
61
|
matches.extend(re.finditer(pattern, rdata_data))
|
|
@@ -66,7 +66,7 @@ def extract_config(data: bytes) -> dict:
|
|
|
66
66
|
end = min(len(rdata_data), match.end() + 1024)
|
|
67
67
|
found_strings.update(re.findall(b"[\x20-\x7E]{4,}?\x00", rdata_data[start:end]))
|
|
68
68
|
|
|
69
|
-
|
|
69
|
+
config = {}
|
|
70
70
|
urls = []
|
|
71
71
|
directories = []
|
|
72
72
|
campaign = ""
|
|
@@ -74,7 +74,7 @@ def extract_config(data: bytes) -> dict:
|
|
|
74
74
|
if found_strings:
|
|
75
75
|
key = get_year(pe)
|
|
76
76
|
if not key:
|
|
77
|
-
return
|
|
77
|
+
return {}
|
|
78
78
|
for string in found_strings:
|
|
79
79
|
with suppress(UnicodeDecodeError):
|
|
80
80
|
decoded_string = string.decode("utf-8").rstrip("\x00")
|
|
@@ -88,9 +88,14 @@ def extract_config(data: bytes) -> dict:
|
|
|
88
88
|
elif re.match(r"^(?![A-Z]{6,}$)[a-zA-Z0-9\-=]{6,}$", decoded_string):
|
|
89
89
|
campaign = decoded_string
|
|
90
90
|
|
|
91
|
-
|
|
91
|
+
if urls:
|
|
92
|
+
config["CNCs"] = sorted(urls)
|
|
93
|
+
if campaign:
|
|
94
|
+
config["campaign"] = campaign
|
|
95
|
+
if directories:
|
|
96
|
+
config["raw"] = {"directories": directories}
|
|
92
97
|
|
|
93
|
-
return
|
|
98
|
+
return config
|
|
94
99
|
|
|
95
100
|
|
|
96
101
|
if __name__ == "__main__":
|
|
@@ -542,16 +542,18 @@ def extract_config(data):
|
|
|
542
542
|
injection_method = "Process hollowing IE or Werfault"
|
|
543
543
|
|
|
544
544
|
config = {
|
|
545
|
-
"
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
545
|
+
"raw": {
|
|
546
|
+
"Flag": hex(flag),
|
|
547
|
+
"Payload export hash": hex(u32(payload_export_hash)),
|
|
548
|
+
"Payload filename": w_payload_filename_and_cmdline,
|
|
549
|
+
"Compressed data size": hex(u32(compressed_data_size)),
|
|
550
|
+
"Uncompressed data size": hex(u32(uncompressed_data_size)),
|
|
551
|
+
"Rabbit key": binascii.hexlify(key).decode(),
|
|
552
|
+
"Rabbit IV": binascii.hexlify(iv).decode(),
|
|
553
|
+
"Persistence": persistance,
|
|
554
|
+
"Sleep after injection": sleep_after_injection,
|
|
555
|
+
"Injection method": injection_method,
|
|
556
|
+
}
|
|
555
557
|
}
|
|
556
558
|
|
|
557
559
|
return config
|
|
@@ -2,22 +2,35 @@ from contextlib import suppress
|
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
def extract_config(data):
|
|
5
|
-
|
|
5
|
+
config = {}
|
|
6
6
|
|
|
7
7
|
with suppress(Exception):
|
|
8
8
|
i = 0
|
|
9
9
|
lines = data.decode().split("\n")
|
|
10
10
|
for line in lines:
|
|
11
11
|
if line.startswith("Mozilla"):
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
cncs = list(set(lines[i - 2].split(",")))
|
|
13
|
+
port = lines[i - 1]
|
|
14
|
+
uris = lines[i + 3].split(",")
|
|
15
|
+
keys = [lines[i + 1], lines[i + 2]]
|
|
16
|
+
|
|
17
|
+
for cnc in cncs:
|
|
18
|
+
# ToDo need to verify if we have schema and uri has slash
|
|
19
|
+
for uri in uris:
|
|
20
|
+
config.setdefault("CNCs", []).append(f"{cnc}:{port}{uri}")
|
|
21
|
+
|
|
22
|
+
config["raw"] = {
|
|
23
|
+
"User Agent": line,
|
|
24
|
+
"C2": cncs,
|
|
25
|
+
"Port": port,
|
|
26
|
+
"URI": uri,
|
|
27
|
+
# ToDo move to proper field
|
|
28
|
+
"Keys": keys,
|
|
29
|
+
}
|
|
17
30
|
break
|
|
18
31
|
i += 1
|
|
19
32
|
|
|
20
|
-
return
|
|
33
|
+
return config
|
|
21
34
|
|
|
22
35
|
|
|
23
36
|
if __name__ == "__main__":
|