.NET Dynamic Decryption and Countermeasures
Reflection is an important feature in .NET. Based on this feature, we often don’t need to fully analyze the encryption algorithm itself. We can simply use reflection APIs to complete decryption. This article will introduce dynamic decryption techniques in .NET and their corresponding countermeasures.
Introduction
de4dot has not been updated for a long time, so there are no ready-made tools for removing some obfuscators. If you want to remove them, you can almost only write your own tools.
There are generally two ways to decrypt .NET encryption: static decryption and dynamic decryption. Static decryption is faster, but writing a static decryption tool is very difficult, and the compatibility is not necessarily good (for example, most of the decryption in de4dot is static decryption, and when the obfuscator updates, de4dot must be updated).
Therefore, we need to use dynamic decryption. Dynamic decryption is not without drawbacks, such as the biggest disadvantage that the decrypted assembly must be able to run normally. But compared with the advantages of easy development, easy maintenance, good compatibility, etc., this disadvantage is not significant. This article will introduce some simple dynamic decryption and anti dynamic decryption.
Agile.NET’s String Encryption
Analysis
Let’s start with the simplest one first, and try Agile.NET’s string encryption. Let’s take a look at Agile.NET’s string encryption. This is a relatively simple dynamic decryption.
We open UnpackMe with dnSpy and see what the strings look like after they are encrypted.
As you can see, all the strings have turned into a garbled mess and are passed to a special method. This special method will convert the garbled string into a normal string, which is decryption.
Let’s click on this method and see how it decrypts the strings inside.
As you can see, this decryption is very simple, mainly using XOR. For the sake of explanation, I removed the proxy call first, otherwise it is difficult to see what the string decryptor method looks like. (We will explain Agile.NET’s proxy call later, so don’t worry about it here.)
Such a simple decryption can certainly be done with a static decryption tool, and it is not complicated, and the efficiency is higher. But this article explains dynamic decryption. So next, we will explain how to write a dynamic decryption tool.
Write a Decryption Tool
In the previous figure, we can see that Agile.NET’s string encryption is very simple, just encrypting the string itself, and then passing it to the string decryptor. At least at the C# level, but is it the same at the IL level? Are there no other obfuscations? Let’s switch dnSpy’s decompilation mode from C# to IL and take a look.
As you can see, this is really the same as shown in C#, where the string is pushed onto the stack and the string decryptor method is called. (This is how Agile.NET does it, but it doesn’t mean that other obfuscators do it like this. This needs to be analyzed specifically.) This makes it easier for us to write a decryption tool.
Here I would like to mention that, still for the sake of explanation, we write the simplest decryption tool, which cannot automatically recognize the runtime version of the target program, that is, it cannot automatically adapt to .NET 2.0 or 4.0 programs. If you want to write an adaptive one, you can read the de4dot code yourself. De4dot’s code is actually quite complicated, with too many design patterns, so I didn’t use the subprocess like de4dot does. I used a loader to load our decryption tool, and we manually select the loader. If you don’t understand this paragraph, it doesn’t matter. After writing more decryption tools, you will understand what this paragraph is talking about. Let’s continue.
We create a new project and select the same version of the target runtime as the decryption tool. For example, if our UnpackMe is .NET 4.5, we choose 4.5. (Actually, 4.0 can also be used, because the CLR version is the same, but I won’t go into too much detail here. You can study some technical details of .NET by yourself.)
Add code like the following to prepare the framework, initialize fields, and write the code in ExecuteImpl().
Let’s use dnSpy again to see what features the Agile.NET string decryptor method has. First, let’s locate this method.
We can see that the string decryptor method is in the <AgileDotNetRT> class with an empty namespace, and the signature of the string decryptor method itself should be string (string). This means that the string decryptor has only one parameter which is a string type, and returns a string. This way, we can use feature to locate the string decryptor.
We write the following code for the location. (Of course, it’s okay if it’s different from mine, as long as it can accurately locate it.) These codes are added to the ExecuteImpl() method.
1 | TypeDef agileDotNetRT; |
In order to traverse all methods in ModuleDefMD more quickly, we need an extension method. We write it like this:
1 | internal static class ModuleDefExtensions { |
The Method table mentioned in the above code is a table in the .NET metadata table stream that stores information about all methods in an assembly, which is very important. Each element in the Method table is continuous. Don’t ask me why, this is metadata knowledge, and it can’t be explained clearly for a while. Readers need to study it by themselves. Of course, for writing a string decryption tool, we do not need to understand such low-level knowledge.
Perhaps readers still have doubts, why do we have to write like this? Can’t we traverse each method like this?
1 | foreach (TypeDef typeDef in _moduleDef.Types) |
It looks okay, but this will not traverse methods in nested types. For example, this is a nested type, and a class B is declared in a class.
So this is not possible, ModuleDef.Types will not return nested types, we need to use ModuleDef.GetTypes(). We need to write 2 foreach loops every time we traverse a method, so it is better to use an extension method instead.
1 | foreach (MethodDef methodDef in _moduleDef.EnumerateAllMethodDefs()) { |
This way we can traverse the instructions of all methods with CliBody. Let’s switch back to dnSpy and see how Agile.NET calls the string decryptor method.
So, we locate the position of the string to be decrypted in this way, decrypt the string, and then replace it back.
1 | if (instructionList[i].OpCode.Code == Code.Call && instructionList[i].Operand == decryptorDef && instructionList[i - 1].OpCode.Code == Code.Ldstr) { |
This way, our string decryption tool is complete.
Agile.NET’s Proxy Call
The decryption of this proxy invocation is the most difficult one in this explanation. If readers haven’t understood the string decryption above, it is strongly recommended to skip this section.
Analysis
Let’s open that UnpackMe with dnSpy again.
We can see that some external method calls are obfuscated, but the method calls in the current assembly are not obfuscated. Let’s debug and see what these delegates are.
Press F11 to directly enter here, with no gains.
Let’s see where this delegate field is initialized. We can notice something.
We enter the dau method, and the dnSpy decompiled result is as follows:
1 | using System; |
This piece of code is relatively simple. It takes in a token representing a proxy type and then iterates through each field in the type, getting the MemberRef Token for the proxy method via its name and then resolving it using ResolveMethod(). If it’s a static method, a delegate is created directly; if it’s an instance method, a DynamicMethod is used to create a method to be invoked. Static decryption may still be simpler than dynamic decryption.
Write a Decryption Tool
We will still write a framework like this and add the code to ExecuteImpl().
Based on the feature, we find where the proxy fields are initialized.
1 | TypeDef[] globalTypes; |
Because the static constructors of all proxy classes automatically decrypt the real methods, we do not need to manually call the proxy method decrypters. We only need to iterate through the fields of these proxy classes and find the corresponding MemberRef for the field.
1 | foreach (TypeDef typeDef in globalTypes) { |
If a class static constructor calls decryptor, it means that this class is a proxy class. We iterate through the fields of the proxy class.
1 | foreach (FieldInfo fieldInfo in _module.ResolveType(typeDef.MDToken.ToInt32()).GetFields(BindingFlags.NonPublic | BindingFlags.Static)) { |
The realMethod here may also be a dynamic method created by Agile.NET runtime because it supports the callvirt instruction. We write a method to determine whether it is a dynamic method.
1 | private static bool IsDynamicMethod(MethodBase methodBase) { |
We first check if it’s a dynamic method before replacing it.
1 | if (IsDynamicMethod(realMethod)) { |
The implementation of ReplaceAllOperand is as follows.
1 | private void ReplaceAllOperand(FieldDef proxyFieldDef, OpCode callOrCallvirt, MemberRef realMethod) { |
ConfuserEx’s AntiTamper
Analysis
Some time ago, I posted a post about AntiTamper. That post was about static decryption, and there seemed to be some compatibility issues. This time, let’s try dynamic decryption. First, let’s open the ConfuserEx project.
This is what I commented on before: The principle of AntiTamper is to put all method bodies in a separate section and use the hash of other sections for decryption. Therefore, if the file itself has been tampered with, the runtime decryption of the section will definitely fail. This section is always inserted by ConfuserEx before other sections and can be considered encrypted as a whole, so dynamic decryption will be very easy.
Write a Decryption Tool
Still, we write a framework like before and put the code in ExecuteImpl().
We add a PEInfo class.
1 | [ ] |
Then, we read the RVA and Size of the first Section. Call the module’s static constructor, and finally restore it back.
1 | PEInfo peInfo; |
_peImage here is a byte array that represents the program assembly to be decrypted in byte array form. Dynamic decryption of AntiTamper does not even require the use of dnlib, which is much more convenient than static decryption. After decryption, manually patch the runtime of AntiTamper.
Anti Dynamic Decryption
Dynamic decryption also has its own disadvantages, such as being easily detected. The article describes three dynamic decryption methods, which are actually similar in principle, and the core is still the reflection API. We can use this to write some anti dynamic decryption code.
- The simplest way is to check the calling source, like ILProtector. If the caller of the current method is the bottom-level Invoke method, then it indicates that it has been illegally called.
- We can go even further and check the entire call stack, such as whether there is a de4dot in the call stack.
- Get all loaded assemblies through AppDomain.CurrentDomain.GetAssemblies(), and determine if there are any illegal assemblies inside them.
- If a program is an executable file and will not be referenced by other assemblies, you can use Assembly.GetEntryAssembly() to check whether the entry assembly is itself. If it is not, it means that the current assembly has been loaded by another assembly using reflection API.
.NET Dynamic Decryption and Countermeasures
https://wwh1004.com/en/net-dynamic-decryption-and-countermeasures/