CAPE-parsers 0.1.44__py3-none-any.whl → 0.1.46__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 (83) hide show
  1. cape_parsers/CAPE/community/AgentTesla.py +18 -9
  2. cape_parsers/CAPE/community/Arkei.py +13 -15
  3. cape_parsers/CAPE/community/AsyncRAT.py +4 -2
  4. cape_parsers/CAPE/community/AuroraStealer.py +9 -6
  5. cape_parsers/CAPE/community/Carbanak.py +7 -7
  6. cape_parsers/CAPE/community/CobaltStrikeBeacon.py +2 -1
  7. cape_parsers/CAPE/community/CobaltStrikeStager.py +4 -1
  8. cape_parsers/CAPE/community/DCRat.py +4 -2
  9. cape_parsers/CAPE/community/Fareit.py +8 -9
  10. cape_parsers/CAPE/community/KoiLoader.py +3 -3
  11. cape_parsers/CAPE/community/LokiBot.py +1 -1
  12. cape_parsers/CAPE/community/Lumma.py +49 -36
  13. cape_parsers/CAPE/community/NanoCore.py +9 -9
  14. cape_parsers/CAPE/community/Nighthawk.py +1 -0
  15. cape_parsers/CAPE/community/Njrat.py +4 -4
  16. cape_parsers/CAPE/community/PhemedroneStealer.py +2 -0
  17. cape_parsers/CAPE/community/Snake.py +29 -16
  18. cape_parsers/CAPE/community/SparkRAT.py +3 -1
  19. cape_parsers/CAPE/community/Stealc.py +86 -64
  20. cape_parsers/CAPE/community/VenomRAT.py +4 -2
  21. cape_parsers/CAPE/community/XWorm.py +4 -2
  22. cape_parsers/CAPE/community/XenoRAT.py +4 -2
  23. cape_parsers/CAPE/community/monsterv2.py +96 -0
  24. cape_parsers/CAPE/core/AdaptixBeacon.py +7 -5
  25. cape_parsers/CAPE/core/Azorult.py +5 -3
  26. cape_parsers/CAPE/core/BitPaymer.py +5 -2
  27. cape_parsers/CAPE/core/BlackDropper.py +10 -5
  28. cape_parsers/CAPE/core/Blister.py +12 -10
  29. cape_parsers/CAPE/core/BruteRatel.py +20 -7
  30. cape_parsers/CAPE/core/BumbleBee.py +29 -17
  31. cape_parsers/CAPE/core/DarkGate.py +3 -3
  32. cape_parsers/CAPE/core/DoppelPaymer.py +4 -2
  33. cape_parsers/CAPE/core/DridexLoader.py +4 -3
  34. cape_parsers/CAPE/core/Formbook.py +2 -2
  35. cape_parsers/CAPE/core/GuLoader.py +2 -5
  36. cape_parsers/CAPE/core/IcedID.py +5 -5
  37. cape_parsers/CAPE/core/IcedIDLoader.py +4 -4
  38. cape_parsers/CAPE/core/Latrodectus.py +10 -7
  39. cape_parsers/CAPE/core/Oyster.py +8 -6
  40. cape_parsers/CAPE/core/PikaBot.py +6 -6
  41. cape_parsers/CAPE/core/PlugX.py +3 -1
  42. cape_parsers/CAPE/core/QakBot.py +2 -1
  43. cape_parsers/CAPE/core/Quickbind.py +7 -11
  44. cape_parsers/CAPE/core/RedLine.py +2 -2
  45. cape_parsers/CAPE/core/Remcos.py +58 -50
  46. cape_parsers/CAPE/core/Rhadamanthys.py +18 -8
  47. cape_parsers/CAPE/core/SmokeLoader.py +2 -2
  48. cape_parsers/CAPE/core/Socks5Systemz.py +5 -5
  49. cape_parsers/CAPE/core/SquirrelWaffle.py +3 -3
  50. cape_parsers/CAPE/core/Strrat.py +1 -1
  51. cape_parsers/CAPE/core/WarzoneRAT.py +3 -2
  52. cape_parsers/CAPE/core/Zloader.py +21 -15
  53. cape_parsers/RATDecoders/test_rats.py +1 -0
  54. cape_parsers/__init__.py +13 -4
  55. cape_parsers/deprecated/BlackNix.py +59 -0
  56. cape_parsers/{CAPE/core → deprecated}/BuerLoader.py +1 -1
  57. cape_parsers/{CAPE/core → deprecated}/ChChes.py +3 -3
  58. cape_parsers/{CAPE/core → deprecated}/Enfal.py +1 -1
  59. cape_parsers/{CAPE/core → deprecated}/EvilGrab.py +5 -6
  60. cape_parsers/{CAPE/community → deprecated}/Greame.py +3 -1
  61. cape_parsers/{CAPE/core → deprecated}/HttpBrowser.py +7 -8
  62. cape_parsers/{CAPE/community → deprecated}/Pandora.py +2 -0
  63. cape_parsers/{CAPE/community → deprecated}/Punisher.py +2 -1
  64. cape_parsers/{CAPE/core → deprecated}/RCSession.py +7 -9
  65. cape_parsers/{CAPE/community → deprecated}/REvil.py +10 -5
  66. cape_parsers/{CAPE/core → deprecated}/RedLeaf.py +5 -7
  67. cape_parsers/{CAPE/community → deprecated}/Retefe.py +0 -2
  68. cape_parsers/{CAPE/community → deprecated}/Rozena.py +2 -5
  69. cape_parsers/{CAPE/community → deprecated}/SmallNet.py +6 -2
  70. {cape_parsers-0.1.44.dist-info → cape_parsers-0.1.46.dist-info}/METADATA +20 -1
  71. cape_parsers-0.1.46.dist-info/RECORD +112 -0
  72. cape_parsers/CAPE/community/BlackNix.py +0 -57
  73. cape_parsers/CAPE/core/Stealc.py +0 -21
  74. cape_parsers-0.1.44.dist-info/RECORD +0 -112
  75. /cape_parsers/{CAPE/community → deprecated}/BackOffLoader.py +0 -0
  76. /cape_parsers/{CAPE/community → deprecated}/BackOffPOS.py +0 -0
  77. /cape_parsers/{CAPE/core → deprecated}/Emotet.py +0 -0
  78. /cape_parsers/{CAPE/community → deprecated}/PoisonIvy.py +0 -0
  79. /cape_parsers/{CAPE/community → deprecated}/TSCookie.py +0 -0
  80. /cape_parsers/{CAPE/community → deprecated}/TrickBot.py +0 -0
  81. /cape_parsers/{CAPE/core → deprecated}/UrsnifV3.py +0 -0
  82. {cape_parsers-0.1.44.dist-info → cape_parsers-0.1.46.dist-info}/LICENSE +0 -0
  83. {cape_parsers-0.1.44.dist-info → cape_parsers-0.1.46.dist-info}/WHEEL +0 -0
@@ -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 {"Type": "Telegram", "C2": 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__()
@@ -46,18 +46,26 @@ def handle_plain(dotnet_file, c2_type, user_strings):
46
46
  smtp_to = dotnet_file.net.user_strings.get(user_strings_list[10]).value.__str__()
47
47
  smtp_port = dotnet_file.net.user_strings.get(user_strings_list[11]).value.__str__()
48
48
  return {
49
- "Type": "SMTP",
50
- "Host": smtp_host,
51
- "Port": smtp_port,
52
- "From Address": smtp_from,
53
- "To Address": smtp_to,
54
- "Password": smtp_password,
49
+ "raw": {
50
+ "Type": "SMTP",
51
+ "Host": smtp_host,
52
+ "Port": smtp_port,
53
+ "From Address": smtp_from,
54
+ "To Address": smtp_to,
55
+ "Password": smtp_password,
56
+ },
57
+ "CNCs": [f"smtp://{smtp_host}:{smtp_port}"]
55
58
  }
56
59
  elif c2_type == "FTP":
57
60
  ftp_username = dotnet_file.net.user_strings.get(user_strings_list[12]).value.__str__()
58
61
  ftp_password = dotnet_file.net.user_strings.get(user_strings_list[13]).value.__str__()
59
62
  ftp_host = dotnet_file.net.user_strings.get(user_strings_list[14]).value.__str__()
60
- return {"Type": "FTP", "Host": ftp_host, "Username": ftp_username, "Password": ftp_password}
63
+ return {
64
+ "raw": {
65
+ "Type": "FTP", "Host": ftp_host, "Username": ftp_username, "Password": ftp_password},
66
+ "CNCs": [f"ftp://{ftp_username}:{ftp_password}@{ftp_host}"]
67
+ }
68
+
61
69
 
62
70
 
63
71
  def handle_encrypted(dotnet_file, data, c2_type, user_strings):
@@ -98,20 +106,25 @@ def handle_encrypted(dotnet_file, data, c2_type, user_strings):
98
106
  if decrypted_strings:
99
107
  if c2_type == "Telegram":
100
108
  token, chat_id = decrypted_strings
101
- config_dict = {"Type": "Telegram", "C2": 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}"}
102
110
  elif c2_type == "SMTP":
103
111
  smtp_from, smtp_password, smtp_host, smtp_to, smtp_port = decrypted_strings
104
112
  config_dict = {
105
- "Type": "SMTP",
106
- "Host": smtp_host,
107
- "Port": smtp_port,
108
- "From Address": smtp_from,
109
- "To Address": smtp_to,
110
- "Password": smtp_password,
113
+ "raw": {
114
+ "Type": "SMTP",
115
+ "Host": smtp_host,
116
+ "Port": smtp_port,
117
+ "From Address": smtp_from,
118
+ "To Address": smtp_to,
119
+ "Password": smtp_password,
120
+ }
111
121
  }
112
122
  elif c2_type == "FTP":
113
123
  ftp_username, ftp_password, ftp_host = decrypted_strings
114
- config_dict = {"Type": "FTP", "Host": ftp_host, "Username": ftp_username, "Password": ftp_password}
124
+ config_dict = {
125
+ "raw": {"Type": "FTP", "Host": ftp_host, "Username": ftp_username, "Password": ftp_password},
126
+ "CNCs": [f"ftp://{ftp_username}:{ftp_password}@{ftp_host}"]
127
+ }
115
128
  return config_dict
116
129
 
117
130
 
@@ -59,7 +59,9 @@ def extract_config(data):
59
59
  key = f.read(16)
60
60
  iv = f.read(16)
61
61
  enc_data = f.read(data_len - 32)
62
- return decrypt_config(enc_data, key, iv)
62
+ config = decrypt_config(enc_data, key, iv)
63
+ if config:
64
+ return {"raw": config}
63
65
  except Exception as e:
64
66
  log.error("Configuration decryption failed: %s", e)
65
67
  return {}
@@ -1,31 +1,35 @@
1
1
  import struct
2
2
  import pefile
3
3
  import yara
4
+ from contextlib import suppress
4
5
 
5
6
 
6
- # Hash = 619751f5ed0a9716318092998f2e4561f27f7f429fe6103406ecf16e33837470
7
+ # V1 hash = 619751f5ed0a9716318092998f2e4561f27f7f429fe6103406ecf16e33837470
8
+ # V2 hash = 2f42dcf05dd87e6352491ff9d4ea3dc3f854df53d548a8da0c323be42df797b6 (32-bit payload)
9
+ # V2 hash = 8301936f439f43579cffe98e11e3224051e2fb890ffe9df680bbbd8db0729387 (64-bit payload)
7
10
 
8
- RULE_SOURCE = """rule StealC
11
+ RULE_SOURCE = """
12
+ rule StealC
9
13
  {
10
14
  meta:
11
15
  author = "Yung Binary"
12
16
  strings:
13
- $decode_1 = {
14
- 6A ??
15
- 68 ?? ?? ?? ??
16
- 68 ?? ?? ?? ??
17
- E8 ?? ?? ?? ??
18
- }
19
- $decode_2 = {
20
- 6A ??
21
- 68 ?? ?? ?? ??
22
- 68 ?? ?? ?? ??
23
- [0-5]
24
- E8 ?? ?? ?? ??
25
- }
17
+ $decode_1 = {6A ?? 68 [4] 68 [4] E8}
18
+ $decode_2 = {6A ?? 68 [4] 68 [4] [0-5] E8}
26
19
  condition:
27
20
  any of them
28
- }"""
21
+ }
22
+ rule StealcV2
23
+ {
24
+ meta:
25
+ author = "kevoreilly"
26
+ strings:
27
+ $botnet32 = {AB AB AB AB 89 4B ?? C7 43 ?? 0F 00 00 00 88 0B A0 [4] EB 12 3C 20 74 0B 0F B6 06 8B CB 50 E8}
28
+ $botnet64 = {0F 11 01 48 C7 41 ?? 00 00 00 00 48 8B D9 48 C7 41 ?? 0F 00 00 00 C6 01 00 8A 05 [4] EB ?? 3C 20 74 ?? 48 8B 4B ?? 44 8A 0F}
29
+ condition:
30
+ any of them
31
+ }
32
+ """
29
33
 
30
34
 
31
35
  def yara_scan(raw_data):
@@ -45,78 +49,96 @@ def xor_data(data, key):
45
49
  return decoded
46
50
 
47
51
 
48
- def extract_config(data):
49
- config_dict = {}
52
+ def extract_ascii_string(data: bytes, offset: int, max_length=4096) -> str:
53
+ if offset >= len(data):
54
+ raise ValueError("Offset beyond data bounds")
55
+ end = data.find(b'\x00', offset, offset + max_length)
56
+ if end == -1:
57
+ end = offset + max_length
58
+ return data[offset:end].decode('ascii', errors='replace')
50
59
 
51
- # Attempt to extract via old method
52
- try:
53
- domain = ""
54
- uri = ""
60
+
61
+ def parse_text(data):
62
+ global domain, uri
63
+ with suppress(Exception):
55
64
  lines = data.decode().split("\n")
65
+ if not lines:
66
+ return
56
67
  for line in lines:
57
68
  if line.startswith("http") and "://" in line:
58
69
  domain = line
59
70
  if line.startswith("/") and line[-4] == ".":
60
71
  uri = line
61
- if domain and uri:
62
- config_dict.setdefault("C2", []).append(f"{domain}{uri}")
63
- return config_dict
64
- except Exception:
65
- pass
66
-
67
- # Try with new method
68
-
69
- #config_dict["Strings"] = []
70
- pe = pefile.PE(data=data, fast_load=True)
71
- image_base = pe.OPTIONAL_HEADER.ImageBase
72
- domain = ""
73
- uri = ""
74
- botnet_id = ""
72
+
73
+
74
+ def parse_pe(data):
75
+ global domain, uri, botnet_id
76
+ pe = None
77
+ image_base = 0
75
78
  last_str = ""
79
+ with suppress(Exception):
80
+ pe = pefile.PE(data=data, fast_load=True)
81
+ if not pe:
82
+ return
83
+ image_base = pe.OPTIONAL_HEADER.ImageBase
84
+ if not image_base:
85
+ return
76
86
  for match in yara_scan(data):
77
87
  try:
78
88
  rule_str_name, str_decode_offset = match
89
+ if rule_str_name.startswith("$botnet"):
90
+ botnet_var = struct.unpack("I", data[str_decode_offset - 4 : str_decode_offset])[0]
91
+ if hasattr(pe, 'OPTIONAL_HEADER'):
92
+ magic = pe.OPTIONAL_HEADER.Magic
93
+ if magic == 0x10b: # 32-bit
94
+ botnet_offset = pe.get_offset_from_rva(botnet_var - image_base)
95
+ elif magic == 0x20b: # 64-bit
96
+ botnet_offset = pe.get_offset_from_rva(pe.get_rva_from_offset(str_decode_offset) + botnet_var)
97
+ if botnet_offset:
98
+ botnet_id = extract_ascii_string(data, botnet_offset)
79
99
  str_size = int(data[str_decode_offset + 1])
80
100
  # Ignore size 0 strings
81
101
  if not str_size:
82
102
  continue
83
-
84
103
  if rule_str_name.startswith("$decode"):
85
104
  key_rva = data[str_decode_offset + 3 : str_decode_offset + 7]
86
105
  encoded_str_rva = data[str_decode_offset + 8 : str_decode_offset + 12]
87
- #dword_rva = data[str_decode_offset + 21 : str_decode_offset + 25]
88
-
89
- key_offset = pe.get_offset_from_rva(struct.unpack("i", key_rva)[0] - image_base)
90
- encoded_str_offset = pe.get_offset_from_rva(struct.unpack("i", encoded_str_rva)[0] - image_base)
91
- #dword_offset = struct.unpack("i", dword_rva)[0]
92
- #dword_name = f"dword_{hex(dword_offset)[2:]}"
93
-
94
- key = data[key_offset : key_offset + str_size]
95
- encoded_str = data[encoded_str_offset : encoded_str_offset + str_size]
96
- decoded_str = xor_data(encoded_str, key).decode()
97
- #config_dict["Strings"].append({dword_name : decoded_str})
98
-
99
- if last_str in ("http://", "https://"):
100
- domain += decoded_str
101
- elif decoded_str in ("http://", "https://"):
102
- domain = decoded_str
103
- elif "http" in decoded_str and "://" in decoded_str:
104
- domain = decoded_str
105
- elif uri == "" and decoded_str.startswith("/") and decoded_str[-4] == ".":
106
- uri = decoded_str
107
- elif last_str[0] == '/' and last_str[-1] == '/':
108
- botnet_id = decoded_str
109
-
110
- last_str = decoded_str
111
-
106
+ key_offset = pe.get_offset_from_rva(struct.unpack("i", key_rva)[0] - image_base)
107
+ encoded_str_offset = pe.get_offset_from_rva(struct.unpack("i", encoded_str_rva)[0] - image_base)
108
+ key = data[key_offset : key_offset + str_size]
109
+ encoded_str = data[encoded_str_offset : encoded_str_offset + str_size]
110
+ decoded_str = xor_data(encoded_str, key).decode()
111
+ if last_str in ("http://", "https://"):
112
+ domain += decoded_str
113
+ elif decoded_str in ("http://", "https://"):
114
+ domain = decoded_str
115
+ elif "http" in decoded_str and "://" in decoded_str:
116
+ domain = decoded_str
117
+ elif uri is None and decoded_str.startswith("/") and decoded_str[-4] == ".":
118
+ uri = decoded_str
119
+ elif last_str[0] == "/" and last_str[-1] == "/":
120
+ botnet_id = decoded_str
121
+ last_str = decoded_str
112
122
  except Exception:
113
123
  continue
124
+ return
125
+
126
+
127
+ def extract_config(data):
128
+ global domain, uri, botnet_id
129
+ domain = uri = botnet_id = None
130
+ config_dict = {}
131
+
132
+ if data[:2] == b'MZ':
133
+ parse_pe(data)
134
+ else:
135
+ parse_text(data)
114
136
 
115
137
  if domain and uri:
116
- config_dict.setdefault("C2", []).append(f"{domain}{uri}")
138
+ config_dict.setdefault("CNCs", []).append(f"{domain}{uri}")
117
139
 
118
140
  if botnet_id:
119
- config_dict.setdefault("Botnet ID", botnet_id)
141
+ config_dict.setdefault("botnet", botnet_id)
120
142
 
121
143
  return config_dict
122
144
 
@@ -5,10 +5,10 @@ import os
5
5
  from rat_king_parser.rkp import RATConfigParser
6
6
 
7
7
  HAVE_ASYNCRAT_COMMON = False
8
- module_file_path = '/opt/CAPEv2/data/asyncrat_common.py'
8
+ module_file_path = "/opt/CAPEv2/data/asyncrat_common.py"
9
9
  if os.path.exists(module_file_path):
10
10
  try:
11
- module_name = os.path.basename(module_file_path).replace('.py', '')
11
+ module_name = os.path.basename(module_file_path).replace(".py", "")
12
12
  spec = importlib.util.spec_from_file_location(module_name, module_file_path)
13
13
  asyncrat_common = importlib.util.module_from_spec(spec)
14
14
  sys.modules[module_name] = asyncrat_common
@@ -17,6 +17,7 @@ if os.path.exists(module_file_path):
17
17
  except Exception as e:
18
18
  print("Error loading asyncrat_common.py", e)
19
19
 
20
+
20
21
  def extract_config(data: bytes):
21
22
  config = RATConfigParser(data=data, remap_config=True).report.get("config", {})
22
23
  if config and HAVE_ASYNCRAT_COMMON:
@@ -24,6 +25,7 @@ def extract_config(data: bytes):
24
25
 
25
26
  return config
26
27
 
28
+
27
29
  if __name__ == "__main__":
28
30
  data = open(sys.argv[1], "rb").read()
29
31
  print(extract_config(data))
@@ -5,10 +5,10 @@ import os
5
5
  from rat_king_parser.rkp import RATConfigParser
6
6
 
7
7
  HAVE_ASYNCRAT_COMMON = False
8
- module_file_path = '/opt/CAPEv2/data/asyncrat_common.py'
8
+ module_file_path = "/opt/CAPEv2/data/asyncrat_common.py"
9
9
  if os.path.exists(module_file_path):
10
10
  try:
11
- module_name = os.path.basename(module_file_path).replace('.py', '')
11
+ module_name = os.path.basename(module_file_path).replace(".py", "")
12
12
  spec = importlib.util.spec_from_file_location(module_name, module_file_path)
13
13
  asyncrat_common = importlib.util.module_from_spec(spec)
14
14
  sys.modules[module_name] = asyncrat_common
@@ -17,6 +17,7 @@ if os.path.exists(module_file_path):
17
17
  except Exception as e:
18
18
  print("Error loading asyncrat_common.py", e)
19
19
 
20
+
20
21
  def extract_config(data: bytes):
21
22
  config = RATConfigParser(data=data, remap_config=True).report.get("config", {})
22
23
  if config and HAVE_ASYNCRAT_COMMON:
@@ -24,6 +25,7 @@ def extract_config(data: bytes):
24
25
 
25
26
  return config
26
27
 
28
+
27
29
  if __name__ == "__main__":
28
30
  data = open(sys.argv[1], "rb").read()
29
31
  print(extract_config(data))
@@ -5,10 +5,10 @@ import os
5
5
  from rat_king_parser.rkp import RATConfigParser
6
6
 
7
7
  HAVE_ASYNCRAT_COMMON = False
8
- module_file_path = '/opt/CAPEv2/data/asyncrat_common.py'
8
+ module_file_path = "/opt/CAPEv2/data/asyncrat_common.py"
9
9
  if os.path.exists(module_file_path):
10
10
  try:
11
- module_name = os.path.basename(module_file_path).replace('.py', '')
11
+ module_name = os.path.basename(module_file_path).replace(".py", "")
12
12
  spec = importlib.util.spec_from_file_location(module_name, module_file_path)
13
13
  asyncrat_common = importlib.util.module_from_spec(spec)
14
14
  sys.modules[module_name] = asyncrat_common
@@ -17,6 +17,7 @@ if os.path.exists(module_file_path):
17
17
  except Exception as e:
18
18
  print("Error loading asyncrat_common.py", e)
19
19
 
20
+
20
21
  def extract_config(data: bytes):
21
22
  config = RATConfigParser(data=data, remap_config=True).report.get("config", {})
22
23
  if config and HAVE_ASYNCRAT_COMMON:
@@ -24,6 +25,7 @@ def extract_config(data: bytes):
24
25
 
25
26
  return config
26
27
 
28
+
27
29
  if __name__ == "__main__":
28
30
  data = open(sys.argv[1], "rb").read()
29
31
  print(extract_config(data))
@@ -0,0 +1,96 @@
1
+ import hashlib
2
+ from Crypto.Cipher import ChaCha20_Poly1305
3
+ from contextlib import suppress
4
+ import zlib
5
+ import struct
6
+ import json
7
+ import yara
8
+ import pefile
9
+
10
+
11
+ RULE_SOURCE = """rule MonsterV2Config
12
+ {
13
+ meta:
14
+ author = "doomedraven,YungBinary"
15
+ strings:
16
+ $chunk_1 = {
17
+ 41 B8 0E 04 00 00
18
+ 48 8D 15 ?? ?? ?? 00
19
+ 48 8B CB
20
+ E8 ?? ?? ?? ??
21
+ 48 8D 83 0E 04 00 00
22
+ 48 89 44 24 30
23
+ 48 89 6C 24 70
24
+ 4C 8B C7
25
+ 48 8D 54 24 28
26
+ 48 8B CE
27
+ E8 ?? ?? ?? ??
28
+ }
29
+ condition:
30
+ $chunk_1
31
+ }"""
32
+
33
+
34
+ def derive_chacha_key_nonce_blake2b(seed: bytes): # -> tuple[bytes, bytes]:
35
+ """
36
+ Derives a 32-byte ChaCha20 key and a 24-byte ChaCha20 nonce
37
+ using BLAKE2b from a given seed.
38
+ """
39
+ output_length = 56 # 32 bytes for key + 24 bytes for nonce
40
+ h = hashlib.blake2b(digest_size=output_length)
41
+ h.update(seed)
42
+ derived_material = h.digest()
43
+ chacha20_key = derived_material[0:32]
44
+ chacha20_nonce = derived_material[32:56]
45
+ return chacha20_key, chacha20_nonce
46
+
47
+
48
+ def yara_scan(raw_data, rule_source):
49
+ yara_rules = yara.compile(source=rule_source)
50
+ matches = yara_rules.match(data=raw_data)
51
+
52
+ for match in matches:
53
+ for block in match.strings:
54
+ for instance in block.instances:
55
+ return instance.offset
56
+
57
+ def extract_config(data: bytes) -> dict:
58
+ config_dict = {}
59
+ with suppress(Exception):
60
+ pe = pefile.PE(data=data)
61
+ offset = yara_scan(data, RULE_SOURCE)
62
+
63
+ # image_base = pe.OPTIONAL_HEADER.ImageBase
64
+ disp_offset = data[offset + 9 : offset + 13]
65
+ disp_offset = struct.unpack('i', disp_offset)[0]
66
+ instruction_pointer_va = pe.get_rva_from_offset(offset + 13)
67
+ config_offset_va = instruction_pointer_va + disp_offset
68
+ config_offset = pe.get_offset_from_rva(config_offset_va)
69
+
70
+
71
+ blake_seed = data[config_offset : config_offset + 32]
72
+ chacha20_key, chacha20_nonce = derive_chacha_key_nonce_blake2b(blake_seed)
73
+ cipher_len = int.from_bytes(data[config_offset + 32 : config_offset + 40], byteorder="big")
74
+ cipher_text = data[config_offset + 40 : config_offset + 40 + cipher_len]
75
+
76
+ cipher = ChaCha20_Poly1305.new(key=chacha20_key, nonce=chacha20_nonce)
77
+ decrypted_zlib_data = cipher.decrypt(cipher_text)
78
+ decompressed_data = zlib.decompress(decrypted_zlib_data)
79
+ config_dict = json.loads(decompressed_data)
80
+
81
+ if config_dict:
82
+ final_config = {"raw": config_dict}
83
+ if "ip" in config_dict and "port" in config_dict:
84
+ final_config["CNCs"] = [f"tcp://{config_dict['ip']}:{config_dict['port']}"]
85
+ if "build_name" in config_dict:
86
+ final_config["build"] = config_dict["build_name"]
87
+ return final_config
88
+
89
+ return {}
90
+
91
+
92
+ if __name__ == "__main__":
93
+ import sys
94
+
95
+ with open(sys.argv[1], "rb") as f:
96
+ print(extract_config(f.read()))
@@ -26,11 +26,12 @@ def parse_http_config(rc4_key: bytes, data: bytes) -> dict:
26
26
 
27
27
  def read_str(length: int):
28
28
  nonlocal offset
29
- value = data[offset:offset + length].decode("utf-8", errors="replace")
29
+ value = data[offset : offset + length].decode("utf-8", errors="replace")
30
30
  offset += length
31
31
  return value
32
32
 
33
- config["config_rc4_key"] = rc4_key.hex()
33
+ config["cryptokey"] = rc4_key.hex()
34
+ config["cryptokey_type"] = "RC4"
34
35
  config["agent_type"] = f"{read('<I'):8X}"
35
36
  config["use_ssl"] = read("<B")
36
37
  host_count = read("<I")
@@ -58,7 +59,8 @@ def parse_http_config(rc4_key: bytes, data: bytes) -> dict:
58
59
  config["sleep_delay"] = read("<I")
59
60
  config["jitter_delay"] = read("<I")
60
61
 
61
- return config
62
+ return {"raw": config}
63
+
62
64
 
63
65
  def extract_config(filebuf: bytes) -> dict:
64
66
  pe = pefile.PE(data=filebuf, fast_load=True)
@@ -78,9 +80,9 @@ def extract_config(filebuf: bytes) -> dict:
78
80
  pos = start_offset + 1
79
81
  continue
80
82
 
81
- encrypted_data = data[pos:pos + key_offset]
83
+ encrypted_data = data[pos : pos + key_offset]
82
84
  pos += key_offset
83
- rc4_key = data[pos:pos + 16]
85
+ rc4_key = data[pos : pos + 16]
84
86
 
85
87
  if key_offset == 787:
86
88
  pass
@@ -30,7 +30,7 @@ rule Azorult
30
30
  cape_type = "Azorult Payload"
31
31
  strings:
32
32
  $ref_c2 = {6A 00 6A 00 6A 00 6A 00 68 ?? ?? ?? ?? FF 55 F0 8B D8 C7 47 10 ?? ?? ?? ?? 90 C7 45 B0 C0 C6 2D 00 6A 04 8D 45 B0 50 6A 06 53 FF 55 D4}
33
- condition:
33
+ condition:
34
34
  uint16(0) == 0x5A4D and all of them
35
35
  }
36
36
  """
@@ -48,16 +48,18 @@ def extract_config(filebuf):
48
48
  for instance in block.instances:
49
49
  try:
50
50
  cnc_offset = struct.unpack("i", instance.matched_data[21:25])[0]
51
- cnc = pe.get_data(cnc_offset-image_base, 32).split(b"\x00")[0]
51
+ cnc = pe.get_data(cnc_offset - image_base, 32).split(b"\x00")[0]
52
52
  if cnc:
53
53
  if not cnc.startswith(b"http"):
54
54
  cnc = b"http://" + cnc
55
- return {"cncs": [cnc.decode()]}
55
+ return {"CNCs": [cnc.decode()]}
56
56
  except Exception as e:
57
57
  log.error("Error parsing Azorult config: %s", e)
58
58
  return {}
59
59
 
60
+
60
61
  if __name__ == "__main__":
61
62
  import sys
63
+
62
64
  with open(sys.argv[1], "rb") as f:
63
65
  print(extract_config(f.read()))
@@ -86,7 +86,10 @@ def extract_config(file_data):
86
86
  for item in raw.split(b"\x00"):
87
87
  data = "".join(convert_char(c) for c in item)
88
88
  if len(data) == 760:
89
- config["RSA public key"] = data
89
+ config.setdefault("cryptokey", data)
90
+ # ToDO proper naming here
91
+ config.setdefault("raw", {})["cryptokey_type"] = "RSA public key"
92
+
90
93
  elif len(data) > 1 and "\\x" not in data:
91
- config["strings"] = data
94
+ config.setdefault("raw", {})["strings"] = data
92
95
  return config
@@ -55,7 +55,7 @@ def extract_config(data: bytes) -> dict:
55
55
  return {}
56
56
 
57
57
  rdata_data = rdata_section.get_data()
58
- patterns = [br"Builder\.dll\x00", br"Builder\.exe\x00"]
58
+ patterns = [rb"Builder\.dll\x00", rb"Builder\.exe\x00"]
59
59
  matches = []
60
60
  for pattern in patterns:
61
61
  matches.extend(re.finditer(pattern, rdata_data))
@@ -66,7 +66,7 @@ def extract_config(data: bytes) -> dict:
66
66
  end = min(len(rdata_data), match.end() + 1024)
67
67
  found_strings.update(re.findall(b"[\x20-\x7E]{4,}?\x00", rdata_data[start:end]))
68
68
 
69
- result = {}
69
+ config = {}
70
70
  urls = []
71
71
  directories = []
72
72
  campaign = ""
@@ -74,7 +74,7 @@ def extract_config(data: bytes) -> dict:
74
74
  if found_strings:
75
75
  key = get_year(pe)
76
76
  if not key:
77
- return result
77
+ return {}
78
78
  for string in found_strings:
79
79
  with suppress(UnicodeDecodeError):
80
80
  decoded_string = string.decode("utf-8").rstrip("\x00")
@@ -88,9 +88,14 @@ def extract_config(data: bytes) -> dict:
88
88
  elif re.match(r"^(?![A-Z]{6,}$)[a-zA-Z0-9\-=]{6,}$", decoded_string):
89
89
  campaign = decoded_string
90
90
 
91
- result = {"urls": sorted(urls), "directories": directories, "campaign": campaign}
91
+ if urls:
92
+ config["CNCs"] = sorted(urls)
93
+ if campaign:
94
+ config["campaign"] = campaign
95
+ if directories:
96
+ config["raw"] = {"directories": directories}
92
97
 
93
- return result
98
+ return config
94
99
 
95
100
 
96
101
  if __name__ == "__main__":
@@ -542,16 +542,18 @@ def extract_config(data):
542
542
  injection_method = "Process hollowing IE or Werfault"
543
543
 
544
544
  config = {
545
- "Flag": hex(flag),
546
- "Payload export hash": hex(u32(payload_export_hash)),
547
- "Payload filename": w_payload_filename_and_cmdline,
548
- "Compressed data size": hex(u32(compressed_data_size)),
549
- "Uncompressed data size": hex(u32(uncompressed_data_size)),
550
- "Rabbit key": binascii.hexlify(key).decode(),
551
- "Rabbit IV": binascii.hexlify(iv).decode(),
552
- "Persistence": persistance,
553
- "Sleep after injection": sleep_after_injection,
554
- "Injection method": injection_method,
545
+ "raw": {
546
+ "Flag": hex(flag),
547
+ "Payload export hash": hex(u32(payload_export_hash)),
548
+ "Payload filename": w_payload_filename_and_cmdline,
549
+ "Compressed data size": hex(u32(compressed_data_size)),
550
+ "Uncompressed data size": hex(u32(uncompressed_data_size)),
551
+ "Rabbit key": binascii.hexlify(key).decode(),
552
+ "Rabbit IV": binascii.hexlify(iv).decode(),
553
+ "Persistence": persistance,
554
+ "Sleep after injection": sleep_after_injection,
555
+ "Injection method": injection_method,
556
+ }
555
557
  }
556
558
 
557
559
  return config