C# Reversing - Unpacking A Packer [Part 2]

Hey mates,

today I’ve got a much more interesting challenge for you to solve :wink:. We’ll reverse a CrackMe and expand our skills in MSIL Patching to dump the unpacked executable straight from within the program. This will be much more complicated than our copy & paste solution from last time but when you’ve understood the content in this article you’ll have learned a lot!


Simple Introduction To MSIL

I first thought of splitting it into two parts: Introduction to MSIL and the usage of it but MSIL is that easy that you won’t even have to re-read this for understanding it. This is not an introduction about how to use MSIL for writing complete programs but for learning the syntax and getting in touch with it before editing it in the wild.

.method static void main()
{
    .entrypoint
    .maxstack      1
    ldstr          "Hello world!"
    call           void [mscorlib]System.Console::WriteLine(string)
    ret
}

The example has been taken from here

This would be the basic Hello world example in MSIL. Pretty easy, isn’t it? Anyway, there are a few points I should speak about. At first I can tell you that you don’t have to name the entry function main. It is only indicated by the .entrypoint which can be used only once in a program. The .maxstack indicates how big the stack has to be at maximum. This is not that interesting for us but useful to know. Here we come to an important fact about MSIL: MSIL is completly stack-based! This is cool for the ease of understanding because I always found the stack pretty simple. Last In First Out (LIFO). Not that hard :wink:.

Then we come to the first “real” instructions. As we already know it from assembler (I told you in the part before that MSIL reminds of assembler due to its bytecode-style) we have opcodes and values. The first command we can see is ldstr which pushes (Again: stack-based system) the string “Hello world!” onto the stack. Then a function is called:

void [mscorlib]System.Console::WriteLine(string)

It returns void, it is located in the mscorlib assembly, the namespace is System.Console, the name of the method is WriteLine and it needs a string as parameter. As I said: assembler for noobs :grin:.

And finally the ret is run. End of function reached. Don’t tell me you can’t understand this :smile:.


Examining The Target

Today we’ll use a new tool called dnSpy. Big thanks to @FFY00 who recommended it to me! It’s really awesome when it comes to reversing + patching. I swear to you: When you tried this one once, you won’t go back to other tools :wink:.

Please install it, when you want to follow the tutorial. You don’t need any plugins because everything we need and wish is already built into it! After you’ve done that I recommend you to have a look at our todays target which I’ve found on a german board. It is not obfuscated, so you won’t need additional tools for working with it. Try to figure out what it does and come back when you’re ready.


If you’re not interested in the workings of our target and just want to see how to patch it, jump to the next heading :slight_smile:


Okay, first we should open the CrackMe in dnSpy to get an overlook about its structure:

Mmh… AssemblyResolve? We should hold that in mind when we work on main(). So, the content of main for the ones who are too lazy for opening dnSpy :wink::

// Stub.Program
// Token: 0x06000002 RID: 2 RVA: 0x00002058 File Offset: 0x00000258
[STAThread]
private static void Main(string[] args)
{
    Assembly assembly = null;
    string[] manifestResourceNames = typeof(Program).Assembly.GetManifestResourceNames();
    for (int i = 0; i < manifestResourceNames.Length; i++)
    {
       string text = manifestResourceNames[i];
       using (BinaryReader binaryReader = new BinaryReader(new  GZipStream(typeof(Program).Assembly.GetManifestResourceStream(text), CompressionMode.Decompress)))
       {
            byte[] array = binaryReader.ReadBytes(binaryReader.ReadInt32());
            try
            {
                Assembly assembly2 = Assembly.Load(array);
                if (text == "DATA")
                {
                    assembly = assembly2;
                }
                Program.d.Add(assembly2.FullName, assembly2);
            }
            catch
            {
                Directory.CreateDirectory(Program.TempDir);
                string text2 = Program.TempDir + text;
                try
                {
                    File.WriteAllBytes(text2, array);
                }
                catch
                {
                }
                Program.TempFiles.Add(text2, Program.LoadLibraryW(text2));
            }
        }
    }
    AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(Program.AssemblyResolve);
    AppDomain.CurrentDomain.ProcessExit += new EventHandler(Program.ProcessExitHandler);
    assembly.EntryPoint.Invoke(null, (assembly.EntryPoint.GetParameters().Length > 0) ? new object[]
    {
        args
    } : null);
}

Have you found the interesting part? It’s really easy to estimate what the program does without understanding each and every line.

The program loops through the resources and searches for a section named DATA. When it finds this section, it saves everything in it in the variable assembly. After it finished its sneaky search, it runs the assembly:

    assembly.EntryPoint.Invoke(null, (assembly.EntryPoint.GetParameters().Length > 0) ? new object[]
    {
        args
    } : null);

More experienced guys maybe even saw that the data in the section is GZIP-compressed, so we are confronted with a typical packer :slight_smile:.


How To Dump The Packed Executable

As nearly always we got more than one way to accomplish this goal:

  • Write a program which reads the resources of the executable and saves them
  • Patch the program itself to save the executable

Because programers are lazy and reversers are lazier we’ll use the second approach :wink:.

Where to start? Maybe we want to try out using the decompiled source for rebuilding the stub and adding a File.WriteAllBytes() call? Yes… No. Because the resources are saved only in the original executable we couldn’t read them without editing the program further. But here comes our new magic hero into play: MSIL Patching!

If we’d know how to add the call directly to the code, we could save a lot of time and research. So what do we have to do?

  1. Find a place where this call would do the job
  2. Find this place in the MSIL code
  3. Add the call into the MSIL code

Do you think that’s easy? Then you’re right :grin:. The first two steps can be done by you without further knowledge. Just try it and come back when you think you’re ready!

Step 1: Finding The Required Place In The C# Source

Where would this call be useful? Probably exactly when the assembly variable is defined! The interesting code can be seen here again:

       string text = manifestResourceNames[i];
       using (BinaryReader binaryReader = new BinaryReader(new  GZipStream(typeof(Program).Assembly.GetManifestResourceStream(text), CompressionMode.Decompress)))
       {
            byte[] array = binaryReader.ReadBytes(binaryReader.ReadInt32());
            try
            {
                Assembly assembly2 = Assembly.Load(array);
                if (text == "DATA")
                {
                    // Don't you think a call to File.WriteAllBytes("Dumped.exe", array) would fit nicely into this line?

                    assembly = assembly2;
                }
                Program.d.Add(assembly2.FullName, assembly2);
            }

At this point the array holds the raw bytes of our hunted executable and we can be sure to get the right ones.

Step 2: Finding The Place In MSIL

How to look at the MSIL code? Just change the view at the top bar to IL:

Again for the lazy ones the MSIL code of main() here:

// Token: 0x06000002 RID: 2 RVA: 0x00002058 File Offset: 0x00000258
.method private hidebysig static 
    void Main (
        string[] args
    ) cil managed 
{
    .custom instance void [mscorlib]System.STAThreadAttribute::.ctor() = (
        01 00 00 00
    )
    // Header Size: 12 bytes
    // Code Size: 309 (0x135) bytes
    // LocalVarSig Token: 0x11000001 RID: 1
    .maxstack 5
    .entrypoint
    .locals init (
        [0] class [mscorlib]System.Reflection.Assembly,
        [1] string,
        [2] class [mscorlib]System.IO.BinaryReader,
        [3] uint8[],
        [4] class [mscorlib]System.Reflection.Assembly,
        [5] string,
        [6] string[],
        [7] int32,
        [8] object[]
    )

    /* 0x00000264 14           */ IL_0000: ldnull
    /* 0x00000265 0A           */ IL_0001: stloc.0
    /* 0x00000266 D003000002   */ IL_0002: ldtoken   Stub.Program
    /* 0x0000026B 281100000A   */ IL_0007: call      class [mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle(valuetype [mscorlib]System.RuntimeTypeHandle)
    /* 0x00000270 6F1200000A   */ IL_000C: callvirt  instance class [mscorlib]System.Reflection.Assembly [mscorlib]System.Type::get_Assembly()
    /* 0x00000275 6F1300000A   */ IL_0011: callvirt  instance string[] [mscorlib]System.Reflection.Assembly::GetManifestResourceNames()
    /* 0x0000027A 1306         */ IL_0016: stloc.s   6
    /* 0x0000027C 16           */ IL_0018: ldc.i4.0
    /* 0x0000027D 1307         */ IL_0019: stloc.s   7
    /* 0x0000027F 38AE000000   */ IL_001B: br        IL_00CE

    // loop start (head: IL_00CE)
        /* 0x00000284 1106         */ IL_0020: ldloc.s   6
        /* 0x00000286 1107         */ IL_0022: ldloc.s   7
        /* 0x00000288 9A           */ IL_0024: ldelem.ref
        /* 0x00000289 0B           */ IL_0025: stloc.1
        /* 0x0000028A D003000002   */ IL_0026: ldtoken   Stub.Program
        /* 0x0000028F 281100000A   */ IL_002B: call      class [mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle(valuetype [mscorlib]System.RuntimeTypeHandle)
        /* 0x00000294 6F1200000A   */ IL_0030: callvirt  instance class [mscorlib]System.Reflection.Assembly [mscorlib]System.Type::get_Assembly()
        /* 0x00000299 07           */ IL_0035: ldloc.1
        /* 0x0000029A 6F1400000A   */ IL_0036: callvirt  instance class [mscorlib]System.IO.Stream [mscorlib]System.Reflection.Assembly::GetManifestResourceStream(string)
        /* 0x0000029F 16           */ IL_003B: ldc.i4.0
        /* 0x000002A0 731500000A   */ IL_003C: newobj    instance void [System]System.IO.Compression.GZipStream::.ctor(class [mscorlib]System.IO.Stream, valuetype [System]System.IO.Compression.CompressionMode)
        /* 0x000002A5 731600000A   */ IL_0041: newobj    instance void [mscorlib]System.IO.BinaryReader::.ctor(class [mscorlib]System.IO.Stream)
        /* 0x000002AA 0C           */ IL_0046: stloc.2

        .try
        {
            /* 0x000002AB 08           */ IL_0047: ldloc.2
            /* 0x000002AC 08           */ IL_0048: ldloc.2
            /* 0x000002AD 6F1700000A   */ IL_0049: callvirt  instance int32 [mscorlib]System.IO.BinaryReader::ReadInt32()
            /* 0x000002B2 6F1800000A   */ IL_004E: callvirt  instance uint8[] [mscorlib]System.IO.BinaryReader::ReadBytes(int32)
            /* 0x000002B7 0D           */ IL_0053: stloc.3
            .try
            {
                /* 0x000002B8 09           */ IL_0054: ldloc.3
                /* 0x000002B9 281900000A   */ IL_0055: call      class [mscorlib]System.Reflection.Assembly [mscorlib]System.Reflection.Assembly::Load(uint8[])
                /* 0x000002BE 1304         */ IL_005A: stloc.s   4
                /* 0x000002C0 07           */ IL_005C: ldloc.1
                /* 0x000002C1 7201000070   */ IL_005D: ldstr     "DATA"
                /* 0x000002C6 281A00000A   */ IL_0062: call      bool [mscorlib]System.String::op_Equality(string, string)
                /* 0x000002CB 2C03         */ IL_0067: brfalse.s IL_006C

                /* 0x000002CD 1104         */ IL_0069: ldloc.s   4
                /* 0x000002CF 0A           */ IL_006B: stloc.0

                /* 0x000002D0 7E01000004   */ IL_006C: ldsfld    class [mscorlib]System.Collections.Generic.Dictionary`2<string, class [mscorlib]System.Reflection.Assembly> Stub.Program::d
                /* 0x000002D5 1104         */ IL_0071: ldloc.s   4
                /* 0x000002D7 6F1B00000A   */ IL_0073: callvirt  instance string [mscorlib]System.Reflection.Assembly::get_FullName()
                /* 0x000002DC 1104         */ IL_0078: ldloc.s   4
                /* 0x000002DE 6F1C00000A   */ IL_007A: callvirt  instance void class [mscorlib]System.Collections.Generic.Dictionary`2<string, class [mscorlib]System.Reflection.Assembly>::Add(!0, !1)
                /* 0x000002E3 DE3B         */ IL_007F: leave.s   IL_00BC
            } // end .try

            catch [mscorlib]System.Object
            {
                /* 0x000002E5 26           */ IL_0081: pop
                /* 0x000002E6 7E03000004   */ IL_0082: ldsfld    string Stub.Program::TempDir
                /* 0x000002EB 281D00000A   */ IL_0087: call      class [mscorlib]System.IO.DirectoryInfo [mscorlib]System.IO.Directory::CreateDirectory(string)
                /* 0x000002F0 26           */ IL_008C: pop
                /* 0x000002F1 7E03000004   */ IL_008D: ldsfld    string Stub.Program::TempDir
                /* 0x000002F6 07           */ IL_0092: ldloc.1
                /* 0x000002F7 281E00000A   */ IL_0093: call      string [mscorlib]System.String::Concat(string, string)
                /* 0x000002FC 1305         */ IL_0098: stloc.s   5
                .try
                {
                    /* 0x000002FE 1105         */ IL_009A: ldloc.s   5
                    /* 0x00000300 09           */ IL_009C: ldloc.3
                    /* 0x00000301 281F00000A   */ IL_009D: call      void [mscorlib]System.IO.File::WriteAllBytes(string, uint8[])
                    /* 0x00000306 DE03         */ IL_00A2: leave.s   IL_00A7
                } // end .try

                catch [mscorlib]System.Object
                {
                    /* 0x00000308 26           */ IL_00A4: pop
                    /* 0x00000309 DE00         */ IL_00A5: leave.s   IL_00A7
                } // end handler

                /* 0x0000030B 7E02000004   */ IL_00A7: ldsfld    class [mscorlib]System.Collections.Generic.Dictionary`2<string, native int> Stub.Program::TempFiles
                /* 0x00000310 1105         */ IL_00AC: ldloc.s   5
                /* 0x00000312 1105         */ IL_00AE: ldloc.s   5
                /* 0x00000314 2805000006   */ IL_00B0: call      native int Stub.Program::LoadLibraryW(string)
                /* 0x00000319 6F2000000A   */ IL_00B5: callvirt  instance void class [mscorlib]System.Collections.Generic.Dictionary`2<string, native int>::Add(!0, !1)
                /* 0x0000031E DE00         */ IL_00BA: leave.s   IL_00BC
            } // end handler

            /* 0x00000320 DE0A         */ IL_00BC: leave.s   IL_00C8
        } // end .try

        finally
        {
            /* 0x00000322 08           */ IL_00BE: ldloc.2
            /* 0x00000323 2C06         */ IL_00BF: brfalse.s IL_00C7

            /* 0x00000325 08           */ IL_00C1: ldloc.2
            /* 0x00000326 6F2100000A   */ IL_00C2: callvirt  instance void [mscorlib]System.IDisposable::Dispose()

            /* 0x0000032B DC           */ IL_00C7: endfinally
        } // end handler

        /* 0x0000032C 1107         */ IL_00C8: ldloc.s   7
        /* 0x0000032E 17           */ IL_00CA: ldc.i4.1
        /* 0x0000032F 58           */ IL_00CB: add
        /* 0x00000330 1307         */ IL_00CC: stloc.s   7
        /* 0x00000332 1107         */ IL_00CE: ldloc.s   7
        /* 0x00000334 1106         */ IL_00D0: ldloc.s   6
        /* 0x00000336 8E           */ IL_00D2: ldlen
        /* 0x00000337 69           */ IL_00D3: conv.i4
        /* 0x00000338 3F47FFFFFF   */ IL_00D4: blt       IL_0020
    // end loop

    /* 0x0000033D 282200000A   */ IL_00D9: call      class [mscorlib]System.AppDomain [mscorlib]System.AppDomain::get_CurrentDomain()
    /* 0x00000342 14           */ IL_00DE: ldnull
    /* 0x00000343 FE0603000006 */ IL_00DF: ldftn     class [mscorlib]System.Reflection.Assembly Stub.Program::AssemblyResolve(object, class [mscorlib]System.ResolveEventArgs)
    /* 0x00000349 732300000A   */ IL_00E5: newobj    instance void [mscorlib]System.ResolveEventHandler::.ctor(object, native int)
    /* 0x0000034E 6F2400000A   */ IL_00EA: callvirt  instance void [mscorlib]System.AppDomain::add_AssemblyResolve(class [mscorlib]System.ResolveEventHandler)
    /* 0x00000353 282200000A   */ IL_00EF: call      class [mscorlib]System.AppDomain [mscorlib]System.AppDomain::get_CurrentDomain()
    /* 0x00000358 14           */ IL_00F4: ldnull
    /* 0x00000359 FE0604000006 */ IL_00F5: ldftn     void Stub.Program::ProcessExitHandler(object, class [mscorlib]System.EventArgs)
    /* 0x0000035F 732500000A   */ IL_00FB: newobj    instance void [mscorlib]System.EventHandler::.ctor(object, native int)
    /* 0x00000364 6F2600000A   */ IL_0100: callvirt  instance void [mscorlib]System.AppDomain::add_ProcessExit(class [mscorlib]System.EventHandler)
    /* 0x00000369 06           */ IL_0105: ldloc.0
    /* 0x0000036A 6F2700000A   */ IL_0106: callvirt  instance class [mscorlib]System.Reflection.MethodInfo [mscorlib]System.Reflection.Assembly::get_EntryPoint()
    /* 0x0000036F 14           */ IL_010B: ldnull
    /* 0x00000370 06           */ IL_010C: ldloc.0
    /* 0x00000371 6F2700000A   */ IL_010D: callvirt  instance class [mscorlib]System.Reflection.MethodInfo [mscorlib]System.Reflection.Assembly::get_EntryPoint()
    /* 0x00000376 6F2800000A   */ IL_0112: callvirt  instance class [mscorlib]System.Reflection.ParameterInfo[] [mscorlib]System.Reflection.MethodBase::GetParameters()
    /* 0x0000037B 8E           */ IL_0117: ldlen
    /* 0x0000037C 69           */ IL_0118: conv.i4
    /* 0x0000037D 16           */ IL_0119: ldc.i4.0
    /* 0x0000037E 3003         */ IL_011A: bgt.s     IL_011F

    /* 0x00000380 14           */ IL_011C: ldnull
    /* 0x00000381 2B0F         */ IL_011D: br.s      IL_012E

    /* 0x00000383 17           */ IL_011F: ldc.i4.1
    /* 0x00000384 8D02000001   */ IL_0120: newarr    [mscorlib]System.Object
    /* 0x00000389 1308         */ IL_0125: stloc.s   8
    /* 0x0000038B 1108         */ IL_0127: ldloc.s   8
    /* 0x0000038D 16           */ IL_0129: ldc.i4.0
    /* 0x0000038E 02           */ IL_012A: ldarg.0
    /* 0x0000038F A2           */ IL_012B: stelem.ref
    /* 0x00000390 1108         */ IL_012C: ldloc.s   8

    /* 0x00000392 6F2900000A   */ IL_012E: callvirt  instance object [mscorlib]System.Reflection.MethodBase::Invoke(object, object[])
    /* 0x00000397 26           */ IL_0133: pop
    /* 0x00000398 2A           */ IL_0134: ret
} // end of method Program::Main

It can be pretty easily spotted by searching for specific keywords like strings, function calls, etc… In this case you can find the interesting part by searching for the keyword “DATA”. You remember? This is the section name which is used close before the place we want to use!

Here is the part:

                /* 0x000002C1 7201000070   */ IL_005D: ldstr     "DATA"    // The string used for comparison is pushed onto the stack
                /* 0x000002C6 281A00000A   */ IL_0062: call      bool [mscorlib]System.String::op_Equality(string, string)    // The comparison is called
                /* 0x000002CB 2C03         */ IL_0067: brfalse.s IL_006C    // If false --> Jump over the next two lines

                /* 0x000002CD 1104         */ IL_0069: ldloc.s   4    // Pushes variable at index 4 onto the stack
                /* 0x000002CF 0A           */ IL_006B: stloc.0    // Pops value into the variable at index 0

What are these mysterious indexes? Have a look at the beginning of the MSIL code in main():

    .locals init (
        [0] class [mscorlib]System.Reflection.Assembly,
        [1] string,
        [2] class [mscorlib]System.IO.BinaryReader,
        [3] uint8[],
        [4] class [mscorlib]System.Reflection.Assembly,
        [5] string,
        [6] string[],
        [7] int32,
        [8] object[]
    )

These are the variables used in the code. Okay, what is at index 4? Of course the assembly which is defined in the if-clause!

The code in C# looks like this:

if (text == "DATA")    // The comparison
{
     assembly /* positioned at index 0 */= assembly2 /* positioned at index 4 */;
}

Step 3: Patching The Code

We’ve found the place but what are the instructions we need? Just copy and paste different lines from the code :wink:. Pushing the array variable onto the stack is used after the bytes have been read in the resources (line 27): ldloc.3. Pushing a string onto the stack is used directly before the comparison (line 32): ldstr "Dumped.exe". And finally a call to File.WriteAllBytes() can be found in line 53: call void [mscorlib]System.IO.File::WriteAllBytes(string, uint8[]). Everything’s already there! Be a hacker and find them on your own, you don’t always have to google for suitable OpCodes :grin:. This will help you much more understanding the code and to be honest: MSIL is not that hard that you can’t understand it without further help.

So our patchwork code to add into line 35 is:

                /* 0x000002CD 72????????   */ IL_0069: ldstr     "Dumped.exe"
                /* 0x000002D2 09           */ IL_006E: ldloc.3
                /* 0x000002D3 281F00000A   */ IL_006F: call      void [mscorlib]System.IO.File::WriteAllBytes(string, uint8[])

Select main(), click on Edit in the top bar and open Edit Method Body. Here you can add the code and save the changes.

Now switch back to the C# mode in the top bar and have a look at what you’ve done!

Finally run the program and you’ll see the newly created Dumped.exe. If you want to crack the program completly, open the executable in dnSpy and do it! Maybe even with MSIL patching? I’ve done it and can tell you that it’s a good way to deepen your reversing skills :wink:.


Conclusion

This was a simple introduction about unpacking a packer in C# with your new best friend MSIL. Just play around with MSIL in the program and have a look at what it would look like in C#. It really helps to understand the language! At least I hope you learned something new :slight_smile:.

Was that what you want? Or too much code snippets, theory, challenges, etc… Should I explain everything more detailed? Please tell me and I’ll try to change the style for our next time :wink:.

|-TheDoctor-|

3 Likes

Great tut!.. This MSIL is pretty similar to Java bytecodes, also stack-based, instead of register based solutions as Parrot (actually PASM).

Looking forward to the next tutorial!

Yes SMALI is pretty alike. I prefer MSIL because due to its well-ordered structure, although SMALI got the better instruction names… It’s a hard decision :wink:.

1 Like