Deobfuscating Mutation of VMProtect.NET

This article introduces the mutation protection of VMProtect in .NET assembly protection and how to restore it using control flow analysis techniques.

Introduction

This is about VMProtect deobfuscation in .NET, not C++.

VMProtect v3.4 added support for .NET programs with features such as anti-debugging, anti-dumping, mutation, and virtualization. Virtualization is indeed strong and I couldn’t handle it, but I understood how it works after studying it carefully. Maybe by writing a tool to automatically rename it, it can be understood more clearly, but its strength may still not be as good as KoiVM. However, mutation is relatively easy to deal with, which is a kind of constant encryption related to control flow that can be described as control flow obfuscation.

Analysis

As the program protected by unregistered VMProtect can only run on local computer, I created a sample and then protected it with VMProtect using the “Mutation” compilation mode.

Using dnSpy to open the program, I found many loops that were generated by the obfuscator.

Upon careful observation, it can be seen that the obfuscation is relatively simple, and the variable “num” can be called Context, which updates itself once per instruction execution. Why did we choose a sample with a switch statement to protect? Because we needed to know whether when entering different basic blocks and then transferring to the same basic block, the context is the same (generally speaking, it is the same, as I cannot think of any exceptions…).

Perhaps this statement is not easy to understand. Let me debug it directly to explain it more clearly. Each case block in the sample corresponds to the different basic blocks mentioned just now, and they will all eventually transfer to the same basic block, which is the basic block starting with “Console.WriteLine(“输入完毕”)”. So we set a breakpoint on this basic block.

We input different numbers respectively to make the switch statement jump to different case blocks and see if the value of “num” is the same when it finally executes to “Console.WriteLine(“输入完毕”)”.

In this way, our speculation is correct. We have a clear understanding of the macrostructure of mutation and can proceed to the next analysis.

We need to think a simple solution to restore mutation, so we switch dnSpy’s decompilation mode back to IL to observe what the statements that update “num” look like.

The entry point of this method is a br jump, so we directly look at what the basic block that br jumps to looks like.

Since this basic block is the entry point of the entire method, it must also be the place where mutation is initialized and the entry point we emulate with the emulator. The details can be seen in my previous article .NET Control Flow Analysis (II) - Deobfuscation, which is similar to clearing the switch obfuscation of ConfuserEx and clearing VMProtect.NET’s mutation.

The first arrow points to the initialization of the context variable “num”.

1
2
ldc.i4    1149763845
stloc.0

Later, code similar to the following will be used to update “num”:

1
2
3
4
ldc.i4    1099382934
ldloc.0
sub
stloc.0

If there are constants in the code, their decryption is similar to this as well, as it was analyzed beforehand.

Deobfuscation

Although this type of obfuscation is simple, it can be difficult to clean up in practice. Why? Because you have to emulate the entire control flow and every possible branch condition to ensure that your decryption result is foolproof. Simulation can be very complicated and may cause an infinite loop. Another problem is how to handle the decrypted results during emulation.

Attempt

The following content is all about my attempts (only part of them), and I found the solution after various attempts.

My initial idea was to use feature matching:

1
2
3
ldc.i4
ldloc
add/sub/mul/div ...

When encountering this type of code, replace it directly with:

1
2
3
nop
nop
ldc.i4

In the end, it failed because the effect was not good. Some places did not have such features. For example, the obfuscated branch jump instruction before (it’s called obfuscated branch jump because the jump result is certain and the result is the same every time).

So I came up with a very opportunistic method, just replacing:

1
ldloc

1
ldc.i4

This works for all situations.

After each emulation, I checked whether the emulated instruction read the variable “num”. If so, I replaced it directly with:

1
ldc.i4

But I found a big problem with this. Because I cannot accurately identify the methods obfuscated by VMProtect.NET mutation. It is indeed easy to recognize by visual inspection of C# decompiled results, but identifying it by code is a big challenge. Sometimes misjudgment occurs, and non-obfuscated methods are identified as obfuscated. Then the result is emulated, replaced directly, and finally found that it should not be replaced, which is very troublesome. There are also other problems with replacing in place after decryption, which is not ideal overall.

Finally, I decided to use a collection to store the decryption results and can verify that each emulation result is the same. If it is different, it means that there is a problem with the code or VMProtect has a bug, which can enhance stability.

Implementing the Logic

In fact, I initially wrote the logic and specific implementation together because it was convenient to modify. It wasn’t until later when the tool was mostly stable without many bugs that I abstracted the logic and separated the underlying decryption operations.

Because I have already developed the tool, let me explain based on my source code first.

The abstract class has the following member list:

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
namespace ControlFlow.Deobfuscation {
/// <summary>
/// Constant mixed with control flow decryption
/// </summary>
public abstract class ConstantFlowDeobfuscatorBase {
/// <summary>
/// Method block to be deobfuscated
/// </summary>
protected readonly MethodBlock _methodBlock;
/// <summary>
/// Instruction emulator
/// </summary>
protected readonly Emulator _emulator;
/// <summary>
/// Variable related to control flow
/// </summary>
protected Local _flowContext;
/// <summary>
/// Decrypted count
/// </summary>
protected int _decryptedCount;

#if DEBUG
/// <summary />
protected int _indent;
/// <summary />
public bool DEBUG;
#endif

/// <summary>
/// Constructor
/// </summary>
/// <param name="methodBlock"></param>
protected ConstantFlowDeobfuscatorBase(MethodBlock methodBlock);

/// <summary>
/// Deobfuscate
/// </summary>
protected virtual void Deobfuscate();

/// <summary>
/// Visit the specified basic block and recursively visit all jump targets of the basic block
/// </summary>
/// <param name="basicBlock"></param>
protected virtual void VisitAllBasicBlocks(BasicBlock basicBlock);

/// <summary>
/// Triggered before all operations start
/// In this method, extra information must be added to all basic blocks and the <see cref="_flowContext"/> field must be set
/// If <see cref="_flowContext"/> is not found, return directly instead of throwing an exception
/// </summary>
protected abstract void OnBegin();

/// <summary>
/// Triggered after all operations are completed
/// In this method, all additional information for basic blocks must be removed
/// </summary>
protected abstract void OnEnd();

/// <summary>
/// Get available emulated entry points
/// </summary>
/// <returns></returns>
protected abstract IEnumerable<BasicBlock> GetEntries();

/// <summary>
/// Triggered before emulating a specified instruction in a specified basic block
/// </summary>
/// <param name="basicBlock"></param>
/// <param name="index">Index of instruction</param>
protected abstract void OnEmulateBegin(BasicBlock basicBlock, int index);

/// <summary>
/// Triggered after emulating a specified instruction in a specified basic block
/// </summary>
/// <param name="basicBlock"></param>
/// <param name="index">Index of instruction</param>
protected abstract void OnEmulateEnd(BasicBlock basicBlock, int index);

/// <summary>
/// Triggered before emulating a specified basic block
/// </summary>
/// <param name="basicBlock"></param>
protected virtual void OnEmulateBegin(BasicBlock basicBlock);

/// <summary>
/// Triggered after emulating a specified basic block
/// </summary>
/// <param name="basicBlock"></param>
protected virtual void OnEmulateEnd(BasicBlock basicBlock);

/// <summary>
/// After emulating running a basic block, get the next basic block through emulating branch instructions. If unable to obtain, return <see langword="null"/>
/// Be sure to balance the stack regardless of success or failure
/// </summary>
/// <param name="basicBlock"></param>
/// <returns></returns>
protected virtual BasicBlock EmulateBranch(BasicBlock basicBlock);

/// <summary>
/// When encountering conditional jumps, call <see cref="VisitAllBasicBlocks"/> recursively
/// </summary>
/// <param name="basicBlock">Basic blocks with conditional jumps</param>
protected virtual void CallNextVisitAllBasicBlocksConditional(BasicBlock basicBlock);

#if DEBUG
private static string DEBUG_ToString(BasicBlock basicBlock);
#endif

/// <summary>
/// Basic block extra information base class
/// </summary>
protected abstract class BlockInfoBase {
/// <summary>
/// Emulation marks. If the specified instruction needs to be emulated, set the element of the corresponding index to <see langword="true"/>
/// </summary>
public bool[] EmulationMarks;

/// <summary>
/// The next basic block to be emulated (only possible, but certain if <see cref="HashSet{T}.Count"/> is 1)
/// </summary>
public List<BasicBlock> NextBasicBlocks;

/// <summary>
/// Preventing a loop by checking if the current basic block has already been entered
/// </summary>
public bool IsEntered;

/// <summary>
/// Constructor
/// </summary>
/// <param name="basicBlock"></param>
protected BlockInfoBase(BasicBlock basicBlock);
}
}
}

If you have read my article on cleaning up switch obfuscation in ConfuserEx, you will notice that this abstract class and that switch cleanup are very similar. Both require all available entry points to be provided for emulation until completion, covering the entire method to achieve the decryption effect.

The most crucial logic lies in VisitAllBasicBlocks, which I have modified multiple times. All of the bugs I encountered were due to logical issues in this method.

Therefore, instead of providing code with bugs, I will directly provide the working code below, which includes comments that I wrote due to previous bugs.

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
protected virtual void VisitAllBasicBlocks(BasicBlock basicBlock) {
BlockInfoBase blockInfo;

blockInfo = basicBlock.PeekExtraData<BlockInfoBase>();
if (blockInfo.IsEntered)
// If the current basic block has already been entered, prevent looping and return immediately.
return;
#if DEBUG
if (DEBUG)
Console.WriteLine($"{new string(' ', _indent)}{DEBUG_ToString(basicBlock)}: {_emulator.Locals[_flowContext]}");
#endif
blockInfo.IsEntered = true;
OnEmulateBegin(basicBlock);
for (int i = 0; i < basicBlock.Instructions.Count; i++)
if (blockInfo.EmulationMarks[i]) {
OnEmulateBegin(basicBlock, i);
if (!_emulator.Emulate(basicBlock.Instructions[i]))
throw new NotImplementedException("Failure handling for unsuccessful emulation has not been implemented yet. The deobfuscation model needs to be updated or it must be checked whether unnecessary instructions have been emulated.");
OnEmulateEnd(basicBlock, i);
}
OnEmulateEnd(basicBlock);
switch (basicBlock.BranchOpcode.FlowControl) {
case FlowControl.Return:
case FlowControl.Throw:
break;
default:
BasicBlock nextBasicBlock;

nextBasicBlock = EmulateBranch(basicBlock);
if (nextBasicBlock is null) {
#if DEBUG
_indent += 2;
if (DEBUG)
Console.WriteLine(new string(' ', _indent) + "conditional");
#endif
CallNextVisitAllBasicBlocksConditional(basicBlock);
#if DEBUG
_indent -= 2;
#endif
}
else {
// The current emulation result is based on the basic blocks before the current one.
// Because the emulation results of previous basic blocks may not occur (refer to CallNextVisitAllBasicBlocksConditional), we cannot clean up the branch directly here, but rather save it for later use.
//nextBasicBlock.PeekExtraData<BlockInfoBase>().IsEntered = false;
// This line of code might cause an infinite loop if there is a while(true){} loop
// Initially, my idea was to force the emulation of the next basic block A if the branching instruction can be evaluated, regardless of whether the current basic block is already in basic block A.
// This may not be necessary at the moment.
if (!blockInfo.NextBasicBlocks.Contains(nextBasicBlock))
blockInfo.NextBasicBlocks.Add(nextBasicBlock);
// If the branch result is the same in all cases, we can conclude that there is obfuscated branching instructions here and we can clean them up.
// Assuming that the emulation result of the previous basic block has occurred, the emulation result of this basic block is correct, so we need to force the emulation of the next basic block.
VisitAllBasicBlocks(nextBasicBlock);
}
break;
}
blockInfo.IsEntered = false;
}

The variable blockInfo.IsEntered was originally named IsVisited and was changed by me to IsEntered. In order to prevent entering an infinite loop during emulation, it is necessary to prevent a basic block from being executed repeatedly, but this must be done carefully. Initially, I planned to skip emulating a basic block if it had already been emulated. It is obvious that this idea is flawed. For example, in the sample provided, the same basic block is reached under different conditions in different branches. If IsVisited is used to indicate that a basic block can only be emulated once, then it may not be possible to emulate every branch condition and may lead to incorrect judgment.

To illustrate, consider the following scenario:

1
2
3
4
uint num = 0;
if (xxx)
num = RandomUInt32();
Console.WriteLine(num);

Assume that the logic is such that each basic block can only be executed once. The case we emulate first is that the “if” statement is not executed, then the execution reaches

1
Console.WriteLine(num);

The value of num is determined and is 0.

Let’s emulate the execution of the if branch. Since the basic block for Console.WriteLine has already been executed, we will not execute it again.

Therefore, we obtain a decryption result where the value of num used in Console.WriteLine is 0, but this is not actually the case.

So, we should use IsEntered to indicate that if we are already in a basic block, we should not emulate that basic block again to prevent infinite loops like while(true). Before emulating a basic block, set its IsEntered to true, and after emulating the branching instruction of this basic block, set it to false. This will solve the problem perfectly.

The code for CallNextVisitAllBasicBlocksConditional is the same as the code for cleaning up the switch obfuscation in ConfuserEx (the code for CallNextVisitAllBasicBlocksConditional in the previous post had a bug, which you can see by comparing it carefully with the one I will provide below; note that my EmulationContext is a reference type).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
protected virtual void CallNextVisitAllBasicBlocksConditional(BasicBlock basicBlock) {
EmulationContext context;

context = _emulator.Context.Clone();
// Conditional jumps have multiple jump targets, so we need to backup the current emulator context.
if (!(basicBlock.FallThrough is null)) {
VisitAllBasicBlocks(basicBlock.FallThrough);
_emulator.Context = context.Clone();
// Restore emulator context.
}
if (!(basicBlock.ConditionalTarget is null)) {
VisitAllBasicBlocks(basicBlock.ConditionalTarget);
_emulator.Context = context.Clone();
}
if (!(basicBlock.SwitchTargets is null))
foreach (BasicBlock target in basicBlock.SwitchTargets) {
VisitAllBasicBlocks(target);
_emulator.Context = context.Clone();
}
}

Next, we implement all the abstract methods and some virtual methods. The code is directly pasted below.

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
214
215
216
217
218
219
220
221
namespace ControlFlow.Deobfuscation.Specials.VMProtect {
public sealed class MutationDeobfuscator : ConstantFlowDeobfuscatorBase {
private static readonly Code[] InitializeFlowContextCodes = new Code[] { Code.Ldc_I4, Code.Stloc };
private static readonly Code[] CanBeEmulatedCodes = new Code[] {
Code.Add, Code.Add_Ovf, Code.Add_Ovf_Un, Code.And, Code.Div, Code.Div_Un, Code.Mul, Code.Mul_Ovf, Code.Mul_Ovf_Un, Code.Neg, Code.Not, Code.Or, Code.Rem, Code.Rem_Un, Code.Shl, Code.Shr, Code.Shr_Un, Code.Sub, Code.Sub_Ovf, Code.Sub_Ovf_Un, Code.Xor,
Code.Ceq, Code.Cgt, Code.Cgt_Un, Code.Clt, Code.Clt_Un,
Code.Ldc_I4,
Code.Ldloc, Code.Stloc,
Code.Beq, Code.Bge, Code.Bge_Un, Code.Bgt, Code.Bgt_Un, Code.Ble, Code.Ble_Un, Code.Blt, Code.Blt_Un, Code.Bne_Un, Code.Br, Code.Brfalse, Code.Brtrue, Code.Endfilter, Code.Endfinally, Code.Leave, Code.Ret, Code.Rethrow, Code.Switch, Code.Throw
};

private List<BasicBlock> _basicBlocks;
private List<BasicBlock> _entries;
private bool _isNotMutation;

private MutationDeobfuscator(MethodBlock methodBlock) : base(methodBlock) {
}

public static int Deobfuscate(MethodBlock methodBlock) {
MutationDeobfuscator deobfuscator;

deobfuscator = new MutationDeobfuscator(methodBlock);
deobfuscator.Deobfuscate();
if (deobfuscator._decryptedCount > 0) {
NopRemover.Remove(methodBlock);
ConstantArithmeticRemover.Remove(methodBlock);
}
return deobfuscator._decryptedCount;
}

protected override void OnBegin() {
Dictionary<Local, int> frequencies;
int maxFrequency;
Local flowContext;

frequencies = new Dictionary<Local, int>();
// Used to calculate the frequency of local variable occurrences.
_basicBlocks = _methodBlock.GetAllBasicBlocks();
foreach (BasicBlock basicBlock in _basicBlocks)
foreach (Instruction instruction in basicBlock.Instructions) {
Local local;

if (instruction.OpCode.Code != Code.Ldloc && instruction.OpCode.Code != Code.Stloc)
// There is no ldloca because mutation does not use ldloca.
continue;
local = (Local)instruction.Operand;
if (!frequencies.ContainsKey(local))
frequencies[local] = 1;
else
frequencies[local]++;
}
maxFrequency = 0;
flowContext = null;
foreach (KeyValuePair<Local, int> frequency in frequencies)
if (frequency.Value > maxFrequency) {
maxFrequency = frequency.Value;
flowContext = frequency.Key;
}
if (!(flowContext is null) && (flowContext.Type.ElementType != ElementType.U4 /*|| !MayBeEntry(_methodBlock.GetFirstBasicBlock(), flowContext)*/))
flowContext = null;
// Check if it is a flowContext.
if (flowContext is null)
return;
_flowContext = flowContext;
_entries = new List<BasicBlock>();
foreach (BasicBlock basicBlock in _basicBlocks)
if (MayBeEntry(basicBlock, flowContext))
_entries.Add(basicBlock);
// Get all entry points.
foreach (BasicBlock basicBlock in _basicBlocks)
basicBlock.PushExtraData(new BlockInfo(basicBlock));
_emulator.Interceptor = Interceptor;
}

private static bool MayBeEntry(BasicBlock basicBlock, Local flowContext) {
int index;

index = basicBlock.Instructions.IndexOf(InitializeFlowContextCodes);
if (index == -1)
// No features.
return false;
if (basicBlock.Instructions[index + 1].Operand != flowContext)
// Operands are not flowContext.
return false;
for (int i = 0; i < index; i++)
if (basicBlock.Instructions[i].Operand == flowContext)
// flowContext is used before initialization.
return false;
return true;
}

private bool Interceptor(Emulator emulator, Instruction instruction) {
if (!CanBeEmulatedCodes.Contains(instruction.OpCode.Code)) {
// Do not emulate instructions that are not in the list.
emulator.UpdateStack(instruction);
return true;
}
if (instruction.Operand is Local && instruction.Operand != _flowContext) {
// Do not emulate ldloc and stloc instructions whose operands are not _flowContext.
emulator.UpdateStack(instruction);
return true;
}
return false;
}

protected override void OnEnd() {
if (_isNotMutation)
_decryptedCount = 0;
foreach (BasicBlock basicBlock in _basicBlocks) {
if (_decryptedCount != 0) {
List<BasicBlock> nextBasicBlocks;

foreach (KeyValuePair<int, List<int>> decryptedValue in basicBlock.PeekExtraData<BlockInfo>().DecryptedValues) {
if (decryptedValue.Value.Count != 1)
// Do not replace decrypted values that are different.
continue;
basicBlock.Instructions[decryptedValue.Key] = OpCodes.Ldc_I4.ToInstruction(decryptedValue.Value[0]);
}
// Replace instructions.
nextBasicBlocks = basicBlock.PeekExtraData<BlockInfoBase>().NextBasicBlocks;
if (nextBasicBlocks.Count == 1)
switch (basicBlock.BranchOpcode.StackBehaviourPop) {
case StackBehaviour.Popi:
// brfalse brtrue
basicBlock.Instructions.Add(OpCodes.Pop.ToInstruction());
basicBlock.SetBr(nextBasicBlocks[0]);
break;
case StackBehaviour.Pop1_pop1:
// bgt bge blt ble ...
basicBlock.Instructions.Add(OpCodes.Pop.ToInstruction());
basicBlock.Instructions.Add(OpCodes.Pop.ToInstruction());
basicBlock.SetBr(nextBasicBlocks[0]);
break;
}
#if DEBUG
if (nextBasicBlocks.Count > 1) {
System.Console.WriteLine(BlockPrinter.ToString(_methodBlock));
System.Diagnostics.Debug.Assert(false, "Different branch emulation results exist.");
}
#endif
// Clean up the branches.
}
basicBlock.PopExtraData();
}
}

protected override IEnumerable<BasicBlock> GetEntries() {
return _entries;
}

protected override void OnEmulateBegin(BasicBlock basicBlock, int index) {
}

protected override void OnEmulateEnd(BasicBlock basicBlock, int index) {
List<Instruction> instructions;

if (_isNotMutation)
return;
instructions = basicBlock.Instructions;
if (instructions[index].OpCode.Code == Code.Ldloc && instructions[index].Operand == _flowContext) {
// ldloc flowContext should be a constant, replace it directly.
Int32Value value;
Dictionary<int, List<int>> decryptedValues;
List<int> existingValues;

value = _emulator.EvaluationStack.Peek() as Int32Value;
if (value is null) {
// We cannot accurately identify whether it is VMProtect's mutation. If an exception occurs, it is considered not to be mutation.
_isNotMutation = true;
return;
}
decryptedValues = basicBlock.PeekExtraData<BlockInfo>().DecryptedValues;
if (!decryptedValues.TryGetValue(index, out existingValues)) {
existingValues = new List<int>();
decryptedValues.Add(index, existingValues);
}
if (!existingValues.Contains(value.Signed)) {
existingValues.Add(value.Signed);
_decryptedCount++;
}
// Save the emulation result.
if (existingValues.Count > 1) {
_decryptedCount--;
_isNotMutation = true;
// Either it's not a mutation, or VMProtect has problems.
#if DEBUG
System.Console.WriteLine(BlockPrinter.ToString(_methodBlock));
System.Diagnostics.Debug.Assert(false, "存在不同的解密结果");
#endif
}
}
}

protected override void VisitAllBasicBlocks(BasicBlock basicBlock) {
if (_isNotMutation)
return;
base.VisitAllBasicBlocks(basicBlock);
}

protected override void CallNextVisitAllBasicBlocksConditional(BasicBlock basicBlock) {
if (_isNotMutation)
return;
base.CallNextVisitAllBasicBlocksConditional(basicBlock);
}

private sealed class BlockInfo : BlockInfoBase {
private readonly Dictionary<int, List<int>> _decryptedValues;

/// <summary>
/// The decrypted value, where the key is the index of the instruction and the value is the decrypted value, is replaced by ldc.i4.
/// </summary>
public Dictionary<int, List<int>> DecryptedValues => _decryptedValues;

public BlockInfo(BasicBlock basicBlock) : base(basicBlock) {
_decryptedValues = new Dictionary<int, List<int>>();
for (int i = 0; i < EmulationMarks.Length; i++)
EmulationMarks[i] = true;
}
}
}
}

First of all, we need to find out which local variable is the context of Mutation, but in reality, I can only roughly judge whether it is or not. It is really difficult to determine without emulating it once. We write this in OnBegin because it is the initialization step. The specific implementation is given above.

The last line of OnBegin has an

1
_emulator.Interceptor = Interceptor;

This Interceptor is I newly added. This thing is like a hook.

1
2
3
4
5
6
7
/// <summary>
/// The interceptor, if it returns <see langword="true"/>, <see cref="Emulator"/> will no longer emulate the current instruction.
/// </summary>
/// <param name="emulator"></param>
/// <param name="instruction"></param>
/// <returns></returns>
public delegate bool Interceptor(Emulator emulator, Instruction instruction);

If you want to use it, just modify the source code of the emulator that I previously released.

Returning to the Interceptor implemented in the deobfuscator. In order to improve stability and speed, I filtered out many instructions because VMProtect.NET’s Mutation only requires four types of instructions: arithmetic, comparison, value retrieval, and branching.

For ldloc, the instruction that reads variables, special processing is required to prevent reading the values ​​of irrelevant Mutation variables, to prevent the emulator from calculating incorrectly and causing problems with branch emulation.

VMProtect.NET’s Mutation encrypts constants and adds obfuscated branching code, so we need to restore everything after emulation ends. This part is implemented in OnEnd.

Overall, VMProtect.NET’s Mutation is okay, and you shouldn’t be scared by the results of C# decompilation because that is intentionally scaring you. How do you know if you can’t try it?

As for VMProtect.NET’s virtualization mode, I haven’t figured it out yet. I don’t know if someone researching devirtualization are willing to share their experience. I’m not very clear about this part and plan to study it.

Tool Download

DeMutation.zip

Currently, the tool is very stable and has been tested many times without any bugs. Occasionally, it may prompt an invalid branch instruction, but there is no need to worry as it has no impact on the program’s execution.

Before and after cleanup comparison:

The results are still very impressive.

Author

wwh1004

Posted on

2019-08-09

Updated on

2023-04-11

Licensed under

CC BY 4.0


Comments