forked from RussianPanda95/Configuration_extractors
-
Notifications
You must be signed in to change notification settings - Fork 0
/
metastealer_string_decryptor.py
213 lines (172 loc) · 7.3 KB
/
metastealer_string_decryptor.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
# Author: RussianPanda
# Reference: https://n1ght-w0lf.github.io/tutorials/dotnet-string-decryptor/
# Tested on sample:
# 008f9352765d1b3360726363e3e179b527a566bc59acecea06bd16eb16b66c5d
import os
from pathlib import Path
from sys import argv
from tempfile import NamedTemporaryFile
from typing import BinaryIO, List, Optional
import clr
from maco.extractor import Extractor
from maco.model import ConnUsageEnum, ExtractorModel
# Check default location from package install
dn_lib_found = list(Path("/usr/lib").glob("**/dnlib.dll"))
DNLIB_PACKAGE_PATH = str(dn_lib_found[0]) if dn_lib_found else "dnlib"
DNLIB_PATH = os.environ.get("DNLIB_PATH", DNLIB_PACKAGE_PATH)
if not os.path.exists(DNLIB_PATH):
raise FileNotFoundError(DNLIB_PATH)
clr.AddReference(DNLIB_PATH)
import dnlib
from dnlib.DotNet import ModuleDefMD
from dnlib.DotNet.Emit import OpCodes
from System import Convert, Int32, String
from System.Reflection import Assembly, BindingFlags
decryption_signature = [{"Parameters": ["System.Int32"], "ReturnType": "System.String"}]
def load_net_module(file_path):
return ModuleDefMD.Load(file_path)
def load_net_assembly(file_path):
return Assembly.LoadFile(file_path)
def find_decryption_methods(assembly):
suspected_methods = []
flags = BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic
for module_type in assembly.GetTypes():
for method in module_type.GetMethods(flags):
for sig in decryption_signature:
if method_matches_signature(method, sig):
suspected_methods.append(method)
return suspected_methods
def method_matches_signature(method, signature):
parameters = method.GetParameters()
return (
len(parameters) == len(signature["Parameters"])
and method.ReturnType.FullName == signature["ReturnType"]
and all(parameters[i].ParameterType.FullName == signature["Parameters"][i] for i in range(len(parameters)))
)
def get_operand_value(insn, param_type):
if "Int32" in param_type and insn.IsLdcI4():
return Int32(insn.GetLdcI4Value())
elif "String" in param_type and insn.OpCode == OpCodes.Ldstr:
return insn.Operand
return None
def invoke_methods(module, suspected_methods):
results = {}
for method in suspected_methods:
for module_type in module.Types:
if not module_type.HasMethods:
continue
for m in module_type.Methods:
if m.HasBody:
for insnIdx, insn in enumerate(m.Body.Instructions):
if insn.OpCode == OpCodes.Call:
called_method_name = str(insn.Operand)
if method.Name in called_method_name:
params = extract_parameters(m.Body.Instructions, insnIdx, method)
if len(params) == len(method.GetParameters()):
try:
result = invoke_method_safely(method, params)
if result is not None:
location = f"{module_type.FullName}.{m.Name}"
results[location] = result
except Exception as e:
None
return results
def invoke_method_safely(method, params):
try:
if method.Name == "Get" and isinstance(params[0], int):
# Adjust the range as necessary
if 0 <= params[0] < 100:
return method.Invoke(None, params)
else:
return method.Invoke(None, params)
except System.ArgumentOutOfRangeException:
# Silently handle ArgumentOutOfRangeException for method Get
if method.Name == "Get":
return None
except Exception as e:
print(f"Error invoking method {method.Name}: {e}")
return None
def extract_parameters(instructions, insn_idx, method):
params = []
num_params = len(method.GetParameters())
if insn_idx < num_params:
return []
for i, param_type in enumerate(method.GetParameters()):
operand = get_operand_value(instructions[insn_idx - 1], str(param_type.ParameterType))
if operand is not None:
params.append(operand)
return list(reversed(params))
def invoke_and_process(method, params):
try:
result = method.Invoke(None, params)
if method.ReturnType.FullName == "System.Object" and result is not None:
return Convert.ToString(result)
return result
except Exception as e:
print(f"Error invoking method {method.Name}: {e}")
return None
class MetaStealer(Extractor):
family = "MetaStealer"
author = "@RussianPanda"
last_modified = "2024-02-02"
sharing: str = "TLP:CLEAR"
reference: str = "https://russianpanda.com/2023/11/20/MetaStealer-Redline%27s-Doppelganger/"
yara_rule: str = """
import "pe"
rule MetaStealer_core_payload {
meta:
author = "RussianPanda"
decription = "Detects MetaStealer Core Payload"
date = "12/29/2023"
strings:
$s1 = "FileScannerRule"
$s2 = "TreeObject"
$s3 = "Schema"
$s4 = "StringDecrypt"
$s5 = "AccountDetails"
condition:
4 of ($s*)
and pe.imports("mscoree.dll")
}
"""
def run(self, stream: BinaryIO, matches: List = []) -> Optional[ExtractorModel]:
with NamedTemporaryFile() as file:
file.write(stream.read())
file.flush()
file_path = file.name
module = load_net_module(file_path)
assembly = load_net_assembly(file_path)
suspected_methods = find_decryption_methods(assembly)
results = invoke_methods(module, suspected_methods)
C2 = None
Build_ID = None
for location, decrypted_string in results.items():
self.logger.info(f"Decryption Location: {location}, Decrypted String: {decrypted_string}")
if location == "Program.ReadLine":
C2 = decrypted_string
elif location == "Schema13.TreeObject25":
Build_ID = decrypted_string
cfg = None
if C2 or Build_ID:
cfg = ExtractorModel(family=self.family)
else:
# Nothing found
return
# Print the values of C2 and Build ID if they are found
if C2:
self.logger.info("-----------------------------------------------------------------------------------")
self.logger.info(f"C2: {C2}")
cfg.http.append(cfg.Http(uri=C2, usage=ConnUsageEnum.c2))
if Build_ID:
self.logger.info(f"Build ID: {Build_ID}")
cfg.other["Build ID"] = Build_ID
return cfg
if __name__ == "__main__":
parser = MetaStealer()
file_path = argv[1]
with open(file_path, "rb") as f:
result = parser.run(f)
if result:
print(result.model_dump_json(indent=2, exclude_none=True, exclude_defaults=True))
else:
print("No configuration extracted")