Join the discord

crackmes.de : Grzzlwmpf's CrackMe

05 Jan, 2014 15:16
Yet another crackme solution from crackmes.de

This time I picked a .NET crackme, written in C# and although it's very easy to solve it offers a nice training field, in IL code reverse engineering.

The author gave freedom for the acceptable solution:
ReadMe.txt1: Use the Fail-MsgBox to give you the correct serial or
2: Crack it, so it accept ANY serial or
3: write a KeyGen or
4: find my personal Backdoor ^^

Because I'm about to deal with a .NET crackme, here's some good tools for .NET reverse engineering:

.NET Reflector - originally free, its now paid. It has useful (free) addons like Deblector and Reflexil which make it quite powerful.

JetBrains dotPeek - Is a good choice too, and it's also free. However I had some problems with the code caching.

ILSpy - is free, created as free alternative of the original Reflector. It has a similar interface and functionality, and it's definitely going in my toolbox.

IL DASM - the IL Disassembler provided by Microsoft with their .NET Framework. It's crappy, ugly and does excellent job for reading IL code.

Having the toolbox full, I can start with the real deal.





I. Finding the backdoor


Not often seen in real life, but still in existence. For example check out the latest articles about some Linksys routers... yikes...

To find the backdoor, I've loaded the crackme in ILSpy, and few clicks later got this:


I've got the verification code, and also the username "Grzzlwmpf" and password "backdoor", that are the backdoor credentials.


Nothing interesting here, moving on to the keygen.





II. Writing a keygen


Most of the time, the keygen is considered as the hardest solution. It requires a lot of skill, not only in RE, but also in math algorithms and programming.
According to the code above, the valid serial is the result of Encryption.EncryptString() function that uses the username for it's two arguments (the username is passed twice).

Moving to that function shows this:
C#public static string EncryptString(string clearText, string Password) {
   byte[] bytes = Encoding.Unicode.GetBytes(clearText);
   PasswordDeriveBytes passwordDeriveBytes = new PasswordDeriveBytes(Password, 
      new byte[] {73, 118, 97, 110, 32, 77, 101, 100, 118, 101, 100, 101, 118});
   byte[] inArray = Encryption.EncryptString(bytes, passwordDeriveBytes.GetBytes(32), passwordDeriveBytes.GetBytes(16));
   return Convert.ToBase64String(inArray);
}

private static byte[] EncryptString(byte[] clearText, byte[] Key, byte[] IV) {
   MemoryStream memoryStream = new MemoryStream();
   Rijndael rijndael = Rijndael.Create();
   rijndael.Key = Key;
   rijndael.IV = IV;
   CryptoStream cryptoStream = new CryptoStream(memoryStream, rijndael.CreateEncryptor(), CryptoStreamMode.Write);
   cryptoStream.Write(clearText, 0, clearText.Length);
   cryptoStream.Close();
   return memoryStream.ToArray();
}

All other functions used are native .NET Framework functions, so to simplify it, I can write the following keygen code:
C#using System;
using System.Text;
using System.Security.Cryptography;
using System.IO;

namespace ConsoleApplication1 {
    class Program {
        static void Main(string[] args) {
            string Username = "XpoZed";
            byte[] bytes = Encoding.Unicode.GetBytes(Username);
            PasswordDeriveBytes passwordDeriveBytes = new PasswordDeriveBytes(Username,
                                new byte[] {73, 118, 97, 110, 32, 77, 101, 100, 118, 101, 100, 101, 118});
            MemoryStream memoryStream = new MemoryStream();
            Rijndael rijndael = Rijndael.Create();
            rijndael.Key = passwordDeriveBytes.GetBytes(32);
            rijndael.IV = passwordDeriveBytes.GetBytes(16);
            CryptoStream cryptoStream = new CryptoStream(memoryStream, rijndael.CreateEncryptor(), CryptoStreamMode.Write);
            cryptoStream.Write(bytes, 0, bytes.Length);
            cryptoStream.Close();
            Console.WriteLine(Username);
            Console.WriteLine(Convert.ToBase64String(memoryStream.ToArray()));
        }
    }
}



That's easy, so I've decided to make my life harder by writing a C+WinApi keygen (available for download).
This wasn't that easy because it involved a PBKDF1 hashing(extended version), SHA1 hashing, AES256 encryption and Base64 encoding.

The algorithm is:
- Username "XpoZed" and salt "Ivan Medvedev" (those bytes 73, 118, 97, etc.) are used to produce two PBKDF1 (extending a SHA1 hash) keys: KeyA - 32 bytes long, and KeyB - 16 bytes long;
- KeyA is used as Key and KeyB is used as IV (Initialization Vector) for an AES256 encryption (Rijndael based cipher);
- The Unicode version of my username "XpoZed" is then AES256 encrypted with the Key and IV
- Since the result is composed out of binary data (non human readable bytes), it finally gets Base64 encoded

And that Base64 encoded string is the correct serial.
The keygen and its source are available for download at the bottom of this article.





III. Crack to accept any serial


The most exploited attack vector is always this one - crack the application, to skip the checks.

Here's where IL DASM gets really handy.

Having the IL Assembly code and the opcodes I can easily locate the button1_Click method in the HEX editor:


But what should I change here?
IL codeIL_0000:  /* 00   |            */ nop
IL_0001:  /* 16   |            */ ldc.i4.0
IL_0002:  /* 0A   |            */ stloc.0
IL_0003:  /* 02   |            */ ldarg.0
IL_0004:  /* 7B   | (04)000006 */ ldfld      CrackMe.Form1::textBox2
IL_0009:  /* 6F   | (0A)00004C */ callvirt   Control::get_Text()
IL_000e:  /* 02   |            */ ldarg.0
IL_000f:  /* 7B   | (04)000005 */ ldfld      CrackMe.Form1::textBox1
IL_0014:  /* 6F   | (0A)00004C */ callvirt   Control::get_Text()
IL_0019:  /* 02   |            */ ldarg.0
IL_001a:  /* 7B   | (04)000005 */ ldfld      CrackMe.Form1::textBox1
IL_001f:  /* 6F   | (0A)00004C */ callvirt   Control::get_Text()
IL_0024:  /* 28   | (06)000002 */ call       Encryption::EncryptString(string, string)
IL_0029:  /* 28   | (0A)00004D */ call       String::op_Equality(string, string)
IL_002e:  /* 16   |            */ ldc.i4.0
IL_002f:  /* FE01 |            */ ceq
IL_0031:  /* 0B   |            */ stloc.1
IL_0032:  /* 07   |            */ ldloc.1
IL_0033:  /* 2D   | 0F         */ brtrue.s   IL_0044
IL_0035:  /* 00   |            */ nop
IL_0036:  /* 72   | (70)000173 */ ldstr      "Good Job. Should hire you for hacking\r\nmy school's Website."
IL_003b:  /* 28   | (0A)00004E */ call       MessageBox::Show(string)

The op_Equality() function is comparing my serial with the calculated one, and the result is pushed into the stack.
There's a brtrue.s (Branch to target if value is non-zero (true)) that obviously does the jump to the Good/Bad message.
Both instructions ldc.i4.0 and ceq are PUSHing data into the stack.
If I just NOP the brtrue.s, without fixing the stack I'll break the program.

Let's get back to the IL documentation.
IL documentationldc.i4.0	- Push 0 onto the stack as int32.
ceq		- Push 1 (of type int32) if value1 equals value2, else push 0.
stloc.1		- Pop a value from stack into local variable 1.
ldloc.1		- Load local variable 1 onto stack.

So:
- call op_Equality() pushes its result into the stack and ldc.i4.0 pushes 0 = PUSH x2
- ceq uses these values and depending on them, is pushes 1 or 0 to the stack = POP x2, PUSH x1
- stloc.1 pops a value from the stack = POP x1

At this point, the stack should be empty.
If the next instruction ldloc.1 executes, there will be another PUSH to the stack, that later the brtrue.s will POP, and again empty the stack.

In other words, if I just NOP that brtrue.s, there will be one remaining value in the stack.
So instead of NOP-ing, I'll replace it with a POP to fix the stack.

The theory seems legit, lets test it in practice:


Two bytes crack. Cool.
Again, the crack and its source are available for download at the bottom of this article.





IV. Serial fishing


Probably one of the hardest approaches in cracking is to change not only the code flow (flip a jump instruction, nop another...), but to add features.
In this case you have to do exactly this - add a feature.
Serial fishing is well known technique from the old times, where the attacker uses the bad message to display the correct serial number.

Let's take this pseudo code:
pseudo codeUsername = get_username();
Password = get_password();

if (Password == keygen(Username)) {
   MessageBox("Yes!");
} else {
   MessageBox("No!");
}

As you can see, the correct Password is calculated in one shot, so we can "wiretap" that keygen() function and display it to the user.

However if the code above was slightly different:
pseudo codeUsername = get_username();
Password = get_password();

for(i = 0; i < 20; i++) {
   if (Password[i] != keygen(Username)[i]) {
      MessageBox("No!");
      break;
   }
}

Then obviously such solution will be harder to achieve.

I already know that the valid Serial is calculated on one shot, so let's move to the IL code again, but this time with some explanations.

The main validation code is here:
IL codeIL_0000:  /* 00   |            */ nop
IL_0001:  /* 16   |            */ ldc.i4.0                                              ;/ Init the bad message flag to 0 (FALSE)
IL_0002:  /* 0A   |            */ stloc.0                                               ;\
IL_0003:  /* 02   |            */ ldarg.0                                               ;/ Put the password in the stack
IL_0004:  /* 7B   | (04)000006 */ ldfld      CrackMe.Form1::textBox2                    ;|
IL_0009:  /* 6F   | (0A)00004C */ callvirt   Control::get_Text()                        ;\
IL_000e:  /* 02   |            */ ldarg.0                                               ;/ Put the username in the stack
IL_000f:  /* 7B   | (04)000005 */ ldfld      CrackMe.Form1::textBox1                    ;|
IL_0014:  /* 6F   | (0A)00004C */ callvirt   Control::get_Text()                        ;\
IL_0019:  /* 02   |            */ ldarg.0                                               ;/ Again put the username in the stack
IL_001a:  /* 7B   | (04)000005 */ ldfld      CrackMe.Form1::textBox1                    ;|
IL_001f:  /* 6F   | (0A)00004C */ callvirt   Control::get_Text()                        ;\
IL_0024:  /* 28   | (06)000002 */ call       Encryption::EncryptString(string, string)  ;/ Calculate the correct Password, by using
                                                                                        ;\   the two Username entries from the stack
IL_0029:  /* 28   | (0A)00004D */ call       String::op_Equality(string, string)        ;/ Compare the calculated pass with that 
                                                                                        ;\   the user entered
IL_002e:  /* 16   |            */ ldc.i4.0                                              ;/ This is where I patch it to accept any serial
IL_002f:  /* FE01 |            */ ceq                                                   ;|
IL_0031:  /* 0B   |            */ stloc.1                                               ;|
IL_0032:  /* 07   |            */ ldloc.1                                               ;|
IL_0033:  /* 2D   | 0F         */ brtrue.s   IL_0044                                    ;\ Jump if serials don't match
IL_0035:  /* 00   |            */ nop
IL_0036:  /* 72   | (70)000173 */ ldstr      "Good Job. ..."                            ;/ The good message
IL_003b:  /* 28   | (0A)00004E */ call       MessageBox::Show(string)                   ;|
IL_0040:  /* 26   |            */ pop                                                   ;\ pop the MessageBox result from the stack
IL_0041:  /* 17   |            */ ldc.i4.1                                              ;/ Set the bad message flag to 1 (TRUE)
IL_0042:  /* 0A   |            */ stloc.0                                               ;\
IL_0043:  /* 00   |            */ nop
IL_0044:  /* 02   |            */ ldarg.0                                               ; more code ...

Then, the "backdoor" code check:
IL codeIL_0044:  /* 02   |            */ ldarg.0                                               ;/ Load the username into stack
IL_0045:  /* 7B   | (04)000005 */ ldfld      CrackMe.Form1::textBox1                    ;| 
IL_004a:  /* 6F   | (0A)00004C */ callvirt   Control::get_Text()                        ;\
IL_004f:  /* 72   | (70)0001EB */ ldstr      "Grzzlwmpf"                                ; Load the string "Grzzlwmpf" into stack
IL_0054:  /* 28   | (0A)00004D */ call       String::op_Equality(string, string)        ; Compare both values
IL_0059:  /* 2C   | 1A         */ brfalse.s  IL_0075                                    ; If they don't match, jump down
IL_005b:  /* 02   |            */ ldarg.0                                               ;/ Load the password into stack
IL_005c:  /* 7B   | (04)000006 */ ldfld      CrackMe.Form1::textBox2                    ;|
IL_0061:  /* 6F   | (0A)00004C */ callvirt   Control::get_Text()                        ;\
IL_0066:  /* 72   | (70)0001FF */ ldstr      "backdoor"                                 ; Load the string "backdoor" into stack
IL_006b:  /* 28   | (0A)00004D */ call       String::op_Equality(string, string)        ; Compare both values
IL_0070:  /* 16   |            */ ldc.i4.0
IL_0071:  /* FE01 |            */ ceq
IL_0073:  /* 2B   | 01         */ br.s       IL_0076
IL_0075:  /* 17   |            */ ldc.i4.1
IL_0076:  /* 0B   |            */ stloc.1
IL_0077:  /* 07   |            */ ldloc.1
IL_0078:  /* 2D   | 0F         */ brtrue.s   IL_0089                                    ; If the backdoor creds doesn't match, jump down
IL_007a:  /* 00   |            */ nop
IL_007b:  /* 72   | (70)000211 */ ldstr      "I see you've found my backdoor. Nice."    ;/ Backdoor message
IL_0080:  /* 28   | (0A)00004E */ call       MessageBox::Show(string)                   ;|
IL_0085:  /* 26   |            */ pop                                                   ;\ pop the MessageBox result from the stack
IL_0086:  /* 17   |            */ ldc.i4.1                                              ;/ Set the bad message flag to 1 (TRUE)
IL_0087:  /* 0A   |            */ stloc.0                                               ;\
IL_0088:  /* 00   |            */ nop
IL_0089:  /* 06   |            */ ldloc.0                                               ; More code...

And finally the bad message code:
IL codeIL_0089:  /* 06   |            */ ldloc.0                                         ;/ Load 0 into the stack
IL_008a:  /* 0B   |            */ stloc.1                                         ;|
IL_008b:  /* 07   |            */ ldloc.1                                         ;| Load the bad message flag
IL_008c:  /* 2D   | 0D         */ brtrue.s   IL_009b                              ;\ If they match, show the bad message
IL_008e:  /* 00   |            */ nop
IL_008f:  /* 72   | (70)00025D */ ldstr      "Nope. Guessing is not allowed."     ;/ The bad message
IL_0094:  /* 28   | (0A)00004E */ call       MessageBox::Show(string)             ;|
IL_0099:  /* 26   |            */ pop                                             ;\ pop the MessageBox result from the stack
IL_009a:  /* 00   |            */ nop
IL_009b:  /* 2A   |            */ ret

Now, to show the correct message I have to replace that "Nope. Guessing is not allowed." with the result of EncryptString(string, string)
But to do that I need to also provide the username twice.
If that was a x86 assembly code, compiled by some high level language, there would be plenty of code caves where I can write my shellcode that overwrites the bad message.
Unfortunately, in .NET we don't have such freedom so I'll have to think of a better solution.
Luckily, the code is written pretty stretchy, so what I can do is to optimize it a bit and therefore gain some space where I can put my shellcode.

The first place where this can be done, is here:
IL codeIL_000e:  /* 02   |            */ ldarg.0
IL_000f:  /* 7B   | (04)000005 */ ldfld      CrackMe.Form1::textBox1
IL_0014:  /* 6F   | (0A)00004C */ callvirt   Control::get_Text()
IL_0019:  /* 02   |            */ ldarg.0
IL_001a:  /* 7B   | (04)000005 */ ldfld      CrackMe.Form1::textBox1
IL_001f:  /* 6F   | (0A)00004C */ callvirt   Control::get_Text()

Instead double calling the same get_Text(), I can replace the second one with the DUP instruction like that:
IL codeIL_000e:  /* 02   |            */ ldarg.0
IL_000f:  /* 7B   | (04)000005 */ ldfld      CrackMe.Form1::textBox1
IL_0014:  /* 6F   | (0A)00004C */ callvirt   Control::get_Text()
IL_0019:  /* 25   |            */ dup                                 ; Duplicate the last stack value, eg. get_Text(textBox1)
IL_001a:  /* 90   |            */ nop
IL_001b:  /* 90   |            */ nop
IL_001c:  /* 90   |            */ nop
IL_001d:  /* 90   |            */ nop
IL_001e:  /* 90   |            */ nop
IL_001f:  /* 90   |            */ nop
IL_0020:  /* 90   |            */ nop
IL_0021:  /* 90   |            */ nop
IL_0022:  /* 90   |            */ nop
IL_0023:  /* 90   |            */ nop

And I got ten free bytes for my code. Cool.

Now, because I need the correct serial code calculated by EncryptString() I can also dup it, and later use it as parameter for the bad MessageBox.
On first glance, that sounds good, but then again we have a stack to look at, or it will crash the program.
Lets see the logic then.
code flow1. Password is pushed to the stack
   stack contains 1 value

2. Username is pushed to the stack (twice)
   stack contains 3 values

3. Encryption::EncryptString() POPs the last two stack values (username) and PUSHes the result back to stack
   stack contains 2 values

4. String::op_Equality() POPs the last two stack values (username and valid serial) and PUSHes the result back to stack
   stack contains 1 value

5. ldc.i4.0;ceq;stloc.1;ldloc.1;brtrue.s do their job using the above stack value, and POPs it out
   stack contains no values

So, if I just dup EncryptString's result, then the op_Equality's will compare the two EncryptString results instead of the user password and one of the EncryptString results.

The best workaround I can think of was to change the code flow a bit.
First, I'll get the username twice, encrypt it, and finally dup the result:
IL codeldarg.0
ldfld      CrackMe.Form1::textBox1
callvirt   Control::get_Text()
dup
call       Encryption::EncryptString(string, string)
dup

The stack will then contain the correct password - twice.

And to perform the correct comparison, I'll change the code like this:
IL codeldarg.0
ldfld      CrackMe.Form1::textBox2
callvirt   Control::get_Text()
call       String::op_Equality(string, string)

The code below continues as usual, and now after brtrue.s I'll have my valid serial as a last stack value, so this code:
IL codeIL_008e:  /* 00   |            */ nop
IL_008f:  /* 72   | (70)00025D */ ldstr      "Nope. Guessing is not allowed."
IL_0094:  /* 28   | (0A)00004E */ call       MessageBox::Show(string)
IL_0099:  /* 26   |            */ pop

can be changed to:
IL codeIL_008e:  /* 00   |            */ nop
IL_008f:  /* 00   |            */ nop       ;/ I no longer need the bad message
IL_0090:  /* 00   |            */ nop       ;| because now, my valid serial
IL_0091:  /* 00   |            */ nop       ;| is the bad message :)
IL_0092:  /* 00   |            */ nop       ;|
IL_0093:  /* 00   |            */ nop       ;\
IL_0094:  /* 28   | (0A)00004E */ call       MessageBox::Show(string)
IL_0099:  /* 26   |            */ pop

When I first did these changes I was very surprised when upon pressing the "Check!" button, the program crashes instead of showing me the correct serial.

Few hundred Bulgarian swear words later (most of them where pretty juicy!), and the word "stack" came to my mind.

(Later that day I creatively produced the douchebag reply "fuck JIT, fuck IL, fuck you too." as an answer to a Skype message from a friend of mine.)

Sooo, the stack.
Because I changed the code to put another value in the stack, if the user enters the correct login (or the backdoor one), upon exit the stack will still hold this one value in it.
And that's bad.
In this case, I need to POP it out.

Back to the IL code, but this time with the changes I've made.
IL codeIL_008c:  /* 2D   | 0D         */ brtrue.s   IL_009b
IL_008e:  /* 00   |            */ nop
IL_008f:  /* 00   |            */ nop       ;/ I no longer need the bad message
IL_0090:  /* 00   |            */ nop       ;| because now, my valid serial
IL_0091:  /* 00   |            */ nop       ;| is the bad message :)
IL_0092:  /* 00   |            */ nop       ;|
IL_0093:  /* 00   |            */ nop       ;\
IL_0094:  /* 28   | (0A)00004E */ call       MessageBox::Show(string)
IL_0099:  /* 26   |            */ pop
IL_009b:  /* 2A   |            */ ret

The brtrue.s decides where the code flow should go, depending on the bad message flag.
If the serial is wrong, the code will continue to IL_008e, where the MessageBox will pop the last stack value to use it as parameter.
And that's perfectly fine.

But if the user enters a correct serial, this message box will be skipped, and the last stack value will remain present, causing the app to crash.
There's a handy little POP after the MessageBox, so I can use it in case the user fills in the correct serial.

So, I can change the branch offset pointed by that brtrue.s at IL_008c, from 0x0D, to 0x0B (eg. two bytes later, where the POP is):
IL codeIL_008c:  /* 2D   | 0B         */ brtrue.s   IL_0099
IL_008e:  /* 00   |            */ nop
IL_008f:  /* 00   |            */ nop
IL_0090:  /* 00   |            */ nop
IL_0091:  /* 00   |            */ nop
IL_0092:  /* 00   |            */ nop
IL_0093:  /* 00   |            */ nop
IL_0094:  /* 28   | (0A)00004E */ call       MessageBox::Show(string)
IL_0099:  /* 26   |            */ pop
IL_009a:  /* 00   |            */ nop
IL_009b:  /* 2A   |            */ ret

and done!

Also, the patch is a farly easy one:



I think that's everything worth writing about this one.

Source code and other goodies can be downloaded from here: http://nullsecurity.org/download/c7a1d7b53f21acd15b95146611b15b66

Comments

* You have an opinion? Let us all hear it!

There's no comments on this article yet. Be the first!
© nullsecurity.org 2011-2024 | legal | terms & rules | contacts