CAPE-parsers 0.1.55__tar.gz → 0.1.57__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.55 → cape_parsers-0.1.57}/PKG-INFO +1 -1
  2. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/AgentTesla.py +7 -1
  3. cape_parsers-0.1.57/cape_parsers/CAPE/community/Amatera.py +186 -0
  4. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/Carbanak.py +7 -2
  5. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/Fareit.py +3 -3
  6. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/KoiLoader.py +1 -1
  7. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/NanoCore.py +11 -1
  8. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/Njrat.py +1 -1
  9. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/Snake.py +2 -2
  10. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/SparkRAT.py +5 -1
  11. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/WinosStager.py +6 -3
  12. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/AdaptixBeacon.py +13 -1
  13. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/Blister.py +4 -1
  14. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/DarkGate.py +30 -1
  15. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/IcedIDLoader.py +2 -1
  16. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/Latrodectus.py +2 -0
  17. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/NitroBunnyDownloader.py +33 -29
  18. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/Oyster.py +128 -21
  19. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/PikaBot.py +1 -2
  20. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/QakBot.py +7 -4
  21. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/Quickbind.py +3 -2
  22. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/pyproject.toml +1 -1
  23. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/LICENSE +0 -0
  24. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/README.md +0 -0
  25. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/__init__.py +0 -0
  26. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/Amadey.py +0 -0
  27. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/Arkei.py +0 -0
  28. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/AsyncRAT.py +0 -0
  29. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/AuroraStealer.py +0 -0
  30. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/CobaltStrikeBeacon.py +0 -0
  31. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/CobaltStrikeStager.py +0 -0
  32. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/DCRat.py +0 -0
  33. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/LokiBot.py +0 -0
  34. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/Lumma.py +0 -0
  35. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/MonsterV2.py +0 -0
  36. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/MyKings.py +0 -0
  37. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/Nighthawk.py +0 -0
  38. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/PhemedroneStealer.py +0 -0
  39. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/QuasarRAT.py +0 -0
  40. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/README.md +0 -0
  41. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/Stealc.py +0 -0
  42. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/VenomRAT.py +0 -0
  43. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/XWorm.py +0 -0
  44. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/XenoRAT.py +0 -0
  45. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/community/__init__.py +0 -0
  46. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/AuraStealer.py +0 -0
  47. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/Azorult.py +0 -0
  48. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/BitPaymer.py +0 -0
  49. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/BlackDropper.py +0 -0
  50. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/BruteRatel.py +0 -0
  51. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/BumbleBee.py +0 -0
  52. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/DoppelPaymer.py +0 -0
  53. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/DridexLoader.py +0 -0
  54. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/Formbook.py +0 -0
  55. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/GuLoader.py +0 -0
  56. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/IcedID.py +0 -0
  57. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/PlugX.py +0 -0
  58. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/README.md +0 -0
  59. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/RedLine.py +0 -0
  60. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/Remcos.py +0 -0
  61. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/Rhadamanthys.py +0 -0
  62. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/SmokeLoader.py +0 -0
  63. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/Socks5Systemz.py +0 -0
  64. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/SquirrelWaffle.py +0 -0
  65. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/Strrat.py +0 -0
  66. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/WarzoneRAT.py +0 -0
  67. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/Zloader.py +0 -0
  68. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/__init__.py +0 -0
  69. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/CAPE/core/test_cape.py +0 -0
  70. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/RATDecoders/README.md +0 -0
  71. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/RATDecoders/__init__.py +0 -0
  72. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/RATDecoders/test_rats.py +0 -0
  73. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/__init__.py +0 -0
  74. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/deprecated/BackOffLoader.py +0 -0
  75. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/deprecated/BackOffPOS.py +0 -0
  76. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/deprecated/BlackNix.py +0 -0
  77. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/deprecated/BuerLoader.py +0 -0
  78. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/deprecated/ChChes.py +0 -0
  79. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/deprecated/Emotet.py +0 -0
  80. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/deprecated/Enfal.py +0 -0
  81. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/deprecated/EvilGrab.py +0 -0
  82. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/deprecated/Greame.py +0 -0
  83. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/deprecated/Hancitor.py +0 -0
  84. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/deprecated/HttpBrowser.py +0 -0
  85. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/deprecated/JavaDropper.py +0 -0
  86. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/deprecated/Nymaim.py +0 -0
  87. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/deprecated/Pandora.py +0 -0
  88. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/deprecated/PoisonIvy.py +0 -0
  89. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/deprecated/PredatorPain.py +0 -0
  90. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/deprecated/Punisher.py +0 -0
  91. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/deprecated/RCSession.py +0 -0
  92. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/deprecated/REvil.py +0 -0
  93. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/deprecated/RedLeaf.py +0 -0
  94. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/deprecated/Retefe.py +0 -0
  95. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/deprecated/Rozena.py +0 -0
  96. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/deprecated/SmallNet.py +0 -0
  97. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/deprecated/TSCookie.py +0 -0
  98. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/deprecated/TrickBot.py +0 -0
  99. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/deprecated/UrsnifV3.py +0 -0
  100. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/deprecated/_ShadowTech.py +0 -0
  101. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/deprecated/_VirusRat.py +0 -0
  102. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/deprecated/_jRat.py +0 -0
  103. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/deprecated/unrecom.py +0 -0
  104. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/deprecated/xRAT.py +0 -0
  105. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/malduck/LICENSE +0 -0
  106. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/malduck/README.md +0 -0
  107. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/malduck/__init__.py +0 -0
  108. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/malduck/test_malduck.py +0 -0
  109. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/mwcp/README.md +0 -0
  110. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/mwcp/__init__.py +0 -0
  111. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/mwcp/test_mwcp.py +0 -0
  112. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/utils/__init__.py +0 -0
  113. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/utils/aplib.py +0 -0
  114. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/utils/blzpack.py +0 -0
  115. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/utils/blzpack_lib.so +0 -0
  116. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/utils/dotnet_utils.py +0 -0
  117. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/cape_parsers/utils/lznt1.py +0 -0
  118. {cape_parsers-0.1.55 → cape_parsers-0.1.57}/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.55
3
+ Version: 0.1.57
4
4
  Summary: CAPE: Malware Configuration Extraction
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -83,7 +83,13 @@ def extract_config(data: bytes):
83
83
  config["CNCs"] = lines[base + index + x]
84
84
  break
85
85
  if config or config_dict:
86
- return config.setdefault("raw", config_dict)
86
+ config.setdefault("raw", config_dict)
87
+
88
+ # If the data exfiltration is done through SMTP, then patch the extracted CNCs to include SMTP credentials
89
+ if config_dict.get("Protocol") == "SMTP":
90
+ config['CNCs'] = [f"smtp://{config_dict.get('Username')}:{config_dict.get('Password')}@{domain}:{config_dict.get('Port','587')}" for domain in config_dict.get('CNCs', [])]
91
+
92
+ return config
87
93
 
88
94
  if __name__ == "__main__":
89
95
  import sys
@@ -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)
@@ -18,7 +18,6 @@ import re
18
18
  from contextlib import suppress
19
19
 
20
20
  import pefile
21
-
22
21
  import yara
23
22
 
24
23
  log = logging.getLogger(__name__)
@@ -169,7 +168,13 @@ def extract_config(filebuf):
169
168
  c2_dec = decode_string(items[10], sbox).decode("utf8")
170
169
  if "|" in c2_dec:
171
170
  c2_dec = c2_dec.split("|")
172
- cfg["CNCs"] = c2_dec
171
+ for c2 in c2_dec:
172
+ # Assign the proper scheme based on the port
173
+ if c2.endswith(':443'):
174
+ c2 = f"https://{c2}"
175
+ else:
176
+ c2 = f"http://{c2}"
177
+ cfg.setdefault("CNCs", []).append(c2)
173
178
  if float(cfg["version"]) < 1.7:
174
179
  cfg["campaign"] = decode_string(items[276], sbox).decode("utf8")
175
180
  else:
@@ -55,11 +55,11 @@ def extract_config(memdump_path, read=False):
55
55
  if gate_url.match(url):
56
56
  config.setdefault("CNCs", []).append(url.decode())
57
57
  elif exe_url.match(url) or dll_url.match(url):
58
- artifacts_raw["downloads"].append(url.decode())
58
+ artifacts_raw.setdefault("downloads", []).append(url.decode())
59
59
  except Exception as e:
60
60
  print(e, sys.exc_info(), "PONY")
61
- config["CNCs"] = list(set(config["controllers"]))
62
- config.setdefault("raw", {})["downloads"] = list(set(artifacts_raw["downloads"]))
61
+ if "downloads" in artifacts_raw:
62
+ config.setdefault("raw", {})["downloads"] = list(set(artifacts_raw["downloads"]))
63
63
  return config
64
64
 
65
65
 
@@ -119,7 +119,7 @@ def extract_config(data):
119
119
  encoded_payload = remove_nulls(encoded_payload, encoded_payload_size)
120
120
  decoded_payload = xor_data(encoded_payload, xor_key)
121
121
  cncs = find_c2(decoded_payload)
122
- if cncs and list(filter(cncs, None)):
122
+ if cncs:
123
123
  config["CNCs"] = cncs
124
124
 
125
125
  return config
@@ -188,7 +188,17 @@ def extract_config(filebuf):
188
188
  cncs.append(config_dict["raw"]["BackupConnectionHost"])
189
189
  if config_dict.get("raw", {}).get("ConnectionPort") and cncs:
190
190
  port = config_dict["raw"]["ConnectionPort"]
191
- config_dict["CNCs"] = [f"{cnc}:{port}" for cnc in cncs]
191
+ config_dict["CNCs"] = [f"tcp://{cnc}:{port}" for cnc in cncs]
192
+
193
+ if "Mutex" in config_dict.get("raw", {}):
194
+ config_dict["mutex"] = config_dict["raw"]["Mutex"]
195
+
196
+ if "Version" in config_dict.get("raw", {}):
197
+ config_dict["version"] = config_dict["raw"]["Version"]
198
+
199
+ if "DefaultGroup" in config_dict.get("raw", {}):
200
+ config_dict["campaign"] = config_dict["raw"]["DefaultGroup"]
201
+
192
202
  return config_dict
193
203
 
194
204
 
@@ -176,7 +176,7 @@ def extract_config(data):
176
176
  config = get_clean_config(config_dict)
177
177
  if config:
178
178
  if config.get("domain") and config.get("port"):
179
- conf["CNCs"] = [f"{config['domain']}:{config['port']}"]
179
+ conf["CNCs"] = [f"tcp://{config['domain']}:{config['port']}"]
180
180
 
181
181
  if config.get("campaign_id"):
182
182
  conf["campaign"] = config["campaign_id"]
@@ -38,7 +38,7 @@ def handle_plain(dotnet_file, c2_type, user_strings):
38
38
  if c2_type == "Telegram":
39
39
  token = dotnet_file.net.user_strings.get(user_strings_list[15]).value.__str__()
40
40
  chat_id = dotnet_file.net.user_strings.get(user_strings_list[16]).value.__str__()
41
- return {"raw": {"Type": "Telegram"}, "CNCs": f"https://api.telegram.org/bot{token}/sendMessage?chat_id={chat_id}"}
41
+ return {"raw": {"Type": "Telegram"}, "CNCs": [f"https://api.telegram.org/bot{token}/sendMessage?chat_id={chat_id}"]}
42
42
  elif c2_type == "SMTP":
43
43
  smtp_from = dotnet_file.net.user_strings.get(user_strings_list[7]).value.__str__()
44
44
  smtp_password = dotnet_file.net.user_strings.get(user_strings_list[8]).value.__str__()
@@ -106,7 +106,7 @@ def handle_encrypted(dotnet_file, data, c2_type, user_strings):
106
106
  if decrypted_strings:
107
107
  if c2_type == "Telegram":
108
108
  token, chat_id = decrypted_strings
109
- config_dict = {"raw": {"Type": "Telegram"}, "CNCs": f"https://api.telegram.org/bot{token}/sendMessage?chat_id={chat_id}"}
109
+ config_dict = {"raw": {"Type": "Telegram"}, "CNCs": [f"https://api.telegram.org/bot{token}/sendMessage?chat_id={chat_id}"]}
110
110
  elif c2_type == "SMTP":
111
111
  smtp_from, smtp_password, smtp_host, smtp_to, smtp_port = decrypted_strings
112
112
  config_dict = {
@@ -61,7 +61,11 @@ def extract_config(data):
61
61
  enc_data = f.read(data_len - 32)
62
62
  config = decrypt_config(enc_data, key, iv)
63
63
  if config:
64
- return {"raw": config}
64
+ output = {"raw": config}
65
+ output["CNCs"] = [
66
+ f"{'http' if not config['secure'] else 'https'}://{config['host']}:{config['port']}{config['path']}"
67
+ ]
68
+ return output
65
69
  except Exception as e:
66
70
  log.error("Configuration decryption failed: %s", e)
67
71
  return {}
@@ -3,9 +3,8 @@ Description: Winos 4.0 "OnlineModule" config parser
3
3
  Author: x.com/YungBinary
4
4
  """
5
5
 
6
- from contextlib import suppress
7
6
  import re
8
-
7
+ from contextlib import suppress
9
8
 
10
9
  CONFIG_KEY_MAP = {
11
10
  "dd": "execution_delay_seconds",
@@ -60,7 +59,11 @@ def extract_config(data: bytes) -> dict:
60
59
 
61
60
  final_config["CNCs"] = list(set(final_config["CNCs"]))
62
61
  # Extract campaign ID
63
- final_config["campaign_id"] = "default" if config_dict["fz"] == "\u9ed8\u8ba4" else config_dict["fz"]
62
+ final_config["campaign"] = "default" if config_dict["fz"] == "\u9ed8\u8ba4" else config_dict["fz"]
63
+
64
+ # Check if the version has been extracted
65
+ if "bb" in config_dict:
66
+ final_config["version"] = config_dict["bb"]
64
67
 
65
68
  # Map keys, e.g. dd -> execution_delay_seconds
66
69
  final_config["raw"] = {v: config_dict[k] for k, v in CONFIG_KEY_MAP.items() if k in config_dict}
@@ -59,7 +59,19 @@ def parse_http_config(rc4_key: bytes, data: bytes) -> dict:
59
59
  config["sleep_delay"] = read("<I")
60
60
  config["jitter_delay"] = read("<I")
61
61
 
62
- return {"raw": config}
62
+ output = {"raw": config}
63
+
64
+ # Map some fields to CAPE's output format, where possible
65
+ output['cryptokey'] = config['cryptokey']
66
+ output['cryptokey_type'] = config['cryptokey_type']
67
+ output['user_agent'] = config['user_agent']
68
+ output['CNCs'] = [f"{'https' if config['use_ssl'] else 'http'}://{server}:{ports[i]}{config['uri']}"
69
+ for i, server in enumerate(servers)]
70
+
71
+ # TODO: Does agent_type map to version or build?
72
+ # output['version'] = output['raw']['agent_type']
73
+
74
+ return output
63
75
 
64
76
 
65
77
  def extract_config(filebuf: bytes) -> dict:
@@ -15,8 +15,8 @@ from pathlib import Path
15
15
  from struct import pack, unpack
16
16
 
17
17
  import pefile
18
-
19
18
  import yara
19
+
20
20
  from cape_parsers.utils.lznt1 import lznt1
21
21
 
22
22
  log = logging.getLogger(__name__)
@@ -556,4 +556,7 @@ def extract_config(data):
556
556
  }
557
557
  }
558
558
 
559
+ config["cryptokey"] = config["raw"]["Rabbit key"]
560
+ config["cryptokey_type"] = "Rabbit"
561
+
559
562
  return config
@@ -92,11 +92,14 @@ def decode(data):
92
92
 
93
93
 
94
94
  def extract_config(data):
95
+ config_extracted = False
95
96
  with suppress(pefile.PEFormatError):
96
97
  pe = pefile.PE(data=data, fast_load=True)
97
98
  for section in pe.sections:
98
99
  if b"CODE" in section.Name:
99
- return decode(section.get_data())
100
+ config = decode(section.get_data())
101
+ config_extracted = True
102
+ break
100
103
 
101
104
  if b"1=Yes" in data or b"1=No" in data:
102
105
  config = {}
@@ -105,6 +108,32 @@ def extract_config(data):
105
108
  config["CNCs"] = [x for x in item[2:].decode("utf-8").split("|") if x.strip() != ""]
106
109
  else:
107
110
  config.update(parse_config(item, config_map_2))
111
+ config_extracted = True
112
+
113
+ if config_extracted:
114
+ # Map information to CAPE's format, if possible
115
+ if "c2_port" in config["raw"] and "CNCs" in config:
116
+ # We're making the assumpton the same port is used for all CNCs
117
+ config["CNCs"] = [f"{url}:{config['raw']['c2_port']}" for url in config["CNCs"]]
118
+
119
+ if "internal_mutex" in config["raw"]:
120
+ # Bring mutex to top level
121
+ config["mutex"] = config["raw"]["internal_mutex"]
122
+
123
+ if "crypto_key" in config["raw"]:
124
+ # Bring crypto_key to top level
125
+ config["cryptokey"] = config["raw"]["crypto_key"]
126
+
127
+ if "campaign_id" in config["raw"]:
128
+ # Bring campaign_id to top level
129
+ config["campaign"] = config["raw"]["campaign_id"]
130
+
131
+ if "xor_key" in config["raw"]:
132
+ # Bring xor_key to top level
133
+ config["cryptokey"] = config["raw"]["xor_key"]
134
+ config["cruptokey_type"] = "XOR"
135
+
136
+
108
137
  return config
109
138
 
110
139
  return ""
@@ -35,7 +35,8 @@ def extract_config(filebuf):
35
35
  if n > 32:
36
36
  break
37
37
  campaign, c2 = struct.unpack("I30s", bytes(dec))
38
- config["CNCs"] = c2.split(b"\00", 1)[0].decode()
38
+ c2 = c2.split(b"\00", 1)[0].decode()
39
+ config["CNCs"] = f"http://{c2}"
39
40
  config["campaign"] = campaign
40
41
  return config
41
42
 
@@ -154,6 +154,7 @@ def extract_config(filebuf):
154
154
  minor = int.from_bytes(data[18:19], byteorder="big")
155
155
  release = int.from_bytes(data[26:27], byteorder="big")
156
156
  version = f"{major}.{minor}.{release}"
157
+
157
158
  if "$key" in item.identifier:
158
159
  key = instance.matched_data[4::5]
159
160
  try:
@@ -161,6 +162,7 @@ def extract_config(filebuf):
161
162
  data_sections = [s for s in pe.sections if s.Name.find(b".data") != -1]
162
163
  if not data_sections:
163
164
  return
165
+
164
166
  data = data_sections[0].get_data()
165
167
  str_vals = []
166
168
  c2 = []
@@ -14,6 +14,7 @@
14
14
 
15
15
  import logging
16
16
  import struct
17
+ from typing import List
17
18
 
18
19
  import pefile
19
20
  import yara
@@ -32,8 +33,8 @@ rule NitroBunnyDownloader
32
33
  cape_type = "NitroBunnyDownloader Payload"
33
34
  hash = "960e59200ec0a4b5fb3b44e6da763f5fec4092997975140797d4eec491de411b"
34
35
  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}
36
+ $config1 = {E8 [3] 00 41 B8 ?? ?? 00 00 48 8D 15 [3] 00 48 89 C1 4? 89 ?? E8 [3] 00}
37
+ $config2 = {E8 [3] 00 48 8D 15 [3] 00 41 B8 ?? ?? 00 00 48 89 C1 4? 89 ?? E8 [3] 00}
37
38
  $string1 = "X-Amz-User-Agent:" wide
38
39
  $string2 = "Amz-Security-Flag:" wide
39
40
  $string3 = "/cart" wide
@@ -87,6 +88,20 @@ def read_string_list(data, off, count):
87
88
  return items, off
88
89
 
89
90
 
91
+ def make_endpoints(cncs: List[str], port: int, uris: List[str]) -> List[str]:
92
+ endpoints = []
93
+ schema = {80: "http", 443: "https"}.get(port, "tcp")
94
+ for cnc in cncs:
95
+ base_url = f"{schema}://{cnc}"
96
+ if port not in (80, 443):
97
+ base_url += f":{port}"
98
+
99
+ for uri in uris:
100
+ endpoints.append(f"{base_url}/{uri.lstrip('/')}")
101
+
102
+ return endpoints
103
+
104
+
90
105
  def extract_config(filebuf):
91
106
  yara_hit = yara_scan(filebuf)
92
107
  if not yara_hit:
@@ -98,24 +113,20 @@ def extract_config(filebuf):
98
113
  config_offset = None
99
114
  rva_offset = None
100
115
 
116
+ CONFIG_OFFSETS = {
117
+ "$config1": (7, 14, 18),
118
+ "$config2": (14, 8, 12),
119
+ }
120
+
101
121
  for hit in yara_hit:
102
122
  if hit.rule != "NitroBunnyDownloader":
103
123
  continue
104
124
 
105
125
  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
126
+ if item.identifier in CONFIG_OFFSETS and item.instances:
127
+ config_code_offset = item.instances[0].offset
128
+ config_size_offset, config_offset, rva_offset = CONFIG_OFFSETS[item.identifier]
129
+ break
119
130
 
120
131
  if config_code_offset:
121
132
  break
@@ -126,33 +137,26 @@ def extract_config(filebuf):
126
137
  try:
127
138
  pe = pefile.PE(data=filebuf, fast_load=True)
128
139
  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)
140
+ config_data_offset = pe.get_dword_from_offset(config_code_offset + config_offset)
130
141
  rva = pe.get_rva_from_offset(config_code_offset + rva_offset)
131
- config_rva = rva + config
142
+ config_rva = rva + config_data_offset
132
143
  data = pe.get_data(config_rva, config_length)
133
144
  off = 0
134
- raw = cfg["raw"] = {}
145
+ cfg["CNCs"] = []
135
146
  port, off = read_dword(data, off)
136
147
  num, off = read_dword(data, off)
137
148
  cncs, off = read_string_list(data, off, num)
138
149
  num, off = read_qword(data, off)
139
- raw["user_agent"], off = read_utf16le_string(data, off, num)
150
+ cfg["user_agent"], off = read_utf16le_string(data, off, num)
140
151
  num, off = read_dword(data, off)
152
+ raw = cfg["raw"] = {}
141
153
  raw["http_header_items"], off = read_string_list(data, off, num)
142
154
  num, off = read_dword(data, off)
143
- raw["uri_list"], off = read_string_list(data, off, num)
155
+ uris, off = read_string_list(data, off, num)
144
156
  raw["unknown_1"], off = read_dword(data, off)
145
157
  raw["unknown_2"], off = read_dword(data, off)
146
-
147
158
  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)
159
+ cfg["CNCs"] = make_endpoints(cncs, port, uris)
156
160
 
157
161
  except Exception as e:
158
162
  log.error("Error: %s", e)