Dotnet String Decryptor
Welcome back! This is a short blog post about reverse engineering dotnet malware.
When working with dotnet malware samples I always come around samples with obfuscated strings which makes analysis harder.
My go to way to handle this situation was to identify the string decryption routine (through static/dynamic analysis) then use de4dot
to decrypt the strings.
But sometimes you don’t want to go through every sample and find the decryption routine or you need to automate this process for a collection of different samples.
While looking around for a solution I found this cool blog, so I will be building on it to write a generic dotnet string decryptor which will hopefully make life a bit easier.
We will be working on an obfuscated sample of DCRat to test our script. c6244c8e4e4cdecd641017d52d344b1db6a23d05fd6a8ad338c8f4f77481f483
Writing the deobfuscation script
Step 1 : Importing libs and loading the .NET file
We first need to install pythonnet
which allows CLR namespaces to be treated essentially as python packages.
pip install pythonnet
Then we can import the required reflection modules which we will use later to get and invoke decryption methods.
import clr
from System.Reflection import Assembly, BindingFlags, MethodInfo
We also need to add a reference to dnlib.dll
which we will use to parse the .NET assemblies and modules.
clr.AddReference("./dnlib")
import dnlib
from dnlib.DotNet import ModuleDef, ModuleDefMD
from dnlib.DotNet.Emit import OpCodes
from dnlib.DotNet.Writer import ModuleWriterOptions
Now we can load our .NET file.
file_module = ModuleDefMD.Load(file_path)
file_assembly = Assembly.LoadFile(file_path)
Step 2 : Finding suspected decryption methods
Before we get any further we need to define the signatures of the suspected methods that are used for string decryption.
A method signature consists of the type of its parameters and its return type.
Below is the string decryption method in the sample we are working on:
I also found some wrapper methods that call the decryption method and they had a different signature.
So we can define our suspected method signatures as follows:
DECRYPTION_METHOD_SIGNATURES = [
{
"Parameters": ["System.Int32"],
"ReturnType": "System.String"
},
{
"Parameters": ["System.Int32"],
"ReturnType": "System.Object"
},
]
Of course there could be other methods with similar signatures which are not related to string decryption, but invoking them shouldn’t affect the end result (and you better run the script in a sandboxed environment).
Next we use the reflection modules to loop through the methods of each Type (classes, interfaces, …) and find suspected methods based on the list of signatures we defined above.
# Search for static, public and non public members
eFlags = BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic
for module_type in file_assembly.GetTypes():
for method in module_type.GetMethods(eFlags):
If we find a suspected method we need to store its corresponding signature and MethodInfo object which we will use later to invoke that method.
# Check if the current method has a suspected signature
for sig in StringDecryptor.DECRYPTION_METHOD_SIGNATURES:
# Check number of parameters and return type
parameters = method.GetParameters()
if ((len(parameters) == len(sig["Parameters"])) and
(method.ReturnType.FullName == sig["ReturnType"])):
# Check parameters types
param_types_match = True
for i in range(len(parameters)):
if parameters[i].ParameterType.FullName != sig["Parameters"][i]:
param_types_match = False
break
if param_types_match:
# Store the signature and MethodInfo object of the current method
method_name = f"{method.DeclaringType.FullName}::{method.Name}"
suspected_methods[method_name] = (sig, method)
Step 3 : Finding references to suspected methods
The next step is to find references to the suspected methods so we can get the required parameters.
To do this we can use dnlib modules to loop through the CIL instructions of each method and find calls to these methods.
for module_type in file_module.Types:
if not module_type.HasMethods:
continue
for method in module_type.Methods:
if not method.HasBody:
continue
# Loop through method instructions
for insnIdx, insn in enumerate(method.Body.Instructions):
# Find Call instructions
if insn.OpCode == OpCodes.Call:
for s_method_name, (s_method_sig, s_method_info) in suspected_methods.items():
# Check if the callee is one of the suspected methods
if str(s_method_name) in str(insn.Operand):
If we find a reference call, we need to get the required parameters (note that they are pushed to the stack in reverse order).
# Get method parameters in reverse order
params = []
for i in range(len(s_method_sig["Parameters"])):
operand = GetOperandValue(
method.Body.Instructions[insnIdx - i - 1],
s_method_sig["Parameters"][-i - 1])
if operand is not None:
params.append(operand)
# Check if we got all the parameters
if len(params) == len(s_method_sig["Parameters"]):
Next we can invoke suspected methods to get the decrypted strings
# Invoke suspected method
try:
result = str(s_method_info.Invoke(None, params[::-1]))
except Exception as e:
continue
Step 4 : Patching
If the method invoke succeeded we can safely patch the method parameters with NOPs and patch the method call itself with the decrypted string.
# Patch suspected method parameters with NOPs
for i in range(len(s_method_sig["Parameters"])):
method.Body.Instructions[insnIdx - i - 1].OpCode = OpCodes.Nop
# Patch suspected method call with the result string
method.Body.Instructions[insnIdx].OpCode = OpCodes.Ldstr
method.Body.Instructions[insnIdx].Operand = result
decrypted_strings.append(result)
Step 5 : Saving
Finally we can save the deobfuscated file to disk.
# Add writer options to ignore dnlib errors
options = ModuleWriterOptions(file_module)
options.Logger = dnlib.DotNet.DummyLogger.NoThrowInstance
# Write cleaned module content
file_module.Write("out.bin", options)
Testing and final notes
Let’s run the script on the sample we have and see the results.
Perfect, now it’s much easier to work on the sample and analyze its functionalities.
A little something before we wrap up, you can check if a PE is a dotnet file by checking the existence of the IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR
data directory (at index 14).
dotnet_dir = pefile.DIRECTORY_ENTRY['IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR'] # COM descriptor table index
if pe.OPTIONAL_HEADER.DATA_DIRECTORY[dotnet_dir].VirtualAddress == 0:
sys.exit("[-] File is not .NET")
The full code can be found here.
Until next time, cheers!