CAPE-parsers 0.1.52__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.
@@ -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.52
3
+ Version: 0.1.53
4
4
  Summary: CAPE: Malware Configuration Extraction
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -52,7 +52,7 @@ cape_parsers/CAPE/core/Quickbind.py,sha256=5A077RFQQOL8dtr2Q9vmlTKsWk96JkRWuHGse
52
52
  cape_parsers/CAPE/core/README.md,sha256=Zd84WEUj9NzKzGnVZV1jx6gMiEOtz01m32B7xEuS91k,17
53
53
  cape_parsers/CAPE/core/RedLine.py,sha256=bZeKLvxaS6HDpWY4RDXtSEBt93qTNzZG5iE6FNS0dOY,5734
54
54
  cape_parsers/CAPE/core/Remcos.py,sha256=MIpO2FwehBGIhO7hS0TT2hdDsgvxlI5ps4rAwyFwdTY,9483
55
- cape_parsers/CAPE/core/Rhadamanthys.py,sha256=mx7kEF1e8LJZbwh2uUwU56ZKgrpLqZvYVDoqm-Dvl9w,6075
55
+ cape_parsers/CAPE/core/Rhadamanthys.py,sha256=E6cPVMjNxIOypY-gPwAJJjyt-Wf8PON7fawiLicjuM8,10197
56
56
  cape_parsers/CAPE/core/SmokeLoader.py,sha256=ruQ_GDiZvqtGxUTbN2N6fajUYWkIylFTvMXijgZ8L20,3890
57
57
  cape_parsers/CAPE/core/Socks5Systemz.py,sha256=jSt6QejL5K99dIB3qdItvUHL28w6N60xuwc8EQHM5Mk,783
58
58
  cape_parsers/CAPE/core/SquirrelWaffle.py,sha256=UMha7l60fL64VPHxueFUnCEGaO-CXau5ftEyK-Wv__o,3308
@@ -110,7 +110,7 @@ cape_parsers/utils/blzpack_lib.so,sha256=5PJtnggw8fV5q4DlhwMJk4ZadvC3fFTsVTNZKvE
110
110
  cape_parsers/utils/dotnet_utils.py,sha256=pzQGbCqccz7DRv8T_i1JURlrKDIlDT2axxViiFF9hsU,1672
111
111
  cape_parsers/utils/lznt1.py,sha256=X-BmJtP6AwYSl0ORg5dfSt-NIuXbHrtCO5kUaaJI2C8,4066
112
112
  cape_parsers/utils/strings.py,sha256=a-nbvP9jYST7b6t_H37Ype-fK2jEmQr-wMF5a4i04e4,3062
113
- cape_parsers-0.1.52.dist-info/METADATA,sha256=c354f-gNdv6cYW0XkKCvLw0tjVbE8flYpLsW1TrS7zw,1826
114
- cape_parsers-0.1.52.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
115
- cape_parsers-0.1.52.dist-info/licenses/LICENSE,sha256=88c01_HLG8WPj7R7aU_b-O-UoF38vrrifvcko4KDxcE,1069
116
- cape_parsers-0.1.52.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,,