CAPE-parsers 0.1.53__tar.gz → 0.1.55__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 (117) hide show
  1. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/PKG-INFO +1 -1
  2. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/CAPE/community/KoiLoader.py +3 -2
  3. cape_parsers-0.1.55/cape_parsers/CAPE/core/NitroBunnyDownloader.py +168 -0
  4. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/CAPE/core/Rhadamanthys.py +83 -56
  5. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/pyproject.toml +1 -1
  6. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/LICENSE +0 -0
  7. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/README.md +0 -0
  8. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/CAPE/__init__.py +0 -0
  9. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/CAPE/community/AgentTesla.py +0 -0
  10. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/CAPE/community/Amadey.py +0 -0
  11. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/CAPE/community/Arkei.py +0 -0
  12. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/CAPE/community/AsyncRAT.py +0 -0
  13. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/CAPE/community/AuroraStealer.py +0 -0
  14. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/CAPE/community/Carbanak.py +0 -0
  15. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/CAPE/community/CobaltStrikeBeacon.py +0 -0
  16. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/CAPE/community/CobaltStrikeStager.py +0 -0
  17. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/CAPE/community/DCRat.py +0 -0
  18. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/CAPE/community/Fareit.py +0 -0
  19. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/CAPE/community/LokiBot.py +0 -0
  20. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/CAPE/community/Lumma.py +0 -0
  21. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/CAPE/community/MonsterV2.py +0 -0
  22. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/CAPE/community/MyKings.py +0 -0
  23. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/CAPE/community/NanoCore.py +0 -0
  24. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/CAPE/community/Nighthawk.py +0 -0
  25. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/CAPE/community/Njrat.py +0 -0
  26. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/CAPE/community/PhemedroneStealer.py +0 -0
  27. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/CAPE/community/QuasarRAT.py +0 -0
  28. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/CAPE/community/README.md +0 -0
  29. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/CAPE/community/Snake.py +0 -0
  30. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/CAPE/community/SparkRAT.py +0 -0
  31. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/CAPE/community/Stealc.py +0 -0
  32. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/CAPE/community/VenomRAT.py +0 -0
  33. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/CAPE/community/WinosStager.py +0 -0
  34. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/CAPE/community/XWorm.py +0 -0
  35. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/CAPE/community/XenoRAT.py +0 -0
  36. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/CAPE/community/__init__.py +0 -0
  37. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/CAPE/core/AdaptixBeacon.py +0 -0
  38. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/CAPE/core/AuraStealer.py +0 -0
  39. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/CAPE/core/Azorult.py +0 -0
  40. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/CAPE/core/BitPaymer.py +0 -0
  41. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/CAPE/core/BlackDropper.py +0 -0
  42. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/CAPE/core/Blister.py +0 -0
  43. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/CAPE/core/BruteRatel.py +0 -0
  44. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/CAPE/core/BumbleBee.py +0 -0
  45. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/CAPE/core/DarkGate.py +0 -0
  46. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/CAPE/core/DoppelPaymer.py +0 -0
  47. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/CAPE/core/DridexLoader.py +0 -0
  48. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/CAPE/core/Formbook.py +0 -0
  49. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/CAPE/core/GuLoader.py +0 -0
  50. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/CAPE/core/IcedID.py +0 -0
  51. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/CAPE/core/IcedIDLoader.py +0 -0
  52. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/CAPE/core/Latrodectus.py +0 -0
  53. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/CAPE/core/Oyster.py +0 -0
  54. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/CAPE/core/PikaBot.py +0 -0
  55. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/CAPE/core/PlugX.py +0 -0
  56. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/CAPE/core/QakBot.py +0 -0
  57. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/CAPE/core/Quickbind.py +0 -0
  58. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/CAPE/core/README.md +0 -0
  59. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/CAPE/core/RedLine.py +0 -0
  60. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/CAPE/core/Remcos.py +0 -0
  61. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/CAPE/core/SmokeLoader.py +0 -0
  62. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/CAPE/core/Socks5Systemz.py +0 -0
  63. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/CAPE/core/SquirrelWaffle.py +0 -0
  64. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/CAPE/core/Strrat.py +0 -0
  65. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/CAPE/core/WarzoneRAT.py +0 -0
  66. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/CAPE/core/Zloader.py +0 -0
  67. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/CAPE/core/__init__.py +0 -0
  68. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/CAPE/core/test_cape.py +0 -0
  69. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/RATDecoders/README.md +0 -0
  70. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/RATDecoders/__init__.py +0 -0
  71. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/RATDecoders/test_rats.py +0 -0
  72. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/__init__.py +0 -0
  73. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/deprecated/BackOffLoader.py +0 -0
  74. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/deprecated/BackOffPOS.py +0 -0
  75. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/deprecated/BlackNix.py +0 -0
  76. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/deprecated/BuerLoader.py +0 -0
  77. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/deprecated/ChChes.py +0 -0
  78. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/deprecated/Emotet.py +0 -0
  79. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/deprecated/Enfal.py +0 -0
  80. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/deprecated/EvilGrab.py +0 -0
  81. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/deprecated/Greame.py +0 -0
  82. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/deprecated/Hancitor.py +0 -0
  83. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/deprecated/HttpBrowser.py +0 -0
  84. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/deprecated/JavaDropper.py +0 -0
  85. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/deprecated/Nymaim.py +0 -0
  86. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/deprecated/Pandora.py +0 -0
  87. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/deprecated/PoisonIvy.py +0 -0
  88. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/deprecated/PredatorPain.py +0 -0
  89. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/deprecated/Punisher.py +0 -0
  90. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/deprecated/RCSession.py +0 -0
  91. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/deprecated/REvil.py +0 -0
  92. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/deprecated/RedLeaf.py +0 -0
  93. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/deprecated/Retefe.py +0 -0
  94. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/deprecated/Rozena.py +0 -0
  95. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/deprecated/SmallNet.py +0 -0
  96. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/deprecated/TSCookie.py +0 -0
  97. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/deprecated/TrickBot.py +0 -0
  98. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/deprecated/UrsnifV3.py +0 -0
  99. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/deprecated/_ShadowTech.py +0 -0
  100. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/deprecated/_VirusRat.py +0 -0
  101. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/deprecated/_jRat.py +0 -0
  102. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/deprecated/unrecom.py +0 -0
  103. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/deprecated/xRAT.py +0 -0
  104. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/malduck/LICENSE +0 -0
  105. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/malduck/README.md +0 -0
  106. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/malduck/__init__.py +0 -0
  107. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/malduck/test_malduck.py +0 -0
  108. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/mwcp/README.md +0 -0
  109. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/mwcp/__init__.py +0 -0
  110. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/mwcp/test_mwcp.py +0 -0
  111. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/utils/__init__.py +0 -0
  112. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/utils/aplib.py +0 -0
  113. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/utils/blzpack.py +0 -0
  114. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/utils/blzpack_lib.so +0 -0
  115. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/utils/dotnet_utils.py +0 -0
  116. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/cape_parsers/utils/lznt1.py +0 -0
  117. {cape_parsers-0.1.53 → cape_parsers-0.1.55}/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.53
3
+ Version: 0.1.55
4
4
  Summary: CAPE: Malware Configuration Extraction
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -118,8 +118,9 @@ def extract_config(data):
118
118
 
119
119
  encoded_payload = remove_nulls(encoded_payload, encoded_payload_size)
120
120
  decoded_payload = xor_data(encoded_payload, xor_key)
121
-
122
- config["CNCs"] = find_c2(decoded_payload)
121
+ cncs = find_c2(decoded_payload)
122
+ if cncs and list(filter(cncs, None)):
123
+ config["CNCs"] = cncs
123
124
 
124
125
  return config
125
126
 
@@ -0,0 +1,168 @@
1
+ # Copyright (C) 2024 enzok
2
+ # This program is free software: you can redistribute it and/or modify
3
+ # it under the terms of the GNU General Public License as published by
4
+ # the Free Software Foundation, either version 3 of the License, or
5
+ # (at your option) any later version.
6
+ #
7
+ # This program is distributed in the hope that it will be useful,
8
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
9
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10
+ # GNU General Public License for more details.
11
+ #
12
+ # You should have received a copy of the GNU General Public License
13
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
14
+
15
+ import logging
16
+ import struct
17
+
18
+ import pefile
19
+ import yara
20
+
21
+ log = logging.getLogger(__name__)
22
+
23
+ DESCRIPTION = "NitroBunnyDownloader configuration parser."
24
+ AUTHOR = "enzok"
25
+
26
+ yara_rule = """
27
+ rule NitroBunnyDownloader
28
+ {
29
+ meta:
30
+ author = "enzok"
31
+ description = "NitroBunnyDownloader Payload"
32
+ cape_type = "NitroBunnyDownloader Payload"
33
+ hash = "960e59200ec0a4b5fb3b44e6da763f5fec4092997975140797d4eec491de411b"
34
+ 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}
37
+ $string1 = "X-Amz-User-Agent:" wide
38
+ $string2 = "Amz-Security-Flag:" wide
39
+ $string3 = "/cart" wide
40
+ $string4 = "Cookie: " wide
41
+ $string5 = "wishlist" wide
42
+ condition:
43
+ uint16(0) == 0x5A4D and 1 of ($config*) and 2 of ($string*)
44
+ }
45
+ """
46
+
47
+ yara_rules = yara.compile(source=yara_rule)
48
+
49
+
50
+ def yara_scan(raw_data):
51
+ try:
52
+ return yara_rules.match(data=raw_data)
53
+ except Exception as e:
54
+ print(e)
55
+ return None
56
+
57
+
58
+ def read_dword(data, off):
59
+ if off + 4 > len(data):
60
+ raise ValueError(f"EOF reading dword at {off}")
61
+ val = struct.unpack_from("<I", data, off)[0]
62
+ return val, off + 4
63
+
64
+
65
+ def read_qword(data, off):
66
+ """Read a 64-bit unsigned little-endian value."""
67
+ if off + 8 > len(data):
68
+ raise ValueError(f"EOF reading qword at {off}")
69
+ val = struct.unpack_from("<Q", data, off)[0]
70
+ return val, off + 8
71
+
72
+
73
+ def read_utf16le_string(data, off, length):
74
+ if off + length > len(data):
75
+ raise ValueError(f"EOF reading string at {off} len={length}")
76
+ raw = data[off:off + length]
77
+ s = raw.decode("utf-16le", errors="replace").rstrip("\x00")
78
+ return s, off + length
79
+
80
+
81
+ def read_string_list(data, off, count):
82
+ items = []
83
+ for i in range(count):
84
+ length_words, off = read_qword(data, off)
85
+ s, off = read_utf16le_string(data, off, length_words)
86
+ items.append(s)
87
+ return items, off
88
+
89
+
90
+ def extract_config(filebuf):
91
+ yara_hit = yara_scan(filebuf)
92
+ if not yara_hit:
93
+ return None
94
+
95
+ cfg = {}
96
+ config_code_offset = None
97
+ config_size_offset = None
98
+ config_offset = None
99
+ rva_offset = None
100
+
101
+ for hit in yara_hit:
102
+ if hit.rule != "NitroBunnyDownloader":
103
+ continue
104
+
105
+ 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
119
+
120
+ if config_code_offset:
121
+ break
122
+
123
+ if config_code_offset is None:
124
+ return None
125
+
126
+ try:
127
+ pe = pefile.PE(data=filebuf, fast_load=True)
128
+ 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)
130
+ rva = pe.get_rva_from_offset(config_code_offset + rva_offset)
131
+ config_rva = rva + config
132
+ data = pe.get_data(config_rva, config_length)
133
+ off = 0
134
+ raw = cfg["raw"] = {}
135
+ port, off = read_dword(data, off)
136
+ num, off = read_dword(data, off)
137
+ cncs, off = read_string_list(data, off, num)
138
+ num, off = read_qword(data, off)
139
+ raw["user_agent"], off = read_utf16le_string(data, off, num)
140
+ num, off = read_dword(data, off)
141
+ raw["http_header_items"], off = read_string_list(data, off, num)
142
+ num, off = read_dword(data, off)
143
+ raw["uri_list"], off = read_string_list(data, off, num)
144
+ raw["unknown_1"], off = read_dword(data, off)
145
+ raw["unknown_2"], off = read_dword(data, off)
146
+
147
+ 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)
156
+
157
+ except Exception as e:
158
+ log.error("Error: %s", e)
159
+ return None
160
+
161
+ return cfg
162
+
163
+
164
+ if __name__ == "__main__":
165
+ import sys
166
+
167
+ with open(sys.argv[1], "rb") as f:
168
+ print(extract_config(f.read()))
@@ -7,6 +7,15 @@ import json
7
7
  DESCRIPTION = "Rhadamanthys parser"
8
8
  AUTHOR = "kevoreilly, YungBinary"
9
9
 
10
+ CUSTOM_ALPHABETS = [
11
+ b"3Fijkbc|l4NOPQRSTUVWXY567DdewxEqrstuvyz-ABC1fghop2mnGHIJKLMZ089a", # 0.9.3
12
+ b"4NOPQRSTUVWXY567DdeEqrstuvwxyz-ABC1fghop23Fijkbc|lmnGHIJKLMZ089a", # 0.9.2
13
+ b"ABC1fghijklmnop234NOPQRSTUVWXY567DEFGHIJKLMZ089abcdeqrstuvwxyz-|", # 0.9.X
14
+ ]
15
+
16
+ CHACHA_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"
17
+ CHACHA_NONCE = b"\x5F\x14\xD7\x9C\xFC\xFC\x43\x9E\xC3\x40\x6B\xBA"
18
+
10
19
 
11
20
  def mask32(x):
12
21
  return x & 0xFFFFFFFF
@@ -150,6 +159,8 @@ def lzo_noheader_decompress(data: bytes, decompressed_size: int):
150
159
  ctrl = data[src]
151
160
  src += 1
152
161
 
162
+ # Special short match
163
+ # Copies exactly 3 bytes from dst starting match_len + 1 bytes back.
153
164
  if ctrl == 0x20:
154
165
  match_len = data[src]
155
166
  src += 1
@@ -159,21 +170,28 @@ def lzo_noheader_decompress(data: bytes, decompressed_size: int):
159
170
  dst.extend(dst[start:end])
160
171
 
161
172
  elif ctrl >= 0xE0 or ctrl == 0x40:
162
- x = ((ctrl >> 5) - 1) + 3
163
- if ctrl >= 0xE0:
164
- copy_len = data[src] + x
165
- elif ctrl == 0x40:
166
- copy_len = x
167
- start = data[src + 1]
168
- if ctrl == 0x40:
169
- start = data[src]
173
+ # Compute base copy length from the upper bits of ctrl
174
+ base_len = ((ctrl >> 5) - 1) + 3
175
+
170
176
  if ctrl >= 0xE0:
177
+ # Long copy: extra length byte follows
178
+ copy_len = base_len + data[src]
179
+ # Offset is byte after
180
+ start = data[src + 1]
171
181
  src += 2
172
182
  elif ctrl == 0x40:
183
+ # Short copy: offset byte after control code
184
+ copy_len = base_len
185
+ start = data[src]
173
186
  src += 1
187
+
188
+ # Calculate offset in output buffer
174
189
  offset = len(dst) - start - 1
190
+
175
191
  #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)}")
176
- dst.extend(dst[offset:offset+copy_len])
192
+
193
+ # Copy from previously decompressed data
194
+ dst.extend(dst[offset:offset + copy_len])
177
195
 
178
196
  else:
179
197
  # Literal run
@@ -231,62 +249,71 @@ def parse_compression_header(config: bytes):
231
249
  }
232
250
 
233
251
 
252
+ def handle_encrypted_string(encrypted_string: str) -> list:
253
+ """
254
+ Args:
255
+ encrypted_string: a str representing
256
+ Returns:
257
+ Command and Control server list, may be empty
258
+ """
259
+
260
+ for alphabet in CUSTOM_ALPHABETS:
261
+ try:
262
+ custom_b64_decoded = custom_b64decode(encrypted_string, alphabet)
263
+ xor_key = chacha20_xor(custom_b64_decoded, CHACHA_KEY, CHACHA_NONCE)
264
+ # Decrypted, may still be the compressed malware configuration
265
+ config = decrypt_config(xor_key)
266
+
267
+ # First byte should be 0xFF
268
+ if config[0] != 0xFF:
269
+ continue
270
+
271
+ # Attempt to extract C2 url, only works in version prior to 0.9.2
272
+ c2_url = extract_c2_url(config)
273
+ if c2_url:
274
+ return [c2_url]
275
+
276
+ # Parse header
277
+ parsed = parse_compression_header(config)
278
+ if not parsed:
279
+ continue
280
+
281
+ # Decompress LZO-like compression
282
+ decompressed = lzo_noheader_decompress(parsed['compressed_data'], parsed['decompressed_size'])
283
+ pattern = re.compile(b'.' + bytes([decompressed[1]]))
284
+
285
+ cncs = [f"https://{chunk.decode()}" for chunk in pattern.split(decompressed) if chunk]
286
+ return cncs
287
+ except Exception:
288
+ continue
289
+
290
+ return []
291
+
292
+
234
293
  def extract_config(data):
294
+ """
295
+ Extract Rhadamanthys malware configuration.
296
+ """
235
297
  config_dict = {}
298
+ # Extract very old variant
236
299
  magic = struct.unpack("I", data[:4])[0]
237
300
  if magic == 0x59485221:
238
301
  config_dict["CNCs"] = [data[24:].split(b"\0", 1)[0].decode()]
239
302
  return config_dict
240
- else:
241
- 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"
242
- nonce = b"\x5F\x14\xD7\x9C\xFC\xFC\x43\x9E\xC3\x40\x6B\xBA"
243
-
244
- custom_alphabets = [
245
- b"ABC1fghijklmnop234NOPQRSTUVWXY567DEFGHIJKLMZ089abcdeqrstuvwxyz-|",
246
- b"4NOPQRSTUVWXY567DdeEqrstuvwxyz-ABC1fghop23Fijkbc|lmnGHIJKLMZ089a"
247
- ]
248
-
249
- # Extract base64 strings
250
- extracted_strings = extract_base64_strings(data, 140, 256)
251
- if not extracted_strings:
252
- return config_dict
253
-
254
- pattern = re.compile(b'.\x80')
255
- for string in extracted_strings:
256
- try:
257
- custom_b64_decoded = custom_b64decode(string, custom_alphabets[0])
258
-
259
- xor_key = chacha20_xor(custom_b64_decoded, key, nonce)
260
-
261
- # Decrypted, but may still be the compressed malware configuration
262
- config = decrypt_config(xor_key)
263
- # Attempt to extract C2 url, only works in version prior to 0.9.2
264
- c2_url = extract_c2_url(config)
265
- if c2_url:
266
- config_dict = {"CNCs": [c2_url]}
267
- return config_dict
268
- else:
269
- # Handle new variants that compress the Command and Control server(s)
270
- custom_b64_decoded = custom_b64decode(string, custom_alphabets[1])
271
- xor_key = chacha20_xor(custom_b64_decoded, key, nonce)
272
- config = decrypt_config(xor_key)
273
-
274
- parsed = parse_compression_header(config)
275
- if not parsed:
276
- return config_dict
277
-
278
- decompressed = lzo_noheader_decompress(parsed['compressed_data'], parsed['decompressed_size'])
279
-
280
- cncs = [f"https://{chunk.decode()}" for chunk in pattern.split(decompressed) if chunk]
281
- if cncs:
282
- config_dict = {"CNCs": cncs}
283
- return config_dict
284
-
285
- except Exception:
286
- continue
287
303
 
304
+ # New variants, extract base64 strings
305
+ extracted_strings = extract_base64_strings(data, 100, 256)
306
+ if not extracted_strings:
288
307
  return config_dict
289
308
 
309
+ # Handle each encrypted string
310
+ for string in extracted_strings:
311
+ cncs = handle_encrypted_string(string)
312
+ if cncs:
313
+ return {"CNCs": cncs}
314
+
315
+ return config_dict
316
+
290
317
 
291
318
  if __name__ == "__main__":
292
319
  import sys
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "CAPE-parsers"
3
- version = "0.1.53"
3
+ version = "0.1.55"
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