CAPE-parsers 0.1.58__tar.gz → 0.1.59__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.58 → cape_parsers-0.1.59}/PKG-INFO +1 -1
  2. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/CAPE/community/Amatera.py +92 -61
  3. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/CAPE/core/Zloader.py +151 -44
  4. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/pyproject.toml +1 -1
  5. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/LICENSE +0 -0
  6. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/README.md +0 -0
  7. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/CAPE/__init__.py +0 -0
  8. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/CAPE/community/AgentTesla.py +0 -0
  9. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/CAPE/community/Amadey.py +0 -0
  10. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/CAPE/community/Arkei.py +0 -0
  11. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/CAPE/community/AsyncRAT.py +0 -0
  12. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/CAPE/community/AuroraStealer.py +0 -0
  13. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/CAPE/community/Carbanak.py +0 -0
  14. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/CAPE/community/CobaltStrikeBeacon.py +0 -0
  15. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/CAPE/community/CobaltStrikeStager.py +0 -0
  16. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/CAPE/community/DCRat.py +0 -0
  17. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/CAPE/community/Fareit.py +0 -0
  18. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/CAPE/community/KoiLoader.py +0 -0
  19. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/CAPE/community/LokiBot.py +0 -0
  20. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/CAPE/community/Lumma.py +0 -0
  21. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/CAPE/community/MonsterV2.py +0 -0
  22. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/CAPE/community/MyKings.py +0 -0
  23. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/CAPE/community/NanoCore.py +0 -0
  24. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/CAPE/community/Nighthawk.py +0 -0
  25. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/CAPE/community/Njrat.py +0 -0
  26. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/CAPE/community/PhemedroneStealer.py +0 -0
  27. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/CAPE/community/QuasarRAT.py +0 -0
  28. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/CAPE/community/README.md +0 -0
  29. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/CAPE/community/Snake.py +0 -0
  30. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/CAPE/community/SparkRAT.py +0 -0
  31. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/CAPE/community/Stealc.py +0 -0
  32. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/CAPE/community/VenomRAT.py +0 -0
  33. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/CAPE/community/WinosStager.py +0 -0
  34. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/CAPE/community/XWorm.py +0 -0
  35. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/CAPE/community/XenoRAT.py +0 -0
  36. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/CAPE/community/__init__.py +0 -0
  37. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/CAPE/core/AdaptixBeacon.py +0 -0
  38. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/CAPE/core/AuraStealer.py +0 -0
  39. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/CAPE/core/Azorult.py +0 -0
  40. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/CAPE/core/BitPaymer.py +0 -0
  41. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/CAPE/core/BlackDropper.py +0 -0
  42. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/CAPE/core/Blister.py +0 -0
  43. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/CAPE/core/BruteRatel.py +0 -0
  44. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/CAPE/core/BumbleBee.py +0 -0
  45. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/CAPE/core/DarkGate.py +0 -0
  46. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/CAPE/core/DoppelPaymer.py +0 -0
  47. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/CAPE/core/DridexLoader.py +0 -0
  48. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/CAPE/core/Formbook.py +0 -0
  49. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/CAPE/core/GuLoader.py +0 -0
  50. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/CAPE/core/IcedID.py +0 -0
  51. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/CAPE/core/IcedIDLoader.py +0 -0
  52. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/CAPE/core/Latrodectus.py +0 -0
  53. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/CAPE/core/NitroBunnyDownloader.py +0 -0
  54. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/CAPE/core/Oyster.py +0 -0
  55. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/CAPE/core/PikaBot.py +0 -0
  56. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/CAPE/core/PlugX.py +0 -0
  57. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/CAPE/core/QakBot.py +0 -0
  58. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/CAPE/core/Quickbind.py +0 -0
  59. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/CAPE/core/README.md +0 -0
  60. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/CAPE/core/RedLine.py +0 -0
  61. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/CAPE/core/Remcos.py +0 -0
  62. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/CAPE/core/Rhadamanthys.py +0 -0
  63. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/CAPE/core/SmokeLoader.py +0 -0
  64. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/CAPE/core/Socks5Systemz.py +0 -0
  65. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/CAPE/core/SquirrelWaffle.py +0 -0
  66. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/CAPE/core/Strrat.py +0 -0
  67. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/CAPE/core/WarzoneRAT.py +0 -0
  68. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/CAPE/core/__init__.py +0 -0
  69. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/CAPE/core/test_cape.py +0 -0
  70. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/RATDecoders/README.md +0 -0
  71. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/RATDecoders/__init__.py +0 -0
  72. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/RATDecoders/test_rats.py +0 -0
  73. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/__init__.py +0 -0
  74. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/deprecated/BackOffLoader.py +0 -0
  75. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/deprecated/BackOffPOS.py +0 -0
  76. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/deprecated/BlackNix.py +0 -0
  77. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/deprecated/BuerLoader.py +0 -0
  78. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/deprecated/ChChes.py +0 -0
  79. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/deprecated/Emotet.py +0 -0
  80. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/deprecated/Enfal.py +0 -0
  81. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/deprecated/EvilGrab.py +0 -0
  82. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/deprecated/Greame.py +0 -0
  83. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/deprecated/Hancitor.py +0 -0
  84. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/deprecated/HttpBrowser.py +0 -0
  85. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/deprecated/JavaDropper.py +0 -0
  86. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/deprecated/Nymaim.py +0 -0
  87. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/deprecated/Pandora.py +0 -0
  88. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/deprecated/PoisonIvy.py +0 -0
  89. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/deprecated/PredatorPain.py +0 -0
  90. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/deprecated/Punisher.py +0 -0
  91. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/deprecated/RCSession.py +0 -0
  92. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/deprecated/REvil.py +0 -0
  93. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/deprecated/RedLeaf.py +0 -0
  94. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/deprecated/Retefe.py +0 -0
  95. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/deprecated/Rozena.py +0 -0
  96. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/deprecated/SmallNet.py +0 -0
  97. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/deprecated/TSCookie.py +0 -0
  98. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/deprecated/TrickBot.py +0 -0
  99. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/deprecated/UrsnifV3.py +0 -0
  100. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/deprecated/_ShadowTech.py +0 -0
  101. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/deprecated/_VirusRat.py +0 -0
  102. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/deprecated/_jRat.py +0 -0
  103. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/deprecated/unrecom.py +0 -0
  104. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/deprecated/xRAT.py +0 -0
  105. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/malduck/LICENSE +0 -0
  106. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/malduck/README.md +0 -0
  107. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/malduck/__init__.py +0 -0
  108. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/malduck/test_malduck.py +0 -0
  109. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/mwcp/README.md +0 -0
  110. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/mwcp/__init__.py +0 -0
  111. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/mwcp/test_mwcp.py +0 -0
  112. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/utils/__init__.py +0 -0
  113. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/utils/aplib.py +0 -0
  114. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/utils/blzpack.py +0 -0
  115. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/utils/blzpack_lib.so +0 -0
  116. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/utils/dotnet_utils.py +0 -0
  117. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/cape_parsers/utils/lznt1.py +0 -0
  118. {cape_parsers-0.1.58 → cape_parsers-0.1.59}/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.58
3
+ Version: 0.1.59
4
4
  Summary: CAPE: Malware Configuration Extraction
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -3,11 +3,8 @@ import json
3
3
  import pefile
4
4
  import yara
5
5
  import struct
6
- import re
7
- import ipaddress
8
6
  from contextlib import suppress
9
7
 
10
-
11
8
  DESCRIPTION = "Amatera Stealer parser"
12
9
  AUTHOR = "YungBinary"
13
10
 
@@ -18,7 +15,7 @@ rule AmateraDecrypt
18
15
  author = "YungBinary"
19
16
  description = "Find Amatera XOR key"
20
17
  strings:
21
- $decrypt = {
18
+ $decrypt_global = {
22
19
  A1 ?? ?? ?? ?? // mov eax, dword ptr ds:szXorKey ; "852149723"
23
20
  89 45 ?? // mov dword ptr [ebp+xor_key], eax
24
21
  8B 0D ?? ?? ?? ?? // mov ecx, dword ptr ds:szXorKey+4 ; "49723"
@@ -29,12 +26,18 @@ rule AmateraDecrypt
29
26
  50 // push eax
30
27
  E8 // call
31
28
  }
29
+ $decrypt_stack = {
30
+ 83 EC 1C // sub esp, 1Ch
31
+ 56 // push esi
32
+ 89 ?? ?? // mov [ebp+var], reg
33
+ C6 45 ?? ?? // mov [ebp+char_1], imm
34
+ C6 45 ?? ?? // mov [ebp+char_2], imm
35
+ }
32
36
  condition:
33
- uint16(0) == 0x5A4D and $decrypt
37
+ uint16(0) == 0x5A4D and ($decrypt_global or $decrypt_stack)
34
38
  }
35
39
  """
36
40
 
37
-
38
41
  RULE_SOURCE_AES_KEY = """
39
42
  rule AmateraAESKey
40
43
  {
@@ -83,28 +86,18 @@ rule AmateraAESKey
83
86
  }
84
87
  """
85
88
 
86
- DOMAIN_REGEX = r'^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$'
89
+ B64_CHARS = set(b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=")
87
90
 
88
91
 
89
92
  def yara_scan(raw_data: bytes, rule_source: str):
90
93
  yara_rules = yara.compile(source=rule_source)
91
94
  matches = yara_rules.match(data=raw_data)
92
-
93
95
  for match in matches:
94
96
  for block in match.strings:
95
97
  for instance in block.instances:
96
98
  return instance.offset
97
99
 
98
100
 
99
- def extract_base64_strings(data: bytes, minchars: int, maxchars: int):
100
- """
101
- Generator that returns ASCII formatted base64 strings
102
- """
103
- apat = b"([A-Za-z0-9+/=]{" + str(minchars).encode() + b"," + str(maxchars).encode() + b"})\x00"
104
- for s in re.findall(apat, data):
105
- yield s.decode()
106
-
107
-
108
101
  def xor_data(data, key):
109
102
  decoded = bytearray()
110
103
  for i in range(len(data)):
@@ -112,68 +105,107 @@ def xor_data(data, key):
112
105
  return decoded
113
106
 
114
107
 
115
- def is_public_ip(ip):
116
- try:
117
- # This will raise a ValueError if the IP format is incorrect
118
- ip_obj = ipaddress.ip_address(ip.decode())
119
- if ip_obj.is_private:
120
- return False
121
- return True
122
- except Exception:
123
- return False
108
+ def decode_and_decrypt(data_bytes, xor_key):
109
+ if not data_bytes:
110
+ return ""
124
111
 
112
+ clean_bytes = data_bytes.rstrip(b"\x00")
113
+
114
+ # Heuristic: Check if valid Base64
115
+ if any(b not in B64_CHARS for b in clean_bytes):
116
+ return clean_bytes.decode("utf-8", errors="ignore")
125
117
 
126
- def is_valid_domain(data):
127
118
  try:
128
- if re.fullmatch(DOMAIN_REGEX, data.decode()):
129
- return True
130
- return False
119
+ decoded = base64.b64decode(clean_bytes, validate=True)
120
+ decrypted = xor_data(decoded, xor_key)
121
+ # Heuristic: Check if result is printable ASCII
122
+ if all(0x20 <= b <= 0x7E for b in decrypted if b != 0):
123
+ return decrypted.decode("utf-8", errors="ignore").rstrip("\x00")
131
124
  except Exception:
132
- return False
125
+ pass
126
+
127
+ return clean_bytes.decode("utf-8", errors="ignore")
133
128
 
134
129
 
135
130
  def extract_config(data):
136
- """
137
- Extract Amatera malware configuration.
138
- """
139
131
  config_dict = {}
140
132
 
141
133
  with suppress(Exception):
142
134
  pe = pefile.PE(data=data)
143
135
  image_base = pe.OPTIONAL_HEADER.ImageBase
144
136
 
145
- # Identify XOR key decryption routine and extract key
146
137
  offset = yara_scan(data, RULE_SOURCE)
147
- if not offset:
138
+ if offset is None:
148
139
  return config_dict
149
- key_str_va = struct.unpack('i', data[offset + 1: offset + 5])[0]
150
- key_str = pe.get_string_at_rva(key_str_va - image_base, max_length=20) + b'\x00'
151
140
 
152
- # Extract AES 256 key
141
+ key_str = b""
142
+ if data[offset] == 0xA1: # $decrypt_global
143
+ key_str_va = struct.unpack("i", data[offset + 1 : offset + 5])[0]
144
+ key_str = (
145
+ pe.get_string_at_rva(key_str_va - image_base, max_length=20) + b"\x00"
146
+ )
147
+
148
+ elif data[offset] == 0x83: # $decrypt_stack
149
+ key_bytes = bytearray()
150
+ # Skip sub/push/mov reg (7 bytes)
151
+ current_idx = offset + 7
152
+ for _ in range(32):
153
+ if data[current_idx] != 0xC6 or data[current_idx + 1] != 0x45:
154
+ break
155
+ val = data[current_idx + 3]
156
+ if val == 0x00:
157
+ break
158
+ key_bytes.append(val)
159
+ current_idx += 4
160
+ key_str = key_bytes + b"\x00"
161
+
162
+ if key_str:
163
+ config_dict["xor_key"] = key_str.rstrip(b"\x00").decode(
164
+ "utf-8", errors="ignore"
165
+ )
166
+
153
167
  aes_key_offset = yara_scan(data, RULE_SOURCE_AES_KEY)
154
- aes_key = bytearray()
155
168
  if aes_key_offset:
169
+ aes_key = bytearray()
156
170
  aes_block = data[aes_key_offset : aes_key_offset + 131]
157
171
  for i in range(0, len(aes_block) - 4, 4):
158
- aes_key.append(aes_block[i+6])
159
-
160
- # Handle each base64 string -> decode -> decrypt with XOR key
161
- for b64_str in extract_base64_strings(data, 8, 20):
162
- try:
163
- decoded = base64.b64decode(b64_str, validate=True)
164
- decrypted = xor_data(decoded, key_str)
165
- if not is_public_ip(decrypted) and not is_valid_domain(decrypted):
166
- continue
167
-
168
- config_dict["CNCs"] = [f"https://{decrypted.decode()}"]
169
-
170
- if aes_key:
171
- config_dict["cryptokey"] = aes_key.hex()
172
- config_dict["cryptokey_type"] = "AES"
173
-
174
- return config_dict
175
- except Exception:
176
- continue
172
+ aes_key.append(aes_block[i + 6])
173
+ config_dict["cryptokey"] = aes_key.hex()
174
+ config_dict["cryptokey_type"] = "AES"
175
+
176
+ data_section = next(
177
+ (
178
+ s
179
+ for s in pe.sections
180
+ if s.Name.decode().strip("\x00").lower() == ".data"
181
+ ),
182
+ None,
183
+ )
184
+ if data_section:
185
+ # First 16 bytes = 4 pointers
186
+ pointers_raw = pe.get_data(data_section.VirtualAddress, 16)
187
+ if len(pointers_raw) == 16:
188
+ ptrs = struct.unpack("4I", pointers_raw)
189
+ extracted = []
190
+
191
+ for ptr in ptrs:
192
+ rva = ptr - image_base
193
+ if 0 < rva < pe.OPTIONAL_HEADER.SizeOfImage:
194
+ extracted.append(pe.get_string_at_rva(rva))
195
+ else:
196
+ extracted.append(b"")
197
+
198
+ if len(extracted) == 4:
199
+ config_dict["payload_guid_1"] = decode_and_decrypt(
200
+ extracted[0], key_str
201
+ )
202
+ config_dict["payload_guid_2"] = decode_and_decrypt(
203
+ extracted[1], key_str
204
+ )
205
+ config_dict["fake_c2"] = decode_and_decrypt(extracted[2], key_str)
206
+
207
+ real_c2 = decode_and_decrypt(extracted[3], key_str)
208
+ config_dict["CNCs"] = [f"https://{real_c2}"]
177
209
 
178
210
  return config_dict
179
211
 
@@ -182,5 +214,4 @@ if __name__ == "__main__":
182
214
  import sys
183
215
 
184
216
  with open(sys.argv[1], "rb") as f:
185
- config_json = json.dumps(extract_config(f.read()), indent=4)
186
- print(config_json)
217
+ print(json.dumps(extract_config(f.read()), indent=4))
@@ -15,14 +15,15 @@
15
15
  DESCRIPTION = "Zloader configuration parser"
16
16
  AUTHOR = "kevoreilly"
17
17
 
18
+ import json
18
19
  import logging
20
+ import re
19
21
  import socket
20
22
  import struct
21
23
 
22
24
  import pefile
23
- from Cryptodome.Cipher import ARC4
24
-
25
25
  import yara
26
+ from Cryptodome.Cipher import ARC4
26
27
 
27
28
  log = logging.getLogger(__name__)
28
29
 
@@ -59,6 +60,20 @@ rule Zloader2024
59
60
  condition:
60
61
  uint16(0) == 0x5A4D and $conf_1 and 2 of ($confkey_*)
61
62
  }
63
+
64
+ rule Zloader2025
65
+ {
66
+ meta:
67
+ author = "enzok"
68
+ description = "Zloader Payload"
69
+ cape_type = "Zloader Payload"
70
+ strings:
71
+ $conf = {4? 01 ?? [4] E8 [4] 4? 8D 15 [4] 4? 89 ?? 4? 89 ?? E8 [4] C7 46 30 00 00 00 00 8B 7E 34}
72
+ $confkey_1 = {4? 01 ?? [2] E8 [4] 4? 8D 15 [4] 4? 89 ?? 4? 89 ?? E8 [4] C7 46 34 00 00 00 00 8B 46 38}
73
+ $confkey_2 = {4? 01 ?? [2] E8 [4] 4? 8D 15 [4] 4? 89 ?? 4? 89 ?? E8 [4] C7 46 38 00 00 00 00 48 83 C4 28}
74
+ condition:
75
+ uint16(0) == 0x5A4D and $conf and all of ($confkey_*)
76
+ }
62
77
  """
63
78
  MAX_STRING_SIZE = 32
64
79
 
@@ -71,41 +86,73 @@ def decrypt_rc4(key, data):
71
86
 
72
87
 
73
88
  def string_from_offset(data, offset):
74
- return data[offset : offset + MAX_STRING_SIZE].split(b"\0", 1)[0]
89
+ return data[offset: offset + MAX_STRING_SIZE].split(b"\0", 1)[0]
90
+
75
91
 
92
+ def parse_config(data, version=None):
93
+ for i in range(len(data) - 3, -1, -1):
94
+ if data[i : i + 3] == b"\x00\x00\x00":
95
+ data = data[:i]
96
+ break
76
97
 
77
- def parse_config(data):
78
98
  parsed = {}
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")
99
+ net_params = []
100
+ dns_ips = []
101
+ tls_sni = ""
102
+ cryptokey = ""
103
+ fields = [part.strip() for part in data.split(b'\x00') if part and part.strip()]
104
+ parsed["botnet"] = fields[0].decode("utf-8") if len(fields) > 0 else ""
105
+ parsed["campaign"] = fields[1].decode("utf-8") if len(fields) > 1 else ""
81
106
  c2s = []
82
- c2_data = data[46:686]
83
- for i in range(10):
84
- chunk = c2_data[i * 64 : (i + 1) * 64]
85
- chunk = chunk.rstrip(b"\x00")
86
- if chunk:
87
- c2s.append(chunk.decode("utf-8"))
107
+ for f in fields:
108
+ if f.startswith(b"http"):
109
+ f = f.decode("utf-8")
110
+ if "~" in f:
111
+ tls_sni, f = map(str.strip, f.split("~", 1))
112
+
113
+ if f:
114
+ c2s.append(f)
115
+
116
+ elif b"PUBLIC KEY" in f:
117
+ cryptokey = f.decode("utf-8").replace("\n", "")
118
+
119
+ elif version == 3 and b"\x08\x08" in f and len(f) % 4 == 0:
120
+ idx = 0
121
+ for i in range(len(f) // 4):
122
+ dns_ips.append(socket.inet_ntoa(f[idx: idx + 4]))
123
+ idx += 4
124
+
125
+ elif version == 4 and f.startswith(b"[") and f.endswith(b"]"):
126
+ try:
127
+ params = json.loads(f)
128
+ for param in params:
129
+ proto = param.get("proto", "unknown")
130
+ ip = param.get("ip", "")
131
+ port = param.get("port", 0)
132
+ qps = param.get("qps", "")
133
+ net_params.append(f"{proto}, {ip}:{port}, qps={qps}".strip())
134
+
135
+ except json.JSONDecodeError:
136
+ params = None
137
+
88
138
  parsed["CNCs"] = c2s
89
- parsed["cryptokey"] = data[704:].split(b"\x00", 1)[0]
139
+ parsed["cryptokey"] = cryptokey
90
140
  parsed["cryptokey_type"] = "RSA Public Key"
91
- dns_data = data[1004:].split(b"\x00", 1)[0]
92
- parsed["TLS SNI"] = dns_data.split(b"~")[0].decode("utf-8").rstrip()
93
- parsed["DNS C2"] = dns_data.split(b"~")[1].decode("utf-8").strip()
94
- dns_ips = []
95
- dns_ips.append(socket.inet_ntoa(data[1204:1208].split(b"\x00", 1)[0]))
96
- dns_ip_data = data[1208:1248]
97
- for i in range(10):
98
- chunk = dns_ip_data[i * 4 : (i + 1) * 4]
99
- chunk = chunk.rstrip(b"\x00")
100
- if chunk:
101
- dns_ips.append(socket.inet_ntoa(chunk))
102
- parsed["DNS Servers"] = dns_ips
141
+ raw = parsed["raw"] = {}
142
+ if tls_sni:
143
+ raw["tls sni"] = tls_sni
144
+
145
+ if net_params:
146
+ raw["dns config"] = net_params
147
+
148
+ if dns_ips:
149
+ raw["dns ips"] = dns_ips
150
+
103
151
  return parsed
104
152
 
105
153
 
106
154
  def extract_config(filebuf):
107
155
  config = {}
108
- end_config = {}
109
156
  pe = pefile.PE(data=filebuf, fast_load=False)
110
157
  image_base = pe.OPTIONAL_HEADER.ImageBase
111
158
  matches = yara_rules.match(data=filebuf)
@@ -113,7 +160,7 @@ def extract_config(filebuf):
113
160
  return
114
161
  conf_type = ""
115
162
  decrypt_key = ""
116
- conf_size = 1020
163
+ conf_size = None
117
164
  for match in matches:
118
165
  if match.rule == "Zloader":
119
166
  for item in match.strings:
@@ -137,17 +184,23 @@ def extract_config(filebuf):
137
184
  elif "$decrypt_key_3" == item.identifier:
138
185
  decrypt_key = item.instances[0].offset
139
186
  kva_s = 3
187
+ break
188
+
140
189
  elif match.rule == "Zloader2024":
141
- conf_size = 1264
190
+ conf_size = None
142
191
  rc4_chunk1 = None
143
192
  rc4_chunk2 = None
144
193
  numchunks = 0
145
194
  for item in match.strings:
146
- if "$conf_1" == item.identifier:
195
+ item_id = item.identifier
196
+ if item_id == "$conf_1":
147
197
  decrypt_conf = item.instances[0].offset + 6
148
198
  conf_size = item.instances[0].offset + 12
199
+ if conf_size > 2048:
200
+ conf_size = 2048
201
+
149
202
  conf_type = "3"
150
- elif item.identifier.startswith("$confkey_") and numchunks < 2:
203
+ elif item_id.startswith("$confkey_") and numchunks < 2:
151
204
  matched_data = item.instances[0].matched_data[:2]
152
205
  if matched_data == b"\x48\x8D":
153
206
  offset = 3
@@ -161,11 +214,32 @@ def extract_config(filebuf):
161
214
  rc4_chunk2 = chunk_offset
162
215
 
163
216
  numchunks += 1
217
+ break
218
+
219
+ elif match.rule == "Zloader2025":
220
+ conf_size = None
221
+ rc4_chunk1 = None
222
+ rc4_chunk2 = None
223
+ for item in match.strings:
224
+ item_id = item.identifier
225
+ if item_id == "$conf":
226
+ decrypt_conf = item.instances[0].offset + 15
227
+ size_base_offset = item.instances[0].offset + 5
228
+ call_func_offset = item.instances[0].offset + 7
229
+ call_func_size_offset = call_func_offset + 1
230
+ conf_type = "4"
231
+
232
+ elif item_id == "$confkey_1":
233
+ rc4_chunk1 = item.instances[0].offset + 13
234
+
235
+ elif item_id == "$confkey_2":
236
+ rc4_chunk2 = item.instances[0].offset + 13
237
+ break
164
238
 
165
239
  if conf_type == "1":
166
- va = struct.unpack("I", filebuf[decrypt_conf : decrypt_conf + 4])[0]
240
+ va = struct.unpack("I", filebuf[decrypt_conf: decrypt_conf + 4])[0]
167
241
  key = string_from_offset(filebuf, pe.get_offset_from_rva(va - image_base))
168
- data_offset = pe.get_offset_from_rva(struct.unpack("I", filebuf[decrypt_conf + 5 : decrypt_conf + 9])[0] - image_base)
242
+ data_offset = pe.get_offset_from_rva(struct.unpack("I", filebuf[decrypt_conf + 5: decrypt_conf + 9])[0] - image_base)
169
243
  enc_data = filebuf[data_offset:].split(b"\0\0", 1)[0]
170
244
  raw = decrypt_rc4(key, enc_data)
171
245
  items = list(filter(None, raw.split(b"\x00\x00")))
@@ -178,15 +252,17 @@ def extract_config(filebuf):
178
252
  elif len(item) == 16:
179
253
  config["cryptokey"] = item
180
254
  config["cryptokey_type"] = "RC4"
255
+
181
256
  elif conf_type == "2" and decrypt_key:
182
- conf_va = struct.unpack("I", filebuf[decrypt_conf + cva : decrypt_conf + cva + 4])[0]
257
+ conf_size = 1020
258
+ conf_va = struct.unpack("I", filebuf[decrypt_conf + cva: decrypt_conf + cva + 4])[0]
183
259
  conf_offset = pe.get_offset_from_rva(conf_va + pe.get_rva_from_offset(decrypt_conf) + cva + 4)
184
260
  # if not conf_size:
185
261
  # conf_size = struct.unpack("I", filebuf[decrypt_key + size_s : decrypt_key + size_s + 4])[0]
186
- key_va = struct.unpack("I", filebuf[decrypt_key + kva_s : decrypt_key + kva_s + 4])[0]
262
+ key_va = struct.unpack("I", filebuf[decrypt_key + kva_s: decrypt_key + kva_s + 4])[0]
187
263
  key_offset = pe.get_offset_from_rva(key_va + pe.get_rva_from_offset(decrypt_key) + kva_s + 4)
188
264
  key = string_from_offset(filebuf, key_offset)
189
- conf_data = filebuf[conf_offset : conf_offset + conf_size]
265
+ conf_data = filebuf[conf_offset: conf_offset + conf_size]
190
266
  raw = decrypt_rc4(key, conf_data)
191
267
  items = list(filter(None, raw.split(b"\x00\x00")))
192
268
  config["botnet"] = items[0].decode("utf-8")
@@ -198,25 +274,56 @@ def extract_config(filebuf):
198
274
  elif b"PUBLIC KEY" in item:
199
275
  config["cryptokey"] = item.decode("utf-8").replace("\n", "")
200
276
  config["cryptokey_type"] = "RSA Public key"
277
+
201
278
  elif conf_type == "3" and rc4_chunk1 and rc4_chunk2:
202
- conf_va = struct.unpack("I", filebuf[decrypt_conf : decrypt_conf + 4])[0]
279
+ conf_va = struct.unpack("I", filebuf[decrypt_conf: decrypt_conf + 4])[0]
203
280
  conf_offset = pe.get_offset_from_rva(conf_va + pe.get_rva_from_offset(decrypt_conf) + 4)
204
- conf_data = filebuf[conf_offset : conf_offset + conf_size]
205
- keychunk1_va = struct.unpack("I", filebuf[rc4_chunk1 : rc4_chunk1 + 4])[0]
281
+ conf_data = filebuf[conf_offset: conf_offset + conf_size]
282
+ keychunk1_va = struct.unpack("I", filebuf[rc4_chunk1: rc4_chunk1 + 4])[0]
206
283
  keychunk1_offset = pe.get_offset_from_rva(keychunk1_va + pe.get_rva_from_offset(rc4_chunk1) + 4)
207
- keychunk1 = filebuf[keychunk1_offset : keychunk1_offset + 16]
208
- keychunk2_va = struct.unpack("I", filebuf[rc4_chunk2 : rc4_chunk2 + 4])[0]
284
+ keychunk1 = filebuf[keychunk1_offset: keychunk1_offset + 16]
285
+ keychunk2_va = struct.unpack("I", filebuf[rc4_chunk2: rc4_chunk2 + 4])[0]
209
286
  keychunk2_offset = pe.get_offset_from_rva(keychunk2_va + pe.get_rva_from_offset(rc4_chunk2) + 4)
210
- keychunk2 = filebuf[keychunk2_offset : keychunk2_offset + 16]
287
+ keychunk2 = filebuf[keychunk2_offset: keychunk2_offset + 16]
211
288
  decrypt_key = bytes(a ^ b for a, b in zip(keychunk1, keychunk2))
212
289
  conf = decrypt_rc4(decrypt_key, conf_data)
213
- end_config = parse_config(conf)
290
+ config = parse_config(conf, 3)
291
+
292
+ elif conf_type == "4" and rc4_chunk1 and rc4_chunk2:
293
+ conf_va = struct.unpack("I", filebuf[decrypt_conf: decrypt_conf + 4])[0]
294
+ conf_offset = pe.get_offset_from_rva(conf_va + pe.get_rva_from_offset(decrypt_conf) + 4)
295
+ call_rva = pe.get_rva_from_offset(call_func_offset)
296
+ call_target_rva = call_rva + 5 + struct.unpack("i", filebuf[call_func_size_offset: call_func_size_offset + 4])[0]
297
+ call_target_offset = pe.get_offset_from_rva(call_target_rva)
298
+ function_tail = b"\x5F\x5E\xC3"
299
+ index = filebuf.find(function_tail, call_target_offset)
300
+ if index != -1:
301
+ index += 256
302
+
303
+ function_end_offset = index + len(function_tail)
304
+ function_data = filebuf[call_target_offset: function_end_offset]
305
+ pattern = re.compile(b"\x66\x81\xF1..\x66\x89\x4D.", re.DOTALL)
306
+ key = 0
307
+ for match in pattern.finditer(function_data):
308
+ off = match.start()
309
+ key = struct.unpack_from("<H", function_data, off + 3)[0]
214
310
 
215
- if config and end_config:
216
- config = config.update({"raw": end_config})
311
+ size_base = struct.unpack_from("<H", filebuf[size_base_offset: size_base_offset + 2])[0]
312
+ conf_size = size_base ^ key & 0xFFFF
313
+ conf_data = filebuf[conf_offset: conf_offset + conf_size]
314
+ keychunk1_va = struct.unpack("I", filebuf[rc4_chunk1: rc4_chunk1 + 4])[0]
315
+ keychunk1_offset = pe.get_offset_from_rva(keychunk1_va + pe.get_rva_from_offset(rc4_chunk1) + 4)
316
+ keychunk1 = filebuf[keychunk1_offset: keychunk1_offset + 16]
317
+ keychunk2_va = struct.unpack("I", filebuf[rc4_chunk2: rc4_chunk2 + 4])[0]
318
+ keychunk2_offset = pe.get_offset_from_rva(keychunk2_va + pe.get_rva_from_offset(rc4_chunk2) + 4)
319
+ keychunk2 = filebuf[keychunk2_offset: keychunk2_offset + 16]
320
+ decrypt_key = bytes(a ^ b for a, b in zip(keychunk1, keychunk2))
321
+ conf = decrypt_rc4(decrypt_key, conf_data)
322
+ config = parse_config(conf, 4)
217
323
 
218
324
  return config
219
325
 
326
+
220
327
  if __name__ == "__main__":
221
328
  import sys
222
329
  from pathlib import Path
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "CAPE-parsers"
3
- version = "0.1.58"
3
+ version = "0.1.59"
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