CAPE-parsers 0.1.51__py3-none-any.whl → 0.1.53__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.
@@ -14,15 +14,12 @@ RULE_SOURCE = """rule MonsterV2Config
14
14
  author = "doomedraven,YungBinary"
15
15
  strings:
16
16
  $chunk_1 = {
17
- 41 B8 ?? ?? ?? ??
18
- 48 8D 15 ?? ?? ?? ??
19
- 48 8B 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 ??
17
+ 41 B8 0E 04 00 00
18
+ 48 8D 15 ?? ?? ?? 00
19
+ 48 8B C?
20
+ E8 ?? ?? ?? ?? [3-17]
21
+ 4C 8B C?
22
+ 48 8D 54 24 28
26
23
  48 8B CE
27
24
  E8 ?? ?? ?? ??
28
25
  }
@@ -0,0 +1,52 @@
1
+ """
2
+ Description: MyKings AKA Smominru config parser
3
+ Author: x.com/YungBinary
4
+ """
5
+
6
+ from contextlib import suppress
7
+ import json
8
+ import re
9
+ import base64
10
+
11
+
12
+ def contains_non_printable(byte_array):
13
+ for byte in byte_array:
14
+ if not chr(byte).isprintable():
15
+ return True
16
+ return False
17
+
18
+
19
+ def extract_base64_strings(data: bytes, minchars: int, maxchars: int) -> list:
20
+ pattern = b"([A-Za-z0-9+/=]{" + str(minchars).encode() + b"," + str(maxchars).encode() + b"})\x00{4}"
21
+ strings = []
22
+ for string in re.findall(pattern, data):
23
+ decoded_string = base64_and_printable(string.decode())
24
+ if decoded_string:
25
+ strings.append(decoded_string)
26
+ return strings
27
+
28
+
29
+ def base64_and_printable(b64_string: str):
30
+ with suppress(Exception):
31
+ decoded_bytes = base64.b64decode(b64_string)
32
+ if not contains_non_printable(decoded_bytes):
33
+ return decoded_bytes.decode('ascii')
34
+
35
+
36
+ def extract_config(data: bytes) -> dict:
37
+ config_dict = {}
38
+ with suppress(Exception):
39
+ cncs = extract_base64_strings(data, 12, 60)
40
+ if cncs:
41
+ # as they don't have schema they going under raw
42
+ config_dict["raw"] = {"CNCs": cncs}
43
+ return config_dict
44
+
45
+ return {}
46
+
47
+
48
+ if __name__ == "__main__":
49
+ import sys
50
+
51
+ with open(sys.argv[1], "rb") as f:
52
+ print(json.dumps(extract_config(f.read()), indent=4))
@@ -0,0 +1,75 @@
1
+ """
2
+ Description: Winos 4.0 "OnlineModule" config parser
3
+ Author: x.com/YungBinary
4
+ """
5
+
6
+ from contextlib import suppress
7
+ import re
8
+
9
+
10
+ CONFIG_KEY_MAP = {
11
+ "dd": "execution_delay_seconds",
12
+ "cl": "communication_interval_seconds",
13
+ "bb": "version",
14
+ "bz": "comment",
15
+ "jp": "keylogger",
16
+ "bh": "end_bluescreen",
17
+ "ll": "anti_traffic_monitoring",
18
+ "dl": "entrypoint",
19
+ "sh": "process_daemon",
20
+ "kl": "process_hollowing"
21
+ }
22
+
23
+
24
+ def find_config(data):
25
+ start = ":db|".encode("utf-16le")
26
+ end = ":1p|".encode("utf-16le")
27
+ pattern = re.compile(re.escape(start) + b".*?" + re.escape(end), re.DOTALL)
28
+ match = pattern.search(data)
29
+ if match:
30
+ return match.group(0).decode("utf-16le")
31
+
32
+
33
+ def extract_config(data: bytes) -> dict:
34
+ config_dict = {}
35
+ final_config = {}
36
+
37
+ with suppress(Exception):
38
+ config = find_config(data)
39
+ if not config:
40
+ return config_dict
41
+
42
+ # Reverse the config string, which is delimited by '|'
43
+ config = config[::-1]
44
+ # Remove leading/trailing pipes and split into key/value pairs
45
+ elements = [element for element in config.strip('|').split('|') if ':' in element]
46
+ # Split each element for key : value in a dictionary
47
+ config_dict = dict(element.split(':', 1) for element in elements)
48
+ if config_dict:
49
+ # Handle extraction and formatting of CNCs
50
+ for i in range(1, 4):
51
+ p, o, t = config_dict.get(f"p{i}"), config_dict.get(f"o{i}"), config_dict.get(f"t{i}")
52
+ if p and p != "127.0.0.1" and o:
53
+ protocol = {"0": "udp", "1": "tcp"}.get(t)
54
+ if protocol:
55
+ cnc = f"{protocol}://{p}:{o}"
56
+ final_config.setdefault("CNCs", []).append(cnc)
57
+
58
+ if "CNCs" not in final_config:
59
+ return {}
60
+
61
+ final_config["CNCs"] = list(set(final_config["CNCs"]))
62
+ # Extract campaign ID
63
+ final_config["campaign_id"] = "default" if config_dict["fz"] == "\u9ed8\u8ba4" else config_dict["fz"]
64
+
65
+ # Map keys, e.g. dd -> execution_delay_seconds
66
+ final_config["raw"] = {v: config_dict[k] for k, v in CONFIG_KEY_MAP.items() if k in config_dict}
67
+
68
+ return final_config
69
+
70
+
71
+ if __name__ == "__main__":
72
+ import sys
73
+
74
+ with open(sys.argv[1], "rb") as f:
75
+ print(extract_config(f.read()))
@@ -91,7 +91,7 @@ def chacha20_stream(key, nonce, length, blocknum):
91
91
 
92
92
 
93
93
  def decrypt_config(data):
94
- decrypted_config = b"\x21\x52\x48\x59"
94
+ decrypted_config = b""
95
95
  data_len = len(data)
96
96
  v3 = 0
97
97
  while True:
@@ -118,40 +118,119 @@ def chacha20_xor(custom_b64_decoded, key, nonce):
118
118
  return xor_key
119
119
 
120
120
 
121
- def extract_strings(data, minchars, maxchars):
122
- apat = b"([\x20-\x7e]{" + str(minchars).encode() + b"," + str(maxchars).encode() + b"})\x00"
123
- strings = [string.decode() for string in re.findall(apat, data)]
124
- match = re.search(apat, data)
125
- if not match:
126
- return None
127
- upat = b"((?:[\x20-\x7e][\x00]){" + str(minchars).encode() + b"," + str(maxchars).encode() + b"})\x00\x00"
128
- strings.extend(str(ws.decode("utf-16le")) for ws in re.findall(upat, data))
121
+ def extract_base64_strings(data, minchars, maxchars):
122
+ apat = b"([A-Za-z0-9-|]{" + str(minchars).encode() + b"," + str(maxchars).encode() + b"})\x00"
123
+ strings = [s.decode() for s in re.findall(apat, data)]
124
+ upat = b"((?:[A-Za-z0-9-|]\x00){" + str(minchars).encode() + b"," + str(maxchars).encode() + b"})\x00\x00"
125
+ strings.extend(ws.decode("utf-16le") for ws in re.findall(upat, data))
129
126
  return strings
130
127
 
131
128
 
132
129
  def extract_c2_url(data):
133
130
  pattern = b"(http[\x20-\x7e]+)\x00"
134
131
  match = re.search(pattern, data)
135
- return match.group(1).decode()
132
+ if match:
133
+ return match.group(1).decode()
136
134
 
137
135
 
138
- def is_potential_custom_base64(string):
139
- custom_alphabet = "ABC1fghijklmnop234NOPQRSTUVWXY567DEFGHIJKLMZ089abcdeqrstuvwxyz-|"
140
- for c in string:
141
- if c not in custom_alphabet:
142
- return False
143
- return True
144
-
145
-
146
- def custom_b64decode(data):
136
+ def custom_b64decode(data: bytes, custom_alphabet: bytes):
147
137
  """Decodes base64 data using a custom alphabet."""
148
138
  standard_alphabet = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
149
- custom_alphabet = b"ABC1fghijklmnop234NOPQRSTUVWXY567DEFGHIJKLMZ089abcdeqrstuvwxyz-|"
150
139
  # Translate the data back to the standard alphabet before decoding
151
140
  table = bytes.maketrans(custom_alphabet, standard_alphabet)
152
141
  return base64.b64decode(data.translate(table), validate=True)
153
142
 
154
143
 
144
+ def lzo_noheader_decompress(data: bytes, decompressed_size: int):
145
+ src = 0
146
+ dst = bytearray()
147
+ length = len(data)
148
+
149
+ while src < length:
150
+ ctrl = data[src]
151
+ src += 1
152
+
153
+ if ctrl == 0x20:
154
+ match_len = data[src]
155
+ src += 1
156
+ start = len(dst) - match_len - 1
157
+ end = start + 3
158
+ #print(f"Control code: {hex(ctrl)}, Offset backtrack length: {hex(match_len)}, Current offset: {hex(len(dst))}, New offset: {hex(start)}")
159
+ dst.extend(dst[start:end])
160
+
161
+ elif ctrl >= 0xE0 or ctrl == 0x40:
162
+ x = ((ctrl >> 5) - 1) + 3
163
+ if ctrl >= 0xE0:
164
+ copy_len = data[src] + x
165
+ elif ctrl == 0x40:
166
+ copy_len = x
167
+ start = data[src + 1]
168
+ if ctrl == 0x40:
169
+ start = data[src]
170
+ if ctrl >= 0xE0:
171
+ src += 2
172
+ elif ctrl == 0x40:
173
+ src += 1
174
+ offset = len(dst) - start - 1
175
+ #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)}")
176
+ dst.extend(dst[offset:offset+copy_len])
177
+
178
+ else:
179
+ # Literal run
180
+ literal_len = (ctrl & 0x1F) + 1
181
+ #print(f"Control code: {hex(ctrl)}, Literal length: {hex(literal_len)}")
182
+ dst.extend(data[src:src+literal_len])
183
+ src += literal_len
184
+
185
+ if len(dst) == decompressed_size:
186
+ return bytes(dst)
187
+
188
+
189
+ def parse_compression_header(config: bytes):
190
+ """Parse compressed size, decompressed size, and data offset from config"""
191
+
192
+ # 0x2A when looking at the config in memory
193
+ base_offset = 0x26
194
+
195
+ # Compressed data offset field, for calculating the offset to the compressed buffer
196
+ comp_offset_field = config[base_offset]
197
+ # Number of bytes the field spans
198
+ comp_offset_size_len = (comp_offset_field & 3) + 1
199
+ for i in range(1, comp_offset_size_len):
200
+ comp_offset_field |= config[base_offset + i] << (8 * i)
201
+
202
+ comp_size_offset = comp_offset_field >> 2
203
+
204
+ # Compressed size field, for finding the size of the compressed buffer
205
+ comp_offset = base_offset + comp_offset_size_len
206
+ comp_size_field = config[comp_offset]
207
+ # Number of bytes the field spans
208
+ comp_size_len = (comp_size_field & 3) + 1
209
+ for i in range(1, comp_size_len):
210
+ comp_size_field |= config[comp_offset + i] << (8 * i)
211
+
212
+ # Decompressed size field
213
+ decomp_field_offset = base_offset + comp_offset_size_len + comp_size_len
214
+ decomp_size_field = config[decomp_field_offset]
215
+ # Number of bytes the field spans
216
+ decomp_field_len = (decomp_size_field & 3) + 1
217
+ for i in range(1, decomp_field_len):
218
+ decomp_size_field |= config[decomp_field_offset + i] << (8 * i)
219
+
220
+ # Calculate return values
221
+ decompressed_size = decomp_size_field >> 2
222
+ compressed_data_offset = decomp_field_offset + decomp_field_len + comp_size_offset
223
+ compressed_size_key = config[0x28] << 8
224
+ compressed_size = (compressed_size_key | comp_size_field) >> 2
225
+ compressed_data = config[compressed_data_offset : compressed_data_offset + compressed_size]
226
+
227
+ return {
228
+ "compressed_size": compressed_size,
229
+ "decompressed_size": decompressed_size,
230
+ "compressed_data": compressed_data
231
+ }
232
+
233
+
155
234
  def extract_config(data):
156
235
  config_dict = {}
157
236
  magic = struct.unpack("I", data[:4])[0]
@@ -162,25 +241,52 @@ def extract_config(data):
162
241
  key = b"\x52\xAB\xDF\x06\xB6\xB1\x3A\xC0\xDA\x2D\x22\xDC\x6C\xD2\xBE\x6C\x20\x17\x69\xE0\x12\xB5\xE6\xEC\x0E\xAB\x4C\x14\x73\x4A\xED\x51"
163
242
  nonce = b"\x5F\x14\xD7\x9C\xFC\xFC\x43\x9E\xC3\x40\x6B\xBA"
164
243
 
165
- extracted_strings = extract_strings(data, 0x100, 0x100)
244
+ custom_alphabets = [
245
+ b"ABC1fghijklmnop234NOPQRSTUVWXY567DEFGHIJKLMZ089abcdeqrstuvwxyz-|",
246
+ b"4NOPQRSTUVWXY567DdeEqrstuvwxyz-ABC1fghop23Fijkbc|lmnGHIJKLMZ089a"
247
+ ]
248
+
249
+ # Extract base64 strings
250
+ extracted_strings = extract_base64_strings(data, 140, 256)
251
+ if not extracted_strings:
252
+ return config_dict
253
+
254
+ pattern = re.compile(b'.\x80')
166
255
  for string in extracted_strings:
167
256
  try:
168
- if not is_potential_custom_base64(string):
169
- continue
257
+ custom_b64_decoded = custom_b64decode(string, custom_alphabets[0])
170
258
 
171
- custom_b64_decoded = custom_b64decode(string)
172
259
  xor_key = chacha20_xor(custom_b64_decoded, key, nonce)
173
- decrypted_config = decrypt_config(xor_key)
174
- reexecution_delay = int.from_bytes(decrypted_config[5:7], byteorder="little")
175
-
176
- c2_url = extract_c2_url(decrypted_config)
177
- if not c2_url:
178
- continue
179
- config_dict = {"raw": {"Reexecution_delay": reexecution_delay}, "CNCs": [c2_url]}
180
- return config_dict
260
+
261
+ # Decrypted, but may still be the compressed malware configuration
262
+ config = decrypt_config(xor_key)
263
+ # Attempt to extract C2 url, only works in version prior to 0.9.2
264
+ c2_url = extract_c2_url(config)
265
+ if c2_url:
266
+ config_dict = {"CNCs": [c2_url]}
267
+ return config_dict
268
+ else:
269
+ # Handle new variants that compress the Command and Control server(s)
270
+ custom_b64_decoded = custom_b64decode(string, custom_alphabets[1])
271
+ xor_key = chacha20_xor(custom_b64_decoded, key, nonce)
272
+ config = decrypt_config(xor_key)
273
+
274
+ parsed = parse_compression_header(config)
275
+ if not parsed:
276
+ return config_dict
277
+
278
+ decompressed = lzo_noheader_decompress(parsed['compressed_data'], parsed['decompressed_size'])
279
+
280
+ cncs = [f"https://{chunk.decode()}" for chunk in pattern.split(decompressed) if chunk]
281
+ if cncs:
282
+ config_dict = {"CNCs": cncs}
283
+ return config_dict
284
+
181
285
  except Exception:
182
286
  continue
183
287
 
288
+ return config_dict
289
+
184
290
 
185
291
  if __name__ == "__main__":
186
292
  import sys
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: CAPE-parsers
3
- Version: 0.1.51
3
+ Version: 0.1.53
4
4
  Summary: CAPE: Malware Configuration Extraction
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -12,7 +12,8 @@ cape_parsers/CAPE/community/Fareit.py,sha256=OyKeZdcvyAhjxZgJqkDPJHP4Npv1ArvTHJZ
12
12
  cape_parsers/CAPE/community/KoiLoader.py,sha256=F2gsgCvrVuwxY1bg8rlexsjCjikAP5HIGGOqU8zhT8E,4008
13
13
  cape_parsers/CAPE/community/LokiBot.py,sha256=355kqLx0LNMr8XcGfPL7cxG8QZalcmE7ttVBqoWtTWE,5754
14
14
  cape_parsers/CAPE/community/Lumma.py,sha256=Iqd9yvt3g0FeV_bYRmL1RKp4C1H92qeGg4fXivVDSxw,12206
15
- cape_parsers/CAPE/community/MonsterV2.py,sha256=eVEs4VIeS3PiZtRjNb69itmDq2Zkbrpn5k3M68GujiI,2995
15
+ cape_parsers/CAPE/community/MonsterV2.py,sha256=cFxhYxo7FruTMmFY3OtBO-E0hDyxfsC3zWX3BlcB-qI,2915
16
+ cape_parsers/CAPE/community/MyKings.py,sha256=bcypBMJf6Jeg0yrHZ-J-XjnhHvFbb-lpDF_zwW63gOk,1397
16
17
  cape_parsers/CAPE/community/NanoCore.py,sha256=8QZnf1AcY9481kSfsf3SHQShwPLn97peGAf8_xEasQc,6230
17
18
  cape_parsers/CAPE/community/Nighthawk.py,sha256=8ss8yvslrwUt53zV6U0xuwGKU3hgYfOt13S5lkOVpNo,12105
18
19
  cape_parsers/CAPE/community/Njrat.py,sha256=GiwSENBB43RUqyJ7zT7ZPkPUYqo8Ew4kd5MJUj0jzdc,4702
@@ -23,6 +24,7 @@ cape_parsers/CAPE/community/Snake.py,sha256=v_MAPmg86ZdgGOkzc9GVHbi-lu4nLa1_0Lp9
23
24
  cape_parsers/CAPE/community/SparkRAT.py,sha256=OVDty_1i9PTGuEumT0BHoDn0bD2UtdhHVNjThah80pg,2140
24
25
  cape_parsers/CAPE/community/Stealc.py,sha256=18EkQ-lMMAreKV5vA9xLBmOK5B4JtYcBwVqNfof4K2A,5321
25
26
  cape_parsers/CAPE/community/VenomRAT.py,sha256=0-FRT3d2x63KQ_cs1xmKFj7x0JRf7ID6QDc_DvBa0PM,1003
27
+ cape_parsers/CAPE/community/WinosStager.py,sha256=6jkfTJiVdz6oyyqTjh3I4qunYgXbD4qXS_bd-uogwGA,2407
26
28
  cape_parsers/CAPE/community/XWorm.py,sha256=0-FRT3d2x63KQ_cs1xmKFj7x0JRf7ID6QDc_DvBa0PM,1003
27
29
  cape_parsers/CAPE/community/XenoRAT.py,sha256=0-FRT3d2x63KQ_cs1xmKFj7x0JRf7ID6QDc_DvBa0PM,1003
28
30
  cape_parsers/CAPE/community/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -50,7 +52,7 @@ cape_parsers/CAPE/core/Quickbind.py,sha256=5A077RFQQOL8dtr2Q9vmlTKsWk96JkRWuHGse
50
52
  cape_parsers/CAPE/core/README.md,sha256=Zd84WEUj9NzKzGnVZV1jx6gMiEOtz01m32B7xEuS91k,17
51
53
  cape_parsers/CAPE/core/RedLine.py,sha256=bZeKLvxaS6HDpWY4RDXtSEBt93qTNzZG5iE6FNS0dOY,5734
52
54
  cape_parsers/CAPE/core/Remcos.py,sha256=MIpO2FwehBGIhO7hS0TT2hdDsgvxlI5ps4rAwyFwdTY,9483
53
- cape_parsers/CAPE/core/Rhadamanthys.py,sha256=mx7kEF1e8LJZbwh2uUwU56ZKgrpLqZvYVDoqm-Dvl9w,6075
55
+ cape_parsers/CAPE/core/Rhadamanthys.py,sha256=E6cPVMjNxIOypY-gPwAJJjyt-Wf8PON7fawiLicjuM8,10197
54
56
  cape_parsers/CAPE/core/SmokeLoader.py,sha256=ruQ_GDiZvqtGxUTbN2N6fajUYWkIylFTvMXijgZ8L20,3890
55
57
  cape_parsers/CAPE/core/Socks5Systemz.py,sha256=jSt6QejL5K99dIB3qdItvUHL28w6N60xuwc8EQHM5Mk,783
56
58
  cape_parsers/CAPE/core/SquirrelWaffle.py,sha256=UMha7l60fL64VPHxueFUnCEGaO-CXau5ftEyK-Wv__o,3308
@@ -108,7 +110,7 @@ cape_parsers/utils/blzpack_lib.so,sha256=5PJtnggw8fV5q4DlhwMJk4ZadvC3fFTsVTNZKvE
108
110
  cape_parsers/utils/dotnet_utils.py,sha256=pzQGbCqccz7DRv8T_i1JURlrKDIlDT2axxViiFF9hsU,1672
109
111
  cape_parsers/utils/lznt1.py,sha256=X-BmJtP6AwYSl0ORg5dfSt-NIuXbHrtCO5kUaaJI2C8,4066
110
112
  cape_parsers/utils/strings.py,sha256=a-nbvP9jYST7b6t_H37Ype-fK2jEmQr-wMF5a4i04e4,3062
111
- cape_parsers-0.1.51.dist-info/METADATA,sha256=vSKRRgEfohGbql5yBH3B_8mkBBWfhBNutBqb1xQ_jaE,1826
112
- cape_parsers-0.1.51.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
113
- cape_parsers-0.1.51.dist-info/licenses/LICENSE,sha256=88c01_HLG8WPj7R7aU_b-O-UoF38vrrifvcko4KDxcE,1069
114
- cape_parsers-0.1.51.dist-info/RECORD,,
113
+ cape_parsers-0.1.53.dist-info/METADATA,sha256=4wgKctXg8lDMexjtTcSmqckuDONZhIbGJHjk4SPNEMg,1826
114
+ cape_parsers-0.1.53.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
115
+ cape_parsers-0.1.53.dist-info/licenses/LICENSE,sha256=88c01_HLG8WPj7R7aU_b-O-UoF38vrrifvcko4KDxcE,1069
116
+ cape_parsers-0.1.53.dist-info/RECORD,,