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.
Files changed (88) hide show
  1. cape_parsers/CAPE/community/AgentTesla.py +25 -10
  2. cape_parsers/CAPE/community/Amadey.py +199 -29
  3. cape_parsers/CAPE/community/Arkei.py +13 -15
  4. cape_parsers/CAPE/community/AsyncRAT.py +4 -2
  5. cape_parsers/CAPE/community/AuroraStealer.py +9 -6
  6. cape_parsers/CAPE/community/Carbanak.py +7 -7
  7. cape_parsers/CAPE/community/CobaltStrikeBeacon.py +5 -4
  8. cape_parsers/CAPE/community/CobaltStrikeStager.py +4 -1
  9. cape_parsers/CAPE/community/DCRat.py +4 -2
  10. cape_parsers/CAPE/community/Fareit.py +8 -9
  11. cape_parsers/CAPE/community/KoiLoader.py +3 -3
  12. cape_parsers/CAPE/community/LokiBot.py +11 -8
  13. cape_parsers/CAPE/community/Lumma.py +58 -40
  14. cape_parsers/CAPE/community/MonsterV2.py +93 -0
  15. cape_parsers/CAPE/community/MyKings.py +52 -0
  16. cape_parsers/CAPE/community/NanoCore.py +9 -9
  17. cape_parsers/CAPE/community/Nighthawk.py +1 -0
  18. cape_parsers/CAPE/community/Njrat.py +4 -4
  19. cape_parsers/CAPE/community/PhemedroneStealer.py +2 -0
  20. cape_parsers/CAPE/community/Snake.py +31 -18
  21. cape_parsers/CAPE/community/SparkRAT.py +3 -1
  22. cape_parsers/CAPE/community/Stealc.py +95 -63
  23. cape_parsers/CAPE/community/VenomRAT.py +4 -2
  24. cape_parsers/CAPE/community/WinosStager.py +75 -0
  25. cape_parsers/CAPE/community/XWorm.py +4 -2
  26. cape_parsers/CAPE/community/XenoRAT.py +4 -2
  27. cape_parsers/CAPE/core/AdaptixBeacon.py +7 -5
  28. cape_parsers/CAPE/core/AuraStealer.py +100 -0
  29. cape_parsers/CAPE/core/Azorult.py +5 -3
  30. cape_parsers/CAPE/core/BitPaymer.py +5 -2
  31. cape_parsers/CAPE/core/BlackDropper.py +10 -5
  32. cape_parsers/CAPE/core/Blister.py +12 -10
  33. cape_parsers/CAPE/core/BruteRatel.py +20 -7
  34. cape_parsers/CAPE/core/BumbleBee.py +34 -22
  35. cape_parsers/CAPE/core/DarkGate.py +3 -3
  36. cape_parsers/CAPE/core/DoppelPaymer.py +4 -2
  37. cape_parsers/CAPE/core/DridexLoader.py +4 -3
  38. cape_parsers/CAPE/core/Formbook.py +2 -2
  39. cape_parsers/CAPE/core/GuLoader.py +2 -5
  40. cape_parsers/CAPE/core/IcedID.py +5 -5
  41. cape_parsers/CAPE/core/IcedIDLoader.py +4 -4
  42. cape_parsers/CAPE/core/Latrodectus.py +14 -10
  43. cape_parsers/CAPE/core/NitroBunnyDownloader.py +151 -0
  44. cape_parsers/CAPE/core/Oyster.py +8 -6
  45. cape_parsers/CAPE/core/PikaBot.py +6 -6
  46. cape_parsers/CAPE/core/PlugX.py +3 -1
  47. cape_parsers/CAPE/core/QakBot.py +2 -1
  48. cape_parsers/CAPE/core/Quickbind.py +7 -11
  49. cape_parsers/CAPE/core/RedLine.py +2 -2
  50. cape_parsers/CAPE/core/Remcos.py +59 -51
  51. cape_parsers/CAPE/core/Rhadamanthys.py +175 -36
  52. cape_parsers/CAPE/core/SmokeLoader.py +2 -2
  53. cape_parsers/CAPE/core/Socks5Systemz.py +5 -5
  54. cape_parsers/CAPE/core/SquirrelWaffle.py +3 -3
  55. cape_parsers/CAPE/core/Strrat.py +1 -1
  56. cape_parsers/CAPE/core/WarzoneRAT.py +3 -2
  57. cape_parsers/CAPE/core/Zloader.py +21 -15
  58. cape_parsers/RATDecoders/test_rats.py +1 -0
  59. cape_parsers/__init__.py +14 -5
  60. cape_parsers/deprecated/BlackNix.py +59 -0
  61. cape_parsers/{CAPE/core → deprecated}/BuerLoader.py +1 -1
  62. cape_parsers/{CAPE/core → deprecated}/ChChes.py +3 -3
  63. cape_parsers/{CAPE/core → deprecated}/Enfal.py +1 -1
  64. cape_parsers/{CAPE/core → deprecated}/EvilGrab.py +5 -6
  65. cape_parsers/{CAPE/community → deprecated}/Greame.py +3 -1
  66. cape_parsers/{CAPE/core → deprecated}/HttpBrowser.py +7 -8
  67. cape_parsers/{CAPE/community → deprecated}/Pandora.py +2 -0
  68. cape_parsers/{CAPE/community → deprecated}/Punisher.py +2 -1
  69. cape_parsers/{CAPE/core → deprecated}/RCSession.py +7 -9
  70. cape_parsers/{CAPE/community → deprecated}/REvil.py +10 -5
  71. cape_parsers/{CAPE/core → deprecated}/RedLeaf.py +5 -7
  72. cape_parsers/{CAPE/community → deprecated}/Retefe.py +0 -2
  73. cape_parsers/{CAPE/community → deprecated}/Rozena.py +2 -5
  74. cape_parsers/{CAPE/community → deprecated}/SmallNet.py +6 -2
  75. {cape_parsers-0.1.42.dist-info → cape_parsers-0.1.54.dist-info}/METADATA +24 -3
  76. cape_parsers-0.1.54.dist-info/RECORD +117 -0
  77. {cape_parsers-0.1.42.dist-info → cape_parsers-0.1.54.dist-info}/WHEEL +1 -1
  78. cape_parsers/CAPE/community/BlackNix.py +0 -57
  79. cape_parsers/CAPE/core/Stealc.py +0 -21
  80. cape_parsers-0.1.42.dist-info/RECORD +0 -113
  81. /cape_parsers/{CAPE/community → deprecated}/BackOffLoader.py +0 -0
  82. /cape_parsers/{CAPE/community → deprecated}/BackOffPOS.py +0 -0
  83. /cape_parsers/{CAPE/core → deprecated}/Emotet.py +0 -0
  84. /cape_parsers/{CAPE/community → deprecated}/PoisonIvy.py +0 -0
  85. /cape_parsers/{CAPE/community → deprecated}/TSCookie.py +0 -0
  86. /cape_parsers/{CAPE/community → deprecated}/TrickBot.py +0 -0
  87. /cape_parsers/{CAPE/core → deprecated}/UrsnifV3.py +0 -0
  88. {cape_parsers-0.1.42.dist-info → cape_parsers-0.1.54.dist-info/licenses}/LICENSE +0 -0
@@ -2,22 +2,35 @@ from contextlib import suppress
2
2
 
3
3
 
4
4
  def extract_config(data):
5
- config_dict = {}
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
- config_dict["User Agent"] = line
13
- config_dict["C2"] = list(set(lines[i - 2].split(",")))
14
- config_dict["Port"] = lines[i - 1]
15
- config_dict["URI"] = lines[i + 3].split(",")
16
- config_dict["Keys"] = [lines[i + 1], lines[i + 2]]
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 config_dict
33
+ return config
21
34
 
22
35
 
23
36
  if __name__ == "__main__":
@@ -6,6 +6,7 @@ import traceback
6
6
  from contextlib import suppress
7
7
 
8
8
  import pefile
9
+
9
10
  # import regex as re
10
11
  # test
11
12
  import re
@@ -49,7 +50,7 @@ def extract_key_data(data, pe, key_match):
49
50
  # Read arbitrary number of byes from key offset and split on null bytes to extract key
50
51
  key = data[key_offset : key_offset + 0x40].split(b"\x00")[0]
51
52
  except Exception as e:
52
- log.debug(f"There was an exception extracting the key: {e}")
53
+ log.debug("There was an exception extracting the key: %s", str(e))
53
54
  log.debug(traceback.format_exc())
54
55
  return False
55
56
  return key
@@ -69,7 +70,7 @@ def extract_config_data(data, pe, config_match):
69
70
  )
70
71
  campaign_id_ct = data[campaign_id_offset : campaign_id_offset + 0x10]
71
72
  except Exception as e:
72
- log.debug(f"There was an exception extracting the campaign id: {e}")
73
+ log.debug("There was an exception extracting the campaign id: %s", str(e))
73
74
  log.debug(traceback.format_exc())
74
75
  return False, False, False
75
76
 
@@ -83,7 +84,7 @@ def extract_config_data(data, pe, config_match):
83
84
  )
84
85
  botnet_id_ct = data[botnet_id_offset : botnet_id_offset + 0x10]
85
86
  except Exception as e:
86
- log.debug(f"There was an exception extracting the botnet id: {e}")
87
+ log.debug("There was an exception extracting the botnet id: %s", str(e))
87
88
  log.debug(traceback.format_exc())
88
89
  return False, False, False
89
90
 
@@ -98,7 +99,7 @@ def extract_config_data(data, pe, config_match):
98
99
  c2s_offset = pe.get_offset_from_rva(c2s_rva + int.from_bytes(config_match.group("c2s"), byteorder="little"))
99
100
  c2s_ct = data[c2s_offset : c2s_offset + 0x400]
100
101
  except Exception as e:
101
- log.debug(f"There was an exception extracting the C2s: {e}")
102
+ log.debug("There was an exception extracting the C2s: %s", str(e))
102
103
  log.debug(traceback.format_exc())
103
104
  return False, False, False
104
105
 
@@ -106,7 +107,7 @@ def extract_config_data(data, pe, config_match):
106
107
 
107
108
 
108
109
  def extract_2024(pe, filebuf):
109
- cfg = {}
110
+ config = {}
110
111
  rc4key_init_offset = 0
111
112
  botid_init_offset = 0
112
113
  port_init_offset = 0
@@ -142,45 +143,55 @@ def extract_2024(pe, filebuf):
142
143
  key_offset = pe.get_dword_from_offset(rc4key_init_offset + 57)
143
144
  key_rva = pe.get_rva_from_offset(rc4key_init_offset + 61) + key_offset
144
145
  key = pe.get_string_at_rva(key_rva)
145
- cfg["RC4 key"] = key.decode()
146
146
 
147
+ botid = ""
147
148
  botid_offset = pe.get_dword_from_offset(botid_init_offset + 51)
148
149
  botid_rva = pe.get_rva_from_offset(botid_init_offset + 55) + botid_offset
149
150
  botid_len_offset = pe.get_dword_from_offset(botidlgt_init_offset + 31)
150
151
  botid_data = pe.get_data(botid_rva)[:botid_len_offset]
151
152
  with suppress(Exception):
152
153
  botid = ARC4.new(key).decrypt(botid_data).split(b"\x00")[0].decode()
153
- cfg["Botid"] = botid
154
154
 
155
+ port = ""
155
156
  port_offset = pe.get_dword_from_offset(port_init_offset + 23)
156
157
  port_rva = pe.get_rva_from_offset(port_init_offset + 27) + port_offset
157
158
  port_len_offset = pe.get_dword_from_offset(botidlgt_init_offset + 4)
158
159
  port_data = pe.get_data(port_rva)[:port_len_offset]
159
160
  with suppress(Exception):
160
161
  port = ARC4.new(key).decrypt(port_data).split(b"\x00")[0].decode()
161
- cfg["Port"] = port
162
162
 
163
163
  dgaseed_offset = pe.get_dword_from_offset(dga1_init_offset + 15)
164
164
  dgaseed_rva = pe.get_rva_from_offset(dga1_init_offset + 19) + dgaseed_offset
165
165
  dgaseed_data = pe.get_qword_at_rva(dgaseed_rva)
166
- cfg["DGA seed"] = str(int(dgaseed_data))
167
166
 
168
167
  numdga_offset = pe.get_dword_from_offset(dga1_init_offset + 22)
169
168
  numdga_rva = pe.get_rva_from_offset(dga1_init_offset + 26) + numdga_offset
170
169
  numdga_data = pe.get_string_at_rva(numdga_rva)
171
- cfg["Number DGA domains"] = numdga_data.decode()
172
170
 
173
171
  domainlen_offset = pe.get_dword_from_offset(dga2_init_offset + 3)
174
172
  domainlen_rva = pe.get_rva_from_offset(dga2_init_offset + 7) + domainlen_offset
175
173
  domainlen_data = pe.get_string_at_rva(domainlen_rva)
176
- cfg["Domain length"] = domainlen_data.decode()
177
174
 
178
175
  tld_offset = pe.get_dword_from_offset(dga2_init_offset + 37)
179
176
  tld_rva = pe.get_rva_from_offset(dga2_init_offset + 41) + tld_offset
180
177
  tld_data = pe.get_string_at_rva(tld_rva).decode()
181
- cfg["TLD"] = tld_data
182
178
 
183
- return cfg
179
+ config = {
180
+ "dga_seed": str(int(dgaseed_data)),
181
+ "cryptokey": key.decode(),
182
+ "cryptokey_type": "RC4",
183
+ "raw": {
184
+ "TLD": tld_data,
185
+ "Domain length": domainlen_data.decode(),
186
+ "Number DGA domains": numdga_data.decode(),
187
+ },
188
+ }
189
+ if port:
190
+ config["raw"]["port"] = port
191
+ if botid:
192
+ config["botnet"] = botid
193
+
194
+ return config
184
195
 
185
196
 
186
197
  def extract_config(data):
@@ -209,29 +220,30 @@ def extract_config(data):
209
220
  if not key:
210
221
  continue
211
222
  if index == 0:
212
- cfg["Botnet ID"] = key.decode()
223
+ cfg["botnet"] = key.decode()
213
224
  elif index == 1:
214
- cfg["Campaign ID"] = key.decode()
225
+ cfg["campaign"] = key.decode()
215
226
  elif index == 2:
216
- cfg["Data"] = key.decode("latin-1")
227
+ cfg.setdefault("raw", {})["Data"] = key.decode("latin-1")
217
228
  elif index == 3:
218
- cfg["C2s"] = list(key.decode().split(","))
229
+ cfg["CNCs"] = list(key.decode().split(","))
219
230
  elif len(key_match) == 1:
220
231
  key = extract_key_data(data, pe, key_match[0])
221
232
  if not key:
222
233
  return cfg
223
- cfg["RC4 Key"] = key.decode()
234
+ cfg["cryptokey"] = key.decode()
235
+ cfg["cryptokey_type"] = "RC4"
224
236
  # Extract config ciphertext
225
237
  config_match = regex.search(data)
226
238
  campaign_id, botnet_id, c2s = extract_config_data(data, pe, config_match)
227
239
  if campaign_id:
228
- cfg["Campaign ID"] = ARC4.new(key).decrypt(campaign_id).split(b"\x00")[0].decode()
240
+ cfg["campaign"] = ARC4.new(key).decrypt(campaign_id).split(b"\x00")[0].decode()
229
241
  if botnet_id:
230
- cfg["Botnet ID"] = ARC4.new(key).decrypt(botnet_id).split(b"\x00")[0].decode()
242
+ cfg["botnet"] = ARC4.new(key).decrypt(botnet_id).split(b"\x00")[0].decode()
231
243
  if c2s:
232
- cfg["C2s"] = list(ARC4.new(key).decrypt(c2s).split(b"\x00")[0].decode().split(","))
244
+ cfg["CNCs"] = list(ARC4.new(key).decrypt(c2s).split(b"\x00")[0].decode().split(","))
233
245
  except Exception as e:
234
- log.error("This is broken: %s", str(e), exc_info=True)
246
+ log.exception("This is broken: %s", str(e))
235
247
 
236
248
  if not cfg:
237
249
  cfg = extract_2024(pe, data)
@@ -70,7 +70,7 @@ def parse_config(data, conf_map):
70
70
  except KeyError:
71
71
  config[f"unknown_{k}"] = v
72
72
 
73
- return config
73
+ return {"raw": config}
74
74
 
75
75
 
76
76
  def decode(data):
@@ -81,7 +81,7 @@ def decode(data):
81
81
  strval = translate_string(strval.decode("utf-8"))
82
82
  decoded_str = base64.b64decode(strval)
83
83
  if decoded_str.startswith(b"http"):
84
- config["C2"] = [x for x in decoded_str.decode("utf-8").split("|") if x.strip() != ""]
84
+ config["CNCs"] = [x for x in decoded_str.decode("utf-8").split("|") if x.strip() != ""]
85
85
  elif b"1=Yes" in decoded_str or b"1=No" in decoded_str:
86
86
  config.update(parse_config(decoded_str, config_map_1))
87
87
  else:
@@ -102,7 +102,7 @@ def extract_config(data):
102
102
  config = {}
103
103
  for item in data.split(b"\r\n")[:-1]:
104
104
  if item.startswith(b"0="):
105
- config["C2"] = [x for x in item[2:].decode("utf-8").split("|") if x.strip() != ""]
105
+ config["CNCs"] = [x for x in item[2:].decode("utf-8").split("|") if x.strip() != ""]
106
106
  else:
107
107
  config.update(parse_config(item, config_map_2))
108
108
  return config
@@ -72,7 +72,9 @@ def extract_config(filebuf):
72
72
  for item in raw.split(b"\x00"):
73
73
  data = "".join(convert_char(c) for c in item)
74
74
  if len(data) == 406:
75
- config["RSA public key"] = data
75
+ config.setdefault("cryptokey", data)
76
+ # ToDO proper naming here
77
+ config.setdefault("raw", {})["cryptokey_type"] = "RSA public key"
76
78
  elif len(data) > 1 and "\\x" not in data:
77
- config["strings"] = data
79
+ config.setdefault("raw", {})["strings"] = data
78
80
  return config
@@ -132,7 +132,7 @@ def extract_config(filebuf):
132
132
  port = str(struct.unpack("H", filebuf[c2_offset + 4 : c2_offset + 6])[0])
133
133
 
134
134
  if c2_address and port:
135
- cfg.setdefault("address", []).append(f"{c2_address}:{port}")
135
+ cfg.setdefault("CNCs", []).append(f"{c2_address}:{port}")
136
136
 
137
137
  c2_offset += 6 + delta
138
138
 
@@ -155,7 +155,8 @@ def extract_config(filebuf):
155
155
  )
156
156
  for item in raw.split(b"\x00"):
157
157
  if len(item) == LEN_BLOB_KEY - 1:
158
- cfg["RC4 key"] = item.split(b";", 1)[0].decode()
158
+ cfg["cryptokey"] = item.split(b";", 1)[0].decode()
159
+ cfg["cryptokey_type"] = "RC4"
159
160
 
160
161
  if botnet_code:
161
162
  botnet_rva = struct.unpack("i", filebuf[botnet_code + 23 : botnet_code + 27])[0] - image_base
@@ -163,7 +164,7 @@ def extract_config(filebuf):
163
164
  with suppress(struct.error):
164
165
  botnet_offset = pe.get_offset_from_rva(botnet_rva)
165
166
  botnet_id = struct.unpack("H", filebuf[botnet_offset : botnet_offset + 2])[0]
166
- cfg["Botnet ID"] = str(botnet_id)
167
+ cfg["botnet"] = str(botnet_id)
167
168
 
168
169
  return cfg
169
170
 
@@ -12,11 +12,11 @@ def extract_config(data):
12
12
  i += 1
13
13
  elif "www." not in lines[0]:
14
14
  return
15
- config_dict["C2"] = lines[i]
15
+ config_dict["CNCs"] = lines[i]
16
16
  decoys = []
17
17
  i += 1
18
18
  while len(lines[i]) > 0:
19
19
  decoys.append(lines[i])
20
20
  i += 1
21
- config_dict["Decoys"] = decoys
21
+ config_dict.setdefault("raw", {})["Decoys"] = decoys
22
22
  return config_dict
@@ -1,7 +1,4 @@
1
- try:
2
- import re2 as re
3
- except ImportError:
4
- import re
1
+ import re
5
2
 
6
3
  url_regex = re.compile(rb"http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+")
7
4
 
@@ -10,7 +7,7 @@ def extract_config(data):
10
7
  try:
11
8
  urls = [url.lower().decode() for url in url_regex.findall(data)]
12
9
  if urls:
13
- return {"URLs": urls}
10
+ return {"CNCs": urls}
14
11
  except Exception as e:
15
12
  print(e)
16
13
 
@@ -77,12 +77,12 @@ def extract_config(filebuf):
77
77
  decrypted_data = ARC4.new(key).decrypt(enc_config)
78
78
  config = list(filter(None, decrypted_data.split(b"\x00")))
79
79
  return {
80
- "family": "IcedID",
81
80
  "version": str(struct.unpack("I", decrypted_data[4:8])[0]),
82
- "paths": [{"path": config[1].decode(), "usage": "other"}],
83
- "http": [{"uri": controller[1:].decode()} for controller in config[2:]],
84
- "other": {
85
- "Bot ID": str(struct.unpack("I", decrypted_data[:4])[0]),
81
+ "botnet": str(struct.unpack("I", decrypted_data[:4])[0]),
82
+ "raw": {
83
+ "family": "IcedID",
84
+ "paths": [{"path": config[1].decode(), "usage": "other"}],
85
+ "http": [{"uri": controller[1:].decode()} for controller in config[2:]],
86
86
  },
87
87
  }
88
88
  except Exception as e:
@@ -19,7 +19,7 @@ import pefile
19
19
 
20
20
 
21
21
  def extract_config(filebuf):
22
- cfg = {}
22
+ config = {}
23
23
  pe = None
24
24
  with suppress(Exception):
25
25
  pe = pefile.PE(data=filebuf, fast_load=False)
@@ -35,9 +35,9 @@ def extract_config(filebuf):
35
35
  if n > 32:
36
36
  break
37
37
  campaign, c2 = struct.unpack("I30s", bytes(dec))
38
- cfg["C2"] = c2.split(b"\00", 1)[0].decode()
39
- cfg["Campaign"] = campaign
40
- return cfg
38
+ config["CNCs"] = c2.split(b"\00", 1)[0].decode()
39
+ config["campaign"] = campaign
40
+ return config
41
41
 
42
42
 
43
43
  if __name__ == "__main__":
@@ -41,7 +41,7 @@ rule Latrodectus
41
41
  $fnvhash2 = {8B 0C 24 33 C8 8B C1 89 04 24 69 04 24 93 01 00 01}
42
42
  $procchk1 = {E8 [3] FF 85 C0 74 [2] FF FF FF FF E9 [4] E8 [4] 89 44 24 ?? E8 [4] 83 F8 4B 73 ?? 83 [3] 06}
43
43
  $procchk2 = {72 [2] FF FF FF FF E9 [4] E8 [4] 83 F8 32 73 ?? 83 [3] 06}
44
- $version = {C7 44 2? ?? ?? 00 00 00 C7 44 2? ?? ?? 00 00 00 8B 05 [4] 89}
44
+ $version = {C7 44 2? ?? ?? 00 00 00 C7 44 2? ?? ?? 00 00 00 C7 44 2? ?? ?? 00 00 00 8B 05 [4] 89}
45
45
  condition:
46
46
  all of them
47
47
  }
@@ -59,7 +59,7 @@ rule Latrodectus_AES
59
59
  $key = {C6 44 2? ?? ?? [150] C6 44 2? ?? ?? B8 02}
60
60
  $aes_ctr_1 = {8B 44 24 ?? FF C8 89 44 24 ?? 83 7C 24 ?? 00 7C ?? 4? 63 44 24 ?? 4? 8B 4C 24 ?? 0F B6 84 01 F0 00 00 00 3D FF 00 00 00}
61
61
  $aes_ctr_2 = {48 03 C8 48 8B C1 0F B6 ?? 48 63 4C 24 ?? 0F B6 4C 0C ?? 33 C1 48 8B 4C 24 ?? 48 8B 54 24 ?? 48 03 D1 48 8B CA 88 01}
62
- $version = {C7 44 2? ?? ?? 00 00 00 C7 44 2? ?? ?? 00 00 00 8B 05 [4] 89}
62
+ $version = {C7 44 2? ?? ?? 00 00 00 C7 44 2? ?? ?? 00 00 00 C7 44 2? ?? ?? 00 00 00 8B 05 [4] 89}
63
63
  condition:
64
64
  all of them
65
65
  }
@@ -152,7 +152,8 @@ def extract_config(filebuf):
152
152
  data = instance.matched_data[::-1]
153
153
  major = int.from_bytes(data[10:11], byteorder="big")
154
154
  minor = int.from_bytes(data[18:19], byteorder="big")
155
- version = f"{major}.{minor}"
155
+ release = int.from_bytes(data[26:27], byteorder="big")
156
+ version = f"{major}.{minor}.{release}"
156
157
  if "$key" in item.identifier:
157
158
  key = instance.matched_data[4::5]
158
159
  try:
@@ -197,17 +198,20 @@ def extract_config(filebuf):
197
198
  str_vals.remove(item)
198
199
 
199
200
  cfg = {
200
- "C2": c2,
201
- "Group name": campaign,
202
- "Campaign ID": fnv_hash(campaign.encode()),
203
- "Version": version,
204
- "RC4 key": rc4_key,
205
- "Strings": str_vals,
201
+ "CNCs": c2,
202
+ "campaign": fnv_hash(campaign.encode()),
203
+ "version": version,
204
+ "cryptokey": rc4_key,
205
+ "cryptokey_type": "RC4",
206
+ "raw": {
207
+ "Strings": str_vals,
208
+ "Group name": campaign,
209
+ },
206
210
  }
207
211
  except Exception as e:
208
212
  log.error("Error: %s", e)
209
213
 
210
- if not cfg.get("C2", False) and not cfg.get("Group name", False):
214
+ if not cfg.get("C2", False) and not cfg.get("raw", {}).get("Group name", False):
211
215
  cfg = None
212
216
  return cfg
213
217
 
@@ -0,0 +1,151 @@
1
+ # Copyright (C) 2024 enzok
2
+ # This program is free software: you can redistribute it and/or modify
3
+ # it under the terms of the GNU General Public License as published by
4
+ # the Free Software Foundation, either version 3 of the License, or
5
+ # (at your option) any later version.
6
+ #
7
+ # This program is distributed in the hope that it will be useful,
8
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
9
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10
+ # GNU General Public License for more details.
11
+ #
12
+ # You should have received a copy of the GNU General Public License
13
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
14
+
15
+ import logging
16
+ import struct
17
+
18
+ import pefile
19
+ import yara
20
+
21
+ log = logging.getLogger(__name__)
22
+
23
+ DESCRIPTION = "NitroBunnyDownloader configuration parser."
24
+ AUTHOR = "enzok"
25
+
26
+ yara_rule = """
27
+ rule NitroBunnyDownloader
28
+ {
29
+ meta:
30
+ author = "enzok"
31
+ description = "NitroBunnyDownloader Payload"
32
+ cape_type = "NitroBunnyDownloader Payload"
33
+ hash = "960e59200ec0a4b5fb3b44e6da763f5fec4092997975140797d4eec491de411b"
34
+ strings:
35
+ $config = {E8 [3] 00 41 B8 ?? ?? 00 00 48 8D 15 [3] 00 48 89 C1 48 89 ?? E8 [3] 00}
36
+ $string1 = "X-Amz-User-Agent:" wide
37
+ $string2 = "Amz-Security-Flag:" wide
38
+ $string3 = "/cart" wide
39
+ $string4 = "Cookie: " wide
40
+ $string5 = "wishlist" wide
41
+ condition:
42
+ uint16(0) == 0x5A4D and $config and 2 of ($string*)
43
+ }
44
+ """
45
+
46
+ yara_rules = yara.compile(source=yara_rule)
47
+
48
+
49
+ def yara_scan(raw_data):
50
+ try:
51
+ return yara_rules.match(data=raw_data)
52
+ except Exception as e:
53
+ print(e)
54
+ return None
55
+
56
+
57
+ def read_dword(data, off):
58
+ if off + 4 > len(data):
59
+ raise ValueError(f"EOF reading dword at {off}")
60
+ val = struct.unpack_from("<I", data, off)[0]
61
+ return val, off + 4
62
+
63
+
64
+ def read_qword(data, off):
65
+ """Read a 64-bit unsigned little-endian value."""
66
+ if off + 8 > len(data):
67
+ raise ValueError(f"EOF reading qword at {off}")
68
+ val = struct.unpack_from("<Q", data, off)[0]
69
+ return val, off + 8
70
+
71
+
72
+ def read_utf16le_string(data, off, length):
73
+ if off + length > len(data):
74
+ raise ValueError(f"EOF reading string at {off} len={length}")
75
+ raw = data[off:off + length]
76
+ s = raw.decode("utf-16le", errors="replace").rstrip("\x00")
77
+ return s, off + length
78
+
79
+
80
+ def read_string_list(data, off, count):
81
+ items = []
82
+ for i in range(count):
83
+ length_words, off = read_qword(data, off)
84
+ s, off = read_utf16le_string(data, off, length_words)
85
+ items.append(s)
86
+ return items, off
87
+
88
+
89
+ def extract_config(filebuf):
90
+ yara_hit = yara_scan(filebuf)
91
+ if not yara_hit:
92
+ return None
93
+
94
+ cfg = {}
95
+ config_code_offset = None
96
+ for hit in yara_hit:
97
+ if hit.rule != "NitroBunnyDownloader":
98
+ continue
99
+
100
+ for item in hit.strings:
101
+ for instance in item.instances:
102
+ if "$config" in item.identifier:
103
+ config_code_offset = instance.offset
104
+ break
105
+
106
+ if config_code_offset is None:
107
+ return None
108
+
109
+ try:
110
+ pe = pefile.PE(data=filebuf, fast_load=True)
111
+ config_length = pe.get_dword_from_offset(config_code_offset + 7)
112
+ config_offset = pe.get_dword_from_offset(config_code_offset + 14)
113
+ rva = pe.get_rva_from_offset(config_code_offset + 18)
114
+ config_rva = rva + config_offset
115
+ data = pe.get_data(config_rva, config_length)
116
+ off = 0
117
+ raw = cfg["raw"] = {}
118
+ port, off = read_dword(data, off)
119
+ num, off = read_dword(data, off)
120
+ cncs, off = read_string_list(data, off, num)
121
+ num, off = read_qword(data, off)
122
+ raw["user_agent"], off = read_utf16le_string(data, off, num)
123
+ num, off = read_dword(data, off)
124
+ raw["http_header_items"], off = read_string_list(data, off, num)
125
+ num, off = read_dword(data, off)
126
+ raw["uri_list"], off = read_string_list(data, off, num)
127
+ raw["unknown_1"], off = read_dword(data, off)
128
+ raw["unknown_2"], off = read_dword(data, off)
129
+
130
+ if cncs:
131
+ cfg["CNCs"] = []
132
+ schema = {80: "http", 443: "https"}.get(port, "tcp")
133
+ for cnc in cncs:
134
+ cnc = f"{schema}://{cnc}"
135
+ if port not in (80, 443):
136
+ cnc += f":{port}"
137
+
138
+ cfg["CNCs"].append(cnc)
139
+
140
+ except Exception as e:
141
+ log.error("Error: %s", e)
142
+ return None
143
+
144
+ return cfg
145
+
146
+
147
+ if __name__ == "__main__":
148
+ import sys
149
+
150
+ with open(sys.argv[1], "rb") as f:
151
+ print(extract_config(f.read()))
@@ -75,7 +75,7 @@ def yara_scan(raw_data):
75
75
 
76
76
  def extract_config(filebuf):
77
77
  yara_hit = yara_scan(filebuf)
78
- cfg = {}
78
+ config = {}
79
79
 
80
80
  for hit in yara_hit:
81
81
  if hit.rule == "Oyster":
@@ -121,14 +121,16 @@ def extract_config(filebuf):
121
121
  if c2_matches:
122
122
  c2.extend(c2_matches)
123
123
 
124
- cfg = {
125
- "C2": c2,
126
- "Dll Version": dll_version,
127
- "Strings": str_vals,
124
+ config = {
125
+ "CNCs": c2,
126
+ 'version': dll_version,
127
+ "raw": {
128
+ "Strings": str_vals,
129
+ },
128
130
  }
129
131
  except Exception as e:
130
132
  log.error("Error: %s", e)
131
- return cfg
133
+ return config
132
134
 
133
135
 
134
136
  if __name__ == "__main__":
@@ -105,13 +105,13 @@ def get_config(input_data):
105
105
  c2s = get_c2s(data, number_of_c2s)
106
106
 
107
107
  return {
108
- "Version": version,
109
- "Campaign Name": campaign_name,
110
- "Registry Key": registry_key,
111
- "User Agent": user_agent,
108
+ "version": version,
109
+ "campaign": campaign_name,
110
+ "raw": {"Registry Key": registry_key},
111
+ "user_agent": user_agent,
112
112
  # "request_headers": request_headers,
113
113
  # "api_cmds": api_cmds,
114
- "C2s": c2s,
114
+ "CNCs": c2s,
115
115
  }
116
116
 
117
117
 
@@ -152,7 +152,7 @@ def extract_config(filebuf):
152
152
  out = wide_finder(test_out_ptxt).decode("utf-16le")
153
153
  if out:
154
154
  url = get_url(out)
155
- return {"C2": [url], "PowerShell": out}
155
+ return {"CNCs": [url], "raw": {"PowerShell": out}}
156
156
 
157
157
  if data:
158
158
  yara_hit = yara_scan(filebuf)
@@ -322,4 +322,6 @@ def extract_config(cfg_blob):
322
322
  reg_list.append(reg_name)
323
323
  if reg_list:
324
324
  config_output.update({"Registry black list": reg_list})
325
- return config_output
325
+
326
+ if config_output:
327
+ return {"raw": config_output}
@@ -493,7 +493,8 @@ def extract_config(filebuf):
493
493
  config = parse_config(filebuf[: len(filebuf) - 20])
494
494
  for k, v in config.items():
495
495
  end_config.setdefault(k, v)
496
- return end_config
496
+ if end_config:
497
+ return {"raw": end_config}
497
498
 
498
499
 
499
500
  if __name__ == "__main__":