/dev/blog

Defeating Reactor's Necrobit

I recently came across a post on tuts4you which basically said that .net reactor uses a hashtable to store pre-JIT information about methods that use Necrobit and wanted to see what it is all about for myself.

The sample

Sample We will be using a simple .NET Framework 4.8 x86 test application on RELEASE mode with .NET Reactor v.6.7.0.0 Demo and just Necrobit with compatibility mode disabled.

The goal

The goal is to dump and recompile the CIL code of certain methods. Goal

How does Necrobit work?

Necrobit encrypts the method body bytes beforehand and decrypts them at runtime. It hooks the JIT compiler and replaces them in memory as soon as the method is JITed.

Approach

Drag n Drop on dnSpy

Opening the file on dnSpy, we see that Hello class is almost empty, with only a call on the static constructor. This is what necrobit did to it. dnSpy1

That method has control-flow obfuscation applied to it so let’s clean it first, so it’s easier to read.

Cleaning the control-flow obfuscation

There is a really handy tool for this called .NetReactorCfCleaner Note: After running it through this tool, the file doesn’t run anymore, we just want to view it.

The hashtable

Now if we open the method that we found previously on the static constructor and scroll down a bit, we will indeed see info added into a hashtable. dnSpy2

If we use dnSpy’s analyzer we see that this hashtable is also used on a method which seems to be the JIT CompileMethod hook dnSpy3

Now we have enough information, let’s break it.

Retrieving the hashtable

Since we are doing this dynamically and not statically, all we need to do is load the assembly and locate the field, then get its value.

var assembly = Assembly.LoadFrom("NecrobitTest.exe");

var type = assembly.GetTypes().First(d => d.Name == "nLvrU8AQJDKRRZAB7e");
var hashtableField = type.GetRuntimeFields().First(d => d.Name == "Nll0SVdCxp");

var hashtableValue = (Hashtable)hashtableField.GetValue(null);

We can now loop through it. The value of the entry is a struct used in the program, let’s use reflection to get the CIL byte array.

foreach (DictionaryEntry entry in hashtableValue)
{
    Console.WriteLine($"{entry.Key} -> {ExtractArray(entry.Value).Length}");
}
byte[] ExtractArray(object entry)
{
    foreach (var field in entry.GetType().GetRuntimeFields())
    {
        if (field.FieldType == typeof(byte[]))
        {
            return (byte[])field.GetValue(entry);
        }
    }
    return new byte[0];
}
85764037 -> 7
85729417 -> 19
85729460 -> 102
85729465 -> 12
85729393 -> 12
85729409 -> 7
85729425 -> 22
85764032 -> 129
85763989 -> 7
85764005 -> 12
0 -> 1

Process finished with exit code 0.

Key to method

We have a key -> cil byte array hashtable, now we need to find which method the key corresponds to. On the screenshot that I showed earlier I mentioned that the key is Method RVA + HInstance of the module. Let’s get the module HInstance from the assembly.

var hInstance = Marshal.GetHINSTANCE(assembly.ManifestModule).ToInt64();

Now if we just subtract it we get the method RVA, but it seems to be +1 so let’s subtract 1 from it.

Console.WriteLine($"{(long)entry.Key - hInstance - 1:X} -> {ExtractArray(entry.Value).Length}");
20B3 -> 102
20B8 -> 12
2070 -> 12
2080 -> 7
2090 -> 22
A7BF -> 129
A794 -> 7
A7A4 -> 12
A7C4 -> 7
2088 -> 19
FFFFFFFFFD99FFFF -> 1

Process finished with exit code 0.

dnSpy4

Now let’s use AsmResolver to get the actual method from RVA. We load in both the PEImage and the ModuleDefinition then we get the TableStream from the DotNetDirectory and then get the MethodDefinitionRow table. We then search for the row that has our RVA and we create a MetadataToken so we can look for the MethodDefinition in the ModuleDefinition

var peImage = PEImage.FromFile("NecrobitTest.exe");
var module = ModuleDefinition.FromFile("NecrobitTest.exe");
var table = peImage.DotNetDirectory.Metadata.GetStream<TablesStream>().GetTable<MethodDefinitionRow>().ToList();
foreach (DictionaryEntry entry in hashtableValue)
{
    var cilBytes = ExtractArray(entry.Value);
    var rva = (long) entry.Key - hInstance - 1;
    var row = table.SingleOrDefault(d => d.Body.Rva == rva);
    var index = table.IndexOf(row) + 1;
    var token = new MetadataToken(row.TableIndex, (uint)index);
    if (module.TryLookupMember(token, out var m))
    {
        if (m is MethodDefinition methodDefinition)
        {
            Console.WriteLine(methodDefinition.Name);
        }
    }
}
.ctor
.ctor
get_Username
SayHello
.ctor
.ctor
.ctor
.ctor

Process finished with exit code 0.

Boom! We got the methodDefinition from the RVA!

Disassembling the cil byte array

AsmResolver has pretty handy tools for this.

var operandResolver =
    new PhysicalCilOperandResolver(module,methodDefinition.CilMethodBody);
BinaryStreamReader reader = ByteArrayDataSource.CreateReader(cilBytes);
var disassembler = new CilDisassembler(in reader, operandResolver);
var instructions = disassembler.ReadInstructions();
foreach (var instruction in instructions)
{
    Console.WriteLine(instruction);
}
.ctor
IL_0000: call System.Void JOoSTcUQYrhd3hbI7F.iehh7boeaNq3xAaFC6::Hn9cnwCrR()
IL_0005: ldarg.0
IL_0006: call System.Void System.Object::.ctor()
IL_000B: ret
.ctor
IL_0000: call System.Void JOoSTcUQYrhd3hbI7F.iehh7boeaNq3xAaFC6::Hn9cnwCrR()
IL_0005: ldarg.0
IL_0006: call System.Void System.Object::.ctor()
IL_000B: ret
get_Username
IL_0000: ldarg.0
IL_0001: ldfld System.String NecrobitTest.Hello::<Username>k__BackingField
IL_0006: ret
SayHello
IL_0000: ldstr "Hello "
IL_0005: ldarg.0
IL_0006: call System.String NecrobitTest.Hello::get_Username()
IL_000B: call System.String System.String::Concat(System.String, System.String)
IL_0010: call System.Void System.Console::WriteLine(System.String)
IL_0015: ret
.ctor
IL_0000: ldarg.0
IL_0001: call System.Void System.Attribute::.ctor()
IL_0006: ret
.ctor
IL_0000: call System.Void JOoSTcUQYrhd3hbI7F.iehh7boeaNq3xAaFC6::Hn9cnwCrR()
IL_0005: ldarg.0
IL_0006: call System.Void System.Object::.ctor()
IL_000B: ret
.ctor
IL_0000: ldarg.0
IL_0001: call System.Void System.Object::.ctor()
IL_0006: ret
.ctor
IL_0000: call System.Void JOoSTcUQYrhd3hbI7F.iehh7boeaNq3xAaFC6::Hn9cnwCrR()
IL_0005: ldarg.0
IL_0006: call System.Void System.Object::.ctor()
IL_000B: ldarg.0
IL_000C: ldarg.1
IL_000D: stfld System.String NecrobitTest.Hello::<Username>k__BackingField
IL_0012: ret

Process finished with exit code 0.

There we go, these instructions look familiar :D. Only thing left is recompiling and rebuilding it.

Recompiling & Rebuilding

foreach (DictionaryEntry entry in hashtableValue)
{
    var cilBytes = ExtractArray(entry.Value);
    var rva = (long) entry.Key - hInstance - 1;
    var row = table.SingleOrDefault(d => d.Body.Rva == rva);
    var index = table.IndexOf(row) + 1;
    var token = new MetadataToken(row.TableIndex, (uint) index);
    if (module.TryLookupMember(token, out var m))
    {
        if (m is MethodDefinition methodDefinition)
        {
            Console.WriteLine(methodDefinition.Name);
            var operandResolver =
                new PhysicalCilOperandResolver(module,methodDefinition.CilMethodBody);
            BinaryStreamReader reader = ByteArrayDataSource.CreateReader(cilBytes);
            var disassembler = new CilDisassembler(in reader, operandResolver);
            var instructions = disassembler.ReadInstructions();
            methodDefinition.CilMethodBody.Instructions.Clear();
            methodDefinition.CilMethodBody.Instructions.AddRange(instructions);
        }
    }
}
module.Write("NecrobitTest-UnNecrobit.exe");

Final Result

dnSpy5

dnSpy6

Goal has been achieved 👍 We could remove the junk but there’s no need in this post.

Limitations

The hashtable only contains the instruction bytes but the local variables are missing too from the method, though this was not a problem for our sample.

Credits

Thanks to Washi1337 for AsmResolver

Thanks to 0xd4d for dnSpy

Thanks to SychicBoy for .NetReactorCfCleaner

Where can I find the code for this?

You can find the repo Here

#Reverse Engineering #.NET