CAPE-parsers 0.1.47__tar.gz → 0.1.49__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 (114) hide show
  1. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/PKG-INFO +1 -1
  2. cape_parsers-0.1.49/cape_parsers/CAPE/community/Amadey.py +213 -0
  3. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/CAPE/community/CobaltStrikeBeacon.py +3 -3
  4. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/CAPE/community/LokiBot.py +11 -8
  5. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/CAPE/community/Lumma.py +3 -3
  6. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/CAPE/community/Snake.py +2 -2
  7. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/CAPE/community/Stealc.py +10 -0
  8. cape_parsers-0.1.49/cape_parsers/CAPE/core/AuraStealer.py +93 -0
  9. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/CAPE/core/BumbleBee.py +5 -5
  10. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/CAPE/core/Remcos.py +1 -1
  11. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/__init__.py +1 -1
  12. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/pyproject.toml +39 -31
  13. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/LICENSE +0 -0
  14. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/README.md +0 -0
  15. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/CAPE/__init__.py +0 -0
  16. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/CAPE/community/AgentTesla.py +0 -0
  17. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/CAPE/community/Arkei.py +0 -0
  18. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/CAPE/community/AsyncRAT.py +0 -0
  19. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/CAPE/community/AuroraStealer.py +0 -0
  20. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/CAPE/community/Carbanak.py +0 -0
  21. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/CAPE/community/CobaltStrikeStager.py +0 -0
  22. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/CAPE/community/DCRat.py +0 -0
  23. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/CAPE/community/Fareit.py +0 -0
  24. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/CAPE/community/KoiLoader.py +0 -0
  25. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/CAPE/community/NanoCore.py +0 -0
  26. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/CAPE/community/Nighthawk.py +0 -0
  27. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/CAPE/community/Njrat.py +0 -0
  28. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/CAPE/community/PhemedroneStealer.py +0 -0
  29. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/CAPE/community/QuasarRAT.py +0 -0
  30. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/CAPE/community/README.md +0 -0
  31. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/CAPE/community/SparkRAT.py +0 -0
  32. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/CAPE/community/VenomRAT.py +0 -0
  33. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/CAPE/community/XWorm.py +0 -0
  34. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/CAPE/community/XenoRAT.py +0 -0
  35. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/CAPE/community/__init__.py +0 -0
  36. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/CAPE/community/monsterv2.py +0 -0
  37. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/CAPE/core/AdaptixBeacon.py +0 -0
  38. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/CAPE/core/Azorult.py +0 -0
  39. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/CAPE/core/BitPaymer.py +0 -0
  40. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/CAPE/core/BlackDropper.py +0 -0
  41. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/CAPE/core/Blister.py +0 -0
  42. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/CAPE/core/BruteRatel.py +0 -0
  43. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/CAPE/core/DarkGate.py +0 -0
  44. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/CAPE/core/DoppelPaymer.py +0 -0
  45. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/CAPE/core/DridexLoader.py +0 -0
  46. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/CAPE/core/Formbook.py +0 -0
  47. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/CAPE/core/GuLoader.py +0 -0
  48. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/CAPE/core/IcedID.py +0 -0
  49. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/CAPE/core/IcedIDLoader.py +0 -0
  50. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/CAPE/core/Latrodectus.py +0 -0
  51. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/CAPE/core/Oyster.py +0 -0
  52. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/CAPE/core/PikaBot.py +0 -0
  53. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/CAPE/core/PlugX.py +0 -0
  54. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/CAPE/core/QakBot.py +0 -0
  55. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/CAPE/core/Quickbind.py +0 -0
  56. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/CAPE/core/README.md +0 -0
  57. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/CAPE/core/RedLine.py +0 -0
  58. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/CAPE/core/Rhadamanthys.py +0 -0
  59. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/CAPE/core/SmokeLoader.py +0 -0
  60. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/CAPE/core/Socks5Systemz.py +0 -0
  61. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/CAPE/core/SquirrelWaffle.py +0 -0
  62. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/CAPE/core/Strrat.py +0 -0
  63. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/CAPE/core/WarzoneRAT.py +0 -0
  64. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/CAPE/core/Zloader.py +0 -0
  65. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/CAPE/core/__init__.py +0 -0
  66. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/CAPE/core/test_cape.py +0 -0
  67. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/RATDecoders/README.md +0 -0
  68. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/RATDecoders/__init__.py +0 -0
  69. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/RATDecoders/test_rats.py +0 -0
  70. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/deprecated/BackOffLoader.py +0 -0
  71. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/deprecated/BackOffPOS.py +0 -0
  72. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/deprecated/BlackNix.py +0 -0
  73. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/deprecated/BuerLoader.py +0 -0
  74. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/deprecated/ChChes.py +0 -0
  75. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/deprecated/Emotet.py +0 -0
  76. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/deprecated/Enfal.py +0 -0
  77. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/deprecated/EvilGrab.py +0 -0
  78. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/deprecated/Greame.py +0 -0
  79. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/deprecated/Hancitor.py +0 -0
  80. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/deprecated/HttpBrowser.py +0 -0
  81. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/deprecated/JavaDropper.py +0 -0
  82. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/deprecated/Nymaim.py +0 -0
  83. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/deprecated/Pandora.py +0 -0
  84. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/deprecated/PoisonIvy.py +0 -0
  85. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/deprecated/PredatorPain.py +0 -0
  86. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/deprecated/Punisher.py +0 -0
  87. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/deprecated/RCSession.py +0 -0
  88. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/deprecated/REvil.py +0 -0
  89. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/deprecated/RedLeaf.py +0 -0
  90. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/deprecated/Retefe.py +0 -0
  91. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/deprecated/Rozena.py +0 -0
  92. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/deprecated/SmallNet.py +0 -0
  93. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/deprecated/TSCookie.py +0 -0
  94. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/deprecated/TrickBot.py +0 -0
  95. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/deprecated/UrsnifV3.py +0 -0
  96. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/deprecated/_ShadowTech.py +0 -0
  97. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/deprecated/_VirusRat.py +0 -0
  98. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/deprecated/_jRat.py +0 -0
  99. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/deprecated/unrecom.py +0 -0
  100. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/deprecated/xRAT.py +0 -0
  101. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/malduck/LICENSE +0 -0
  102. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/malduck/README.md +0 -0
  103. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/malduck/__init__.py +0 -0
  104. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/malduck/test_malduck.py +0 -0
  105. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/mwcp/README.md +0 -0
  106. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/mwcp/__init__.py +0 -0
  107. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/mwcp/test_mwcp.py +0 -0
  108. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/utils/__init__.py +0 -0
  109. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/utils/aplib.py +0 -0
  110. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/utils/blzpack.py +0 -0
  111. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/utils/blzpack_lib.so +0 -0
  112. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/utils/dotnet_utils.py +0 -0
  113. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/utils/lznt1.py +0 -0
  114. {cape_parsers-0.1.47 → cape_parsers-0.1.49}/cape_parsers/utils/strings.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: CAPE-parsers
3
- Version: 0.1.47
3
+ Version: 0.1.49
4
4
  Summary: CAPE: Malware Configuration Extraction
5
5
  License: MIT
6
6
  Keywords: cape,parsers,malware,configuration
@@ -0,0 +1,213 @@
1
+ import base64
2
+ import yara
3
+ import pefile
4
+ import json
5
+ import struct
6
+ import re
7
+
8
+
9
+ RULE_SOURCE_KEY = """
10
+ rule Amadey_Key_String
11
+ {
12
+ meta:
13
+ author = "YungBinary"
14
+ description = "Find decryption key in Amadey."
15
+ strings:
16
+ $chunk_1 = {
17
+ 6A 20
18
+ 68 ?? ?? ?? ??
19
+ B9 ?? ?? ?? ??
20
+ E8 ?? ?? ?? ??
21
+ 68 ?? ?? ?? ??
22
+ E8 ?? ?? ?? ??
23
+ 59
24
+ C3
25
+ }
26
+ condition:
27
+ $chunk_1
28
+ }
29
+ """
30
+
31
+ RULE_SOURCE_ENCODED_STRINGS = """
32
+ rule Amadey_Encoded_Strings
33
+ {
34
+ meta:
35
+ author = "YungBinary"
36
+ description = "Find encoded strings in Amadey."
37
+ strings:
38
+ $chunk_1 = {
39
+ 6A ??
40
+ 68 ?? ?? ?? ??
41
+ B9 ?? ?? ?? ??
42
+ E8 ?? ?? ?? ??
43
+ 68 ?? ?? ?? ??
44
+ E8 ?? ?? ?? ??
45
+ 59
46
+ C3
47
+ }
48
+ condition:
49
+ $chunk_1
50
+ }
51
+ """
52
+
53
+
54
+ def contains_non_printable(byte_array):
55
+ for byte in byte_array:
56
+ if not chr(byte).isprintable():
57
+ return True
58
+ return False
59
+
60
+
61
+ def yara_scan_generator(raw_data, rule_source):
62
+ yara_rules = yara.compile(source=rule_source)
63
+ matches = yara_rules.match(data=raw_data)
64
+
65
+ for match in matches:
66
+ for block in match.strings:
67
+ for instance in block.instances:
68
+ yield instance.offset, block.identifier
69
+
70
+
71
+ def get_keys(pe, data):
72
+ image_base = pe.OPTIONAL_HEADER.ImageBase
73
+ keys = []
74
+ for offset, _ in yara_scan_generator(data, RULE_SOURCE_KEY):
75
+ try:
76
+ key_string_rva = struct.unpack('i', data[offset + 3 : offset + 7])[0]
77
+ key_string_dword_offset = pe.get_offset_from_rva(key_string_rva - image_base)
78
+ key_string = pe.get_string_from_data(key_string_dword_offset, data)
79
+
80
+ if b"=" not in key_string:
81
+ keys.append(key_string.decode())
82
+
83
+ if len(keys) == 2:
84
+ return keys
85
+
86
+ except Exception:
87
+ continue
88
+
89
+ return []
90
+
91
+
92
+ def get_encoded_strings(pe, data):
93
+ encoded_strings = []
94
+ image_base = pe.OPTIONAL_HEADER.ImageBase
95
+ for offset, _ in yara_scan_generator(data, RULE_SOURCE_ENCODED_STRINGS):
96
+
97
+ try:
98
+ encoded_string_size = data[offset + 1]
99
+ encoded_string_rva = struct.unpack('i', data[offset + 3 : offset + 7])[0]
100
+ encoded_string_dword_offset = pe.get_offset_from_rva(encoded_string_rva - image_base)
101
+ encoded_string = pe.get_string_from_data(encoded_string_dword_offset, data)
102
+
103
+ # Make sure the string matches length from operand
104
+ if encoded_string_size != len(encoded_string):
105
+ continue
106
+
107
+ encoded_strings.append(encoded_string.decode())
108
+
109
+ except Exception:
110
+ continue
111
+
112
+ return encoded_strings
113
+
114
+
115
+ def decode_amadey_string(key: str, encoded_str: str) -> bytes:
116
+ """
117
+ Decode Amadey encoded strings that look like base64
118
+ """
119
+ alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 "
120
+
121
+ decoded = ""
122
+ for i in range(len(encoded_str)):
123
+ if encoded_str[i] == "=":
124
+ decoded += "="
125
+ continue
126
+
127
+ index_1 = alphabet.index(encoded_str[i % len(encoded_str)])
128
+ index_2 = alphabet.index(key[i % len(key)])
129
+
130
+ index_result = (index_1 + (0x3F - index_2) + 0x3F) % 0x3F
131
+
132
+ decoded += alphabet[index_result]
133
+
134
+ decoded = base64.b64decode(decoded)
135
+
136
+ return decoded
137
+
138
+
139
+ def find_campaign_id(data):
140
+ pattern = br'\x00\x00\x00([0-9a-f]{6})\x00\x00'
141
+ matches = re.findall(pattern, data)
142
+ if matches:
143
+ return matches[0]
144
+
145
+
146
+ def extract_config(data):
147
+ pe = pefile.PE(data=data, fast_load=True)
148
+ # image_base = pe.OPTIONAL_HEADER.ImageBase
149
+
150
+ keys = get_keys(pe, data)
151
+ if not keys:
152
+ return {}
153
+
154
+ decode_key = keys[0]
155
+ rc4_key = keys[1]
156
+ encoded_strings = get_encoded_strings(pe, data)
157
+
158
+ decoded_strings = []
159
+ for encoded_string in encoded_strings:
160
+ try:
161
+ decoded_string = decode_amadey_string(decode_key, encoded_string)
162
+ if not decoded_string or contains_non_printable(decoded_string):
163
+ continue
164
+ decoded_strings.append(decoded_string.decode())
165
+ except Exception:
166
+ continue
167
+
168
+ if not decoded_strings:
169
+ return {}
170
+
171
+ decoded_strings = decoded_strings[:10]
172
+ final_config = {}
173
+ version = ""
174
+ install_dir = ""
175
+ install_file = ""
176
+ version_pattern = r"^\d+\.\d{1,2}$"
177
+ install_dir_pattern = r"^[0-9a-f]{10}$"
178
+
179
+ for i in range(len(decoded_strings)):
180
+ s = decoded_strings[i]
181
+ if s.endswith(".php"):
182
+ c2 = decoded_strings[i-1]
183
+ final_config.setdefault("CNCs", []).append(f"http://{c2}{s}")
184
+ elif re.match(version_pattern, s):
185
+ version = s
186
+ elif re.match(install_dir_pattern, s):
187
+ install_dir = s
188
+ elif s.endswith(".exe"):
189
+ install_file = s
190
+
191
+ if version:
192
+ final_config["version"] = version
193
+ if install_dir:
194
+ final_config.setdefault("raw", {})["install_dir"] = install_dir
195
+ if install_file:
196
+ final_config.setdefault("raw", {})["install_file"] = install_file
197
+
198
+ final_config["cryptokey"] = rc4_key
199
+ final_config["cryptokey_type"] = "RC4"
200
+
201
+ campaign_id = find_campaign_id(data)
202
+ if campaign_id:
203
+ final_config["campaign_id"] = campaign_id.decode()
204
+
205
+ return final_config
206
+
207
+
208
+
209
+ if __name__ == "__main__":
210
+ import sys
211
+
212
+ with open(sys.argv[1], "rb") as f:
213
+ print(json.dumps(extract_config(f.read()), indent=4))
@@ -360,11 +360,11 @@ class cobaltstrikeConfig:
360
360
  if parsed_setting == "Not Found" and quiet:
361
361
  continue
362
362
  if not isinstance(parsed_setting, list):
363
- log.debug("{: <{width}} - {val}".format(conf_name, width=COLUMN_WIDTH - 3, val=parsed_setting))
363
+ log.debug("{: <{width}} - {val}".format(conf_name, width=COLUMN_WIDTH - 3, val=parsed_setting)) # noqa: G001
364
364
  elif parsed_setting == []:
365
- log.debug("{: <{width}} - {val}".format(conf_name, width=COLUMN_WIDTH - 3, val="Empty"))
365
+ log.debug("{: <{width}} - {val}".format(conf_name, width=COLUMN_WIDTH - 3, val="Empty")) # noqa: G001
366
366
  else:
367
- log.debug("{: <{width}} - {val}".format(conf_name, width=COLUMN_WIDTH - 3, val=parsed_setting[0]))
367
+ log.debug("{: <{width}} - {val}".format(conf_name, width=COLUMN_WIDTH - 3, val=parsed_setting[0])) # noqa: G001
368
368
  for val in parsed_setting[1:]:
369
369
  log.debug(" " * COLUMN_WIDTH, end="")
370
370
  print(val)
@@ -23,7 +23,7 @@
23
23
  import re
24
24
  import struct
25
25
  import sys
26
-
26
+ from contextlib import suppress
27
27
  import pefile
28
28
  from Cryptodome.Cipher import DES3
29
29
  from Cryptodome.Util.Padding import unpad
@@ -128,7 +128,8 @@ def decoder(data):
128
128
  else:
129
129
  x = bytearray(img)
130
130
 
131
- url_re = rb"https?:\/\/[a-zA-Z0-9\/\.:?\-_]+"
131
+ # ToDo add \.php
132
+ url_re = rb"https?:\/\/[a-zA-Z0-9\/\.:?\-_]+\.php"
132
133
  urls = re.findall(url_re, x)
133
134
  if not urls:
134
135
  for i in range(len(x)):
@@ -144,21 +145,23 @@ def decoder(data):
144
145
  confs = find_conf(img)
145
146
  if iv and iv not in (b"", -1) and confs != []:
146
147
  for conf in confs:
147
- try:
148
+ with suppress(ValueError):
148
149
  dec = DES3.new(key, DES3.MODE_CBC, iv)
149
150
  temp = dec.decrypt(conf)
150
151
  temp = unpad(temp, 8)
151
- urls.append(b"http://" + temp)
152
- except ValueError:
153
- # wrong padding
154
- pass
152
+ if not temp.endswith(b".php"):
153
+ continue
154
+ urls.append("http://" + temp.decod())
155
155
  return urls
156
156
 
157
157
 
158
158
  def extract_config(filebuf):
159
159
 
160
160
  urls = decoder(filebuf)
161
- return {"CNCs": [url.decode() for url in urls]}
161
+ if urls:
162
+ return {"CNCs": urls}
163
+ else:
164
+ return {}
162
165
 
163
166
 
164
167
  if __name__ == "__main__":
@@ -318,7 +318,7 @@ def extract_config(data):
318
318
  decoded_c2 = chacha20_xor(encrypted_string, key, nonce, counter).split(b"\x00", 1)[0]
319
319
  if contains_non_printable(decoded_c2):
320
320
  break
321
- config.setdefault("CNCs", []).append(decoded_c2.decode())
321
+ config.setdefault("CNCs", []).append("https://" + decoded_c2.decode())
322
322
  encrypted_strings_offset = encrypted_strings_offset + step_size
323
323
  counter += 2
324
324
 
@@ -351,7 +351,7 @@ def extract_config(data):
351
351
  decrypted = chacha20_xor(c2_encrypted, key, nonce, counter)
352
352
  c2 = extract_c2_domain(decrypted)
353
353
  if c2 is not None and len(c2) > 10:
354
- config["CNCs"].append(c2.decode())
354
+ config["CNCs"].append("https://" + c2.decode())
355
355
  break
356
356
 
357
357
  except Exception:
@@ -384,7 +384,7 @@ def extract_config(data):
384
384
  decoded_c2 = xor_data(encoded_c2, xor_key)
385
385
 
386
386
  if not contains_non_printable(decoded_c2):
387
- config.setdefault("CNCs", []).append(decoded_c2.decode())
387
+ config.setdefault("CNCs", []).append("https://" + decoded_c2.decode())
388
388
  except Exception as e:
389
389
  print(e)
390
390
  continue
@@ -133,7 +133,7 @@ def extract_config(data):
133
133
  try:
134
134
  dotnet_file = dnfile.dnPE(data=data)
135
135
  except Exception as e:
136
- log.debug(f"Exception when attempting to parse .NET file: {e}")
136
+ log.debug("Exception when attempting to parse .NET file: %s", str(e))
137
137
  log.debug(traceback.format_exc())
138
138
 
139
139
  # ldstr, stsfld
@@ -165,7 +165,7 @@ def extract_config(data):
165
165
  else:
166
166
  user_strings[field_name] = string_index
167
167
  except Exception as e:
168
- log.debug(f"There was an exception parsing user strings: {e}")
168
+ log.debug("There was an exception parsing user strings: %s", str(e))
169
169
  log.debug(traceback.format_exc())
170
170
 
171
171
  if c2_type is None:
@@ -1,6 +1,7 @@
1
1
  import struct
2
2
  import pefile
3
3
  import yara
4
+ import ipaddress
4
5
  from contextlib import suppress
5
6
 
6
7
 
@@ -42,6 +43,13 @@ def yara_scan(raw_data):
42
43
  yield block.identifier, instance.offset
43
44
 
44
45
 
46
+ def _is_ip(ip):
47
+ try:
48
+ ipaddress.ip_address(ip)
49
+ return True
50
+ except Exception:
51
+ return False
52
+
45
53
  def xor_data(data, key):
46
54
  decoded = bytearray()
47
55
  for i in range(len(data)):
@@ -67,6 +75,8 @@ def parse_text(data):
67
75
  for line in lines:
68
76
  if line.startswith("http") and "://" in line:
69
77
  domain = line
78
+ elif _is_ip(line):
79
+ domain = line
70
80
  if line.startswith("/") and line[-4] == ".":
71
81
  uri = line
72
82
 
@@ -0,0 +1,93 @@
1
+ import json
2
+ import struct
3
+ from contextlib import suppress
4
+ from typing import Any, Dict, Tuple
5
+
6
+ import pefile
7
+ from Cryptodome.Cipher import AES
8
+ from Cryptodome.Util.Padding import unpad
9
+
10
+
11
+ def parse_blob(data: bytes):
12
+ """
13
+ Parse the blob according to the scheme:
14
+ - 32 bytes = AES key
15
+ - Next 16 bytes = IV
16
+ - Next 2 DWORDs (8 bytes total) = XOR to get cipher data size
17
+ - Remaining bytes = cipher data of that size
18
+ """
19
+ offset = 0
20
+ aes_key = data[offset:offset + 32]
21
+ offset += 32
22
+ iv = data[offset:offset + 16]
23
+ offset += 16
24
+ dword1, dword2 = struct.unpack_from("<II", data, offset)
25
+ cipher_size = dword1 ^ dword2
26
+ offset += 8
27
+ cipher_data = data[offset:offset + cipher_size]
28
+ return aes_key, iv, cipher_data
29
+
30
+
31
+ def decrypt(data: bytes) -> Tuple[bytes, bytes, bytes]:
32
+ aes_key, iv, cipher_data = parse_blob(data)
33
+ cipher = AES.new(aes_key, AES.MODE_CBC, iv)
34
+ plaintext_padded = cipher.decrypt(cipher_data)
35
+ return aes_key, iv, unpad(plaintext_padded, AES.block_size)
36
+
37
+
38
+ def extract_config(data: bytes) -> Dict[str, Any]:
39
+ cfg: Dict[str, Any] = {}
40
+ plaintext = ""
41
+ pe = pefile.PE(data=data, fast_load=True)
42
+ try:
43
+ data_section = [s for s in pe.sections if s.Name.find(b".data") != -1][0]
44
+ except IndexError:
45
+ return cfg
46
+
47
+ if not data_section:
48
+ return cfg
49
+
50
+ data = data_section.get_data()
51
+ block_size = 4096
52
+ zeros = b"\x00" * block_size
53
+ offset = data.find(zeros)
54
+ if offset == -1:
55
+ return cfg
56
+
57
+ while offset > 0:
58
+ with suppress(Exception):
59
+ aes_key, iv, plaintext = decrypt(data[offset : offset + block_size])
60
+ if plaintext and b"conf" in plaintext:
61
+ break
62
+
63
+ offset -= 1
64
+
65
+ if plaintext:
66
+ parsed = json.loads(plaintext.decode("utf-8", errors="ignore").rstrip("\x00"))
67
+ conf = parsed.get("conf", {})
68
+ build = parsed.get("build", {})
69
+ if conf:
70
+ cfg = {
71
+ "CNCs": conf.get("hosts"),
72
+ "user_agent": conf.get("useragents"),
73
+ "version": build.get("ver"),
74
+ "build": build.get("build_id"),
75
+ "cryptokey": aes_key.hex(),
76
+ "cryptokey_type": "AES",
77
+ "raw": {
78
+ "iv": iv.hex(),
79
+ "anti_vm": conf.get("anti_vm"),
80
+ "anti_dbg": conf.get("anti_dbg"),
81
+ "self_del": conf.get("self_del"),
82
+ "run_delay": conf.get("run_delay"),
83
+ }
84
+ }
85
+
86
+ return cfg
87
+
88
+
89
+ if __name__ == "__main__":
90
+ import sys
91
+
92
+ with open(sys.argv[1], "rb") as f:
93
+ print(extract_config(f.read()))
@@ -50,7 +50,7 @@ def extract_key_data(data, pe, key_match):
50
50
  # Read arbitrary number of byes from key offset and split on null bytes to extract key
51
51
  key = data[key_offset : key_offset + 0x40].split(b"\x00")[0]
52
52
  except Exception as e:
53
- log.debug(f"There was an exception extracting the key: {e}")
53
+ log.debug("There was an exception extracting the key: %s", str(e))
54
54
  log.debug(traceback.format_exc())
55
55
  return False
56
56
  return key
@@ -70,7 +70,7 @@ def extract_config_data(data, pe, config_match):
70
70
  )
71
71
  campaign_id_ct = data[campaign_id_offset : campaign_id_offset + 0x10]
72
72
  except Exception as e:
73
- log.debug(f"There was an exception extracting the campaign id: {e}")
73
+ log.debug("There was an exception extracting the campaign id: %s", str(e))
74
74
  log.debug(traceback.format_exc())
75
75
  return False, False, False
76
76
 
@@ -84,7 +84,7 @@ def extract_config_data(data, pe, config_match):
84
84
  )
85
85
  botnet_id_ct = data[botnet_id_offset : botnet_id_offset + 0x10]
86
86
  except Exception as e:
87
- log.debug(f"There was an exception extracting the botnet id: {e}")
87
+ log.debug("There was an exception extracting the botnet id: %s", str(e))
88
88
  log.debug(traceback.format_exc())
89
89
  return False, False, False
90
90
 
@@ -99,7 +99,7 @@ def extract_config_data(data, pe, config_match):
99
99
  c2s_offset = pe.get_offset_from_rva(c2s_rva + int.from_bytes(config_match.group("c2s"), byteorder="little"))
100
100
  c2s_ct = data[c2s_offset : c2s_offset + 0x400]
101
101
  except Exception as e:
102
- log.debug(f"There was an exception extracting the C2s: {e}")
102
+ log.debug("There was an exception extracting the C2s: %s", str(e))
103
103
  log.debug(traceback.format_exc())
104
104
  return False, False, False
105
105
 
@@ -243,7 +243,7 @@ def extract_config(data):
243
243
  if c2s:
244
244
  cfg["CNCs"] = list(ARC4.new(key).decrypt(c2s).split(b"\x00")[0].decode().split(","))
245
245
  except Exception as e:
246
- log.error("This is broken: %s", str(e), exc_info=True)
246
+ log.exception("This is broken: %s", str(e))
247
247
 
248
248
  if not cfg:
249
249
  cfg = extract_2024(pe, data)
@@ -211,7 +211,7 @@ def extract_config(filebuf):
211
211
  config.setdefault("raw", {})[k] = v
212
212
 
213
213
  except Exception as e:
214
- logger.error(f"Caught an exception: {e}")
214
+ logger.error("Caught an exception: %s", str(e))
215
215
 
216
216
  return config
217
217
 
@@ -129,7 +129,7 @@ def load_malwareconfig_parsers() -> Tuple[bool, dict, ModuleType]:
129
129
  except ImportError:
130
130
  log.info("Missed RATDecoders -> poetry run pip install malwareconfig")
131
131
  except Exception as e:
132
- log.error(e, exc_info=True)
132
+ log.exception(e)
133
133
  return False, False, False
134
134
 
135
135
 
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "CAPE-parsers"
3
- version = "0.1.47"
3
+ version = "0.1.49"
4
4
  description = "CAPE: Malware Configuration Extraction"
5
5
  authors = ["Kevin O'Reilly <kev@capesandbox.com>", "doomedraven <doomedraven@capesandbox.com>"]
6
6
  license = "MIT"
@@ -27,7 +27,6 @@ unicorn = ">=2.1.1"
27
27
  rat-king-parser = ">=4.1.0"
28
28
 
29
29
  ruff = ">=0.7.2"
30
- # isort = ">=5.13.2"
31
30
 
32
31
  [tool.distutils.bdist_wheel]
33
32
  universal = false
@@ -36,8 +35,6 @@ universal = false
36
35
  maco = ["maco"]
37
36
 
38
37
  [tool.poetry.group.dev.dependencies]
39
- black = "^24.3.0"
40
- isort = "^5.10.1"
41
38
  pytest = "7.2.2"
42
39
  pytest-pretty = "1.1.0"
43
40
  pytest-cov = "3.0.0"
@@ -51,37 +48,11 @@ httpretty = "^1.1.4"
51
48
  func-timeout = "^4.3.5"
52
49
  pre-commit = "^2.19.0"
53
50
 
54
- # Doesnt work on github CI
55
- # [tool.poetry.requires-plugins]
56
- # poetry-plugin-export = ">=1.8"
57
-
58
- [tool.ruff]
59
- exclude = [
60
- "./analyzer/linux/dbus_next",
61
- ]
62
-
63
- [tool.black]
64
- line-length = 132
65
- include = "\\.py(_disabled)?$"
66
-
67
- [tool.isort]
68
- profile = "black"
69
- no_lines_before = ["FUTURE", "STDLIB"]
70
- line_length = 132
71
- supported_extensions = ["py", "py_disabled"]
72
-
73
- [tool.flake8]
74
- max-line-length = 132
75
- exclude = ".git,__pycache__,.cache,.venv"
76
-
77
51
  [tool.pytest.ini_options]
78
52
  pythonpath = ["."]
79
53
  testpaths = ["tests"]
80
54
  norecursedirs = "tests/zip_compound"
81
-
82
- [lint]
83
- select = ["E", "F"]
84
- ignore = ["E402","E501"]
55
+ asyncio_mode = "auto"
85
56
 
86
57
  [tool.poetry-dynamic-versioning]
87
58
  enable = false # Master switch to enable the plugin
@@ -89,3 +60,40 @@ enable = false # Master switch to enable the plugin
89
60
  [build-system]
90
61
  requires = ["poetry-core>=1.0.0", "poetry-dynamic-versioning"]
91
62
  build-backend = "poetry.core.masonry.api"
63
+
64
+ [tool.ruff]
65
+ line-length = 132
66
+
67
+ [tool.ruff.lint]
68
+ select = [
69
+ "F", # pyflakes
70
+ "E", # pycodestyle errors
71
+ "W", # pycodestyle warnings
72
+ # "I", # isort
73
+ # "N", # pep8-naming
74
+ "G", # flake8-logging-format
75
+ ]
76
+
77
+ ignore = [
78
+ "E501", # ignore due to conflict with formatter
79
+ "N818", # exceptions don't need the Error suffix
80
+ "E741", # allow ambiguous variable names
81
+ "E402",
82
+ "W605", # ToDo to fix - Invalid escape sequence
83
+ ]
84
+
85
+ fixable = ["ALL"]
86
+
87
+
88
+ [tool.ruff.format]
89
+ quote-style = "double"
90
+ indent-style = "space"
91
+ skip-magic-trailing-comma = false
92
+ line-ending = "auto"
93
+
94
+ [tool.ruff.lint.isort]
95
+ known-first-party = ["libqtile", "test"]
96
+ default-section = "third-party"
97
+
98
+ [tool.mypy]
99
+ warn_unused_configs = true
File without changes
File without changes