CAPE-parsers 0.1.56__tar.gz → 0.1.57__tar.gz

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 (118) hide show
  1. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/PKG-INFO +1 -1
  2. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/AgentTesla.py +7 -1
  3. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/Carbanak.py +7 -2
  4. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/Fareit.py +3 -3
  5. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/NanoCore.py +11 -1
  6. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/Njrat.py +1 -1
  7. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/Snake.py +2 -2
  8. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/SparkRAT.py +5 -1
  9. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/WinosStager.py +6 -3
  10. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/AdaptixBeacon.py +13 -1
  11. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/Blister.py +4 -1
  12. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/DarkGate.py +30 -1
  13. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/IcedIDLoader.py +2 -1
  14. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/Latrodectus.py +2 -0
  15. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/NitroBunnyDownloader.py +33 -29
  16. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/Oyster.py +128 -21
  17. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/PikaBot.py +1 -2
  18. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/QakBot.py +7 -4
  19. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/Quickbind.py +3 -2
  20. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/pyproject.toml +1 -1
  21. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/LICENSE +0 -0
  22. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/README.md +0 -0
  23. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/CAPE/__init__.py +0 -0
  24. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/Amadey.py +0 -0
  25. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/Amatera.py +0 -0
  26. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/Arkei.py +0 -0
  27. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/AsyncRAT.py +0 -0
  28. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/AuroraStealer.py +0 -0
  29. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/CobaltStrikeBeacon.py +0 -0
  30. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/CobaltStrikeStager.py +0 -0
  31. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/DCRat.py +0 -0
  32. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/KoiLoader.py +0 -0
  33. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/LokiBot.py +0 -0
  34. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/Lumma.py +0 -0
  35. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/MonsterV2.py +0 -0
  36. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/MyKings.py +0 -0
  37. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/Nighthawk.py +0 -0
  38. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/PhemedroneStealer.py +0 -0
  39. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/QuasarRAT.py +0 -0
  40. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/README.md +0 -0
  41. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/Stealc.py +0 -0
  42. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/VenomRAT.py +0 -0
  43. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/XWorm.py +0 -0
  44. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/XenoRAT.py +0 -0
  45. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/__init__.py +0 -0
  46. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/AuraStealer.py +0 -0
  47. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/Azorult.py +0 -0
  48. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/BitPaymer.py +0 -0
  49. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/BlackDropper.py +0 -0
  50. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/BruteRatel.py +0 -0
  51. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/BumbleBee.py +0 -0
  52. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/DoppelPaymer.py +0 -0
  53. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/DridexLoader.py +0 -0
  54. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/Formbook.py +0 -0
  55. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/GuLoader.py +0 -0
  56. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/IcedID.py +0 -0
  57. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/PlugX.py +0 -0
  58. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/README.md +0 -0
  59. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/RedLine.py +0 -0
  60. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/Remcos.py +0 -0
  61. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/Rhadamanthys.py +0 -0
  62. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/SmokeLoader.py +0 -0
  63. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/Socks5Systemz.py +0 -0
  64. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/SquirrelWaffle.py +0 -0
  65. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/Strrat.py +0 -0
  66. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/WarzoneRAT.py +0 -0
  67. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/Zloader.py +0 -0
  68. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/__init__.py +0 -0
  69. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/test_cape.py +0 -0
  70. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/RATDecoders/README.md +0 -0
  71. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/RATDecoders/__init__.py +0 -0
  72. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/RATDecoders/test_rats.py +0 -0
  73. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/__init__.py +0 -0
  74. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/deprecated/BackOffLoader.py +0 -0
  75. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/deprecated/BackOffPOS.py +0 -0
  76. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/deprecated/BlackNix.py +0 -0
  77. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/deprecated/BuerLoader.py +0 -0
  78. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/deprecated/ChChes.py +0 -0
  79. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/deprecated/Emotet.py +0 -0
  80. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/deprecated/Enfal.py +0 -0
  81. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/deprecated/EvilGrab.py +0 -0
  82. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/deprecated/Greame.py +0 -0
  83. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/deprecated/Hancitor.py +0 -0
  84. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/deprecated/HttpBrowser.py +0 -0
  85. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/deprecated/JavaDropper.py +0 -0
  86. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/deprecated/Nymaim.py +0 -0
  87. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/deprecated/Pandora.py +0 -0
  88. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/deprecated/PoisonIvy.py +0 -0
  89. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/deprecated/PredatorPain.py +0 -0
  90. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/deprecated/Punisher.py +0 -0
  91. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/deprecated/RCSession.py +0 -0
  92. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/deprecated/REvil.py +0 -0
  93. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/deprecated/RedLeaf.py +0 -0
  94. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/deprecated/Retefe.py +0 -0
  95. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/deprecated/Rozena.py +0 -0
  96. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/deprecated/SmallNet.py +0 -0
  97. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/deprecated/TSCookie.py +0 -0
  98. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/deprecated/TrickBot.py +0 -0
  99. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/deprecated/UrsnifV3.py +0 -0
  100. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/deprecated/_ShadowTech.py +0 -0
  101. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/deprecated/_VirusRat.py +0 -0
  102. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/deprecated/_jRat.py +0 -0
  103. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/deprecated/unrecom.py +0 -0
  104. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/deprecated/xRAT.py +0 -0
  105. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/malduck/LICENSE +0 -0
  106. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/malduck/README.md +0 -0
  107. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/malduck/__init__.py +0 -0
  108. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/malduck/test_malduck.py +0 -0
  109. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/mwcp/README.md +0 -0
  110. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/mwcp/__init__.py +0 -0
  111. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/mwcp/test_mwcp.py +0 -0
  112. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/utils/__init__.py +0 -0
  113. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/utils/aplib.py +0 -0
  114. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/utils/blzpack.py +0 -0
  115. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/utils/blzpack_lib.so +0 -0
  116. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/utils/dotnet_utils.py +0 -0
  117. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/utils/lznt1.py +0 -0
  118. {cape_parsers-0.1.56 → cape_parsers-0.1.57}/cape_parsers/utils/strings.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: CAPE-parsers
3
- Version: 0.1.56
3
+ Version: 0.1.57
4
4
  Summary: CAPE: Malware Configuration Extraction
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -83,7 +83,13 @@ def extract_config(data: bytes):
83
83
  config["CNCs"] = lines[base + index + x]
84
84
  break
85
85
  if config or config_dict:
86
- return config.setdefault("raw", config_dict)
86
+ config.setdefault("raw", config_dict)
87
+
88
+ # If the data exfiltration is done through SMTP, then patch the extracted CNCs to include SMTP credentials
89
+ if config_dict.get("Protocol") == "SMTP":
90
+ config['CNCs'] = [f"smtp://{config_dict.get('Username')}:{config_dict.get('Password')}@{domain}:{config_dict.get('Port','587')}" for domain in config_dict.get('CNCs', [])]
91
+
92
+ return config
87
93
 
88
94
  if __name__ == "__main__":
89
95
  import sys
@@ -18,7 +18,6 @@ import re
18
18
  from contextlib import suppress
19
19
 
20
20
  import pefile
21
-
22
21
  import yara
23
22
 
24
23
  log = logging.getLogger(__name__)
@@ -169,7 +168,13 @@ def extract_config(filebuf):
169
168
  c2_dec = decode_string(items[10], sbox).decode("utf8")
170
169
  if "|" in c2_dec:
171
170
  c2_dec = c2_dec.split("|")
172
- cfg["CNCs"] = c2_dec
171
+ for c2 in c2_dec:
172
+ # Assign the proper scheme based on the port
173
+ if c2.endswith(':443'):
174
+ c2 = f"https://{c2}"
175
+ else:
176
+ c2 = f"http://{c2}"
177
+ cfg.setdefault("CNCs", []).append(c2)
173
178
  if float(cfg["version"]) < 1.7:
174
179
  cfg["campaign"] = decode_string(items[276], sbox).decode("utf8")
175
180
  else:
@@ -55,11 +55,11 @@ def extract_config(memdump_path, read=False):
55
55
  if gate_url.match(url):
56
56
  config.setdefault("CNCs", []).append(url.decode())
57
57
  elif exe_url.match(url) or dll_url.match(url):
58
- artifacts_raw["downloads"].append(url.decode())
58
+ artifacts_raw.setdefault("downloads", []).append(url.decode())
59
59
  except Exception as e:
60
60
  print(e, sys.exc_info(), "PONY")
61
- config["CNCs"] = list(set(config["controllers"]))
62
- config.setdefault("raw", {})["downloads"] = list(set(artifacts_raw["downloads"]))
61
+ if "downloads" in artifacts_raw:
62
+ config.setdefault("raw", {})["downloads"] = list(set(artifacts_raw["downloads"]))
63
63
  return config
64
64
 
65
65
 
@@ -188,7 +188,17 @@ def extract_config(filebuf):
188
188
  cncs.append(config_dict["raw"]["BackupConnectionHost"])
189
189
  if config_dict.get("raw", {}).get("ConnectionPort") and cncs:
190
190
  port = config_dict["raw"]["ConnectionPort"]
191
- config_dict["CNCs"] = [f"{cnc}:{port}" for cnc in cncs]
191
+ config_dict["CNCs"] = [f"tcp://{cnc}:{port}" for cnc in cncs]
192
+
193
+ if "Mutex" in config_dict.get("raw", {}):
194
+ config_dict["mutex"] = config_dict["raw"]["Mutex"]
195
+
196
+ if "Version" in config_dict.get("raw", {}):
197
+ config_dict["version"] = config_dict["raw"]["Version"]
198
+
199
+ if "DefaultGroup" in config_dict.get("raw", {}):
200
+ config_dict["campaign"] = config_dict["raw"]["DefaultGroup"]
201
+
192
202
  return config_dict
193
203
 
194
204
 
@@ -176,7 +176,7 @@ def extract_config(data):
176
176
  config = get_clean_config(config_dict)
177
177
  if config:
178
178
  if config.get("domain") and config.get("port"):
179
- conf["CNCs"] = [f"{config['domain']}:{config['port']}"]
179
+ conf["CNCs"] = [f"tcp://{config['domain']}:{config['port']}"]
180
180
 
181
181
  if config.get("campaign_id"):
182
182
  conf["campaign"] = config["campaign_id"]
@@ -38,7 +38,7 @@ def handle_plain(dotnet_file, c2_type, user_strings):
38
38
  if c2_type == "Telegram":
39
39
  token = dotnet_file.net.user_strings.get(user_strings_list[15]).value.__str__()
40
40
  chat_id = dotnet_file.net.user_strings.get(user_strings_list[16]).value.__str__()
41
- return {"raw": {"Type": "Telegram"}, "CNCs": f"https://api.telegram.org/bot{token}/sendMessage?chat_id={chat_id}"}
41
+ return {"raw": {"Type": "Telegram"}, "CNCs": [f"https://api.telegram.org/bot{token}/sendMessage?chat_id={chat_id}"]}
42
42
  elif c2_type == "SMTP":
43
43
  smtp_from = dotnet_file.net.user_strings.get(user_strings_list[7]).value.__str__()
44
44
  smtp_password = dotnet_file.net.user_strings.get(user_strings_list[8]).value.__str__()
@@ -106,7 +106,7 @@ def handle_encrypted(dotnet_file, data, c2_type, user_strings):
106
106
  if decrypted_strings:
107
107
  if c2_type == "Telegram":
108
108
  token, chat_id = decrypted_strings
109
- config_dict = {"raw": {"Type": "Telegram"}, "CNCs": f"https://api.telegram.org/bot{token}/sendMessage?chat_id={chat_id}"}
109
+ config_dict = {"raw": {"Type": "Telegram"}, "CNCs": [f"https://api.telegram.org/bot{token}/sendMessage?chat_id={chat_id}"]}
110
110
  elif c2_type == "SMTP":
111
111
  smtp_from, smtp_password, smtp_host, smtp_to, smtp_port = decrypted_strings
112
112
  config_dict = {
@@ -61,7 +61,11 @@ def extract_config(data):
61
61
  enc_data = f.read(data_len - 32)
62
62
  config = decrypt_config(enc_data, key, iv)
63
63
  if config:
64
- return {"raw": config}
64
+ output = {"raw": config}
65
+ output["CNCs"] = [
66
+ f"{'http' if not config['secure'] else 'https'}://{config['host']}:{config['port']}{config['path']}"
67
+ ]
68
+ return output
65
69
  except Exception as e:
66
70
  log.error("Configuration decryption failed: %s", e)
67
71
  return {}
@@ -3,9 +3,8 @@ Description: Winos 4.0 "OnlineModule" config parser
3
3
  Author: x.com/YungBinary
4
4
  """
5
5
 
6
- from contextlib import suppress
7
6
  import re
8
-
7
+ from contextlib import suppress
9
8
 
10
9
  CONFIG_KEY_MAP = {
11
10
  "dd": "execution_delay_seconds",
@@ -60,7 +59,11 @@ def extract_config(data: bytes) -> dict:
60
59
 
61
60
  final_config["CNCs"] = list(set(final_config["CNCs"]))
62
61
  # Extract campaign ID
63
- final_config["campaign_id"] = "default" if config_dict["fz"] == "\u9ed8\u8ba4" else config_dict["fz"]
62
+ final_config["campaign"] = "default" if config_dict["fz"] == "\u9ed8\u8ba4" else config_dict["fz"]
63
+
64
+ # Check if the version has been extracted
65
+ if "bb" in config_dict:
66
+ final_config["version"] = config_dict["bb"]
64
67
 
65
68
  # Map keys, e.g. dd -> execution_delay_seconds
66
69
  final_config["raw"] = {v: config_dict[k] for k, v in CONFIG_KEY_MAP.items() if k in config_dict}
@@ -59,7 +59,19 @@ def parse_http_config(rc4_key: bytes, data: bytes) -> dict:
59
59
  config["sleep_delay"] = read("<I")
60
60
  config["jitter_delay"] = read("<I")
61
61
 
62
- return {"raw": config}
62
+ output = {"raw": config}
63
+
64
+ # Map some fields to CAPE's output format, where possible
65
+ output['cryptokey'] = config['cryptokey']
66
+ output['cryptokey_type'] = config['cryptokey_type']
67
+ output['user_agent'] = config['user_agent']
68
+ output['CNCs'] = [f"{'https' if config['use_ssl'] else 'http'}://{server}:{ports[i]}{config['uri']}"
69
+ for i, server in enumerate(servers)]
70
+
71
+ # TODO: Does agent_type map to version or build?
72
+ # output['version'] = output['raw']['agent_type']
73
+
74
+ return output
63
75
 
64
76
 
65
77
  def extract_config(filebuf: bytes) -> dict:
@@ -15,8 +15,8 @@ from pathlib import Path
15
15
  from struct import pack, unpack
16
16
 
17
17
  import pefile
18
-
19
18
  import yara
19
+
20
20
  from cape_parsers.utils.lznt1 import lznt1
21
21
 
22
22
  log = logging.getLogger(__name__)
@@ -556,4 +556,7 @@ def extract_config(data):
556
556
  }
557
557
  }
558
558
 
559
+ config["cryptokey"] = config["raw"]["Rabbit key"]
560
+ config["cryptokey_type"] = "Rabbit"
561
+
559
562
  return config
@@ -92,11 +92,14 @@ def decode(data):
92
92
 
93
93
 
94
94
  def extract_config(data):
95
+ config_extracted = False
95
96
  with suppress(pefile.PEFormatError):
96
97
  pe = pefile.PE(data=data, fast_load=True)
97
98
  for section in pe.sections:
98
99
  if b"CODE" in section.Name:
99
- return decode(section.get_data())
100
+ config = decode(section.get_data())
101
+ config_extracted = True
102
+ break
100
103
 
101
104
  if b"1=Yes" in data or b"1=No" in data:
102
105
  config = {}
@@ -105,6 +108,32 @@ def extract_config(data):
105
108
  config["CNCs"] = [x for x in item[2:].decode("utf-8").split("|") if x.strip() != ""]
106
109
  else:
107
110
  config.update(parse_config(item, config_map_2))
111
+ config_extracted = True
112
+
113
+ if config_extracted:
114
+ # Map information to CAPE's format, if possible
115
+ if "c2_port" in config["raw"] and "CNCs" in config:
116
+ # We're making the assumpton the same port is used for all CNCs
117
+ config["CNCs"] = [f"{url}:{config['raw']['c2_port']}" for url in config["CNCs"]]
118
+
119
+ if "internal_mutex" in config["raw"]:
120
+ # Bring mutex to top level
121
+ config["mutex"] = config["raw"]["internal_mutex"]
122
+
123
+ if "crypto_key" in config["raw"]:
124
+ # Bring crypto_key to top level
125
+ config["cryptokey"] = config["raw"]["crypto_key"]
126
+
127
+ if "campaign_id" in config["raw"]:
128
+ # Bring campaign_id to top level
129
+ config["campaign"] = config["raw"]["campaign_id"]
130
+
131
+ if "xor_key" in config["raw"]:
132
+ # Bring xor_key to top level
133
+ config["cryptokey"] = config["raw"]["xor_key"]
134
+ config["cruptokey_type"] = "XOR"
135
+
136
+
108
137
  return config
109
138
 
110
139
  return ""
@@ -35,7 +35,8 @@ def extract_config(filebuf):
35
35
  if n > 32:
36
36
  break
37
37
  campaign, c2 = struct.unpack("I30s", bytes(dec))
38
- config["CNCs"] = c2.split(b"\00", 1)[0].decode()
38
+ c2 = c2.split(b"\00", 1)[0].decode()
39
+ config["CNCs"] = f"http://{c2}"
39
40
  config["campaign"] = campaign
40
41
  return config
41
42
 
@@ -154,6 +154,7 @@ def extract_config(filebuf):
154
154
  minor = int.from_bytes(data[18:19], byteorder="big")
155
155
  release = int.from_bytes(data[26:27], byteorder="big")
156
156
  version = f"{major}.{minor}.{release}"
157
+
157
158
  if "$key" in item.identifier:
158
159
  key = instance.matched_data[4::5]
159
160
  try:
@@ -161,6 +162,7 @@ def extract_config(filebuf):
161
162
  data_sections = [s for s in pe.sections if s.Name.find(b".data") != -1]
162
163
  if not data_sections:
163
164
  return
165
+
164
166
  data = data_sections[0].get_data()
165
167
  str_vals = []
166
168
  c2 = []
@@ -14,6 +14,7 @@
14
14
 
15
15
  import logging
16
16
  import struct
17
+ from typing import List
17
18
 
18
19
  import pefile
19
20
  import yara
@@ -32,8 +33,8 @@ rule NitroBunnyDownloader
32
33
  cape_type = "NitroBunnyDownloader Payload"
33
34
  hash = "960e59200ec0a4b5fb3b44e6da763f5fec4092997975140797d4eec491de411b"
34
35
  strings:
35
- $config1 = {E8 [3] 00 41 B8 ?? ?? 00 00 48 8D 15 [3] 00 48 89 C1 48 89 ?? E8 [3] 00}
36
- $config2 = {E8 [3] 00 48 8D 15 [3] 00 41 B8 ?? ?? 00 00 48 89 C1 48 89 ?? E8 [3] 00}
36
+ $config1 = {E8 [3] 00 41 B8 ?? ?? 00 00 48 8D 15 [3] 00 48 89 C1 4? 89 ?? E8 [3] 00}
37
+ $config2 = {E8 [3] 00 48 8D 15 [3] 00 41 B8 ?? ?? 00 00 48 89 C1 4? 89 ?? E8 [3] 00}
37
38
  $string1 = "X-Amz-User-Agent:" wide
38
39
  $string2 = "Amz-Security-Flag:" wide
39
40
  $string3 = "/cart" wide
@@ -87,6 +88,20 @@ def read_string_list(data, off, count):
87
88
  return items, off
88
89
 
89
90
 
91
+ def make_endpoints(cncs: List[str], port: int, uris: List[str]) -> List[str]:
92
+ endpoints = []
93
+ schema = {80: "http", 443: "https"}.get(port, "tcp")
94
+ for cnc in cncs:
95
+ base_url = f"{schema}://{cnc}"
96
+ if port not in (80, 443):
97
+ base_url += f":{port}"
98
+
99
+ for uri in uris:
100
+ endpoints.append(f"{base_url}/{uri.lstrip('/')}")
101
+
102
+ return endpoints
103
+
104
+
90
105
  def extract_config(filebuf):
91
106
  yara_hit = yara_scan(filebuf)
92
107
  if not yara_hit:
@@ -98,24 +113,20 @@ def extract_config(filebuf):
98
113
  config_offset = None
99
114
  rva_offset = None
100
115
 
116
+ CONFIG_OFFSETS = {
117
+ "$config1": (7, 14, 18),
118
+ "$config2": (14, 8, 12),
119
+ }
120
+
101
121
  for hit in yara_hit:
102
122
  if hit.rule != "NitroBunnyDownloader":
103
123
  continue
104
124
 
105
125
  for item in hit.strings:
106
- for instance in item.instances:
107
- if item.identifier == "$config1":
108
- config_code_offset = instance.offset
109
- config_size_offset = 7
110
- config_offset= 14
111
- rva_offset = 18
112
- break
113
- elif item.identifier == "$config2":
114
- config_code_offset = instance.offset
115
- config_size_offset = 14
116
- config_offset= 8
117
- rva_offset = 12
118
- break
126
+ if item.identifier in CONFIG_OFFSETS and item.instances:
127
+ config_code_offset = item.instances[0].offset
128
+ config_size_offset, config_offset, rva_offset = CONFIG_OFFSETS[item.identifier]
129
+ break
119
130
 
120
131
  if config_code_offset:
121
132
  break
@@ -126,33 +137,26 @@ def extract_config(filebuf):
126
137
  try:
127
138
  pe = pefile.PE(data=filebuf, fast_load=True)
128
139
  config_length = pe.get_dword_from_offset(config_code_offset + config_size_offset)
129
- config = pe.get_dword_from_offset(config_code_offset + config_offset)
140
+ config_data_offset = pe.get_dword_from_offset(config_code_offset + config_offset)
130
141
  rva = pe.get_rva_from_offset(config_code_offset + rva_offset)
131
- config_rva = rva + config
142
+ config_rva = rva + config_data_offset
132
143
  data = pe.get_data(config_rva, config_length)
133
144
  off = 0
134
- raw = cfg["raw"] = {}
145
+ cfg["CNCs"] = []
135
146
  port, off = read_dword(data, off)
136
147
  num, off = read_dword(data, off)
137
148
  cncs, off = read_string_list(data, off, num)
138
149
  num, off = read_qword(data, off)
139
- raw["user_agent"], off = read_utf16le_string(data, off, num)
150
+ cfg["user_agent"], off = read_utf16le_string(data, off, num)
140
151
  num, off = read_dword(data, off)
152
+ raw = cfg["raw"] = {}
141
153
  raw["http_header_items"], off = read_string_list(data, off, num)
142
154
  num, off = read_dword(data, off)
143
- raw["uri_list"], off = read_string_list(data, off, num)
155
+ uris, off = read_string_list(data, off, num)
144
156
  raw["unknown_1"], off = read_dword(data, off)
145
157
  raw["unknown_2"], off = read_dword(data, off)
146
-
147
158
  if cncs:
148
- cfg["CNCs"] = []
149
- schema = {80: "http", 443: "https"}.get(port, "tcp")
150
- for cnc in cncs:
151
- cnc = f"{schema}://{cnc}"
152
- if port not in (80, 443):
153
- cnc += f":{port}"
154
-
155
- cfg["CNCs"].append(cnc)
159
+ cfg["CNCs"] = make_endpoints(cncs, port, uris)
156
160
 
157
161
  except Exception as e:
158
162
  log.error("Error: %s", e)
@@ -12,21 +12,21 @@
12
12
  # You should have received a copy of the GNU General Public License
13
13
  # along with this program. If not, see <http://www.gnu.org/licenses/>.
14
14
 
15
-
15
+ import ipaddress
16
16
  import logging
17
17
  import re
18
18
  import struct
19
19
  from contextlib import suppress
20
+ from typing import List
20
21
 
21
- import yara
22
22
  import pefile
23
+ import yara
23
24
 
24
25
  log = logging.getLogger(__name__)
25
26
 
26
27
  DESCRIPTION = "Oyster configuration parser."
27
28
  AUTHOR = "enzok"
28
- yara_rules = yara.compile(
29
- source="""rule Oyster
29
+ yara_rules = yara.compile(source="""rule Oyster
30
30
  {
31
31
  meta:
32
32
  author = "enzok"
@@ -45,8 +45,9 @@ yara_rules = yara.compile(
45
45
  condition:
46
46
  4 of them
47
47
  }
48
- """
49
- )
48
+ """)
49
+
50
+ MIN_CHARS = 6
50
51
 
51
52
 
52
53
  def transform(src, lookup_table):
@@ -63,37 +64,137 @@ def transform(src, lookup_table):
63
64
  result = lookup_table[n]
64
65
  src[pVal] = result
65
66
  pVal -= 1
67
+
66
68
  return src
67
69
 
68
70
 
69
71
  def yara_scan(raw_data):
70
72
  try:
71
73
  return yara_rules.match(data=raw_data)
72
- except Exception as e:
73
- print(e)
74
+ except yara.Error as e:
75
+ log.error("Yara scan failed: %s", e)
76
+ return []
77
+
78
+
79
+ def extract_utf16le(data, min_chars=MIN_CHARS):
80
+ results = []
81
+ n = len(data)
82
+ i = 0
83
+ while i < n - 1:
84
+ if 32 <= data[i] <= 126 and data[i + 1] == 0x00:
85
+ start = i
86
+ chars = []
87
+ while i < n - 1 and 32 <= data[i] <= 126 and data[i + 1] == 0x00:
88
+ chars.append(chr(data[i]))
89
+ i += 2
90
+
91
+ if len(chars) >= min_chars:
92
+ results.append((start, ''.join(chars)))
93
+ else:
94
+ i += 1
95
+
96
+ return results
97
+
98
+
99
+ def is_uri(s: str) -> bool:
100
+ return s.lower().lstrip().startswith(("api/", "/api"))
101
+
102
+
103
+ def is_c2(s: str) -> bool:
104
+ DOMAIN_RE = re.compile(r'^[a-zA-Z0-9.-]+\.(?:com|net)$', re.IGNORECASE)
105
+ IP_RE = re.compile(r'^(?:\d{1,3}\.){3}\d{1,3}$')
106
+
107
+ s = s.strip()
108
+ if DOMAIN_RE.match(s):
109
+ return True
110
+
111
+ if IP_RE.match(s):
112
+ try:
113
+ ip = ipaddress.IPv4Address(s)
114
+ if ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_multicast or ip.is_reserved or ip.is_unspecified:
115
+ return False
116
+
117
+ return True
118
+ except ipaddress.AddressValueError:
119
+ return False
120
+
121
+ return False
122
+
123
+
124
+ def is_useragent(s: str) -> bool:
125
+ return "mozilla" in s.lower()
126
+
127
+
128
+ def is_mutex(s: str) -> bool:
129
+ if s.count('-') < 2:
130
+ return False
131
+
132
+ parts = [p.strip() for p in s.split('-') if p.strip()]
133
+ if len(parts) < 3:
134
+ return False
135
+
136
+ has_digit = any(any(ch.isdigit() for ch in part) for part in parts)
137
+ long_enough = all(len(part) > 1 for part in parts)
138
+ return has_digit and long_enough
139
+
140
+
141
+ def extract_types(data: bytes):
142
+ uris = []
143
+ c2s = []
144
+ user_agent = None
145
+ mutexes = []
146
+
147
+ for off, s in extract_utf16le(data):
148
+ s_str = s.strip()
149
+ if is_uri(s_str) and s_str not in uris:
150
+ uris.append(s_str)
151
+
152
+ if is_c2(s_str) and s_str not in c2s:
153
+ c2s.append(s_str)
154
+
155
+ if user_agent is None and is_useragent(s_str):
156
+ user_agent = s_str
157
+
158
+ if is_mutex(s_str) and s_str not in mutexes:
159
+ mutexes.append(s_str)
160
+
161
+ return uris, c2s, user_agent, mutexes
162
+
163
+
164
+ def make_endpoints(c2s: List[str], uris: List[str]) -> List[str]:
165
+ endpoints = []
166
+ for c2 in c2s:
167
+ for uri in uris:
168
+ endpoints.append(f"https://{c2}/{uri.lstrip('/')}")
169
+
170
+ return endpoints
74
171
 
75
172
 
76
173
  def extract_config(filebuf):
77
- yara_hit = yara_scan(filebuf)
174
+ yara_hits = yara_scan(filebuf)
78
175
  config = {}
79
176
 
80
- for hit in yara_hit:
177
+ for hit in yara_hits:
81
178
  if hit.rule == "Oyster":
82
179
  start_offset = ""
83
180
  lookup_va = ""
181
+
84
182
  for item in hit.strings:
85
183
  if "$start_exit" == item.identifier:
86
184
  start_offset = item.instances[0].offset
185
+
87
186
  if "$decode" == item.identifier:
88
187
  decode_offset = item.instances[0].offset
89
- lookup_va = filebuf[decode_offset + 12 : decode_offset + 16]
188
+ lookup_va = filebuf[decode_offset + 12: decode_offset + 16]
189
+
90
190
  if not (start_offset and lookup_va):
91
- return
191
+ continue
192
+
92
193
  try:
93
194
  pe = pefile.PE(data=filebuf, fast_load=True)
94
195
  lookup_offset = pe.get_offset_from_rva(struct.unpack("I", lookup_va)[0] - pe.OPTIONAL_HEADER.ImageBase)
95
- lookup_table = filebuf[lookup_offset : lookup_offset + 256]
96
- data = filebuf[start_offset + 4 : start_offset + 8092]
196
+ lookup_table = filebuf[lookup_offset: lookup_offset + 256]
197
+ data = filebuf[start_offset + 4: start_offset + 8092]
97
198
  hex_strings = re.split(rb"\x00+", data)
98
199
  hex_strings = [s for s in hex_strings if s]
99
200
  str_vals = []
@@ -105,8 +206,10 @@ def extract_config(filebuf):
105
206
  for item in hex_strings:
106
207
  with suppress(Exception):
107
208
  decoded = transform(bytearray(item), bytearray(lookup_table)).decode("utf-8")
209
+
108
210
  if not decoded:
109
211
  continue
212
+
110
213
  if "http" in decoded:
111
214
  if "\r\n" in decoded:
112
215
  c2.extend(list(filter(None, decoded.split("\r\n"))))
@@ -121,15 +224,19 @@ def extract_config(filebuf):
121
224
  if c2_matches:
122
225
  c2.extend(c2_matches)
123
226
 
124
- config = {
125
- "CNCs": c2,
126
- 'version': dll_version,
127
- "raw": {
128
- "Strings": str_vals,
129
- },
130
- }
227
+ config = {"CNCs": c2, 'version': dll_version, "raw": {"Strings": str_vals, }, }
228
+ return config
131
229
  except Exception as e:
132
230
  log.error("Error: %s", e)
231
+
232
+ if not config:
233
+ urls = []
234
+ uris, c2s, useragent, mutexes = extract_types(filebuf)
235
+ if uris and c2s:
236
+ urls = make_endpoints(c2s, uris)
237
+
238
+ config = {"CNCs": urls, "user_agent": useragent, "mutex": mutexes, }
239
+
133
240
  return config
134
241
 
135
242
 
@@ -6,7 +6,6 @@ from contextlib import suppress
6
6
  from io import BytesIO
7
7
 
8
8
  import pefile
9
-
10
9
  import yara
11
10
 
12
11
  rule_source = """
@@ -83,7 +82,7 @@ def get_c2s(data, count):
83
82
  c2_size = struct.unpack("I", data.read(4))[0]
84
83
  c2 = get_wchar_string(data, c2_size)
85
84
  port, val1, val2 = struct.unpack("III", data.read(12))
86
- c2_list.append(f"{c2}:{port}")
85
+ c2_list.append(f"http://{c2}:{port}")
87
86
  return c2_list
88
87
 
89
88
 
@@ -5,22 +5,21 @@
5
5
  DESCRIPTION = "Qakbot configuration parser."
6
6
  AUTHOR = "threathive, r1n9w0rm"
7
7
 
8
- import sys
9
8
  import datetime
10
9
  import hashlib
11
10
  import ipaddress
12
11
  import logging
13
12
  import socket
14
13
  import struct
14
+ import sys
15
15
  from contextlib import suppress
16
16
 
17
17
  import pefile
18
+ import yara
18
19
  from Cryptodome.Cipher import AES, ARC4
19
20
  from Cryptodome.Hash import SHA256
20
21
  from Cryptodome.Util.Padding import unpad
21
22
 
22
- import yara
23
-
24
23
  try:
25
24
  HAVE_BLZPACK = True
26
25
  from cape_parsers.utils import blzpack
@@ -494,7 +493,11 @@ def extract_config(filebuf):
494
493
  for k, v in config.items():
495
494
  end_config.setdefault(k, v)
496
495
  if end_config:
497
- return {"raw": end_config}
496
+ output = {"raw": end_config}
497
+ if "C2s" in end_config:
498
+ # Prefix with tcp://
499
+ output["CNCs"] = [f"http://{c2}" for c2 in end_config["C2s"]]
500
+ return output
498
501
 
499
502
 
500
503
  if __name__ == "__main__":
@@ -118,10 +118,11 @@ def extract_config(filebuf):
118
118
  cfg["campaign"] = campaign
119
119
 
120
120
  if c2s:
121
- cfg["CNCs"] = c2s
121
+ cfg["CNCs"] = [f"http://{c2}" for c2 in c2s]
122
122
 
123
123
  if mutexes:
124
- cfg["mutex"] = list(set(mutexes))
124
+ mutexes = list(set(mutexes))
125
+ cfg["mutex"] = mutexes[0] if len(mutexes) == 1 else mutexes
125
126
 
126
127
  return cfg
127
128
 
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "CAPE-parsers"
3
- version = "0.1.56"
3
+ version = "0.1.57"
4
4
  description = "CAPE: Malware Configuration Extraction"
5
5
  authors = ["Kevin O'Reilly <kev@capesandbox.com>", "doomedraven <doomedraven@capesandbox.com>"]
6
6
  license = "MIT"
File without changes
File without changes