CAPE-parsers 0.1.42__py3-none-any.whl → 0.1.54__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cape_parsers/CAPE/community/AgentTesla.py +25 -10
- cape_parsers/CAPE/community/Amadey.py +199 -29
- cape_parsers/CAPE/community/Arkei.py +13 -15
- cape_parsers/CAPE/community/AsyncRAT.py +4 -2
- cape_parsers/CAPE/community/AuroraStealer.py +9 -6
- cape_parsers/CAPE/community/Carbanak.py +7 -7
- cape_parsers/CAPE/community/CobaltStrikeBeacon.py +5 -4
- cape_parsers/CAPE/community/CobaltStrikeStager.py +4 -1
- cape_parsers/CAPE/community/DCRat.py +4 -2
- cape_parsers/CAPE/community/Fareit.py +8 -9
- cape_parsers/CAPE/community/KoiLoader.py +3 -3
- cape_parsers/CAPE/community/LokiBot.py +11 -8
- cape_parsers/CAPE/community/Lumma.py +58 -40
- cape_parsers/CAPE/community/MonsterV2.py +93 -0
- cape_parsers/CAPE/community/MyKings.py +52 -0
- cape_parsers/CAPE/community/NanoCore.py +9 -9
- cape_parsers/CAPE/community/Nighthawk.py +1 -0
- cape_parsers/CAPE/community/Njrat.py +4 -4
- cape_parsers/CAPE/community/PhemedroneStealer.py +2 -0
- cape_parsers/CAPE/community/Snake.py +31 -18
- cape_parsers/CAPE/community/SparkRAT.py +3 -1
- cape_parsers/CAPE/community/Stealc.py +95 -63
- cape_parsers/CAPE/community/VenomRAT.py +4 -2
- cape_parsers/CAPE/community/WinosStager.py +75 -0
- cape_parsers/CAPE/community/XWorm.py +4 -2
- cape_parsers/CAPE/community/XenoRAT.py +4 -2
- cape_parsers/CAPE/core/AdaptixBeacon.py +7 -5
- cape_parsers/CAPE/core/AuraStealer.py +100 -0
- cape_parsers/CAPE/core/Azorult.py +5 -3
- cape_parsers/CAPE/core/BitPaymer.py +5 -2
- cape_parsers/CAPE/core/BlackDropper.py +10 -5
- cape_parsers/CAPE/core/Blister.py +12 -10
- cape_parsers/CAPE/core/BruteRatel.py +20 -7
- cape_parsers/CAPE/core/BumbleBee.py +34 -22
- cape_parsers/CAPE/core/DarkGate.py +3 -3
- cape_parsers/CAPE/core/DoppelPaymer.py +4 -2
- cape_parsers/CAPE/core/DridexLoader.py +4 -3
- cape_parsers/CAPE/core/Formbook.py +2 -2
- cape_parsers/CAPE/core/GuLoader.py +2 -5
- cape_parsers/CAPE/core/IcedID.py +5 -5
- cape_parsers/CAPE/core/IcedIDLoader.py +4 -4
- cape_parsers/CAPE/core/Latrodectus.py +14 -10
- cape_parsers/CAPE/core/NitroBunnyDownloader.py +151 -0
- cape_parsers/CAPE/core/Oyster.py +8 -6
- cape_parsers/CAPE/core/PikaBot.py +6 -6
- cape_parsers/CAPE/core/PlugX.py +3 -1
- cape_parsers/CAPE/core/QakBot.py +2 -1
- cape_parsers/CAPE/core/Quickbind.py +7 -11
- cape_parsers/CAPE/core/RedLine.py +2 -2
- cape_parsers/CAPE/core/Remcos.py +59 -51
- cape_parsers/CAPE/core/Rhadamanthys.py +175 -36
- cape_parsers/CAPE/core/SmokeLoader.py +2 -2
- cape_parsers/CAPE/core/Socks5Systemz.py +5 -5
- cape_parsers/CAPE/core/SquirrelWaffle.py +3 -3
- cape_parsers/CAPE/core/Strrat.py +1 -1
- cape_parsers/CAPE/core/WarzoneRAT.py +3 -2
- cape_parsers/CAPE/core/Zloader.py +21 -15
- cape_parsers/RATDecoders/test_rats.py +1 -0
- cape_parsers/__init__.py +14 -5
- cape_parsers/deprecated/BlackNix.py +59 -0
- cape_parsers/{CAPE/core → deprecated}/BuerLoader.py +1 -1
- cape_parsers/{CAPE/core → deprecated}/ChChes.py +3 -3
- cape_parsers/{CAPE/core → deprecated}/Enfal.py +1 -1
- cape_parsers/{CAPE/core → deprecated}/EvilGrab.py +5 -6
- cape_parsers/{CAPE/community → deprecated}/Greame.py +3 -1
- cape_parsers/{CAPE/core → deprecated}/HttpBrowser.py +7 -8
- cape_parsers/{CAPE/community → deprecated}/Pandora.py +2 -0
- cape_parsers/{CAPE/community → deprecated}/Punisher.py +2 -1
- cape_parsers/{CAPE/core → deprecated}/RCSession.py +7 -9
- cape_parsers/{CAPE/community → deprecated}/REvil.py +10 -5
- cape_parsers/{CAPE/core → deprecated}/RedLeaf.py +5 -7
- cape_parsers/{CAPE/community → deprecated}/Retefe.py +0 -2
- cape_parsers/{CAPE/community → deprecated}/Rozena.py +2 -5
- cape_parsers/{CAPE/community → deprecated}/SmallNet.py +6 -2
- {cape_parsers-0.1.42.dist-info → cape_parsers-0.1.54.dist-info}/METADATA +24 -3
- cape_parsers-0.1.54.dist-info/RECORD +117 -0
- {cape_parsers-0.1.42.dist-info → cape_parsers-0.1.54.dist-info}/WHEEL +1 -1
- cape_parsers/CAPE/community/BlackNix.py +0 -57
- cape_parsers/CAPE/core/Stealc.py +0 -21
- cape_parsers-0.1.42.dist-info/RECORD +0 -113
- /cape_parsers/{CAPE/community → deprecated}/BackOffLoader.py +0 -0
- /cape_parsers/{CAPE/community → deprecated}/BackOffPOS.py +0 -0
- /cape_parsers/{CAPE/core → deprecated}/Emotet.py +0 -0
- /cape_parsers/{CAPE/community → deprecated}/PoisonIvy.py +0 -0
- /cape_parsers/{CAPE/community → deprecated}/TSCookie.py +0 -0
- /cape_parsers/{CAPE/community → deprecated}/TrickBot.py +0 -0
- /cape_parsers/{CAPE/core → deprecated}/UrsnifV3.py +0 -0
- {cape_parsers-0.1.42.dist-info → cape_parsers-0.1.54.dist-info/licenses}/LICENSE +0 -0
|
@@ -57,12 +57,7 @@ def extract_config(filebuf):
|
|
|
57
57
|
encrypted_string = struct.unpack_from(data_format, data, offset)[0]
|
|
58
58
|
|
|
59
59
|
with suppress(IndexError, UnicodeDecodeError, ValueError):
|
|
60
|
-
decrypted_result = (
|
|
61
|
-
ARC4.new(key)
|
|
62
|
-
.decrypt(encrypted_string)
|
|
63
|
-
.replace(b"\x00", b"")
|
|
64
|
-
.decode("utf-8")
|
|
65
|
-
)
|
|
60
|
+
decrypted_result = ARC4.new(key).decrypt(encrypted_string).replace(b"\x00", b"").decode("utf-8")
|
|
66
61
|
|
|
67
62
|
if decrypted_result and all(32 <= ord(char) <= 127 for char in decrypted_result):
|
|
68
63
|
if len(decrypted_result) > 2:
|
|
@@ -105,12 +100,13 @@ def extract_config(filebuf):
|
|
|
105
100
|
campaign_found = True
|
|
106
101
|
|
|
107
102
|
elif is_hex(item):
|
|
108
|
-
cfg["
|
|
103
|
+
cfg["cryptokey"] = item
|
|
104
|
+
cfg["cryptokey_type"] = "RC4"
|
|
109
105
|
if i == 1:
|
|
110
106
|
campaign_found = True
|
|
111
107
|
|
|
112
108
|
elif "Mozilla" in item:
|
|
113
|
-
cfg["
|
|
109
|
+
cfg["user_agent"] = item
|
|
114
110
|
if i == 1:
|
|
115
111
|
campaign_found = True
|
|
116
112
|
|
|
@@ -119,13 +115,13 @@ def extract_config(filebuf):
|
|
|
119
115
|
campaign_found = True
|
|
120
116
|
|
|
121
117
|
if campaign_found:
|
|
122
|
-
cfg["
|
|
118
|
+
cfg["campaign"] = campaign
|
|
123
119
|
|
|
124
120
|
if c2s:
|
|
125
|
-
cfg["
|
|
121
|
+
cfg["CNCs"] = c2s
|
|
126
122
|
|
|
127
123
|
if mutexes:
|
|
128
|
-
cfg["
|
|
124
|
+
cfg["mutex"] = list(set(mutexes))
|
|
129
125
|
|
|
130
126
|
return cfg
|
|
131
127
|
|
|
@@ -167,11 +167,11 @@ def extract_config(data):
|
|
|
167
167
|
if not c2 or "." not in c2:
|
|
168
168
|
return
|
|
169
169
|
|
|
170
|
-
config_dict = {"
|
|
170
|
+
config_dict = {"CNCs": c2, "botnet": botnet, "cryptokey": key}
|
|
171
171
|
if "Authorization" in user_strings:
|
|
172
172
|
base_location = user_strings.index("Authorization")
|
|
173
173
|
if base_location:
|
|
174
|
-
config_dict["Authorization"] = user_strings[base_location - 1]
|
|
174
|
+
config_dict.setdefault("raw", {})["Authorization"] = user_strings[base_location - 1]
|
|
175
175
|
return config_dict
|
|
176
176
|
|
|
177
177
|
|
cape_parsers/CAPE/core/Remcos.py
CHANGED
|
@@ -25,34 +25,34 @@ FLAG = {b"\x00": "Disable", b"\x01": "Enable"}
|
|
|
25
25
|
|
|
26
26
|
# From JPCERT and Elastic Security Labs
|
|
27
27
|
idx_list = {
|
|
28
|
-
0: "Host:Port:Password",
|
|
29
|
-
1: "Botnet",
|
|
30
|
-
2: "Connect interval",
|
|
31
|
-
3: "Install flag",
|
|
32
|
-
4: "Setup HKCU\\Run",
|
|
33
|
-
5: "Setup HKLM\\Run",
|
|
28
|
+
0: "Host:Port:Password", # String containing "domain:port:enable_tls" separated by the "\x1e" characte
|
|
29
|
+
1: "Botnet", # Name of the botnet
|
|
30
|
+
2: "Connect interval", # Interval in second between connection attempt to C2
|
|
31
|
+
3: "Install flag", # Install REMCOS on the machine host
|
|
32
|
+
4: "Setup HKCU\\Run", # Enable setup of the persistence in the registry
|
|
33
|
+
5: "Setup HKLM\\Run", # Enable setup of the persistence in the registry
|
|
34
34
|
6: "Setup HKLM\\Explorer\\Run",
|
|
35
|
-
7: "Keylog file max size",
|
|
36
|
-
8: "Setup HKLM\\Explorer\\Run",
|
|
37
|
-
9: "Install parent directory",
|
|
38
|
-
10: "Install filename",
|
|
35
|
+
7: "Keylog file max size", # Maximum size of the keylogging data before rotation
|
|
36
|
+
8: "Setup HKLM\\Explorer\\Run", # Enable setup of the persistence in the registry
|
|
37
|
+
9: "Install parent directory", # Parent directory of the install folder. Integer mapped to an hardcoded path
|
|
38
|
+
10: "Install filename", # Name of the REMCOS binary once installed
|
|
39
39
|
11: "Startup value",
|
|
40
|
-
12: "Hide file",
|
|
41
|
-
13: "Process injection flag",
|
|
42
|
-
14: "Mutex",
|
|
43
|
-
15: "Keylogger mode",
|
|
44
|
-
16: "Keylogger parent directory",
|
|
45
|
-
17: "Keylogger filename",
|
|
46
|
-
18: "Keylog crypt",
|
|
47
|
-
19: "Hide keylog file",
|
|
48
|
-
20: "Screenshot flag",
|
|
49
|
-
21: "Screenshot time",
|
|
50
|
-
22: "Take Screenshot option",
|
|
51
|
-
23: "Take screenshot title",
|
|
52
|
-
24: "Take screenshot time",
|
|
53
|
-
25: "Screenshot parent directory",
|
|
54
|
-
26: "Screenshot folder",
|
|
55
|
-
27: "Screenshot crypt flag",
|
|
40
|
+
12: "Hide file", # Enable super hiding the install directory and binary as well as setting them to read only
|
|
41
|
+
13: "Process injection flag", # Enable running the malware injected in another process
|
|
42
|
+
14: "Mutex", # String used as the malware mutex and registry key
|
|
43
|
+
15: "Keylogger mode", # Set keylogging capability. Keylogging mode, 0 = disabled, 1 = keylogging everything, 2 = keylogging specific window(s)
|
|
44
|
+
16: "Keylogger parent directory", # Parent directory of the keylogging folder. Integer mapped to an hardcoded path
|
|
45
|
+
17: "Keylogger filename", # Filename of the keylogged data
|
|
46
|
+
18: "Keylog crypt", # Enable encryption RC4 of the keylogger data file
|
|
47
|
+
19: "Hide keylog file", # Enable super hiding of the keylogger data file
|
|
48
|
+
20: "Screenshot flag", # Enable screen recording capability
|
|
49
|
+
21: "Screenshot time", # The time interval in minute for capturing each screenshot
|
|
50
|
+
22: "Take Screenshot option", # Enable screen recording for specific window names
|
|
51
|
+
23: "Take screenshot title", # String containing window names separated by the ";" character
|
|
52
|
+
24: "Take screenshot time", # s The time interval in second for capturing each screenshot when a specific window name is found in the current foreground window title
|
|
53
|
+
25: "Screenshot parent directory", # Parent directory of the screenshot folder. Integer mapped to an hardcoded path
|
|
54
|
+
26: "Screenshot folder", # Name of the screenshot folder
|
|
55
|
+
27: "Screenshot crypt flag", # Enable encryption of screenshots
|
|
56
56
|
28: "Mouse option",
|
|
57
57
|
29: "Unknown29",
|
|
58
58
|
30: "Delete file",
|
|
@@ -60,28 +60,28 @@ idx_list = {
|
|
|
60
60
|
32: "Unknown32",
|
|
61
61
|
33: "Unknown33",
|
|
62
62
|
34: "Unknown34",
|
|
63
|
-
35: "Audio recording flag",
|
|
64
|
-
36: "Audio record time",
|
|
65
|
-
37: "Audio parent directory",
|
|
66
|
-
38: "Audio folder",
|
|
67
|
-
39: "Disable UAC flage",
|
|
68
|
-
40: "Logging mode",
|
|
69
|
-
41: "Connect delay",
|
|
70
|
-
42: "Keylogger specific window names",
|
|
71
|
-
43: "Browser cleaning on startup flag",
|
|
72
|
-
44: "Browser cleaning only for the first run flag",
|
|
73
|
-
45: "Browser cleaning sleep time in minutes",
|
|
74
|
-
46: "UAC bypass flag",
|
|
63
|
+
35: "Audio recording flag", # Enable audio recording capability
|
|
64
|
+
36: "Audio record time", # Duration in second of each audio recording
|
|
65
|
+
37: "Audio parent directory", # Parent directory of the audio recording folder. Integer mapped to an hardcoded path
|
|
66
|
+
38: "Audio folder", # Name of the audio recording folder
|
|
67
|
+
39: "Disable UAC flage", # Disable UAC in the registry
|
|
68
|
+
40: "Logging mode", # Set logging mode: 0 = disabled, 1 = minimized in tray, 2 = console logging
|
|
69
|
+
41: "Connect delay", # Delay in second before the first connection attempt to the C2
|
|
70
|
+
42: "Keylogger specific window names", # String containing window names separated by the ";"" character
|
|
71
|
+
43: "Browser cleaning on startup flag", # Enable cleaning web browsers cookies and logins on REMCOS startup
|
|
72
|
+
44: "Browser cleaning only for the first run flag", # Enable web browsers cleaning only on the first run of Remcos
|
|
73
|
+
45: "Browser cleaning sleep time in minutes", # Sleep time in minute before cleaning the web browsers
|
|
74
|
+
46: "UAC bypass flag", # Enable UAC bypass capability
|
|
75
75
|
47: "Unkown47",
|
|
76
|
-
48: "Install directory",
|
|
77
|
-
49: "Keylogger root directory",
|
|
78
|
-
50: "Watchdog flag",
|
|
76
|
+
48: "Install directory", # Name of the install directory
|
|
77
|
+
49: "Keylogger root directory", # Name of the keylogger directory
|
|
78
|
+
50: "Watchdog flag", # Enable watchdog capability
|
|
79
79
|
51: "Unknown51",
|
|
80
|
-
52: "License",
|
|
81
|
-
53: "Screenshot mouse drawing flag",
|
|
82
|
-
54: "TLS raw certificate (base64)",
|
|
83
|
-
55: "TLS key (base64)",
|
|
84
|
-
56: "TLS raw peer certificate (base64)",
|
|
80
|
+
52: "License", # License serial
|
|
81
|
+
53: "Screenshot mouse drawing flag", # Enable drawing the mouse on each screenshot
|
|
82
|
+
54: "TLS raw certificate (base64)", # Certificate in raw format used with tls enabled C2 communication
|
|
83
|
+
55: "TLS key (base64)", # Key of the certificate
|
|
84
|
+
56: "TLS raw peer certificate (base64)", # C2 public certificate in raw format
|
|
85
85
|
57: "TLS client private key (base64)",
|
|
86
86
|
58: "TLS server certificate (base64)",
|
|
87
87
|
59: "Unknown59",
|
|
@@ -107,7 +107,15 @@ setup_list = {
|
|
|
107
107
|
8: "%ProgramData%",
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
utf_16_string_list = [
|
|
110
|
+
utf_16_string_list = [
|
|
111
|
+
"Keylogger specific window names",
|
|
112
|
+
"Install filename",
|
|
113
|
+
"Install directory",
|
|
114
|
+
"Startup value",
|
|
115
|
+
"Keylogger filename",
|
|
116
|
+
"Take screenshot title",
|
|
117
|
+
"Keylogger root directory",
|
|
118
|
+
]
|
|
111
119
|
logger = logging.getLogger(__name__)
|
|
112
120
|
|
|
113
121
|
|
|
@@ -170,7 +178,7 @@ def extract_config(filebuf):
|
|
|
170
178
|
key = blob[1 : keylen + 1]
|
|
171
179
|
decrypted_data = ARC4.new(key).decrypt(blob[keylen + 1 :])
|
|
172
180
|
p_data = OrderedDict()
|
|
173
|
-
|
|
181
|
+
config["version"] = check_version(filebuf)
|
|
174
182
|
|
|
175
183
|
configs = re.split(rb"\|\x1e\x1e\x1f\|", decrypted_data)
|
|
176
184
|
|
|
@@ -189,7 +197,7 @@ def extract_config(filebuf):
|
|
|
189
197
|
# various separators have been observed
|
|
190
198
|
separator = next((x for x in (b"|", b"\x1e", b"\xff\xff\xff\xff") if x in cont))
|
|
191
199
|
host, port, password = cont.split(separator, 1)[0].split(b":")
|
|
192
|
-
|
|
200
|
+
config["CNCs"] = [f"tcp://{host.decode()}:{port.decode()}"]
|
|
193
201
|
p_data["Password"] = password.decode()
|
|
194
202
|
else:
|
|
195
203
|
p_data[idx_list[i]] = cont
|
|
@@ -200,10 +208,10 @@ def extract_config(filebuf):
|
|
|
200
208
|
if isinstance(v, bytes):
|
|
201
209
|
with suppress(Exception):
|
|
202
210
|
v = v.decoed()
|
|
203
|
-
config[k] = v
|
|
211
|
+
config.setdefault("raw", {})[k] = v
|
|
204
212
|
|
|
205
213
|
except Exception as e:
|
|
206
|
-
logger.error(
|
|
214
|
+
logger.error("Caught an exception: %s", str(e))
|
|
207
215
|
|
|
208
216
|
return config
|
|
209
217
|
|
|
@@ -11,12 +11,15 @@ AUTHOR = "kevoreilly, YungBinary"
|
|
|
11
11
|
def mask32(x):
|
|
12
12
|
return x & 0xFFFFFFFF
|
|
13
13
|
|
|
14
|
+
|
|
14
15
|
def add32(x, y):
|
|
15
16
|
return mask32(x + y)
|
|
16
17
|
|
|
18
|
+
|
|
17
19
|
def left_rotate(x, n):
|
|
18
20
|
return mask32(x << n) | (x >> (32 - n))
|
|
19
21
|
|
|
22
|
+
|
|
20
23
|
def quarter_round(block, a, b, c, d):
|
|
21
24
|
block[a] = add32(block[a], block[b])
|
|
22
25
|
block[d] ^= block[a]
|
|
@@ -31,6 +34,7 @@ def quarter_round(block, a, b, c, d):
|
|
|
31
34
|
block[b] ^= block[c]
|
|
32
35
|
block[b] = left_rotate(block[b], 7)
|
|
33
36
|
|
|
37
|
+
|
|
34
38
|
def chacha20_permute(block):
|
|
35
39
|
for doubleround in range(10):
|
|
36
40
|
quarter_round(block, 0, 4, 8, 12)
|
|
@@ -42,6 +46,7 @@ def chacha20_permute(block):
|
|
|
42
46
|
quarter_round(block, 2, 7, 8, 13)
|
|
43
47
|
quarter_round(block, 3, 4, 9, 14)
|
|
44
48
|
|
|
49
|
+
|
|
45
50
|
def words_from_bytes(b):
|
|
46
51
|
assert len(b) % 4 == 0
|
|
47
52
|
return [int.from_bytes(b[4 * i : 4 * i + 4], "little") for i in range(len(b) // 4)]
|
|
@@ -50,11 +55,12 @@ def words_from_bytes(b):
|
|
|
50
55
|
def bytes_from_words(w):
|
|
51
56
|
return b"".join(word.to_bytes(4, "little") for word in w)
|
|
52
57
|
|
|
58
|
+
|
|
53
59
|
def chacha20_block(key, nonce, blocknum):
|
|
54
60
|
# This implementation doesn't support 16-byte keys.
|
|
55
61
|
assert len(key) == 32
|
|
56
62
|
assert len(nonce) == 12
|
|
57
|
-
assert blocknum < 2
|
|
63
|
+
assert blocknum < 2**32
|
|
58
64
|
constant_words = words_from_bytes(b"expand 32-byte k")
|
|
59
65
|
key_words = words_from_bytes(key)
|
|
60
66
|
nonce_words = words_from_bytes(nonce)
|
|
@@ -72,6 +78,7 @@ def chacha20_block(key, nonce, blocknum):
|
|
|
72
78
|
permuted_block[i] = add32(permuted_block[i], original_block[i])
|
|
73
79
|
return bytes_from_words(permuted_block)
|
|
74
80
|
|
|
81
|
+
|
|
75
82
|
def chacha20_stream(key, nonce, length, blocknum):
|
|
76
83
|
output = bytearray()
|
|
77
84
|
while length > 0:
|
|
@@ -82,8 +89,9 @@ def chacha20_stream(key, nonce, length, blocknum):
|
|
|
82
89
|
blocknum += 1
|
|
83
90
|
return output
|
|
84
91
|
|
|
92
|
+
|
|
85
93
|
def decrypt_config(data):
|
|
86
|
-
decrypted_config = b"
|
|
94
|
+
decrypted_config = b""
|
|
87
95
|
data_len = len(data)
|
|
88
96
|
v3 = 0
|
|
89
97
|
while True:
|
|
@@ -98,6 +106,7 @@ def decrypt_config(data):
|
|
|
98
106
|
v8 -= 1
|
|
99
107
|
v3 += 1
|
|
100
108
|
|
|
109
|
+
|
|
101
110
|
def chacha20_xor(custom_b64_decoded, key, nonce):
|
|
102
111
|
message_len = len(custom_b64_decoded)
|
|
103
112
|
key_stream = chacha20_stream(key, nonce, message_len, 0x80)
|
|
@@ -108,68 +117,199 @@ def chacha20_xor(custom_b64_decoded, key, nonce):
|
|
|
108
117
|
|
|
109
118
|
return xor_key
|
|
110
119
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
upat = b"((?:[\x20-\x7e][\x00]){" + str(minchars).encode() + b"," + str(maxchars).encode() + b"})\x00\x00"
|
|
118
|
-
strings.extend(str(ws.decode("utf-16le")) for ws in re.findall(upat, data))
|
|
120
|
+
|
|
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))
|
|
119
126
|
return strings
|
|
120
127
|
|
|
128
|
+
|
|
121
129
|
def extract_c2_url(data):
|
|
122
130
|
pattern = b"(http[\x20-\x7e]+)\x00"
|
|
123
131
|
match = re.search(pattern, data)
|
|
124
|
-
|
|
132
|
+
if match:
|
|
133
|
+
return match.group(1).decode()
|
|
125
134
|
|
|
126
|
-
def is_potential_custom_base64(string):
|
|
127
|
-
custom_alphabet = "ABC1fghijklmnop234NOPQRSTUVWXY567DEFGHIJKLMZ089abcdeqrstuvwxyz-|"
|
|
128
|
-
for c in string:
|
|
129
|
-
if c not in custom_alphabet:
|
|
130
|
-
return False
|
|
131
|
-
return True
|
|
132
135
|
|
|
133
|
-
def custom_b64decode(data):
|
|
136
|
+
def custom_b64decode(data: bytes, custom_alphabet: bytes):
|
|
134
137
|
"""Decodes base64 data using a custom alphabet."""
|
|
135
138
|
standard_alphabet = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
|
136
|
-
custom_alphabet = b"ABC1fghijklmnop234NOPQRSTUVWXY567DEFGHIJKLMZ089abcdeqrstuvwxyz-|"
|
|
137
139
|
# Translate the data back to the standard alphabet before decoding
|
|
138
140
|
table = bytes.maketrans(custom_alphabet, standard_alphabet)
|
|
139
141
|
return base64.b64decode(data.translate(table), validate=True)
|
|
140
142
|
|
|
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
|
+
|
|
141
243
|
def extract_config(data):
|
|
142
244
|
config_dict = {}
|
|
143
245
|
magic = struct.unpack("I", data[:4])[0]
|
|
144
246
|
if magic == 0x59485221:
|
|
145
|
-
config_dict["
|
|
247
|
+
config_dict["CNCs"] = [data[24:].split(b"\0", 1)[0].decode()]
|
|
146
248
|
return config_dict
|
|
147
249
|
else:
|
|
148
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"
|
|
149
251
|
nonce = b"\x5F\x14\xD7\x9C\xFC\xFC\x43\x9E\xC3\x40\x6B\xBA"
|
|
150
252
|
|
|
151
|
-
|
|
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')
|
|
152
265
|
for string in extracted_strings:
|
|
153
266
|
try:
|
|
154
|
-
|
|
155
|
-
continue
|
|
267
|
+
custom_b64_decoded = custom_b64decode(string, custom_alphabets[0])
|
|
156
268
|
|
|
157
|
-
custom_b64_decoded = custom_b64decode(string)
|
|
158
269
|
xor_key = chacha20_xor(custom_b64_decoded, key, nonce)
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
+
|
|
170
308
|
except Exception:
|
|
171
309
|
continue
|
|
172
310
|
|
|
311
|
+
return config_dict
|
|
312
|
+
|
|
173
313
|
|
|
174
314
|
if __name__ == "__main__":
|
|
175
315
|
import sys
|
|
@@ -177,4 +317,3 @@ if __name__ == "__main__":
|
|
|
177
317
|
with open(sys.argv[1], "rb") as f:
|
|
178
318
|
config_json = json.dumps(extract_config(f.read()), indent=4)
|
|
179
319
|
print(config_json)
|
|
180
|
-
|
|
@@ -37,7 +37,7 @@ def rc4_decrypt(key, ciphertext):
|
|
|
37
37
|
|
|
38
38
|
|
|
39
39
|
def swap32(x):
|
|
40
|
-
return int.from_bytes(x.to_bytes(4, byteorder=
|
|
40
|
+
return int.from_bytes(x.to_bytes(4, byteorder="little"), byteorder="big", signed=False)
|
|
41
41
|
|
|
42
42
|
|
|
43
43
|
def decode(buffer):
|
|
@@ -104,7 +104,7 @@ def extract_config(filebuf):
|
|
|
104
104
|
break
|
|
105
105
|
c2list_offset += delta
|
|
106
106
|
if c2list != []:
|
|
107
|
-
cfg["
|
|
107
|
+
cfg["CNCs"] = sorted(list(set(c2list)))
|
|
108
108
|
return cfg
|
|
109
109
|
|
|
110
110
|
|
|
@@ -11,15 +11,15 @@ def _is_ip(ip):
|
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
def extract_config(data):
|
|
14
|
-
config_dict = {
|
|
14
|
+
config_dict = {}
|
|
15
15
|
with suppress(Exception):
|
|
16
16
|
if data[:2] == b"MZ":
|
|
17
17
|
return
|
|
18
18
|
for line in data.decode().split("\n"):
|
|
19
|
-
if _is_ip(line) and line not in config_dict
|
|
20
|
-
config_dict["
|
|
19
|
+
if _is_ip(line) and line not in config_dict.get("CNCs", []):
|
|
20
|
+
config_dict["CNCs"].append(line)
|
|
21
21
|
elif line and "\\" in line:
|
|
22
22
|
config_dict.setdefault("Timestamp path", []).append(line)
|
|
23
|
-
elif "." in line and "=" not in line and line not in config_dict["
|
|
24
|
-
config_dict.setdefault("Dummy domain", []).append(line)
|
|
23
|
+
elif "." in line and "=" not in line and line not in config_dict["CNCs"]:
|
|
24
|
+
config_dict.setdefault("raw", {}).setdefault("Dummy domain", []).append(line)
|
|
25
25
|
return config_dict
|
|
@@ -68,9 +68,9 @@ def extract_config(data):
|
|
|
68
68
|
try:
|
|
69
69
|
decrypted = xor_data(line, chunks[i + 1]).decode()
|
|
70
70
|
if "\r\n" in decrypted and "|" not in decrypted:
|
|
71
|
-
config["IP Blocklist"] = list(filter(None, decrypted.split("\r\n")))
|
|
71
|
+
config.setdefault("raw", {})["IP Blocklist"] = list(filter(None, decrypted.split("\r\n")))
|
|
72
72
|
elif "|" in decrypted and "." in decrypted and "\r\n" not in decrypted:
|
|
73
|
-
config["
|
|
73
|
+
config["CNCs"] = list(filter(None, decrypted.split("|")))
|
|
74
74
|
except Exception:
|
|
75
75
|
continue
|
|
76
76
|
matches = yara_rules.match(data=data)
|
|
@@ -84,5 +84,5 @@ def extract_config(data):
|
|
|
84
84
|
c2key_offset = item.instances[0].offset
|
|
85
85
|
key_rva = struct.unpack("i", data[c2key_offset + 28 : c2key_offset + 32])[0] - pe.OPTIONAL_HEADER.ImageBase
|
|
86
86
|
key_offset = pe.get_offset_from_rva(key_rva)
|
|
87
|
-
config["
|
|
87
|
+
config["cryptokey"] = string_from_offset(data, key_offset).decode()
|
|
88
88
|
return config
|
cape_parsers/CAPE/core/Strrat.py
CHANGED
|
@@ -92,7 +92,8 @@ def extract_config(data):
|
|
|
92
92
|
c2_host = dtxt[offset : offset + c2_size].decode("utf-16")
|
|
93
93
|
offset += c2_size
|
|
94
94
|
c2_port = struct.unpack("H", dtxt[offset : offset + 2])[0]
|
|
95
|
-
|
|
95
|
+
# ToDo missed schema
|
|
96
|
+
cfg["CNCs"] = [f"{c2_host}:{c2_port}"]
|
|
96
97
|
offset += 2
|
|
97
98
|
# unk1 = dtxt[offset : offset + 7]
|
|
98
99
|
offset += 7
|
|
@@ -104,7 +105,7 @@ def extract_config(data):
|
|
|
104
105
|
offset += 2
|
|
105
106
|
runkey_size = struct.unpack("i", dtxt[offset : offset + 4])[0]
|
|
106
107
|
offset += 4
|
|
107
|
-
cfg["Run Key Name"] = dtxt[offset : offset + runkey_size].decode("utf-16")
|
|
108
|
+
cfg.setdefault("raw", {})["Run Key Name"] = dtxt[offset : offset + runkey_size].decode("utf-16")
|
|
108
109
|
except struct.error:
|
|
109
110
|
# there is a lot of failed data validation muting it
|
|
110
111
|
return
|