CAPE-parsers 0.1.54__tar.gz → 0.1.56__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.54 → cape_parsers-0.1.56}/PKG-INFO +1 -1
  2. cape_parsers-0.1.56/cape_parsers/CAPE/community/Amatera.py +186 -0
  3. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/CAPE/community/KoiLoader.py +3 -2
  4. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/CAPE/community/Lumma.py +5 -10
  5. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/CAPE/core/NitroBunnyDownloader.py +24 -7
  6. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/CAPE/core/Rhadamanthys.py +65 -61
  7. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/pyproject.toml +1 -1
  8. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/LICENSE +0 -0
  9. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/README.md +0 -0
  10. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/CAPE/__init__.py +0 -0
  11. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/CAPE/community/AgentTesla.py +0 -0
  12. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/CAPE/community/Amadey.py +0 -0
  13. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/CAPE/community/Arkei.py +0 -0
  14. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/CAPE/community/AsyncRAT.py +0 -0
  15. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/CAPE/community/AuroraStealer.py +0 -0
  16. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/CAPE/community/Carbanak.py +0 -0
  17. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/CAPE/community/CobaltStrikeBeacon.py +0 -0
  18. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/CAPE/community/CobaltStrikeStager.py +0 -0
  19. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/CAPE/community/DCRat.py +0 -0
  20. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/CAPE/community/Fareit.py +0 -0
  21. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/CAPE/community/LokiBot.py +0 -0
  22. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/CAPE/community/MonsterV2.py +0 -0
  23. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/CAPE/community/MyKings.py +0 -0
  24. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/CAPE/community/NanoCore.py +0 -0
  25. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/CAPE/community/Nighthawk.py +0 -0
  26. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/CAPE/community/Njrat.py +0 -0
  27. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/CAPE/community/PhemedroneStealer.py +0 -0
  28. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/CAPE/community/QuasarRAT.py +0 -0
  29. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/CAPE/community/README.md +0 -0
  30. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/CAPE/community/Snake.py +0 -0
  31. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/CAPE/community/SparkRAT.py +0 -0
  32. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/CAPE/community/Stealc.py +0 -0
  33. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/CAPE/community/VenomRAT.py +0 -0
  34. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/CAPE/community/WinosStager.py +0 -0
  35. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/CAPE/community/XWorm.py +0 -0
  36. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/CAPE/community/XenoRAT.py +0 -0
  37. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/CAPE/community/__init__.py +0 -0
  38. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/CAPE/core/AdaptixBeacon.py +0 -0
  39. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/CAPE/core/AuraStealer.py +0 -0
  40. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/CAPE/core/Azorult.py +0 -0
  41. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/CAPE/core/BitPaymer.py +0 -0
  42. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/CAPE/core/BlackDropper.py +0 -0
  43. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/CAPE/core/Blister.py +0 -0
  44. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/CAPE/core/BruteRatel.py +0 -0
  45. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/CAPE/core/BumbleBee.py +0 -0
  46. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/CAPE/core/DarkGate.py +0 -0
  47. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/CAPE/core/DoppelPaymer.py +0 -0
  48. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/CAPE/core/DridexLoader.py +0 -0
  49. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/CAPE/core/Formbook.py +0 -0
  50. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/CAPE/core/GuLoader.py +0 -0
  51. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/CAPE/core/IcedID.py +0 -0
  52. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/CAPE/core/IcedIDLoader.py +0 -0
  53. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/CAPE/core/Latrodectus.py +0 -0
  54. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/CAPE/core/Oyster.py +0 -0
  55. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/CAPE/core/PikaBot.py +0 -0
  56. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/CAPE/core/PlugX.py +0 -0
  57. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/CAPE/core/QakBot.py +0 -0
  58. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/CAPE/core/Quickbind.py +0 -0
  59. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/CAPE/core/README.md +0 -0
  60. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/CAPE/core/RedLine.py +0 -0
  61. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/CAPE/core/Remcos.py +0 -0
  62. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/CAPE/core/SmokeLoader.py +0 -0
  63. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/CAPE/core/Socks5Systemz.py +0 -0
  64. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/CAPE/core/SquirrelWaffle.py +0 -0
  65. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/CAPE/core/Strrat.py +0 -0
  66. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/CAPE/core/WarzoneRAT.py +0 -0
  67. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/CAPE/core/Zloader.py +0 -0
  68. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/CAPE/core/__init__.py +0 -0
  69. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/CAPE/core/test_cape.py +0 -0
  70. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/RATDecoders/README.md +0 -0
  71. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/RATDecoders/__init__.py +0 -0
  72. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/RATDecoders/test_rats.py +0 -0
  73. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/__init__.py +0 -0
  74. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/deprecated/BackOffLoader.py +0 -0
  75. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/deprecated/BackOffPOS.py +0 -0
  76. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/deprecated/BlackNix.py +0 -0
  77. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/deprecated/BuerLoader.py +0 -0
  78. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/deprecated/ChChes.py +0 -0
  79. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/deprecated/Emotet.py +0 -0
  80. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/deprecated/Enfal.py +0 -0
  81. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/deprecated/EvilGrab.py +0 -0
  82. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/deprecated/Greame.py +0 -0
  83. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/deprecated/Hancitor.py +0 -0
  84. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/deprecated/HttpBrowser.py +0 -0
  85. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/deprecated/JavaDropper.py +0 -0
  86. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/deprecated/Nymaim.py +0 -0
  87. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/deprecated/Pandora.py +0 -0
  88. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/deprecated/PoisonIvy.py +0 -0
  89. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/deprecated/PredatorPain.py +0 -0
  90. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/deprecated/Punisher.py +0 -0
  91. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/deprecated/RCSession.py +0 -0
  92. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/deprecated/REvil.py +0 -0
  93. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/deprecated/RedLeaf.py +0 -0
  94. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/deprecated/Retefe.py +0 -0
  95. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/deprecated/Rozena.py +0 -0
  96. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/deprecated/SmallNet.py +0 -0
  97. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/deprecated/TSCookie.py +0 -0
  98. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/deprecated/TrickBot.py +0 -0
  99. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/deprecated/UrsnifV3.py +0 -0
  100. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/deprecated/_ShadowTech.py +0 -0
  101. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/deprecated/_VirusRat.py +0 -0
  102. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/deprecated/_jRat.py +0 -0
  103. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/deprecated/unrecom.py +0 -0
  104. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/deprecated/xRAT.py +0 -0
  105. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/malduck/LICENSE +0 -0
  106. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/malduck/README.md +0 -0
  107. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/malduck/__init__.py +0 -0
  108. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/malduck/test_malduck.py +0 -0
  109. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/mwcp/README.md +0 -0
  110. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/mwcp/__init__.py +0 -0
  111. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/mwcp/test_mwcp.py +0 -0
  112. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/utils/__init__.py +0 -0
  113. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/utils/aplib.py +0 -0
  114. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/utils/blzpack.py +0 -0
  115. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/utils/blzpack_lib.so +0 -0
  116. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/utils/dotnet_utils.py +0 -0
  117. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/cape_parsers/utils/lznt1.py +0 -0
  118. {cape_parsers-0.1.54 → cape_parsers-0.1.56}/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.54
3
+ Version: 0.1.56
4
4
  Summary: CAPE: Malware Configuration Extraction
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -0,0 +1,186 @@
1
+ import base64
2
+ import json
3
+ import pefile
4
+ import yara
5
+ import struct
6
+ import re
7
+ import ipaddress
8
+ from contextlib import suppress
9
+
10
+
11
+ DESCRIPTION = "Amatera Stealer parser"
12
+ AUTHOR = "YungBinary"
13
+
14
+ RULE_SOURCE = """
15
+ rule AmateraDecrypt
16
+ {
17
+ meta:
18
+ author = "YungBinary"
19
+ description = "Find Amatera XOR key"
20
+ strings:
21
+ $decrypt = {
22
+ A1 ?? ?? ?? ?? // mov eax, dword ptr ds:szXorKey ; "852149723"
23
+ 89 45 ?? // mov dword ptr [ebp+xor_key], eax
24
+ 8B 0D ?? ?? ?? ?? // mov ecx, dword ptr ds:szXorKey+4 ; "49723"
25
+ 89 4D ?? // mov dword ptr [ebp+xor_key+4], ecx
26
+ 66 8B 15 ?? ?? ?? ?? // mov dx, word ptr ds:szXorKey+8 ; "3"
27
+ 66 89 55 ?? // mov word ptr [ebp+xor_key+8], dx
28
+ 8D 45 ?? // lea eax, [ebp+xor_key]
29
+ 50 // push eax
30
+ E8 // call
31
+ }
32
+ condition:
33
+ uint16(0) == 0x5A4D and $decrypt
34
+ }
35
+ """
36
+
37
+
38
+ RULE_SOURCE_AES_KEY = """
39
+ rule AmateraAESKey
40
+ {
41
+ meta:
42
+ author = "YungBinary"
43
+ description = "Find Amatera AES key"
44
+ strings:
45
+ $aes_key_on_stack = {
46
+ 83 EC 2C // sub esp, 2Ch
47
+ C6 45 D4 ?? // mov byte ptr [ebp-2Ch], ??
48
+ C6 45 D5 ?? // mov byte ptr [ebp-2Bh], ??
49
+ C6 45 D6 ?? // mov byte ptr [ebp-2Ah], ??
50
+ C6 45 D7 ?? // mov byte ptr [ebp-29h], ??
51
+ C6 45 D8 ?? // mov byte ptr [ebp-28h], ??
52
+ C6 45 D9 ?? // mov byte ptr [ebp-27h], ??
53
+ C6 45 DA ?? // mov byte ptr [ebp-26h], ??
54
+ C6 45 DB ?? // mov byte ptr [ebp-25h], ??
55
+ C6 45 DC ?? // mov byte ptr [ebp-24h], ??
56
+ C6 45 DD ?? // mov byte ptr [ebp-23h], ??
57
+ C6 45 DE ?? // mov byte ptr [ebp-22h], ??
58
+ C6 45 DF ?? // mov byte ptr [ebp-21h], ??
59
+ C6 45 E0 ?? // mov byte ptr [ebp-20h], ??
60
+ C6 45 E1 ?? // mov byte ptr [ebp-1Fh], ??
61
+ C6 45 E2 ?? // mov byte ptr [ebp-1Eh], ??
62
+ C6 45 E3 ?? // mov byte ptr [ebp-1Dh], ??
63
+ C6 45 E4 ?? // mov byte ptr [ebp-1Ch], ??
64
+ C6 45 E5 ?? // mov byte ptr [ebp-1Bh], ??
65
+ C6 45 E6 ?? // mov byte ptr [ebp-1Ah], ??
66
+ C6 45 E7 ?? // mov byte ptr [ebp-19h], ??
67
+ C6 45 E8 ?? // mov byte ptr [ebp-18h], ??
68
+ C6 45 E9 ?? // mov byte ptr [ebp-17h], ??
69
+ C6 45 EA ?? // mov byte ptr [ebp-16h], ??
70
+ C6 45 EB ?? // mov byte ptr [ebp-15h], ??
71
+ C6 45 EC ?? // mov byte ptr [ebp-14h], ??
72
+ C6 45 ED ?? // mov byte ptr [ebp-13h], ??
73
+ C6 45 EE ?? // mov byte ptr [ebp-12h], ??
74
+ C6 45 EF ?? // mov byte ptr [ebp-11h], ??
75
+ C6 45 F0 ?? // mov byte ptr [ebp-10h], ??
76
+ C6 45 F1 ?? // mov byte ptr [ebp-0Fh], ??
77
+ C6 45 F2 ?? // mov byte ptr [ebp-0Eh], ??
78
+ C6 45 F3 ?? // mov byte ptr [ebp-0Dh], ??
79
+ C7 45 F4 10 00 00 00 // mov dword ptr [ebp-0Ch], 10h
80
+ }
81
+ condition:
82
+ uint16(0) == 0x5A4D and $aes_key_on_stack
83
+ }
84
+ """
85
+
86
+ DOMAIN_REGEX = r'^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$'
87
+
88
+
89
+ def yara_scan(raw_data: bytes, rule_source: str):
90
+ yara_rules = yara.compile(source=rule_source)
91
+ matches = yara_rules.match(data=raw_data)
92
+
93
+ for match in matches:
94
+ for block in match.strings:
95
+ for instance in block.instances:
96
+ return instance.offset
97
+
98
+
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
+ def xor_data(data, key):
109
+ decoded = bytearray()
110
+ for i in range(len(data)):
111
+ decoded.append(key[i % len(key)] ^ data[i])
112
+ return decoded
113
+
114
+
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
124
+
125
+
126
+ def is_valid_domain(data):
127
+ try:
128
+ if re.fullmatch(DOMAIN_REGEX, data.decode()):
129
+ return True
130
+ return False
131
+ except Exception:
132
+ return False
133
+
134
+
135
+ def extract_config(data):
136
+ """
137
+ Extract Amatera malware configuration.
138
+ """
139
+ config_dict = {}
140
+
141
+ with suppress(Exception):
142
+ pe = pefile.PE(data=data)
143
+ image_base = pe.OPTIONAL_HEADER.ImageBase
144
+
145
+ # Identify XOR key decryption routine and extract key
146
+ offset = yara_scan(data, RULE_SOURCE)
147
+ if not offset:
148
+ 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
+
152
+ # Extract AES 256 key
153
+ aes_key_offset = yara_scan(data, RULE_SOURCE_AES_KEY)
154
+ aes_key = bytearray()
155
+ if aes_key_offset:
156
+ aes_block = data[aes_key_offset : aes_key_offset + 131]
157
+ 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
177
+
178
+ return config_dict
179
+
180
+
181
+ if __name__ == "__main__":
182
+ import sys
183
+
184
+ with open(sys.argv[1], "rb") as f:
185
+ config_json = json.dumps(extract_config(f.read()), indent=4)
186
+ print(config_json)
@@ -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:
123
+ config["CNCs"] = cncs
123
124
 
124
125
  return config
125
126
 
@@ -5,7 +5,7 @@ import struct
5
5
  from contextlib import suppress
6
6
  import pefile
7
7
  import yara
8
- from Cryptodome.Cipher import ChaCha20
8
+
9
9
 
10
10
  RULE_SOURCE_BUILD_ID = """rule LummaBuildId
11
11
  {
@@ -142,7 +142,7 @@ def contains_non_printable(byte_array):
142
142
  return True
143
143
  return False
144
144
 
145
- """
145
+
146
146
  def mask32(x):
147
147
  return x & 0xFFFFFFFF
148
148
 
@@ -242,7 +242,7 @@ def chacha20_xor(message, key, nonce, counter):
242
242
  xor_key.append(message[i] ^ key_stream[i])
243
243
 
244
244
  return xor_key
245
- """
245
+
246
246
 
247
247
  def extract_c2_domain(data):
248
248
  pattern = rb"([\w-]+\.[\w]+)\x00"
@@ -315,9 +315,7 @@ def extract_config(data):
315
315
  counter = 2
316
316
  for i in range(12):
317
317
  encrypted_string = data[encrypted_strings_offset : encrypted_strings_offset + 40]
318
- chacha20_cipher = ChaCha20.new(key=key, nonce=nonce)
319
- chacha20_cipher.seek(counter)
320
- decoded_c2 = chacha20_cipher.decrypt(encrypted_string).split(b"\x00", 1)[0]
318
+ decoded_c2 = chacha20_xor(encrypted_string, key, nonce, counter).split(b"\x00", 1)[0]
321
319
  if contains_non_printable(decoded_c2):
322
320
  break
323
321
  config.setdefault("CNCs", []).append("https://" + decoded_c2.decode())
@@ -350,10 +348,7 @@ def extract_config(data):
350
348
  c2_encrypted = data[c2_dword_offset : c2_dword_offset + 0x80]
351
349
  counters = [0, 2, 4, 6, 8, 10, 12, 14, 16]
352
350
  for counter in counters:
353
- # decrypted = chacha20_xor(c2_encrypted, key, nonce, counter)
354
- chacha20_cipher = ChaCha20.new(key=key, nonce=nonce)
355
- chacha20_cipher.seek(counter)
356
- decrypted = chacha20_cipher.decrypt(c2_encrypted).split(b"\x00", 1)[0]
351
+ decrypted = chacha20_xor(c2_encrypted, key, nonce, counter)
357
352
  c2 = extract_c2_domain(decrypted)
358
353
  if c2 is not None and len(c2) > 10:
359
354
  config["CNCs"].append("https://" + c2.decode())
@@ -32,14 +32,15 @@ rule NitroBunnyDownloader
32
32
  cape_type = "NitroBunnyDownloader Payload"
33
33
  hash = "960e59200ec0a4b5fb3b44e6da763f5fec4092997975140797d4eec491de411b"
34
34
  strings:
35
- $config = {E8 [3] 00 41 B8 ?? ?? 00 00 48 8D 15 [3] 00 48 89 C1 48 89 ?? E8 [3] 00}
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}
36
37
  $string1 = "X-Amz-User-Agent:" wide
37
38
  $string2 = "Amz-Security-Flag:" wide
38
39
  $string3 = "/cart" wide
39
40
  $string4 = "Cookie: " wide
40
41
  $string5 = "wishlist" wide
41
42
  condition:
42
- uint16(0) == 0x5A4D and $config and 2 of ($string*)
43
+ uint16(0) == 0x5A4D and 1 of ($config*) and 2 of ($string*)
43
44
  }
44
45
  """
45
46
 
@@ -93,25 +94,41 @@ def extract_config(filebuf):
93
94
 
94
95
  cfg = {}
95
96
  config_code_offset = None
97
+ config_size_offset = None
98
+ config_offset = None
99
+ rva_offset = None
100
+
96
101
  for hit in yara_hit:
97
102
  if hit.rule != "NitroBunnyDownloader":
98
103
  continue
99
104
 
100
105
  for item in hit.strings:
101
106
  for instance in item.instances:
102
- if "$config" in item.identifier:
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":
103
114
  config_code_offset = instance.offset
115
+ config_size_offset = 14
116
+ config_offset= 8
117
+ rva_offset = 12
104
118
  break
105
119
 
120
+ if config_code_offset:
121
+ break
122
+
106
123
  if config_code_offset is None:
107
124
  return None
108
125
 
109
126
  try:
110
127
  pe = pefile.PE(data=filebuf, fast_load=True)
111
- config_length = pe.get_dword_from_offset(config_code_offset + 7)
112
- config_offset = pe.get_dword_from_offset(config_code_offset + 14)
113
- rva = pe.get_rva_from_offset(config_code_offset + 18)
114
- config_rva = rva + config_offset
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
115
132
  data = pe.get_data(config_rva, config_length)
116
133
  off = 0
117
134
  raw = cfg["raw"] = {}
@@ -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
@@ -240,76 +249,71 @@ def parse_compression_header(config: bytes):
240
249
  }
241
250
 
242
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
+
243
293
  def extract_config(data):
294
+ """
295
+ Extract Rhadamanthys malware configuration.
296
+ """
244
297
  config_dict = {}
298
+ # Extract very old variant
245
299
  magic = struct.unpack("I", data[:4])[0]
246
300
  if magic == 0x59485221:
247
301
  config_dict["CNCs"] = [data[24:].split(b"\0", 1)[0].decode()]
248
302
  return config_dict
249
- else:
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"
251
- nonce = b"\x5F\x14\xD7\x9C\xFC\xFC\x43\x9E\xC3\x40\x6B\xBA"
252
-
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')
265
- for string in extracted_strings:
266
- try:
267
- custom_b64_decoded = custom_b64decode(string, custom_alphabets[0])
268
-
269
- xor_key = chacha20_xor(custom_b64_decoded, key, nonce)
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
-
308
- except Exception:
309
- continue
310
303
 
304
+ # New variants, extract base64 strings
305
+ extracted_strings = extract_base64_strings(data, 100, 256)
306
+ if not extracted_strings:
311
307
  return config_dict
312
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
+
313
317
 
314
318
  if __name__ == "__main__":
315
319
  import sys
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "CAPE-parsers"
3
- version = "0.1.54"
3
+ version = "0.1.56"
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