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!