Rebel.NET Official Guide
This is the official Rebel.NET guide. You can find the latest release of Rebel.NET on NTCore.
Presentation Rebel.NET File Format Basic Usage Overcoming .NET Protections Code Injection Code Obfuscation String Obfuscation String Stream Hiding Native Linking (GAC) Conclusions |
The first question which arises is: what is Rebel.NET? It's necessary to spend a few words about it to fully comprehend what it is, since it's quite a new software concept. Basically, Rebel.NET is a rebuilding tool for .NET assemblies. It is capable of adding and replacing methods and streams. It's possible to replace only a limited number of methods or every method contained in a .NET assembly. The simplicity of Rebel.NET consists in the replacing process: one can choose what to replace. For instance, one may choose to replace only the method code, instead of its signature or method header.
The interface of Rebel.NET is quite a simple one. As input it requires a .NET assembly to be rebuilded and a Rebel.NET rebuilding file. The Rebel.NET file contains the data that has to be replaced in the original assembly.
Rebel.NET can also create a Rebel.NET file from a given assembly. This is a key functionality, since some times the data of the original assembly has to be processed first to produce a Rebel.NET file for the rebuilding of the assembly. This sort of "report" feature can also be used to analyze the methods of an assembly, since reading the original data from a .NET assembly isn't as easy as reading a Rebel.NET file. It's possible to choose what should be contained in the Rebel.NET file.
All the Rebel.NET features can used through command line, which comes very handy when an automated rebuilding process is needed.
Rebel.NET is, mainly, a very solid base to overcome every .NET protection and to re-create a fully decompilable .NET assembly. As such, Rebel.NET has to be considered a research project, not an encouragement to violate licensing terms.
All the definitions necessary to access a Rebel.NET file are contained in the RebelDotNET.h header file, which comes along with the Rebel.NET software. Every Rebel.NET file starts with a REBEL_NET_BASE structure:
typedef
struct _REBEL_NET_BASE
{
QWORD Signature;
DWORD Reserved;
QWORD Mask;
DWORD Flags;
// .NET Header Flags
DWORD EntryPointToken;
// .NET Header Entrypoint
DWORD StreamsOffset;
WORD NumberOfStreams;
DWORD MethodsOffset;
DWORD NumberOfMethods;
} REBEL_NET_BASE, *PREBEL_NET_BASE;
Here are the fields:
Field |
Description |
Signature |
Has to be REBEL_NET_SIGNATURE. It represent the string "REBELNET". Every Rebel.NET file must have this string as its first 8 bytes to be considered valid. |
Reserved |
Must be 0. |
Mask |
Can be set to these flags:
|
Flags |
Flags field in the .NET Directory structure (COR20). |
EntryPointToken |
EntryPointToken field in the .NET Directory structure (COR20). |
StreamsOffset |
Points to the streams array. This value can be 0 if no stream is present in the Rebel.NET file. |
NumberOfStreams |
Number of streams present in the current Rebel.NET file. |
MethodsOffset |
Points to the methods array. This value can be 0 if no method is present in the Rebel.NET file. |
NumberOfMethods |
Number of methods present in the current Rebel.NET file. |
As you can see, this structure, just like the REBEL_METHOD one, holds a Mask field which tells Rebel.NET, among other things, what has to be replaced. I can't stress enough that the REBEL_BASE_MASK_DELETE_METHODS flag should only be used on rare occasions, because if this flag is set Rebel.NET file to provide all the information to re-create the methods. A Rebel.NET file created as a report, for instance, holds all the information necessary to do this operation and, in fact, it works.
As said, the StreamsOffset points to the streams array inside our Rebel.NET file. The streams array is a very simple one. To walk through it, just consider the structure of each stream.
Field |
Description |
szStreamName |
A null terminated ascii string which represents the stream's name. The size of this field is given by the string length plus the terminator. |
StreamSize |
A dword which specifies the stream's size. |
StreamData |
A byte array the size specified by the StreamSize field, which contains the stream's data. |
Every stream present in a Rebel.NET file is going to replace the homonymous one in the original .NET assembly. This is true for every stream except for the "#~" one which has to be created during the rebuilding process.
And now comes the last and most "complex" structure in the Rebel.NET file format: the REBEL_METHOD. The MethodsOffset field points to an offset of methods. Each method starts with a REBEL_METHOD structure:
typedef
struct _REBEL_METHOD
{
DWORD Token;
DWORD Mask;
DWORD RVA; //
information only
WORD Flags;
WORD ImplFlags;
WORD MaxStack;
WORD HeaderFlags;
DWORD ParamList;
DWORD NameOffsetOrSize; //
offset into the #Strings stream or size
DWORD
SignatureOffsetOrSize; // offset into the #Blob
stream or size
DWORD
LocalVarSigOffsetOrSize; // offset into the #Blob
stream or size
DWORD CodeSize;
DWORD ExtraSectionsSize;
} REBEL_METHOD, *PREBEL_METHOD;
Here are the fields:
Field |
Description |
Token |
Represents the token of the method
that has to be replaced or added. A .NET token for the MethodDef table
has this appearence: 0x06000001. 0x06 stands for the MethodDef table and
1 is the method that has to be replaced or added. If the table is not
MethodDef (0x06) the method is considered as invalid and won't be used
in the rebuilding process. Also remember that MetaData entries are
1-based. So, if you want to replace the first method in a .NET assembly,
you'll have to use the token value written above. If the entry is 0, the
method won't replace an existing method. Instead, it will be placed as a
new method at the end of the table. The order of the methods doesn't matter, token 0x06000034 can come before token 0x06000001. Also, duplicates are ignored. If token 0x06000001 has already been processed, it won't be processed again. |
Mask |
Can be set to these flags:
|
RVA |
This value is ignored during the rebuilding process. Its purpose in reports is to inform about the location of the method's header (which is followed by the code) inside the original assembly. |
Flags |
Field in the MetaData MethodDef table. |
ImplFlags |
Field in the MetaData MethodDef table. |
MaxStack |
Field in the method's header. |
HeaderFlags |
Field in the method's header. |
ParamList |
Field in the MetaData MethodDef table. |
NameOffsetOrSize |
Field in the MetaData MethodDef table. |
SignatureOffsetOrSize |
Field in the MetaData MethodDef table. |
LocalVarSigOffsetOrSize |
Indirect field in the method's header. |
CodeSize |
Field in the method's header. |
ExtraSectionsSize |
Indirect field in the method's header. |
Fields such as NameOffsetOrSize can either reference a stream included in the Rebel.NET file or indicate the size of the data. If they specify a size, then their data follows the REBEL_METHOD structure. When used for report purposes, Rebel.NET will automatically reference a stream if that stream is included in the report options.
Every "Size" field specifies the number of bytes of the data it represents. This data comes directly after the REBEL_METHOD structure and all the previous "Size" data bytes (if any). The following lines of code to calculate the size of a method element will give you an exact idea of how it works.
Also one thing you should know is that the #Blob signatures when included as data following the REBEL_METHOD structure are already stripped of their #Blob encoded length, since the length is already specified by the "Size" field.
#define
IS_FLAG(Value,
Flag) ((Value &
Flag) == Flag)
UINT GetMethodSize(REBEL_METHOD
*rbMethod)
{
UINT nMethodSize =
sizeof (REBEL_METHOD);
if (!IS_FLAG(rbMethod->Mask,
REBEL_METHOD_MASK_NAMEOFFSET))
nMethodSize += rbMethod->NameOffsetOrSize;
if (!IS_FLAG(rbMethod->Mask,
REBEL_METHOD_MASK_SIGOFFSET))
nMethodSize += rbMethod->SignatureOffsetOrSize;
if (!IS_FLAG(rbMethod->Mask,
REBEL_METHOD_MASK_LOCVARSIGOFFSET))
nMethodSize += rbMethod->LocalVarSigOffsetOrSize;
nMethodSize += rbMethod->CodeSize;
nMethodSize += rbMethod->ExtraSectionsSize;
return nMethodSize;
}
In spite of the fact that maybe this paragraph seemed a little complicated, the Rebel.NET file format was designed to be extremely easy and you will see that in the next paragraph. Even optimization (like 4-byte alignment) was sacrificed for the sake of simplicity.
If you survived the Rebel.NET File Format paragraph, you'll be glad to read this one. In fact, it will show you how easy it is to rebuild an assembly with Rebel.NET.
Before starting with a real world example, let me introduce you to the Rebel.NET command line:
|
Syntax |
Rebuild |
RebelDotNET.exe /ra /asm="c:\asmtorebuild.exe" /reb="c:\rebfile.rebel" /out="c:\output.exe" |
Syntax |
|
Report |
RebelDotNET.exe /cr
/asm="c:\assembly.exe" /out="c:\output.rebel" /md /st="#Blob #Strings
#US" The /md parameter tells Rebel.NET to include the methods in the report file. This parameter is optional. The /st parameters tells Rebel.NET which streams to include in the report file. This parameter is optional. |
This is fairly simple, I believe.
The real world example will show you how to replace just one method in an assembly. Replacing only one method is just like replacing them all, it doesn't make any difference. The victim of this test will be a little, very naïve application I wrote called rebtest.exe. You can download all the files by clicking here. The application shows a form with a text box and a button in it. When the button is pressed it checks the content of the text box (which is a password). If the password matches a certain criteria, the application displays the message "Right Password!". As said, it's very simple. Let's have a look at the button event code with ildasm:
.method private hidebysig instance void
button1_Click(object sender,
class
[mscorlib]System.EventArgs e) cil managed
{
// Code size 43 (0x2b)
.maxstack 8
IL_0000: ldarg.0
IL_0001: ldarg.0
IL_0002: ldfld class [System.Windows.Forms]System.Windows.Forms.TextBox
rebtest.Form1::textBox1
IL_0007: callvirt instance string
[System.Windows.Forms]System.Windows.Forms.Control::get_Text()
IL_000c: call instance bool rebtest.Form1::CheckPassword(string)
IL_0011: brfalse.s IL_001f
IL_0013: ldstr "Right password!"
IL_0018: call valuetype
[System.Windows.Forms]System.Windows.Forms.DialogResult
[System.Windows.Forms]System.Windows.Forms.MessageBox::Show(string)
IL_001d: pop
IL_001e: ret
IL_001f: ldstr "Wrong password!"
IL_0024: call valuetype
[System.Windows.Forms]System.Windows.Forms.DialogResult
[System.Windows.Forms]System.Windows.Forms.MessageBox::Show(string)
IL_0029: pop
IL_002a: ret
} // end of method Form1::button1_Click
The method CheckPassword is called to check the password's validity. If the CheckPassword method returns false, the password has to be considered invalid. And here's the CheckPassword method's code:
.method /*06000007*/ private hidebysig
instance bool
CheckPassword(string strPass) cil managed
// SIG: 20 01 02 0E
{
// Method begins at RVA 0x20c5
// Code size 17 (0x11)
.maxstack 8
IL_0000: /* 03 | */ ldarg.1
IL_0001: /* 72 | (70)00003B */ ldstr "strongpasswordcheck" /*
7000003B */
IL_0006: /* 28 | (0A)00001C */ call bool
[mscorlib/*23000001*/]System.String/*01000022*/::op_Equality(string,
string) /* 0A00001C */
IL_000b: /* 2C | 02 */ brfalse.s IL_000f
IL_000d: /* 17 | */ ldc.i4.1
IL_000e: /* 2A | */ ret
IL_000f: /* 16 | */ ldc.i4.0
IL_0010: /* 2A | */ ret
} // end of method Form1::CheckPassword
Sense the irony put in the password check. In this sample, I'll replace the method's code with the following one:
ldc.i4.1
ret
I admit, this job can much more easily be done with a hex editor, since the replaced code is bigger than my one, but this is just a sample that demonstrates how to do replace a method. It doesn't matter what I'm replacing.
Here's the code that generates the Rebel.NET file:
#include
"stdafx.h"
#include <Windows.h>
#include <CorHdr.h>
#include "RebelDotNET.h"
// return TRUE if successful
UINT AddMethod(HANDLE
hRebFile, DWORD
dwToken, VOID *pCode,
UINT CodeSize);
int _tmain(int
argc, _TCHAR*
argv[])
{
HANDLE hRebFile =
CreateFile(_T("C:\\rebtest.rebel"),
GENERIC_WRITE,
FILE_SHARE_READ, NULL,
CREATE_ALWAYS, 0, NULL);
if (hRebFile ==
INVALID_HANDLE_VALUE)
return 0;
REBEL_NET_BASE rbBase;
ZeroMemory(&rbBase,
sizeof (REBEL_NET_BASE));
rbBase.Signature =
REBEL_NET_SIGNATURE;
SetFilePointer(hRebFile,
sizeof (REBEL_NET_BASE),
NULL, FILE_BEGIN);
SetEndOfFile(hRebFile);
//
// add the method to replace
//
BYTE CodeBuf[2] = {
0x17, // ldc.i4.1
0x2A // ret
};
rbBase.NumberOfMethods
+= AddMethod(hRebFile,
TokenFromRid(7,
mdtMethodDef), // our method is n. 7, remember?
&CodeBuf, 2);
//
if (rbBase.NumberOfMethods
!= 0)
{
rbBase.MethodsOffset
= sizeof (REBEL_NET_BASE);
}
DWORD BW;
SetFilePointer(hRebFile,
0, NULL, FILE_BEGIN);
WriteFile(hRebFile,
&rbBase, sizeof (REBEL_NET_BASE),
&BW, NULL);
CloseHandle(hRebFile);
return 0;
}
UINT AddMethod(HANDLE
hRebFile, DWORD
dwToken, VOID *pCode,
UINT CodeSize)
{
REBEL_METHOD rbMethod;
ZeroMemory(&rbMethod,
sizeof (REBEL_METHOD));
rbMethod.Token =
dwToken;
rbMethod.CodeSize =
CodeSize;
DWORD BW;
WriteFile(hRebFile,
&rbMethod, sizeof (REBEL_METHOD),
&BW, NULL);
WriteFile(hRebFile,
pCode, CodeSize, &BW,
NULL);
return TRUE;
}
If we run the original assembly against the produced ".rebel" file, we will obtain a new fully functional .NET assembly, which will always approve our password, even if we leave the text box blank.
This sample is trivial, but it gives you the idea of how easy it is to create a Rebel.NET file to replace methods and this has to be kept in mind when reading the next paragraphs.
It's not like Rebel.NET is only useful to break protections, but its main purpose as a research project is exactly this. Rebel.NET can also be used to create .NET protections. I really encourage such activities as long as they remain no-profit projects. Commercial use of Rebel.NET goes against its licensing terms.
.NET code injection is the "strong" brother of .NET packers (which unpack the entire assembly in memory). What .NET code injectors do is to hook the JIT and when the MSIL code of a method is requested they filter the request and provide the real MSIL instead of the MSIL contained in the assembly, which, most of the times, is just a ret. By injecting one method at a time, the MSIL code will remain conceiled. Even if one manages to dump the code, it really isn't to be expected that the protection left the necessary space for the real MSIL code in the .NET assembly. So, the only thing that can be done is rebuilding.
This protection is absolutely the weakest one against Rebel.NET. After a really simple dump process, the whole assembly can be rebuilded and is completely decompilable. This protection method offers no real advantage compared to .NET packers. At least not yet. In my opinion something better still can be achieved in that direction. Anyway, Rebel.NET is capable of keeping up.
Ironically, although the code obfuscation is considered a pretty weak protection, it remains one of the strongest. The way to proceed to de-obfuscate a .NET assembly would be to create a report file of all methods contained in a .NET assembly and then to rebuild the code with a MSIL disasm engine. The Rebel.NET report feature is most important to overcome code obfuscation, because it frees the reverser from reading the MetaData format. The rebuilding process is also important. However, the code of obfuscated methods is generally larger than the de-obfuscated version. The rebuilding comes handy when the reverser decides not to optimize his de-obfuscation engine by using only far jumps (branches) which are bigger than small ones. In that case it could happen that the de-obfuscated code is actually bigger than the obfuscated one.
So, Rebel.NET helps a great deal in overcoming this kind of protection, but writing a de-obfuscation engine remains "a bit of work" for the reverser.
String obfuscation is pretty weak and doesn't stop the decompiling process, but it is annoying when analyzing the code. If the obfuscated strings are bigger (in length) than the de-obfuscated ones, then it is very easy to overcome this protection. All what is necessary to do is to decrypt the #US stream following the string decryption method and then replacing the one contained in the .NET assembly with the decrypted one with Rebel.NET. This really is a piece of cake and is a valid procedure for most string obfuscators.
In the unfortunate case that the strings are somehow "compressed", you'll have to create a new #US stream and process all ldstr instructions in the code. Not difficult at all, but annoying.
This kind of protection is not even worth mentioning, but I do as I have already encountered it in some native protectors. The #US stream is not contained in the original assembly and is "linked" through native code at runtime. To overcome this protection, simply dump the #US stream from the process and rebuild the assembly with Rebel.NET.
By native linking, in this paragraph, I mean putting a native image of a .NET assembly in the Global Assembly Cache (GAC) and leaving the .NET assembly with no MSIL code.
Rebel.NET is capable of extracting the MSIL code from a .NET native image and using that code to rebuild the original .NET assembly.
To test this ability I took the little application used in the Basic Usage paragraph and ngen'ed it. Then I invalidated the code of the original assembly and changed a random byte of the #GUID stream. By changing the #GUID stream the native image is invalidated or, better, unbound from the assembly. If you invalidate the code of an ngen'ed assembly and then run it, it will function, because the JIT will take the code from the native image. But if you change the #GUID stream, the JIT won't be able to bind the assembly to its native image any longer and if the MSIL code contained in the assembly is invalid it will make the JIT show up with an error of incorrect assembly. I then used the ".rebel" file obtained from the native image against the invalid assembly and obtained a new functioning one. You can download the test files from here.
Of course, this procedure is only possible when the native image contains the original MSIL code of the assembly. Native images, and I'm talking about the ones produced by the framework 2.0 and higher (I've never explored 1.0/1.1 native images), contain a double set of MetaData sections. If you open the Native Header of a native image with the CFF Explorer:
You'll notice that it contains the field "Original MetaData RVA". If you overwrite the MetaData RVA of the .NET Directory (COR20) with the value of this field, you will be able to access the MetaData of the ngen'ed assembly:
If the original MetaData is still available in the native image, this protection becomes useless. This concept works only if one forces the framework to load a native image without the MSIL code.
Edit: This paragraph is no longer valid for native images introduced with the .NET Framework 3.5 Service Pack 1. Now, the alternative metadata is the one linked in the Native Header. To create a report, just set to 0 the Nativa Header's RVA in the .NET Directory.
Conclusions
The Rebel.NET is a project I realized mainly to test new parts of the CFF Explorer kernel and it took me no longer than a week to complete it. I'm quite glad I did it, because indeed it fulfilled its purpose by showing me many bugs in the new CFF Explorer kernel and also helped me amplifying it. The support for the Rebel.NET file format will be added to the CFF Explorer starting from version VIII.
Daniel Pistelli