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
@@ -76,8 +76,8 @@ def string_from_offset(data, offset):
76
76
 
77
77
  def parse_config(data):
78
78
  parsed = {}
79
- parsed["Botnet Id"] = data[4:].split(b"\x00", 1)[0].decode("utf-8")
80
- parsed["Campaign Id"] = data[25:].split(b"\x00", 1)[0].decode("utf-8")
79
+ parsed["botnet"] = data[4:].split(b"\x00", 1)[0].decode("utf-8")
80
+ parsed["campaign"] = data[25:].split(b"\x00", 1)[0].decode("utf-8")
81
81
  c2s = []
82
82
  c2_data = data[46:686]
83
83
  for i in range(10):
@@ -85,16 +85,17 @@ def parse_config(data):
85
85
  chunk = chunk.rstrip(b"\x00")
86
86
  if chunk:
87
87
  c2s.append(chunk.decode("utf-8"))
88
- parsed["C2s"] = c2s
89
- parsed["Public Key"] = data[704:].split(b"\x00", 1)[0]
88
+ parsed["CNCs"] = c2s
89
+ parsed["cryptokey"] = data[704:].split(b"\x00", 1)[0]
90
+ parsed["cryptokey_type"] = "RSA Public Key"
90
91
  dns_data = data[1004:].split(b"\x00", 1)[0]
91
92
  parsed["TLS SNI"] = dns_data.split(b"~")[0].decode("utf-8").rstrip()
92
93
  parsed["DNS C2"] = dns_data.split(b"~")[1].decode("utf-8").strip()
93
94
  dns_ips = []
94
- dns_ips.append(socket.inet_ntoa(data[1204:1208].split(b"\x00",1)[0]))
95
+ dns_ips.append(socket.inet_ntoa(data[1204:1208].split(b"\x00", 1)[0]))
95
96
  dns_ip_data = data[1208:1248]
96
97
  for i in range(10):
97
- chunk = dns_ip_data[i * 4:(i + 1) * 4]
98
+ chunk = dns_ip_data[i * 4 : (i + 1) * 4]
98
99
  chunk = chunk.rstrip(b"\x00")
99
100
  if chunk:
100
101
  dns_ips.append(socket.inet_ntoa(chunk))
@@ -103,6 +104,7 @@ def parse_config(data):
103
104
 
104
105
 
105
106
  def extract_config(filebuf):
107
+ config = {}
106
108
  end_config = {}
107
109
  pe = pefile.PE(data=filebuf, fast_load=False)
108
110
  image_base = pe.OPTIONAL_HEADER.ImageBase
@@ -167,14 +169,15 @@ def extract_config(filebuf):
167
169
  enc_data = filebuf[data_offset:].split(b"\0\0", 1)[0]
168
170
  raw = decrypt_rc4(key, enc_data)
169
171
  items = list(filter(None, raw.split(b"\x00\x00")))
170
- end_config["Botnet name"] = items[1].lstrip(b"\x00")
171
- end_config["Campaign ID"] = items[2]
172
+ config["botnet"] = items[1].lstrip(b"\x00")
173
+ config["campaign"] = items[2]
172
174
  for item in items:
173
175
  item = item.lstrip(b"\x00")
174
176
  if item.startswith(b"http"):
175
- end_config.setdefault("address", []).append(item)
177
+ config.setdefault("CNCs", []).append(item)
176
178
  elif len(item) == 16:
177
- end_config["RC4 key"] = item
179
+ config["cryptokey"] = item
180
+ config["cryptokey_type"] = "RC4"
178
181
  elif conf_type == "2" and decrypt_key:
179
182
  conf_va = struct.unpack("I", filebuf[decrypt_conf + cva : decrypt_conf + cva + 4])[0]
180
183
  conf_offset = pe.get_offset_from_rva(conf_va + pe.get_rva_from_offset(decrypt_conf) + cva + 4)
@@ -186,14 +189,15 @@ def extract_config(filebuf):
186
189
  conf_data = filebuf[conf_offset : conf_offset + conf_size]
187
190
  raw = decrypt_rc4(key, conf_data)
188
191
  items = list(filter(None, raw.split(b"\x00\x00")))
189
- end_config["Botnet name"] = items[0].decode("utf-8")
190
- end_config["Campaign ID"] = items[1].decode("utf-8")
192
+ config["botnet"] = items[0].decode("utf-8")
193
+ config["campaign"] = items[1].decode("utf-8")
191
194
  for item in items:
192
195
  item = item.lstrip(b"\x00")
193
196
  if item.startswith(b"http"):
194
- end_config.setdefault("address", []).append(item.decode("utf-8"))
197
+ config.setdefault("CNCs", []).append(item.decode("utf-8"))
195
198
  elif b"PUBLIC KEY" in item:
196
- end_config["Public key"] = item.decode("utf-8").replace("\n", "")
199
+ config["cryptokey"] = item.decode("utf-8").replace("\n", "")
200
+ config["cryptokey_type"] = "RSA Public key"
197
201
  elif conf_type == "3" and rc4_chunk1 and rc4_chunk2:
198
202
  conf_va = struct.unpack("I", filebuf[decrypt_conf : decrypt_conf + 4])[0]
199
203
  conf_offset = pe.get_offset_from_rva(conf_va + pe.get_rva_from_offset(decrypt_conf) + 4)
@@ -208,8 +212,10 @@ def extract_config(filebuf):
208
212
  conf = decrypt_rc4(decrypt_key, conf_data)
209
213
  end_config = parse_config(conf)
210
214
 
211
- return end_config
215
+ if config and end_config:
216
+ config = config.update({"raw": end_config})
212
217
 
218
+ return config
213
219
 
214
220
  if __name__ == "__main__":
215
221
  import sys
@@ -5,6 +5,7 @@ HAVE_MLW_CONFIGS = False
5
5
  with suppress(ImportError):
6
6
  # We do not install this by default as is outdated now, but if installed will be imported
7
7
  from malwareconfig.common import Decoder
8
+
8
9
  HAVE_MLW_CONFIGS = True
9
10
 
10
11
 
cape_parsers/__init__.py CHANGED
@@ -12,10 +12,11 @@ from typing import Dict, Tuple
12
12
  PARSERS_ROOT = Path(__file__).absolute().parent
13
13
  log = logging.getLogger()
14
14
 
15
- def load_cape_parsers(load: str="all", exclude_parsers: list = []):
15
+
16
+ def load_cape_parsers(load: str = "all", exclude_parsers: list = []):
16
17
  """
17
- load: all, core, community
18
- exclude_parsers: [names of parsers that will be ignored]
18
+ load: all, core, community
19
+ exclude_parsers: [names of parsers that will be ignored]
19
20
  """
20
21
  versions = {
21
22
  "cape": "core",
@@ -25,7 +26,9 @@ def load_cape_parsers(load: str="all", exclude_parsers: list = []):
25
26
  cape_parsers = {}
26
27
  CAPE_DECODERS = {
27
28
  "cape": [os.path.basename(decoder)[:-3] for decoder in glob.glob(os.path.join(PARSERS_ROOT, "CAPE", "core", "[!_]*.py"))],
28
- "community": [os.path.basename(decoder)[:-3] for decoder in glob.glob(os.path.join(PARSERS_ROOT, "CAPE", "community", "[!_]*.py"))],
29
+ "community": [
30
+ os.path.basename(decoder)[:-3] for decoder in glob.glob(os.path.join(PARSERS_ROOT, "CAPE", "community", "[!_]*.py"))
31
+ ],
29
32
  }
30
33
 
31
34
  for version, names in CAPE_DECODERS.items():
@@ -54,6 +57,7 @@ def load_mwcp_parsers() -> Tuple[Dict[str, str], ModuleType]:
54
57
  with suppress(ImportError):
55
58
  # We do not install this by default
56
59
  import mwcp
60
+
57
61
  HAVE_MWCP = True
58
62
 
59
63
  if not HAVE_MWCP:
@@ -66,6 +70,7 @@ def load_mwcp_parsers() -> Tuple[Dict[str, str], ModuleType]:
66
70
  return {}, mwcp
67
71
  return _malware_parsers, mwcp
68
72
 
73
+
69
74
  def _malduck_load_decoders():
70
75
 
71
76
  malduck_modules = {}
@@ -80,6 +85,7 @@ def _malduck_load_decoders():
80
85
 
81
86
  return malduck_modules
82
87
 
88
+
83
89
  """
84
90
  def load_malduck_parsers():
85
91
  HAVE_MALDUCK = False
@@ -108,6 +114,7 @@ def load_malduck_parsers():
108
114
  return malduck_modules
109
115
  """
110
116
 
117
+
111
118
  def load_malwareconfig_parsers() -> Tuple[bool, dict, ModuleType]:
112
119
  try:
113
120
  from malwareconfig import fileparser
@@ -122,15 +129,17 @@ def load_malwareconfig_parsers() -> Tuple[bool, dict, ModuleType]:
122
129
  except ImportError:
123
130
  log.info("Missed RATDecoders -> poetry run pip install malwareconfig")
124
131
  except Exception as e:
125
- log.error(e, exc_info=True)
132
+ log.exception(e)
126
133
  return False, False, False
127
134
 
135
+
128
136
  def load_ratdecoders_parsers():
129
137
  dec_modules = {}
130
138
  HAVE_MLW_CONFIGS = False
131
139
  with suppress(ImportError):
132
140
  # We do not install this by default as is outdated now, but if installed will be imported
133
141
  from malwareconfig.common import Decoder
142
+
134
143
  HAVE_MLW_CONFIGS = True
135
144
 
136
145
  if not HAVE_MLW_CONFIGS:
@@ -0,0 +1,59 @@
1
+ import pefile
2
+
3
+
4
+ def extract_raw_config(raw_data):
5
+ try:
6
+ pe = pefile.PE(data=raw_data)
7
+ rt_string_idx = [entry.id for entry in pe.DIRECTORY_ENTRY_RESOURCE.entries].index(pefile.RESOURCE_TYPE["RT_RCDATA"])
8
+ rt_string_directory = pe.DIRECTORY_ENTRY_RESOURCE.entries[rt_string_idx]
9
+ for entry in rt_string_directory.directory.entries:
10
+ if str(entry.name) == "SETTINGS":
11
+ data_rva = entry.directory.entries[0].data.struct.OffsetToData
12
+ size = entry.directory.entries[0].data.struct.Size
13
+ data = pe.get_memory_mapped_image()[data_rva : data_rva + size]
14
+ return data.split("}")
15
+ except Exception:
16
+ return None
17
+
18
+
19
+ def decode(line):
20
+ return "".join(chr(ord(char) - 1) for char in line)
21
+
22
+
23
+ def domain_parse(config):
24
+ return [domain.split(":", 1)[0] for domain in config["Domains"].split(";")]
25
+
26
+
27
+ def extract_config(data):
28
+ try:
29
+ config_raw = extract_raw_config(data)
30
+ if config_raw:
31
+ return {
32
+ "mutex": decode(config_raw[1])[::-1],
33
+ "raw": {
34
+ "Anti Sandboxie": decode(config_raw[2])[::-1],
35
+ "Max Folder Size": decode(config_raw[3])[::-1],
36
+ "Delay Time": decode(config_raw[4])[::-1],
37
+ "Password": decode(config_raw[5])[::-1],
38
+ "Kernel Mode Unhooking": decode(config_raw[6])[::-1],
39
+ "User More Unhooking": decode(config_raw[7])[::-1],
40
+ "Melt Server": decode(config_raw[8])[::-1],
41
+ "Offline Screen Capture": decode(config_raw[9])[::-1],
42
+ "Offline Keylogger": decode(config_raw[10])[::-1],
43
+ "Copy To ADS": decode(config_raw[11])[::-1],
44
+ "Domain": decode(config_raw[12])[::-1],
45
+ "Persistence Thread": decode(config_raw[13])[::-1],
46
+ "Active X Key": decode(config_raw[14])[::-1],
47
+ "Registry Key": decode(config_raw[15])[::-1],
48
+ "Active X Run": decode(config_raw[16])[::-1],
49
+ "Registry Run": decode(config_raw[17])[::-1],
50
+ "Safe Mode Startup": decode(config_raw[18])[::-1],
51
+ "Inject winlogon.exe": decode(config_raw[19])[::-1],
52
+ "Install Name": decode(config_raw[20])[::-1],
53
+ "Install Path": decode(config_raw[21])[::-1],
54
+ "Campaign Name": decode(config_raw[22])[::-1],
55
+ "Campaign Group": decode(config_raw[23])[::-1],
56
+ }
57
+ }
58
+ except Exception:
59
+ return None
@@ -35,5 +35,5 @@ def extract_config(filebuf):
35
35
  with suppress(Exception):
36
36
  dec = decrypt_string(item.lstrip(b"\x00").rstrip(b"\x00").decode())
37
37
  if "dll" not in dec and " " not in dec and ";" not in dec and "." in dec:
38
- cfg.setdefault("address", []).append(dec)
38
+ cfg.setdefault("CNCs", []).append(dec)
39
39
  return cfg
@@ -55,7 +55,7 @@ def string_from_offset(data, offset):
55
55
 
56
56
 
57
57
  def extract_config(filebuf):
58
- tmp_config = {}
58
+ config = {}
59
59
  yara_matches = yara_scan(filebuf)
60
60
 
61
61
  c2_offsets = []
@@ -70,6 +70,6 @@ def extract_config(filebuf):
70
70
  for c2_offset in c2_offsets:
71
71
  c2_url = string_from_offset(filebuf, c2_offset)
72
72
  if c2_url:
73
- tmp_config.setdefault("c2_url", []).append(c2_url)
73
+ config.setdefault("CNCs", []).append(c2_url)
74
74
 
75
- return tmp_config
75
+ return config
@@ -64,7 +64,7 @@ def extract_config(filebuf):
64
64
 
65
65
  c2_address = string_from_offset(filebuf, yara_offset + 0x2E8)
66
66
  if c2_address:
67
- return_conf["c2_address"] = c2_address
67
+ return_conf["CNCs"] = c2_address
68
68
 
69
69
  c2_url = string_from_offset(filebuf, yara_offset + 0xE8)
70
70
  if c2_url:
@@ -16,9 +16,7 @@ DESCRIPTION = "EvilGrab configuration parser."
16
16
  AUTHOR = "kevoreilly"
17
17
 
18
18
  import struct
19
-
20
19
  import pefile
21
-
22
20
  import yara
23
21
 
24
22
  rule_source = """
@@ -88,21 +86,22 @@ def extract_config(filebuf):
88
86
 
89
87
  yara_offset = int(yara_matches[key])
90
88
 
89
+ # ToDo missed schema
91
90
  c2_address = string_from_va(pe, yara_offset + values[0])
92
91
  if c2_address:
93
- end_config["c2_address"] = c2_address
92
+ end_config["CNCs"] = c2_address
94
93
  port = str(struct.unpack("h", filebuf[yara_offset + values[1] : yara_offset + values[1] + 2])[0])
95
94
  if port:
96
- end_config["port"] = [port, "tcp"]
95
+ end_config.setdefault("raw", {})["port"] = [port, "tcp"]
97
96
  missionid = string_from_va(pe, yara_offset + values[3])
98
97
  if missionid:
99
- end_config["missionid"] = missionid
98
+ end_config.setdefault("raw", {})["missionid"] = missionid
100
99
  version = string_from_va(pe, yara_offset + values[4])
101
100
  if version:
102
101
  end_config["version"] = version
103
102
  injectionprocess = string_from_va(pe, yara_offset + values[5])
104
103
  if injectionprocess:
105
- end_config["injectionprocess"] = injectionprocess
104
+ end_config.setdefault("raw", {})["injectionprocess"] = injectionprocess
106
105
  if key != "$configure3":
107
106
  mutex = string_from_va(pe, yara_offset - values[6])
108
107
  if mutex:
@@ -87,4 +87,6 @@ def parse_config(raw_config):
87
87
  def extract_config(data):
88
88
  raw_config = get_config(data)
89
89
  if raw_config:
90
- return parse_config(raw_config)
90
+ config = parse_config(raw_config)
91
+ if config:
92
+ return {"raw": config}
@@ -17,9 +17,7 @@ AUTHOR = "kevoreilly"
17
17
 
18
18
 
19
19
  import struct
20
-
21
20
  import pefile
22
-
23
21
  import yara
24
22
 
25
23
  rule_source = """
@@ -91,6 +89,7 @@ def extract_config(filebuf):
91
89
  # image_base = pe.OPTIONAL_HEADER.ImageBase
92
90
 
93
91
  yara_matches = yara_scan(filebuf)
92
+ config = {}
94
93
  tmp_config = {}
95
94
  for key, values in match_map.keys():
96
95
  if yara_matches.get(key):
@@ -103,23 +102,23 @@ def extract_config(filebuf):
103
102
 
104
103
  c2_address = unicode_from_va(pe, yara_offset + values[1])
105
104
  if c2_address:
106
- tmp_config.setdefault("c2_address", []).append(c2_address)
105
+ config.setdefault("CNCs", []).append(c2_address)
107
106
 
108
107
  if key == "$connect_3":
109
108
  c2_address = unicode_from_va(pe, yara_offset + values[2])
110
109
  if c2_address:
111
- tmp_config.setdefault("c2_address", []).append(c2_address)
110
+ config.setdefault("CNCs", []).append(c2_address)
112
111
  else:
113
112
  c2_address = unicode_from_va(pe, yara_offset + values[0])
114
113
  if c2_address:
115
- tmp_config["c2_address"] = c2_address
114
+ config["CNCs"] = [c2_address]
116
115
 
117
116
  filepath = unicode_from_va(pe, yara_offset + values[1])
118
117
  if filepath:
119
- tmp_config["filepath"] = filepath
118
+ config["filepath"] = filepath
120
119
 
121
120
  injectionprocess = unicode_from_va(pe, yara_offset - values[2])
122
121
  if injectionprocess:
123
- tmp_config["injectionprocess"] = injectionprocess
122
+ config["injectionprocess"] = injectionprocess
124
123
 
125
- return tmp_config
124
+ return {"raw": tmp_config}.update(config)
@@ -74,4 +74,6 @@ def extract_config(data):
74
74
  clean_config = version_21(raw_config)
75
75
  elif len(raw_config) == 20:
76
76
  clean_config = version_22(raw_config)
77
+ if clean_config:
78
+ clean_config = {"raw": clean_config}
77
79
  return clean_config
@@ -32,4 +32,5 @@ def extract_config(data):
32
32
  config_dict["Install Path"] = "TEMP"
33
33
  if config_parts[19] == "True":
34
34
  config_dict["Install Path"] = "Documents"
35
- return config_dict
35
+ if config_dict:
36
+ return {"raw": config_dict}
@@ -16,9 +16,7 @@ DESCRIPTION = "RCSession configuration parser."
16
16
  AUTHOR = "kevoreilly"
17
17
 
18
18
  import struct
19
-
20
19
  import pefile
21
-
22
20
  import yara
23
21
 
24
22
  rule_source = """
@@ -99,24 +97,24 @@ def extract_config(filebuf):
99
97
 
100
98
  c2_address = str(tmp_config[156 : 156 + MAX_IP_STRING_SIZE])
101
99
  if c2_address:
102
- end_config.setdefault("c2_address", []).append(c2_address)
100
+ end_config.setdefault("CNCs", []).append(c2_address)
103
101
  c2_address = str(tmp_config[224 : 224 + MAX_IP_STRING_SIZE])
104
102
  if c2_address:
105
- end_config.setdefault("c2_address", []).append(c2_address)
103
+ end_config.setdefault("CNCs", []).append(c2_address)
106
104
  installdir = unicode_string_from_offset(bytes(tmp_config), 0x2A8, 128)
107
105
  if installdir:
108
- end_config["directory"] = installdir
106
+ end_config.setdefault("raw", {})["directory"] = installdir
109
107
  executable = unicode_string_from_offset(tmp_config, 0x4B0, 128)
110
108
  if executable:
111
- end_config["filename"] = executable
109
+ end_config.setdefault("raw", {})["filename"] = executable
112
110
  servicename = unicode_string_from_offset(tmp_config, 0x530, 128)
113
111
  if servicename:
114
- end_config["servicename"] = servicename
112
+ end_config.setdefault("raw", {})["servicename"] = servicename
115
113
  displayname = unicode_string_from_offset(tmp_config, 0x738, 128)
116
114
  if displayname:
117
- end_config["servicedisplayname"] = displayname
115
+ end_config.setdefault("raw", {})["servicedisplayname"] = displayname
118
116
  description = unicode_string_from_offset(tmp_config, 0x940, 512)
119
117
  if description:
120
- end_config["servicedescription"] = description
118
+ end_config.setdefault("raw", {})["servicedescription"] = description
121
119
 
122
120
  return end_config
@@ -47,7 +47,7 @@ def decodeREvilConfig(config_key, config_data):
47
47
  ECX = EAX = ESI = 0
48
48
 
49
49
  for char in init255:
50
- ESI = ((char & 0xFF) + (ord(key[EAX % len(key)]) + ESI)) & 0xFF
50
+ ESI = ((char & 0xFF) + (key[EAX % len(key)] + ESI)) & 0xFF
51
51
  init255[EAX] = init255[ESI] & 0xFF
52
52
  EAX += 1
53
53
  init255[ESI] = char & 0xFF
@@ -61,7 +61,7 @@ def decodeREvilConfig(config_key, config_data):
61
61
  ESI = (ESI + DL) & 0xFF
62
62
  init255[ECX] = init255[ESI]
63
63
  init255[ESI] = DL
64
- decoded_config.append((init255[((init255[ECX] + DL) & 0xFF)]) ^ ord(char))
64
+ decoded_config.append((init255[((init255[ECX] + DL) & 0xFF)]) ^ char)
65
65
  EAX = LOCAL1
66
66
 
67
67
  return json.loads("".join(map(chr, decoded_config)))
@@ -74,12 +74,17 @@ def extract_config(data):
74
74
 
75
75
  if len(pe.sections) == 5:
76
76
  section_names = getSectionNames(pe.sections)
77
- required_sections = (".text", ".rdata", ".data", ".reloc")
77
+ required_sections = (b".text", b".rdata", b".data", b".reloc")
78
78
 
79
- # print section_names
80
79
  if all(sections in section_names for sections in required_sections):
81
80
  # print("all required section names found")
82
- config_section_name = [resource for resource in section_names if resource not in required_sections][0]
81
+ section_names_set = set(section_names)
82
+ required_sections_set = set(required_sections)
83
+ config_section_names = section_names_set - required_sections_set
84
+ if len(config_section_names) == 1:
85
+ config_section_name = config_section_names.pop()
86
+ else:
87
+ return None # Or raise an exception, depending on desired behavior
83
88
  config_key, config_data = getREvilKeyAndConfig(pe.sections, config_section_name)
84
89
  if config_key and config_data:
85
90
  return decodeREvilConfig(config_key, config_data)
@@ -16,9 +16,7 @@ DESCRIPTION = "RedLeaf configuration parser."
16
16
  AUTHOR = "kevoreilly"
17
17
 
18
18
  import struct
19
-
20
19
  import pefile
21
-
22
20
  import yara
23
21
 
24
22
  rule_source = """
@@ -90,21 +88,21 @@ def extract_config(filebuf):
90
88
  end_config = {}
91
89
  c2_address = tmp_config[8 : 8 + MAX_IP_STRING_SIZE]
92
90
  if c2_address:
93
- end_config.setdefault("c2_address", []).append(c2_address)
91
+ end_config.setdefault("CNCs", []).append(c2_address)
94
92
  c2_address = tmp_config[0x48 : 0x48 + MAX_IP_STRING_SIZE]
95
93
  if c2_address:
96
- end_config.setdefault("c2_address", []).append(c2_address)
94
+ end_config.setdefault("CNCs", []).append(c2_address)
97
95
  c2_address = tmp_config[0x88 : 0x88 + MAX_IP_STRING_SIZE]
98
96
  if c2_address:
99
- end_config.setdefault("c2_address", []).append(c2_address)
97
+ end_config.setdefault("CNCs", []).append(c2_address)
100
98
  missionid = string_from_offset(tmp_config, 0x1EC)
101
99
  if missionid:
102
- end_config["missionid"] = missionid
100
+ end_config.setdefault("raw", {})["missionid"] = missionid
103
101
  mutex = unicode_string_from_offset(tmp_config, 0x508)
104
102
  if mutex:
105
103
  end_config["mutex"] = mutex
106
104
  key = string_from_offset(tmp_config, 0x832)
107
105
  if key:
108
- end_config["key"] = key
106
+ end_config["cryptokey"] = key
109
107
 
110
108
  return end_config
@@ -7,9 +7,7 @@ DESCRIPTION = "Retefe configuration parser."
7
7
  AUTHOR = "Tomasuh"
8
8
 
9
9
  import struct
10
-
11
10
  import pefile
12
-
13
11
  import yara
14
12
 
15
13
  rule_source = """
@@ -9,8 +9,5 @@ def extract_config(data: bytes):
9
9
  if matches:
10
10
  ip = "".join(".".join(f"{c}" for c in matches[0][0]))
11
11
  port = int.from_bytes(matches[0][1], byteorder="big")
12
-
13
- config_dict["C2"] = ip
14
- config_dict["Port"] = port
15
-
16
- return config_dict
12
+ config_dict["CNCs"] = f"{ip}:{port}"
13
+ return {}
@@ -96,8 +96,12 @@ def ver_5(data):
96
96
 
97
97
 
98
98
  def extract_config(data):
99
+ config = {}
99
100
  if "!!<3SAFIA<3!!" in data:
100
- return ver_52(data)
101
+ config = ver_52(data)
101
102
 
102
103
  elif "!!ElMattadorDz!!" in data:
103
- return ver_5(data)
104
+ config = ver_5(data)
105
+
106
+ if config:
107
+ return {"raw": config}
@@ -1,8 +1,9 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: CAPE-parsers
3
- Version: 0.1.42
3
+ Version: 0.1.54
4
4
  Summary: CAPE: Malware Configuration Extraction
5
5
  License: MIT
6
+ License-File: LICENSE
6
7
  Keywords: cape,parsers,malware,configuration
7
8
  Author: Kevin O'Reilly
8
9
  Author-email: kev@capesandbox.com
@@ -13,6 +14,7 @@ Classifier: Programming Language :: Python :: 3.10
13
14
  Classifier: Programming Language :: Python :: 3.11
14
15
  Classifier: Programming Language :: Python :: 3.12
15
16
  Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
16
18
  Provides-Extra: maco
17
19
  Requires-Dist: capstone (>=4.0.2)
18
20
  Requires-Dist: dncil (>=1.0.2)
@@ -23,7 +25,7 @@ Requires-Dist: pefile
23
25
  Requires-Dist: pycryptodomex (>=3.20.0)
24
26
  Requires-Dist: rat-king-parser (>=4.1.0)
25
27
  Requires-Dist: ruff (>=0.7.2)
26
- Requires-Dist: unicorn (==2.1.1)
28
+ Requires-Dist: unicorn (>=2.1.1)
27
29
  Requires-Dist: yara-python (>=4.5.1)
28
30
  Description-Content-Type: text/markdown
29
31
 
@@ -32,3 +34,22 @@ CAPE core and community parsers
32
34
 
33
35
  [![PyPI version](https://img.shields.io/pypi/v/CAPE-parsers)](https://pypi.org/project/CAPE-parsers/)
34
36
 
37
+ ### Configs structure
38
+ ```
39
+ CNCs: []
40
+ campaign: str
41
+ botnet: str
42
+ dga_seed: hex str
43
+ version: str
44
+ mutex: str
45
+ user_agent: str
46
+ build: str
47
+ cryptokey: str
48
+ cryptokey_type: str (algorithm). Ex: RC4, RSA public key. salsa20, (x)chacha20
49
+ raw: {any other data goes here}
50
+ ```
51
+ * All CNC entries should be in URL format. aka `<schema>://<hostname>:<port>/<uri>`
52
+ * Schema examples: `tcp://`, `ftp://`, `udp://`, `http(s)`, etc.
53
+ * Old CAPE configs still have lack of this structures as most of them are dead families.
54
+ * This CNC simplification make it easier to parse with tools like `tldextract` or `urlparse`
55
+