CAPE-parsers 0.1.42__py3-none-any.whl → 0.1.54__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. cape_parsers/CAPE/community/AgentTesla.py +25 -10
  2. cape_parsers/CAPE/community/Amadey.py +199 -29
  3. cape_parsers/CAPE/community/Arkei.py +13 -15
  4. cape_parsers/CAPE/community/AsyncRAT.py +4 -2
  5. cape_parsers/CAPE/community/AuroraStealer.py +9 -6
  6. cape_parsers/CAPE/community/Carbanak.py +7 -7
  7. cape_parsers/CAPE/community/CobaltStrikeBeacon.py +5 -4
  8. cape_parsers/CAPE/community/CobaltStrikeStager.py +4 -1
  9. cape_parsers/CAPE/community/DCRat.py +4 -2
  10. cape_parsers/CAPE/community/Fareit.py +8 -9
  11. cape_parsers/CAPE/community/KoiLoader.py +3 -3
  12. cape_parsers/CAPE/community/LokiBot.py +11 -8
  13. cape_parsers/CAPE/community/Lumma.py +58 -40
  14. cape_parsers/CAPE/community/MonsterV2.py +93 -0
  15. cape_parsers/CAPE/community/MyKings.py +52 -0
  16. cape_parsers/CAPE/community/NanoCore.py +9 -9
  17. cape_parsers/CAPE/community/Nighthawk.py +1 -0
  18. cape_parsers/CAPE/community/Njrat.py +4 -4
  19. cape_parsers/CAPE/community/PhemedroneStealer.py +2 -0
  20. cape_parsers/CAPE/community/Snake.py +31 -18
  21. cape_parsers/CAPE/community/SparkRAT.py +3 -1
  22. cape_parsers/CAPE/community/Stealc.py +95 -63
  23. cape_parsers/CAPE/community/VenomRAT.py +4 -2
  24. cape_parsers/CAPE/community/WinosStager.py +75 -0
  25. cape_parsers/CAPE/community/XWorm.py +4 -2
  26. cape_parsers/CAPE/community/XenoRAT.py +4 -2
  27. cape_parsers/CAPE/core/AdaptixBeacon.py +7 -5
  28. cape_parsers/CAPE/core/AuraStealer.py +100 -0
  29. cape_parsers/CAPE/core/Azorult.py +5 -3
  30. cape_parsers/CAPE/core/BitPaymer.py +5 -2
  31. cape_parsers/CAPE/core/BlackDropper.py +10 -5
  32. cape_parsers/CAPE/core/Blister.py +12 -10
  33. cape_parsers/CAPE/core/BruteRatel.py +20 -7
  34. cape_parsers/CAPE/core/BumbleBee.py +34 -22
  35. cape_parsers/CAPE/core/DarkGate.py +3 -3
  36. cape_parsers/CAPE/core/DoppelPaymer.py +4 -2
  37. cape_parsers/CAPE/core/DridexLoader.py +4 -3
  38. cape_parsers/CAPE/core/Formbook.py +2 -2
  39. cape_parsers/CAPE/core/GuLoader.py +2 -5
  40. cape_parsers/CAPE/core/IcedID.py +5 -5
  41. cape_parsers/CAPE/core/IcedIDLoader.py +4 -4
  42. cape_parsers/CAPE/core/Latrodectus.py +14 -10
  43. cape_parsers/CAPE/core/NitroBunnyDownloader.py +151 -0
  44. cape_parsers/CAPE/core/Oyster.py +8 -6
  45. cape_parsers/CAPE/core/PikaBot.py +6 -6
  46. cape_parsers/CAPE/core/PlugX.py +3 -1
  47. cape_parsers/CAPE/core/QakBot.py +2 -1
  48. cape_parsers/CAPE/core/Quickbind.py +7 -11
  49. cape_parsers/CAPE/core/RedLine.py +2 -2
  50. cape_parsers/CAPE/core/Remcos.py +59 -51
  51. cape_parsers/CAPE/core/Rhadamanthys.py +175 -36
  52. cape_parsers/CAPE/core/SmokeLoader.py +2 -2
  53. cape_parsers/CAPE/core/Socks5Systemz.py +5 -5
  54. cape_parsers/CAPE/core/SquirrelWaffle.py +3 -3
  55. cape_parsers/CAPE/core/Strrat.py +1 -1
  56. cape_parsers/CAPE/core/WarzoneRAT.py +3 -2
  57. cape_parsers/CAPE/core/Zloader.py +21 -15
  58. cape_parsers/RATDecoders/test_rats.py +1 -0
  59. cape_parsers/__init__.py +14 -5
  60. cape_parsers/deprecated/BlackNix.py +59 -0
  61. cape_parsers/{CAPE/core → deprecated}/BuerLoader.py +1 -1
  62. cape_parsers/{CAPE/core → deprecated}/ChChes.py +3 -3
  63. cape_parsers/{CAPE/core → deprecated}/Enfal.py +1 -1
  64. cape_parsers/{CAPE/core → deprecated}/EvilGrab.py +5 -6
  65. cape_parsers/{CAPE/community → deprecated}/Greame.py +3 -1
  66. cape_parsers/{CAPE/core → deprecated}/HttpBrowser.py +7 -8
  67. cape_parsers/{CAPE/community → deprecated}/Pandora.py +2 -0
  68. cape_parsers/{CAPE/community → deprecated}/Punisher.py +2 -1
  69. cape_parsers/{CAPE/core → deprecated}/RCSession.py +7 -9
  70. cape_parsers/{CAPE/community → deprecated}/REvil.py +10 -5
  71. cape_parsers/{CAPE/core → deprecated}/RedLeaf.py +5 -7
  72. cape_parsers/{CAPE/community → deprecated}/Retefe.py +0 -2
  73. cape_parsers/{CAPE/community → deprecated}/Rozena.py +2 -5
  74. cape_parsers/{CAPE/community → deprecated}/SmallNet.py +6 -2
  75. {cape_parsers-0.1.42.dist-info → cape_parsers-0.1.54.dist-info}/METADATA +24 -3
  76. cape_parsers-0.1.54.dist-info/RECORD +117 -0
  77. {cape_parsers-0.1.42.dist-info → cape_parsers-0.1.54.dist-info}/WHEEL +1 -1
  78. cape_parsers/CAPE/community/BlackNix.py +0 -57
  79. cape_parsers/CAPE/core/Stealc.py +0 -21
  80. cape_parsers-0.1.42.dist-info/RECORD +0 -113
  81. /cape_parsers/{CAPE/community → deprecated}/BackOffLoader.py +0 -0
  82. /cape_parsers/{CAPE/community → deprecated}/BackOffPOS.py +0 -0
  83. /cape_parsers/{CAPE/core → deprecated}/Emotet.py +0 -0
  84. /cape_parsers/{CAPE/community → deprecated}/PoisonIvy.py +0 -0
  85. /cape_parsers/{CAPE/community → deprecated}/TSCookie.py +0 -0
  86. /cape_parsers/{CAPE/community → deprecated}/TrickBot.py +0 -0
  87. /cape_parsers/{CAPE/core → deprecated}/UrsnifV3.py +0 -0
  88. {cape_parsers-0.1.42.dist-info → cape_parsers-0.1.54.dist-info/licenses}/LICENSE +0 -0
@@ -57,12 +57,7 @@ def extract_config(filebuf):
57
57
  encrypted_string = struct.unpack_from(data_format, data, offset)[0]
58
58
 
59
59
  with suppress(IndexError, UnicodeDecodeError, ValueError):
60
- decrypted_result = (
61
- ARC4.new(key)
62
- .decrypt(encrypted_string)
63
- .replace(b"\x00", b"")
64
- .decode("utf-8")
65
- )
60
+ decrypted_result = ARC4.new(key).decrypt(encrypted_string).replace(b"\x00", b"").decode("utf-8")
66
61
 
67
62
  if decrypted_result and all(32 <= ord(char) <= 127 for char in decrypted_result):
68
63
  if len(decrypted_result) > 2:
@@ -105,12 +100,13 @@ def extract_config(filebuf):
105
100
  campaign_found = True
106
101
 
107
102
  elif is_hex(item):
108
- cfg["RC4 Key"] = item
103
+ cfg["cryptokey"] = item
104
+ cfg["cryptokey_type"] = "RC4"
109
105
  if i == 1:
110
106
  campaign_found = True
111
107
 
112
108
  elif "Mozilla" in item:
113
- cfg["User-agent"] = item
109
+ cfg["user_agent"] = item
114
110
  if i == 1:
115
111
  campaign_found = True
116
112
 
@@ -119,13 +115,13 @@ def extract_config(filebuf):
119
115
  campaign_found = True
120
116
 
121
117
  if campaign_found:
122
- cfg["Campaign"] = campaign
118
+ cfg["campaign"] = campaign
123
119
 
124
120
  if c2s:
125
- cfg["C2"] = c2s
121
+ cfg["CNCs"] = c2s
126
122
 
127
123
  if mutexes:
128
- cfg["Mutex"] = list(set(mutexes))
124
+ cfg["mutex"] = list(set(mutexes))
129
125
 
130
126
  return cfg
131
127
 
@@ -167,11 +167,11 @@ def extract_config(data):
167
167
  if not c2 or "." not in c2:
168
168
  return
169
169
 
170
- config_dict = {"C2": c2, "Botnet": botnet, "Key": key}
170
+ config_dict = {"CNCs": c2, "botnet": botnet, "cryptokey": key}
171
171
  if "Authorization" in user_strings:
172
172
  base_location = user_strings.index("Authorization")
173
173
  if base_location:
174
- config_dict["Authorization"] = user_strings[base_location - 1]
174
+ config_dict.setdefault("raw", {})["Authorization"] = user_strings[base_location - 1]
175
175
  return config_dict
176
176
 
177
177
 
@@ -25,34 +25,34 @@ FLAG = {b"\x00": "Disable", b"\x01": "Enable"}
25
25
 
26
26
  # From JPCERT and Elastic Security Labs
27
27
  idx_list = {
28
- 0: "Host:Port:Password", # String containing "domain:port:enable_tls" separated by the "\x1e" characte
29
- 1: "Botnet", # Name of the botnet
30
- 2: "Connect interval", # Interval in second between connection attempt to C2
31
- 3: "Install flag", # Install REMCOS on the machine host
32
- 4: "Setup HKCU\\Run", # Enable setup of the persistence in the registry
33
- 5: "Setup HKLM\\Run", # Enable setup of the persistence in the registry
28
+ 0: "Host:Port:Password", # String containing "domain:port:enable_tls" separated by the "\x1e" characte
29
+ 1: "Botnet", # Name of the botnet
30
+ 2: "Connect interval", # Interval in second between connection attempt to C2
31
+ 3: "Install flag", # Install REMCOS on the machine host
32
+ 4: "Setup HKCU\\Run", # Enable setup of the persistence in the registry
33
+ 5: "Setup HKLM\\Run", # Enable setup of the persistence in the registry
34
34
  6: "Setup HKLM\\Explorer\\Run",
35
- 7: "Keylog file max size", # Maximum size of the keylogging data before rotation
36
- 8: "Setup HKLM\\Explorer\\Run", # Enable setup of the persistence in the registry
37
- 9: "Install parent directory", # Parent directory of the install folder. Integer mapped to an hardcoded path
38
- 10: "Install filename", # Name of the REMCOS binary once installed
35
+ 7: "Keylog file max size", # Maximum size of the keylogging data before rotation
36
+ 8: "Setup HKLM\\Explorer\\Run", # Enable setup of the persistence in the registry
37
+ 9: "Install parent directory", # Parent directory of the install folder. Integer mapped to an hardcoded path
38
+ 10: "Install filename", # Name of the REMCOS binary once installed
39
39
  11: "Startup value",
40
- 12: "Hide file", # Enable super hiding the install directory and binary as well as setting them to read only
41
- 13: "Process injection flag", # Enable running the malware injected in another process
42
- 14: "Mutex", # String used as the malware mutex and registry key
43
- 15: "Keylogger mode", # Set keylogging capability. Keylogging mode, 0 = disabled, 1 = keylogging everything, 2 = keylogging specific window(s)
44
- 16: "Keylogger parent directory", # Parent directory of the keylogging folder. Integer mapped to an hardcoded path
45
- 17: "Keylogger filename", # Filename of the keylogged data
46
- 18: "Keylog crypt", # Enable encryption RC4 of the keylogger data file
47
- 19: "Hide keylog file", # Enable super hiding of the keylogger data file
48
- 20: "Screenshot flag", # Enable screen recording capability
49
- 21: "Screenshot time", # The time interval in minute for capturing each screenshot
50
- 22: "Take Screenshot option", # Enable screen recording for specific window names
51
- 23: "Take screenshot title", # String containing window names separated by the ";” character
52
- 24: "Take screenshot time", #s The time interval in second for capturing each screenshot when a specific window name is found in the current foreground window title
53
- 25: "Screenshot parent directory", # Parent directory of the screenshot folder. Integer mapped to an hardcoded path
54
- 26: "Screenshot folder", # Name of the screenshot folder
55
- 27: "Screenshot crypt flag", # Enable encryption of screenshots
40
+ 12: "Hide file", # Enable super hiding the install directory and binary as well as setting them to read only
41
+ 13: "Process injection flag", # Enable running the malware injected in another process
42
+ 14: "Mutex", # String used as the malware mutex and registry key
43
+ 15: "Keylogger mode", # Set keylogging capability. Keylogging mode, 0 = disabled, 1 = keylogging everything, 2 = keylogging specific window(s)
44
+ 16: "Keylogger parent directory", # Parent directory of the keylogging folder. Integer mapped to an hardcoded path
45
+ 17: "Keylogger filename", # Filename of the keylogged data
46
+ 18: "Keylog crypt", # Enable encryption RC4 of the keylogger data file
47
+ 19: "Hide keylog file", # Enable super hiding of the keylogger data file
48
+ 20: "Screenshot flag", # Enable screen recording capability
49
+ 21: "Screenshot time", # The time interval in minute for capturing each screenshot
50
+ 22: "Take Screenshot option", # Enable screen recording for specific window names
51
+ 23: "Take screenshot title", # String containing window names separated by the ";" character
52
+ 24: "Take screenshot time", # s The time interval in second for capturing each screenshot when a specific window name is found in the current foreground window title
53
+ 25: "Screenshot parent directory", # Parent directory of the screenshot folder. Integer mapped to an hardcoded path
54
+ 26: "Screenshot folder", # Name of the screenshot folder
55
+ 27: "Screenshot crypt flag", # Enable encryption of screenshots
56
56
  28: "Mouse option",
57
57
  29: "Unknown29",
58
58
  30: "Delete file",
@@ -60,28 +60,28 @@ idx_list = {
60
60
  32: "Unknown32",
61
61
  33: "Unknown33",
62
62
  34: "Unknown34",
63
- 35: "Audio recording flag", # Enable audio recording capability
64
- 36: "Audio record time", # Duration in second of each audio recording
65
- 37: "Audio parent directory", # Parent directory of the audio recording folder. Integer mapped to an hardcoded path
66
- 38: "Audio folder", # Name of the audio recording folder
67
- 39: "Disable UAC flage", # Disable UAC in the registry
68
- 40: "Logging mode", # Set logging mode: 0 = disabled, 1 = minimized in tray, 2 = console logging
69
- 41: "Connect delay", # Delay in second before the first connection attempt to the C2
70
- 42: "Keylogger specific window names", # String containing window names separated by the ";” character
71
- 43: "Browser cleaning on startup flag", # Enable cleaning web browsers cookies and logins on REMCOS startup
72
- 44: "Browser cleaning only for the first run flag", # Enable web browsers cleaning only on the first run of Remcos
73
- 45: "Browser cleaning sleep time in minutes", # Sleep time in minute before cleaning the web browsers
74
- 46: "UAC bypass flag", # Enable UAC bypass capability
63
+ 35: "Audio recording flag", # Enable audio recording capability
64
+ 36: "Audio record time", # Duration in second of each audio recording
65
+ 37: "Audio parent directory", # Parent directory of the audio recording folder. Integer mapped to an hardcoded path
66
+ 38: "Audio folder", # Name of the audio recording folder
67
+ 39: "Disable UAC flage", # Disable UAC in the registry
68
+ 40: "Logging mode", # Set logging mode: 0 = disabled, 1 = minimized in tray, 2 = console logging
69
+ 41: "Connect delay", # Delay in second before the first connection attempt to the C2
70
+ 42: "Keylogger specific window names", # String containing window names separated by the ";"" character
71
+ 43: "Browser cleaning on startup flag", # Enable cleaning web browsers cookies and logins on REMCOS startup
72
+ 44: "Browser cleaning only for the first run flag", # Enable web browsers cleaning only on the first run of Remcos
73
+ 45: "Browser cleaning sleep time in minutes", # Sleep time in minute before cleaning the web browsers
74
+ 46: "UAC bypass flag", # Enable UAC bypass capability
75
75
  47: "Unkown47",
76
- 48: "Install directory", # Name of the install directory
77
- 49: "Keylogger root directory", # Name of the keylogger directory
78
- 50: "Watchdog flag", # Enable watchdog capability
76
+ 48: "Install directory", # Name of the install directory
77
+ 49: "Keylogger root directory", # Name of the keylogger directory
78
+ 50: "Watchdog flag", # Enable watchdog capability
79
79
  51: "Unknown51",
80
- 52: "License", # License serial
81
- 53: "Screenshot mouse drawing flag", # Enable drawing the mouse on each screenshot
82
- 54: "TLS raw certificate (base64)", # Certificate in raw format used with tls enabled C2 communication
83
- 55: "TLS key (base64)", # Key of the certificate
84
- 56: "TLS raw peer certificate (base64)", # C2 public certificate in raw format
80
+ 52: "License", # License serial
81
+ 53: "Screenshot mouse drawing flag", # Enable drawing the mouse on each screenshot
82
+ 54: "TLS raw certificate (base64)", # Certificate in raw format used with tls enabled C2 communication
83
+ 55: "TLS key (base64)", # Key of the certificate
84
+ 56: "TLS raw peer certificate (base64)", # C2 public certificate in raw format
85
85
  57: "TLS client private key (base64)",
86
86
  58: "TLS server certificate (base64)",
87
87
  59: "Unknown59",
@@ -107,7 +107,15 @@ setup_list = {
107
107
  8: "%ProgramData%",
108
108
  }
109
109
 
110
- utf_16_string_list = ["Keylogger specific window names", "Install filename", "Install directory", "Startup value", "Keylogger filename", "Take screenshot title", "Keylogger root directory"]
110
+ utf_16_string_list = [
111
+ "Keylogger specific window names",
112
+ "Install filename",
113
+ "Install directory",
114
+ "Startup value",
115
+ "Keylogger filename",
116
+ "Take screenshot title",
117
+ "Keylogger root directory",
118
+ ]
111
119
  logger = logging.getLogger(__name__)
112
120
 
113
121
 
@@ -170,7 +178,7 @@ def extract_config(filebuf):
170
178
  key = blob[1 : keylen + 1]
171
179
  decrypted_data = ARC4.new(key).decrypt(blob[keylen + 1 :])
172
180
  p_data = OrderedDict()
173
- p_data["Version"] = check_version(filebuf)
181
+ config["version"] = check_version(filebuf)
174
182
 
175
183
  configs = re.split(rb"\|\x1e\x1e\x1f\|", decrypted_data)
176
184
 
@@ -189,7 +197,7 @@ def extract_config(filebuf):
189
197
  # various separators have been observed
190
198
  separator = next((x for x in (b"|", b"\x1e", b"\xff\xff\xff\xff") if x in cont))
191
199
  host, port, password = cont.split(separator, 1)[0].split(b":")
192
- p_data["Control"] = f"tcp://{host.decode()}:{port.decode()}"
200
+ config["CNCs"] = [f"tcp://{host.decode()}:{port.decode()}"]
193
201
  p_data["Password"] = password.decode()
194
202
  else:
195
203
  p_data[idx_list[i]] = cont
@@ -200,10 +208,10 @@ def extract_config(filebuf):
200
208
  if isinstance(v, bytes):
201
209
  with suppress(Exception):
202
210
  v = v.decoed()
203
- config[k] = v
211
+ config.setdefault("raw", {})[k] = v
204
212
 
205
213
  except Exception as e:
206
- logger.error(f"Caught an exception: {e}")
214
+ logger.error("Caught an exception: %s", str(e))
207
215
 
208
216
  return config
209
217
 
@@ -11,12 +11,15 @@ AUTHOR = "kevoreilly, YungBinary"
11
11
  def mask32(x):
12
12
  return x & 0xFFFFFFFF
13
13
 
14
+
14
15
  def add32(x, y):
15
16
  return mask32(x + y)
16
17
 
18
+
17
19
  def left_rotate(x, n):
18
20
  return mask32(x << n) | (x >> (32 - n))
19
21
 
22
+
20
23
  def quarter_round(block, a, b, c, d):
21
24
  block[a] = add32(block[a], block[b])
22
25
  block[d] ^= block[a]
@@ -31,6 +34,7 @@ def quarter_round(block, a, b, c, d):
31
34
  block[b] ^= block[c]
32
35
  block[b] = left_rotate(block[b], 7)
33
36
 
37
+
34
38
  def chacha20_permute(block):
35
39
  for doubleround in range(10):
36
40
  quarter_round(block, 0, 4, 8, 12)
@@ -42,6 +46,7 @@ def chacha20_permute(block):
42
46
  quarter_round(block, 2, 7, 8, 13)
43
47
  quarter_round(block, 3, 4, 9, 14)
44
48
 
49
+
45
50
  def words_from_bytes(b):
46
51
  assert len(b) % 4 == 0
47
52
  return [int.from_bytes(b[4 * i : 4 * i + 4], "little") for i in range(len(b) // 4)]
@@ -50,11 +55,12 @@ def words_from_bytes(b):
50
55
  def bytes_from_words(w):
51
56
  return b"".join(word.to_bytes(4, "little") for word in w)
52
57
 
58
+
53
59
  def chacha20_block(key, nonce, blocknum):
54
60
  # This implementation doesn't support 16-byte keys.
55
61
  assert len(key) == 32
56
62
  assert len(nonce) == 12
57
- assert blocknum < 2 ** 32
63
+ assert blocknum < 2**32
58
64
  constant_words = words_from_bytes(b"expand 32-byte k")
59
65
  key_words = words_from_bytes(key)
60
66
  nonce_words = words_from_bytes(nonce)
@@ -72,6 +78,7 @@ def chacha20_block(key, nonce, blocknum):
72
78
  permuted_block[i] = add32(permuted_block[i], original_block[i])
73
79
  return bytes_from_words(permuted_block)
74
80
 
81
+
75
82
  def chacha20_stream(key, nonce, length, blocknum):
76
83
  output = bytearray()
77
84
  while length > 0:
@@ -82,8 +89,9 @@ def chacha20_stream(key, nonce, length, blocknum):
82
89
  blocknum += 1
83
90
  return output
84
91
 
92
+
85
93
  def decrypt_config(data):
86
- decrypted_config = b"\x21\x52\x48\x59"
94
+ decrypted_config = b""
87
95
  data_len = len(data)
88
96
  v3 = 0
89
97
  while True:
@@ -98,6 +106,7 @@ def decrypt_config(data):
98
106
  v8 -= 1
99
107
  v3 += 1
100
108
 
109
+
101
110
  def chacha20_xor(custom_b64_decoded, key, nonce):
102
111
  message_len = len(custom_b64_decoded)
103
112
  key_stream = chacha20_stream(key, nonce, message_len, 0x80)
@@ -108,68 +117,199 @@ def chacha20_xor(custom_b64_decoded, key, nonce):
108
117
 
109
118
  return xor_key
110
119
 
111
- def extract_strings(data, minchars, maxchars):
112
- apat = b"([\x20-\x7e]{" + str(minchars).encode() + b"," + str(maxchars).encode() + b"})\x00"
113
- strings = [string.decode() for string in re.findall(apat, data)]
114
- match = re.search(apat, data)
115
- if not match:
116
- return None
117
- upat = b"((?:[\x20-\x7e][\x00]){" + str(minchars).encode() + b"," + str(maxchars).encode() + b"})\x00\x00"
118
- strings.extend(str(ws.decode("utf-16le")) for ws in re.findall(upat, data))
120
+
121
+ def extract_base64_strings(data, minchars, maxchars):
122
+ apat = b"([A-Za-z0-9-|]{" + str(minchars).encode() + b"," + str(maxchars).encode() + b"})\x00"
123
+ strings = [s.decode() for s in re.findall(apat, data)]
124
+ upat = b"((?:[A-Za-z0-9-|]\x00){" + str(minchars).encode() + b"," + str(maxchars).encode() + b"})\x00\x00"
125
+ strings.extend(ws.decode("utf-16le") for ws in re.findall(upat, data))
119
126
  return strings
120
127
 
128
+
121
129
  def extract_c2_url(data):
122
130
  pattern = b"(http[\x20-\x7e]+)\x00"
123
131
  match = re.search(pattern, data)
124
- return match.group(1).decode()
132
+ if match:
133
+ return match.group(1).decode()
125
134
 
126
- def is_potential_custom_base64(string):
127
- custom_alphabet = "ABC1fghijklmnop234NOPQRSTUVWXY567DEFGHIJKLMZ089abcdeqrstuvwxyz-|"
128
- for c in string:
129
- if c not in custom_alphabet:
130
- return False
131
- return True
132
135
 
133
- def custom_b64decode(data):
136
+ def custom_b64decode(data: bytes, custom_alphabet: bytes):
134
137
  """Decodes base64 data using a custom alphabet."""
135
138
  standard_alphabet = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
136
- custom_alphabet = b"ABC1fghijklmnop234NOPQRSTUVWXY567DEFGHIJKLMZ089abcdeqrstuvwxyz-|"
137
139
  # Translate the data back to the standard alphabet before decoding
138
140
  table = bytes.maketrans(custom_alphabet, standard_alphabet)
139
141
  return base64.b64decode(data.translate(table), validate=True)
140
142
 
143
+
144
+ def lzo_noheader_decompress(data: bytes, decompressed_size: int):
145
+ src = 0
146
+ dst = bytearray()
147
+ length = len(data)
148
+
149
+ while src < length:
150
+ ctrl = data[src]
151
+ src += 1
152
+
153
+ # Special short match
154
+ # Copies exactly 3 bytes from dst starting match_len + 1 bytes back.
155
+ if ctrl == 0x20:
156
+ match_len = data[src]
157
+ src += 1
158
+ start = len(dst) - match_len - 1
159
+ end = start + 3
160
+ #print(f"Control code: {hex(ctrl)}, Offset backtrack length: {hex(match_len)}, Current offset: {hex(len(dst))}, New offset: {hex(start)}")
161
+ dst.extend(dst[start:end])
162
+
163
+ elif ctrl >= 0xE0 or ctrl == 0x40:
164
+ # Compute base copy length from the upper bits of ctrl
165
+ base_len = ((ctrl >> 5) - 1) + 3
166
+
167
+ if ctrl >= 0xE0:
168
+ # Long copy: extra length byte follows
169
+ copy_len = base_len + data[src]
170
+ # Offset is byte after
171
+ start = data[src + 1]
172
+ src += 2
173
+ elif ctrl == 0x40:
174
+ # Short copy: offset byte after control code
175
+ copy_len = base_len
176
+ start = data[src]
177
+ src += 1
178
+
179
+ # Calculate offset in output buffer
180
+ offset = len(dst) - start - 1
181
+
182
+ #print(f"Control code: {hex(ctrl)}, Offset backtrack length: {hex(start)}, Current offset: {hex(len(dst))}, New offset: {hex(len(dst) - start)}, Length to copy: {hex(copy_len)}")
183
+
184
+ # Copy from previously decompressed data
185
+ dst.extend(dst[offset:offset + copy_len])
186
+
187
+ else:
188
+ # Literal run
189
+ literal_len = (ctrl & 0x1F) + 1
190
+ #print(f"Control code: {hex(ctrl)}, Literal length: {hex(literal_len)}")
191
+ dst.extend(data[src:src+literal_len])
192
+ src += literal_len
193
+
194
+ if len(dst) == decompressed_size:
195
+ return bytes(dst)
196
+
197
+
198
+ def parse_compression_header(config: bytes):
199
+ """Parse compressed size, decompressed size, and data offset from config"""
200
+
201
+ # 0x2A when looking at the config in memory
202
+ base_offset = 0x26
203
+
204
+ # Compressed data offset field, for calculating the offset to the compressed buffer
205
+ comp_offset_field = config[base_offset]
206
+ # Number of bytes the field spans
207
+ comp_offset_size_len = (comp_offset_field & 3) + 1
208
+ for i in range(1, comp_offset_size_len):
209
+ comp_offset_field |= config[base_offset + i] << (8 * i)
210
+
211
+ comp_size_offset = comp_offset_field >> 2
212
+
213
+ # Compressed size field, for finding the size of the compressed buffer
214
+ comp_offset = base_offset + comp_offset_size_len
215
+ comp_size_field = config[comp_offset]
216
+ # Number of bytes the field spans
217
+ comp_size_len = (comp_size_field & 3) + 1
218
+ for i in range(1, comp_size_len):
219
+ comp_size_field |= config[comp_offset + i] << (8 * i)
220
+
221
+ # Decompressed size field
222
+ decomp_field_offset = base_offset + comp_offset_size_len + comp_size_len
223
+ decomp_size_field = config[decomp_field_offset]
224
+ # Number of bytes the field spans
225
+ decomp_field_len = (decomp_size_field & 3) + 1
226
+ for i in range(1, decomp_field_len):
227
+ decomp_size_field |= config[decomp_field_offset + i] << (8 * i)
228
+
229
+ # Calculate return values
230
+ decompressed_size = decomp_size_field >> 2
231
+ compressed_data_offset = decomp_field_offset + decomp_field_len + comp_size_offset
232
+ compressed_size_key = config[0x28] << 8
233
+ compressed_size = (compressed_size_key | comp_size_field) >> 2
234
+ compressed_data = config[compressed_data_offset : compressed_data_offset + compressed_size]
235
+
236
+ return {
237
+ "compressed_size": compressed_size,
238
+ "decompressed_size": decompressed_size,
239
+ "compressed_data": compressed_data
240
+ }
241
+
242
+
141
243
  def extract_config(data):
142
244
  config_dict = {}
143
245
  magic = struct.unpack("I", data[:4])[0]
144
246
  if magic == 0x59485221:
145
- config_dict["C2"] = data[24:].split(b"\0", 1)[0].decode()
247
+ config_dict["CNCs"] = [data[24:].split(b"\0", 1)[0].decode()]
146
248
  return config_dict
147
249
  else:
148
250
  key = b"\x52\xAB\xDF\x06\xB6\xB1\x3A\xC0\xDA\x2D\x22\xDC\x6C\xD2\xBE\x6C\x20\x17\x69\xE0\x12\xB5\xE6\xEC\x0E\xAB\x4C\x14\x73\x4A\xED\x51"
149
251
  nonce = b"\x5F\x14\xD7\x9C\xFC\xFC\x43\x9E\xC3\x40\x6B\xBA"
150
252
 
151
- extracted_strings = extract_strings(data, 0x100, 0x100)
253
+ custom_alphabets = [
254
+ b"ABC1fghijklmnop234NOPQRSTUVWXY567DEFGHIJKLMZ089abcdeqrstuvwxyz-|",
255
+ b"4NOPQRSTUVWXY567DdeEqrstuvwxyz-ABC1fghop23Fijkbc|lmnGHIJKLMZ089a", # 0.9.2
256
+ b"3Fijkbc|l4NOPQRSTUVWXY567DdewxEqrstuvyz-ABC1fghop2mnGHIJKLMZ089a", # 0.9.3
257
+ ]
258
+
259
+ # Extract base64 strings
260
+ extracted_strings = extract_base64_strings(data, 100, 256)
261
+ if not extracted_strings:
262
+ return config_dict
263
+
264
+ pattern = re.compile(b'.\x80')
152
265
  for string in extracted_strings:
153
266
  try:
154
- if not is_potential_custom_base64(string):
155
- continue
267
+ custom_b64_decoded = custom_b64decode(string, custom_alphabets[0])
156
268
 
157
- custom_b64_decoded = custom_b64decode(string)
158
269
  xor_key = chacha20_xor(custom_b64_decoded, key, nonce)
159
- decrypted_config = decrypt_config(xor_key)
160
- reexecution_delay = int.from_bytes(decrypted_config[5:7], byteorder='little')
161
-
162
- c2_url = extract_c2_url(decrypted_config)
163
- if not c2_url:
164
- continue
165
- config_dict = {
166
- "Reexecution_delay": reexecution_delay,
167
- "C2": [c2_url]
168
- }
169
- return config_dict
270
+
271
+ # Decrypted, but may still be the compressed malware configuration
272
+ config = decrypt_config(xor_key)
273
+ # Attempt to extract C2 url, only works in version prior to 0.9.2
274
+ c2_url = extract_c2_url(config)
275
+ if c2_url:
276
+ config_dict = {"CNCs": [c2_url]}
277
+ return config_dict
278
+ else:
279
+ # Handle new variants that compress the Command and Control server(s)
280
+ custom_b64_decoded = custom_b64decode(string, custom_alphabets[2])
281
+ xor_key = chacha20_xor(custom_b64_decoded, key, nonce)
282
+ config = decrypt_config(xor_key)
283
+
284
+ parsed = parse_compression_header(config)
285
+ if not parsed:
286
+ return config_dict
287
+
288
+ decompressed = lzo_noheader_decompress(parsed['compressed_data'], parsed['decompressed_size'])
289
+
290
+ # Try old alphabet for 0.9.2
291
+ if not decompressed:
292
+ custom_b64_decoded = custom_b64decode(string, custom_alphabets[1])
293
+ xor_key = chacha20_xor(custom_b64_decoded, key, nonce)
294
+ config = decrypt_config(xor_key)
295
+
296
+ parsed = parse_compression_header(config)
297
+ if not parsed:
298
+ return config_dict
299
+
300
+ decompressed = lzo_noheader_decompress(parsed['compressed_data'], parsed['decompressed_size'])
301
+
302
+
303
+ cncs = [f"https://{chunk.decode()}" for chunk in pattern.split(decompressed) if chunk]
304
+ if cncs:
305
+ config_dict = {"CNCs": cncs}
306
+ return config_dict
307
+
170
308
  except Exception:
171
309
  continue
172
310
 
311
+ return config_dict
312
+
173
313
 
174
314
  if __name__ == "__main__":
175
315
  import sys
@@ -177,4 +317,3 @@ if __name__ == "__main__":
177
317
  with open(sys.argv[1], "rb") as f:
178
318
  config_json = json.dumps(extract_config(f.read()), indent=4)
179
319
  print(config_json)
180
-
@@ -37,7 +37,7 @@ def rc4_decrypt(key, ciphertext):
37
37
 
38
38
 
39
39
  def swap32(x):
40
- return int.from_bytes(x.to_bytes(4, byteorder='little'), byteorder='big', signed=False)
40
+ return int.from_bytes(x.to_bytes(4, byteorder="little"), byteorder="big", signed=False)
41
41
 
42
42
 
43
43
  def decode(buffer):
@@ -104,7 +104,7 @@ def extract_config(filebuf):
104
104
  break
105
105
  c2list_offset += delta
106
106
  if c2list != []:
107
- cfg["C2s"] = sorted(list(set(c2list)))
107
+ cfg["CNCs"] = sorted(list(set(c2list)))
108
108
  return cfg
109
109
 
110
110
 
@@ -11,15 +11,15 @@ def _is_ip(ip):
11
11
 
12
12
 
13
13
  def extract_config(data):
14
- config_dict = {"C2s": []}
14
+ config_dict = {}
15
15
  with suppress(Exception):
16
16
  if data[:2] == b"MZ":
17
17
  return
18
18
  for line in data.decode().split("\n"):
19
- if _is_ip(line) and line not in config_dict["C2s"]:
20
- config_dict["C2s"].append(line)
19
+ if _is_ip(line) and line not in config_dict.get("CNCs", []):
20
+ config_dict["CNCs"].append(line)
21
21
  elif line and "\\" in line:
22
22
  config_dict.setdefault("Timestamp path", []).append(line)
23
- elif "." in line and "=" not in line and line not in config_dict["C2s"]:
24
- config_dict.setdefault("Dummy domain", []).append(line)
23
+ elif "." in line and "=" not in line and line not in config_dict["CNCs"]:
24
+ config_dict.setdefault("raw", {}).setdefault("Dummy domain", []).append(line)
25
25
  return config_dict
@@ -68,9 +68,9 @@ def extract_config(data):
68
68
  try:
69
69
  decrypted = xor_data(line, chunks[i + 1]).decode()
70
70
  if "\r\n" in decrypted and "|" not in decrypted:
71
- config["IP Blocklist"] = list(filter(None, decrypted.split("\r\n")))
71
+ config.setdefault("raw", {})["IP Blocklist"] = list(filter(None, decrypted.split("\r\n")))
72
72
  elif "|" in decrypted and "." in decrypted and "\r\n" not in decrypted:
73
- config["URLs"] = list(filter(None, decrypted.split("|")))
73
+ config["CNCs"] = list(filter(None, decrypted.split("|")))
74
74
  except Exception:
75
75
  continue
76
76
  matches = yara_rules.match(data=data)
@@ -84,5 +84,5 @@ def extract_config(data):
84
84
  c2key_offset = item.instances[0].offset
85
85
  key_rva = struct.unpack("i", data[c2key_offset + 28 : c2key_offset + 32])[0] - pe.OPTIONAL_HEADER.ImageBase
86
86
  key_offset = pe.get_offset_from_rva(key_rva)
87
- config["C2 key"] = string_from_offset(data, key_offset).decode()
87
+ config["cryptokey"] = string_from_offset(data, key_offset).decode()
88
88
  return config
@@ -72,6 +72,6 @@ def extract_config(data):
72
72
  configdata = unzip_config(data)
73
73
 
74
74
  if configdata:
75
- raw_config["config"] = decode(configdata)
75
+ raw_config["raw"] = decode(configdata)
76
76
 
77
77
  return raw_config
@@ -92,7 +92,8 @@ def extract_config(data):
92
92
  c2_host = dtxt[offset : offset + c2_size].decode("utf-16")
93
93
  offset += c2_size
94
94
  c2_port = struct.unpack("H", dtxt[offset : offset + 2])[0]
95
- cfg["C2"] = f"{c2_host}:{c2_port}"
95
+ # ToDo missed schema
96
+ cfg["CNCs"] = [f"{c2_host}:{c2_port}"]
96
97
  offset += 2
97
98
  # unk1 = dtxt[offset : offset + 7]
98
99
  offset += 7
@@ -104,7 +105,7 @@ def extract_config(data):
104
105
  offset += 2
105
106
  runkey_size = struct.unpack("i", dtxt[offset : offset + 4])[0]
106
107
  offset += 4
107
- cfg["Run Key Name"] = dtxt[offset : offset + runkey_size].decode("utf-16")
108
+ cfg.setdefault("raw", {})["Run Key Name"] = dtxt[offset : offset + runkey_size].decode("utf-16")
108
109
  except struct.error:
109
110
  # there is a lot of failed data validation muting it
110
111
  return