Sunday, November 6, 2016

Join ESET Crackme 2015 Solution

Introduction 

This is my solution for the JoinESET Crackme 2015, It was on my disk for more than a year and I'm sharing it maybe someone is interested in how to solve this challenge.

Working files can be downloaded from here.

Stage One 


The crackme has two files EsetCrackme2015.exe and EsetCrackme2015.dll. Both don’t
seem to be packed.


The crackme asks for three passwords, lets hunt the first password.

The executable loads the DLL, which has multiple files appended to it. To enumerate all the files we have first the file ID which is two bytes then four bytes that represent the file size followed by the file.
I coded a small tool to dump all the files and here is the ID list 0x1, 0x2, 0x3, 0x4, 0x101, 0x102, 0x103, 0x104, 0x151, 0x152, 0x153, 0x154, 0x155, 0xAA02, 0xAA06, 0xBB01, 0xBB02, 0xFF00, 0xFF02, 0xFF04 and 0xFF05.

File 0x2 is the name of the pipe, “\\.\pipe\EsetCrackmePipe” that will be created and will be used to communicate with the different crakme's processes.
File 0x3 contains "SXJyZW4lMjBpc3QlMjBtZW5zY2hsaWNo" or "Irren%20ist%20menschlich" base64 decoded, and that string will be used to decrypt later files.

The file 0x151 is an executable that gets injected into SVCHOST.exe through a virtual machine which its engine resides in the file 0x102 and opcode in 0x103. I will explain the virtual machine later in stage two.

The pipe works as follows, the main module waits for either a file request (0x1) or STATUS_COMPLETE (0x2) followed by the file ID and responds with the file requested or OK message in case of STATUS_COMPLETE.

The injected code in svchost.exe starts with retrieving the handle to a window, whose class name is EDIT and then uses SetWindowLong to change the address of the window procedure to 0x00AD2360.
The files 0xBB01 and 0xBB02, both gets decrypted by xoring with “PIPE”. The file 0xBB01 contains the hashes of the three passwords respectively as follows:

869B39E9F2DB16F2A771A3A38FF656E050BB1882
0F30181CF3A9857360A313DB95D5A169BED7CC37 0B6A1C6651D1EB5BD21DF5921261697AA1593B7E

The crackme hashes every entered password using a hash algorithm similar to SHA-1 and compares it with the hashes above.
When entering the first password we get the window procedure 0x00AD2360 triggered. What it do is it base64 encode the password then decrement each odd indexed character, finally it compares it with RFV1aV4fQ1FydFxk which it got from the file 0xBB02.

By reversing this operation we get our first password “Devin Castle”

Upon entering the right password a STATUS_COMPLETE with ID 0xBB01 is sent through the pipe, declaring that we have completed stage one and a drv.zip is written to disk.
The procedure that handles the progress of the crackme is loaded from the file 0x101.



Stage Two


Unpacking the drv.zip file, we will have three files crackme_drv.inf, crackme_drv.sys, and INSTALL.ME which tells us to install the driver as a legacy driver.

Before the installing I will patch the driver entry with 0xCC, an INT3, to trigger the debugger at the start of driver. But before the debugging we have to do a deeply static analysis first.


By loading the driver into IDA we can see the procedures that will handle certain IRP requests.
I will start by analyzing AddDevice.

AddDevice



A new device is created with DeviceType FILE_DEVICE_DISK. However the disk will not appear besides the other partitions as the driver doesn't create a symbolic link for it. For the disk to appear, I used DefineDosDevice(DDD_RAW_TARGET_PATH,"F:","\\Device\\45736574").

The driver requests the file 0xAA02 and decrypts it using RC4 with the Key “3531_4ever”, ESET_ForEver, that file contains the name of registry key “ESETConst”.

Now the driver assumes its initialization process is done successfully and so it sends STATUS_COMPLETE message through the pipe with ID 0xAA10. At the user mode side the main module writes PunchCardReader.exe and PuncherMachine.exe to the same directory of the crackme, and PunchCard.bmp to the newly created disk.

The driver keeps reading the registry key “\REGISTRY\MACHINE\SYSTEM\ControlSet001\Services\crackme_drv\EsetCrackme\ESETConst”, checks for type string then writes the value to a global variable.


The VMEngine and VMOpCode gets decrypted and loaded into memory. That VMEngine is slightly different than the one found in the crackme's main module. I believe that the driver's is the updated version. File 0xAA06 is requested, decrypted and copied to VMOpCode at offset 0x26D.

By that we are done with the analysis of AddDevice.

IRP_MJ_READ_WRITE


The driver does the following; In case of a write request it proceeds with the operation. In case of a read request it proceeds normally if the read offset is smaller than 0x13200, if larger it checks that the registry key “ESETConst” has been read successfully and the VMEngine has been loaded. If any of the checks failed the driver returns random data.

The data starts from offset 0x13200 serves as encrypted data that gets decrypted through the VM using a decryption key which the registry key “ESETConst” holds.



I will not jump into the details of the VMEngine, I will just paste the built code of what the OpCodes presents.


0x0 Cmp D_R12,0
0x4 Je 0x8a
0xa Cmp D_R13,0
0xe Je 0x11e
0x14 Mov D_R2, D_R12
0x17 Mov D_R3, $295
0x1e Mov D_R4, $268
0x25 Mov D_R5, D_R2
0x28 Add D_R5,D_R13
0x2b Mov D_R14, D_R3
0x2e Mov D_R0, $291
0x35 Add D_R14,DWORD PTR [R0]
0x38 Mov D_R15, D_R4
0x3b Add D_R15,4
0x3f Cmp D_R2,D_R5
0x42 Jnz 0x4b
0x48 Mov D_R2, D_R12
0x4b Cmp D_R4,D_R15
0x4e Jnz 0x5b
0x54 Mov D_R4, $268
0x5b Cmp D_R3,D_R14
0x5e Je 0x100
0x64 Mov D_R0, BYTE PTR [R2]//Key
0x67 Xor D_R0,BYTE PTR [R3]//$295
0x6a Add D_R0,1
0x6e INST_5 B_R0,1
0x72 Xor D_R0,BYTE PTR [R4]//$268
0x75 Mov BYTE PTR [R3], B_R0
0x78 Add D_R2,1
0x7c Add D_R3,1
0x80 Add D_R4,1
0x84 Jmp 0x3f
0x8a Mov D_R3, $271
0x91 Mov D_R4, $268
0x98 Mov D_R14, D_R3
0x9b Mov D_R0, $26d
0xa2 Add D_R14,DWORD PTR [R0]
0xa5 Mov D_R15, D_R4
0xa8 Add D_R15,4
0xac Cmp D_R4,D_R15
0xaf Jnz 0xbc
0xb5 Mov D_R4, $268
0xbc Cmp D_R3,D_R14
0xbf Je 0xe8
0xc5 Mov D_R0, 0
0xc9 Xor D_R0,BYTE PTR [R3]//$271
0xcc Add D_R0,1
0xd0 INST5_ B_R0,1
0xd4 Xor D_R0,BYTE PTR [R4]//$268
0xd7 Mov BYTE PTR [R3], B_R0
0xda Add D_R3,1
0xde Add D_R4,1
0xe2 Jmp 0xac
0xe8 Mov D_R0, $26d
0xef Mov D_R0, DWORD PTR [R0]
0xf2 PUSH D_R0
0xf4 PUSH $271
0xfa Jmp 0x112
0x100 Mov D_R0, $291
0x107 Mov D_R0, DWORD PTR [R0]
0x10a PUSH D_R0 //0x12
0x10c PUSH $295 //R_Key
0x112 PUSH D_R11//0x200
0x114 PUSH D_R10 //Ram2
0x116 Call Part2

The VM is 32bit and consisted of 15 register. The last six registers are used as arguments.
A D_, W_ and B_ represent the size of the register. The dollar sign represents an offset in the VMOpCode.

What the above code do is it checks whether argument three (R_12) and four (R_13) are supplied. Argument three is the decryption key got from “ESETConst” and argument four is its length. In case it’s supplied the following algorithm is used to calculate a key:

unsigned char $295[] = {0xD0,0xFC,0xF9,0xF7,0x63,0xED,0x71,0xFA,0xD6,0xAE,0xE6,0x62,0x36,0xFB,0x63,0x7F,0x77,0xE9};
unsigned char $268[] = {0x45,0x54,0x53,0x45};
for(int i=0;i<0x12;i++)
{
Key[i] ^= $295[i];
Key[i]++;
Key[i] = ( Key[i] << (1 & 0xFF) ) | ( (((signed char )Key[i])&0XFF) >> (unsigned char)( (8-1)&0xFF ) );//INST_5(Key[i],1)
Key[i] ^= $268[i%4];
}

Then the resultant key is used to decrypt the encrypted disk section using RC4 algorithm.


In case there is no decryption key is supplied the VM generates the right decryption key as follows:

unsigned char $271[] = {0x82,0x99,0x8F,0x92,0x11,0x9E,0x18,0x94,0xB1,0x8E,0x8F,0x11,0x16,0x9C,0x11,0x1A,0x16,0x9D,0xA8,0xA1,0xA1,0x29,0xA8,0xA1,0xA1,0x29,0xA8,0xA1,0xA1,0x29,0xA8,0xA1};
unsigned char $268[] = {0x45,0x54,0x53,0x45};
unsigned char Key[] = {0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00};
for(int i=0;i<0x12;i++)
{
Key[i] ^= $271[i];
Key[i]++;
Key[i] = ( Key[i] << (1 & 0xFF) ) | ( (((signed char )Key[i])&0XFF) >> (unsigned char)( (8-1)&0xFF ) );//INST_5(Key[i],1)
Key[i] ^= $268[i%4];
}


The second stage key will be “Barbakan Krakowski” and the ESETConst will be “Reversing is great”. By that, stage two key has been found but just before finishing this part, I would like to post the rest of the VM code which is a simple decryptor that decrypts the next VM OpCode followed by RC4 algorithm.

Part2:
0x0 Mov D_R0, D_R7 //Opcode Start $131
0x3 Mov D_R1, BYTE PTR [R0]
0x6 Add D_R0,1
0xa Mov D_R2, BYTE PTR [R0]
0xd Add D_R0,5
0x11 Mov D_R3, DWORD PTR [R0]
0x14 Mov D_R0, D_R8 //0x137
0x17 Add D_R0,D_R7 
0x1a Add D_R3,D_R7
0x1d Mov D_R4, BYTE PTR [R3]
0x20 Xor D_R4,D_R1
0x23 Add D_R1,D_R2
0x26 Mov BYTE PTR [R3], B_R4
0x29 Add D_R3,1
0x2d Cmp D_R3,D_R0
0x30 Jnb 0x1d
0x36 Mov D_R0, D_R7
0x39 Mov BYTE PTR [R0], 37
0x3d Add D_R0,1
0x41 Mov BYTE PTR [R0], 13
0x45 Exit
0x46 malloc 100//R0 has the Result
0x4a Mov D_R5, D_R0
0x4d Mov D_R1, 0
0x51 Mov BYTE PTR [R0], B_R1//Sets table from 0 to 0x100
0x54 Add D_R0,1
0x58 Add D_R1,1
0x5c Cmp D_R1,100
0x61 Jnb 0x51
//Sets RC4 Table
0x67 Mov D_R0, 0
0x6b Mov D_R1, 0
0x6f Mov D_R4, D_R12 //R_Key
0x72 Mov D_R14, D_R4 //R_Key
0x75 Add D_R14,D_R13 //R13 = 0x12
0x78 Cmp D_R4,D_R14
0x7b Jnz 0x84
0x81 Mov D_R4, D_R12
0x84 Mov D_R3, D_R5
0x87 Add D_R3,D_R0
0x8a Add D_R1,BYTE PTR [R3]
0x8d Add D_R1,BYTE PTR [R4]
0x90 Modulus D_R1,100 //Result Saved in Destination
0x95 Mov D_R3, D_R5
0x98 Add D_R3,D_R0
0x9b Mov D_R15, D_R5
0x9e Add D_R15,D_R1
0xa1 Mov D_R2, BYTE PTR [R3]
0xa4 Mov BYTE PTR [R3], BYTE PTR [R15]
0xa7 Mov BYTE PTR [R15], B_R2
0xaa Add D_R0,1
0xae Add D_R4,1
0xb2 Cmp D_R0,100
0xb7 Jnb 0x78
0xbd Mov D_R0, 0
0xc1 Mov D_R1, 0
0xc5 Mov D_R3, D_R10//Ram2
0xc8 Mov D_R4, D_R3
0xcb Add D_R4,D_R11//R_11 = 0x200
0xce Cmp D_R3,D_R4
0xd1 Je 0x122
0xd7 Add D_R0,1
0xdb Modulus D_R0,100 //Result Saved in Destination
0xe0 Mov D_R2, D_R5
0xe3 Add D_R2,D_R0
0xe6 Add D_R1,BYTE PTR [R2]
0xe9 Modulus D_R1,100 //Result Saved in Destination
0xee Mov D_R14, D_R5
0xf1 Add D_R14,D_R1
0xf4 Mov D_R15, BYTE PTR [R2]
0xf7 Mov BYTE PTR [R2], BYTE PTR [R14]
0xfa Mov BYTE PTR [R14], B_R15
0xfd Mov D_R15, 0
0x101 Add D_R15,BYTE PTR [R2]
0x104 Add D_R15,BYTE PTR [R14]
0x107 Modulus D_R15,100 //Result Saved in Destination
0x10c Add D_R15,D_R5
0x10f Mov D_R15, BYTE PTR [R15]
0x112 Xor D_R15,BYTE PTR [R3]
0x115 Mov BYTE PTR [R3], B_R15
0x118 Add D_R3,1
0x11c Jmp 0xce
0x122 Free D_R5
0x124 Exit

Stage Three

In stage three we have PunchCardReader.exe, PuncherMachine.exe, and PunchCard.bmp that has been decrypted from stage two using RC4 with key “Barbakan Krakowski”.



The PunchCard.bmp is a 4 bit bmp image with dimensions of 600x259. Nothing catches my attention there so let’s move to other files.


Punch Card Reader

Let’s start by analyzing the PunchCardReader. By pressing the “read punch cards” button, a verification error pops up. It is time for taking a look on what happens inside there. I used Telerik JustDecompile as a free alternative to reflector and it just works great!

Obfuscation

Both PunchCardReader and PuncherMachine are obfuscated. The names of the classes, variables and methods are obfuscated. Code flow obfuscation is also implemented using a switch case as the following code snippet.


int num3 = 920829957;
while (true)
{
switch (num3 ^ 920829955)
{
case 0:
{
num3 = 920829954;
continue;
}
case 1:
{
if (num1 < str1.Length)
{
goto case 10;
}
num2++;
num++;
num3 = 920829959;
continue;
}
case 2:
{
num1++;
num3 = 920829954;
continue;
}
case 3:
{
str1 = str;
num3 = 920829955;
continue;
}
...
...
...
case 12:
{
num3 = 920829959;
continue;
}
default:
{
return;
}
}
}

As you see the code is chunked and at the end of each block num3, which is used as the flow director, is assigned with the ID of the next block.

I thought at first of coding a control flow deobfuscator but I then realized that it will be waste of time to code a one for this crackme only and since also mostly the order of the execution of the blocks wasn't important. You can already guess the general idea of what the function do. I deobfuscated manually the interesting functions only that I will post later.

For easier analysis I used de4dot and a tool by Kurapika called .NET DeObfuscator to rename the obfuscated names. Deobfuscated files are attached for reference.

The strings are also obfuscated. They are joined into one byte array and encrypted using a simple xor. The byte array gets decrypted in the memory and when a functions wants to retrieve a string it passes the index of the string and its size.

AntiDebug

At the entrypoint there is a debugger check using Debugger.IsAttached and Debugger.IsLogging.
The procedure Class_14_Object.Procedure_3 is fired at start to constantly check for debugger and terminates the process if found any. At Class_14_Object.Function_Byte_4 couple of functions IL bytes are hashed using md5 and the hash will be used later to decrypt files.

That's for the Initialization process now let’s move on to event handlers.

OnLoad

public Class_15_Object(Class_8_Form Parameter44)
{
this.Field_4 = false;
this.Field_5 = Parameter44;
try
{
this.Field_0 = new Class_2_Object();//Generates HashTable
Assembly assembly = this.Function_Assembly_2(); Loads Assembly
if (assembly == null)
{
this.Procedure_6();
}
else
{
this.Field_2 = assembly;
}
}
catch (Exception exception)
{
this.Procedure_6();
}
}

Class_2_Object generates a constant Hash Table, Function_Assembly_2 request the file with ID 0xFF05 through the PIPE “\\.\pipe\EsetCrackmePipe.” then decrypts it using the key that was generated earlier from hashing the procedures IL bytes. The encryption algorithm is AES.

To be able to sniff that key I patched the anti-debug check at the entrypoint then loaded the executable into DILE and set a breakpoint on Class_14_Object.Function_Byte_4. Since the entry point is one of the procedures that get hashed, I had to restore the patched bytes. I just stepped till the procedure bytes get loaded into memory and then restored the bytes using cheat engine.

The decryption key is “a26d11dee294284f38db8a724c119d74”.

The decrypted file is attached as “ff05_AES_PunchCardReader_DynMethod.exe”

Now let’s move on to the procedure the handles the button

Procedure_4


protected void Procedure_4(object Parameter46)
{
try
{
Class_15_Object.Class_16_MulticastDelegate<bool> class16MulticastDelegate = this.Function_Class_16_5(this.Field_0.Function_String_1());
if (class16MulticastDelegate())
{
((Class_2_Object.Class_3_MulticastDelegate)Parameter46).Procedure_1(Class_2_Object.Enumeration_1.Member_2, null);
}
}
catch (Exception exception)
{
((Class_2_Object.Class_3_MulticastDelegate)Parameter46).Procedure_1(Class_2_Object.Enumeration_1.Member_3, exception);
}
this.Field_6.Set();
}


If the class16MulticastDelegate is true we will get our verification passed

Function_Class_16_5 uses Function_String_1 output string as a parameter.


public string[] Function_String_1()
{
int num = 0;
Bitmap bitmap = null;
byte[] numArray = null;
StringBuilder stringBuilder = null;
int num1 = 0;
int num2 = 0;
int num3 = 0;
int num4 = 0;
List<string> strs = new List<string>();
num = 0;
while (num < 1000 || !this.Field_12)
{
string str1 = string.Format(Class_21_Object.Function_String_88(), Convert.ToString(num).PadLeft(3, '0'));//Punch_Card_{0}.bmp
if (!File.Exists(str1))
{
return strs.ToArray();
}
bitmap = new Bitmap(str1, true);

if (bitmap.PixelFormat != PixelFormat.Format4bppIndexed || bitmap.Width != 600 || bitmap.Height != 259)
{
throw new Class_4_Exception(Class_21_Object.Function_String_89());
}
BitmapData bitmapDatum = bitmap.LockBits(new Rectangle(0, 0, bitmap.Width, bitmap.Height), ImageLockMode.ReadOnly, bitmap.PixelFormat);
numArray = new byte[bitmapDatum.Height * bitmapDatum.Stride];
Marshal.Copy(bitmapDatum.Scan0, numArray, 0, (int)numArray.Length);
bitmap.UnlockBits(bitmapDatum);
stringBuilder = new StringBuilder();
num1 = 22;
while (num1 < 582)
{
num2 = 0;
num3 = 1;
num4 = 197;
while (num4 > -7)//8
{
if (this.Function_Boolean_2(numArray, num1, num4))
{
num2 = num2 | num3;
}
num3 = num3 << 1;
num4 = num4 - 17;
}
if (this.Field_13.ContainsKey(num2))
{
stringBuilder.Append(this.Field_13[num2]);
}
num1 = num1 + 7;
}
string str = stringBuilder.ToString();
char[] chrArray = new char[] { ' ' };
strs.Add(str.TrimEnd(chrArray));
bitmap.Dispose();
num++;
}
return strs.ToArray();
}


This function simply iterates through all files named Punch_Card_{0}.bmp with dimensions 600*259, as our PunchCard.bmp, and do the following,
It iterates through certain pixels and every pixel is used to set a bit so that eight pixels form a byte.

this.Function_Boolean_2 Decides whether the pixel will set the bit or not

protected bool Function_Boolean_2(byte[] Parameter1, int Parameter2, int Parameter3)
{
int parameter3 = 300 * Parameter3 + (Parameter2 >> 1);
if ((Parameter2 & 1) == 0)//Parameter2 = Column
{
return (Parameter1[parameter3] & 240) == 0;
}
return (Parameter1[parameter3] & 15) == 0;
}


If the column is even and the left nibble of the pixel is zero the bit is set.
If the column is odd and the right nibble of the pixel is zero the bit is set.

The generated byte is then used as a key for a constant hash table that was generated earlier.

Each key is corresponded to a charterer. A string is then formed.

private Class_15_Object.Class_16_MulticastDelegate<bool> Function_Class_16_5(string[] Parameter47)
{
DynamicMethod dynamicMethod = null;
MethodInfo method = this.Field_2.GetType(Class_21_Object.Function_String_109()).GetMethod(Class_21_Object.Function_String_110());//110 CreateMethod // 109 DynMethodFactory
object[] parameter47 = new object[] { Parameter47 };
dynamicMethod = (DynamicMethod)method.Invoke(null, parameter47);
return (Class_15_Object.Class_16_MulticastDelegate<bool>)dynamicMethod.CreateDelegate(typeof(Class_15_Object.Class_16_MulticastDelegate<bool>));
}

DynMethodFactory.CreateMethod gets invoked with the pixels generated string as a parameter.

public static DynamicMethod createMethod(string[] instructions)
{
OpCode[] array = (
from x in (IEnumerable<FieldInfo>)typeof(OpCodes).GetFields(BindingFlags.Static | BindingFlags.Public)
where x.FieldType == typeof(OpCode)
select (OpCode)x.GetValue(null) into x
where !x.Name.Equals("break")
select x).ToArray<OpCode>();
Hashtable hashtables = new Hashtable();
OpCode[] opCodeArray = array;
for (int i = 0; i < (int)opCodeArray.Length; i++)
{
OpCode opCode = opCodeArray[i];
hashtables.Add(opCode.Name, opCode);
}
Type[] typeArray = new Type[0];
DynamicMethod dynamicMethod = new DynamicMethod("", typeof(bool), typeArray);
ILGenerator lGenerator = dynamicMethod.GetILGenerator();
MethodInfo method = typeof(Encoding).GetMethod("get_ASCII", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic, null, new Type[0], null);
Type type = typeof(Encoding);
Type[] typeArray1 = new Type[] { typeof(string) };
MethodInfo methodInfo = type.GetMethod("GetBytes", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, typeArray1, null);
Type type1 = typeof(BitConverter);
Type[] typeArray2 = new Type[] { typeof(byte[]), typeof(int) };
MethodInfo method1 = type1.GetMethod("ToUInt32", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic, null, typeArray2, null);
lGenerator.DeclareLocal(typeof(uint));
lGenerator.DeclareLocal(typeof(uint));
lGenerator.DeclareLocal(typeof(uint));
lGenerator.DeclareLocal(typeof(uint));
lGenerator.DeclareLocal(typeof(uint));
lGenerator.DeclareLocal(typeof(uint));
lGenerator.DeclareLocal(typeof(byte[]));
lGenerator.DeclareLocal(typeof(uint));
lGenerator.DeclareLocal(typeof(bool));
lGenerator.DeclareLocal(typeof(bool));
Label label = lGenerator.DefineLabel();
Label label1 = lGenerator.DefineLabel();
lGenerator.Emit(OpCodes.Nop);
lGenerator.Emit(OpCodes.Ldc_I4, 57005);//0xDEAD
lGenerator.Emit(OpCodes.Stloc_0);
lGenerator.Emit(OpCodes.Ldc_I4, 48879);//0xBEAF
lGenerator.Emit(OpCodes.Stloc_1);
lGenerator.Emit(OpCodes.Ldc_I4, 51966);//0xCAFE
lGenerator.Emit(OpCodes.Stloc_2);
lGenerator.Emit(OpCodes.Ldc_I4, 47806);//0xBABE
lGenerator.Emit(OpCodes.Stloc_3);
lGenerator.Emit(OpCodes.Ldc_I4, 64206);//0xFACE
lGenerator.Emit(OpCodes.Stloc_S, 4);
lGenerator.Emit(OpCodes.Ldloc_0);
lGenerator.Emit(OpCodes.Ldloc_1);
try
{
lGenerator.Emit((OpCode)hashtables[instructions[0]]);
}
catch (Exception exception)
{
}
lGenerator.Emit(OpCodes.Ldloc_2);
lGenerator.Emit(OpCodes.Ldloc_3);
try
{
lGenerator.Emit((OpCode)hashtables[instructions[1]]);
}
catch (Exception exception1)
{
}
lGenerator.Emit(OpCodes.Xor);
lGenerator.Emit(OpCodes.Ldloc_S, 4);
lGenerator.Emit(OpCodes.Xor);
lGenerator.Emit(OpCodes.Ldc_I4, -229612108);//0xf25065b4
lGenerator.Emit(OpCodes.Xor);
lGenerator.Emit(OpCodes.Stloc_S, 5);
lGenerator.Emit(OpCodes.Call, method);
lGenerator.Emit(OpCodes.Ldstr, "ESET");
lGenerator.Emit(OpCodes.Callvirt, methodInfo);
lGenerator.Emit(OpCodes.Stloc_S, 6);
lGenerator.Emit(OpCodes.Ldloc_S, 6);
lGenerator.Emit(OpCodes.Ldc_I4_0);
lGenerator.Emit(OpCodes.Call, method1);
lGenerator.Emit(OpCodes.Stloc_S, 7);
lGenerator.Emit(OpCodes.Ldloc_S, 5);
lGenerator.Emit(OpCodes.Ldloc_S, 7);
lGenerator.Emit(OpCodes.Ceq);
lGenerator.Emit(OpCodes.Ldc_I4_0);
lGenerator.Emit(OpCodes.Ceq);
lGenerator.Emit(OpCodes.Stloc_S, 9);
lGenerator.Emit(OpCodes.Ldloc_S, 9);
lGenerator.Emit(OpCodes.Brtrue_S, label);
lGenerator.Emit(OpCodes.Ldc_I4_1);
lGenerator.Emit(OpCodes.Stloc_S, 8);
lGenerator.Emit(OpCodes.Br_S, label1);
lGenerator.MarkLabel(label);
lGenerator.Emit(OpCodes.Ldc_I4_0);
lGenerator.Emit(OpCodes.Stloc_S, 8);
lGenerator.Emit(OpCodes.Br_S, label1);
lGenerator.MarkLabel(label1);
lGenerator.Emit(OpCodes.Ldloc_S, 8);
try
{
lGenerator.Emit((OpCode)hashtables[instructions[2]]);
}
catch (Exception exception2)
{
}
return dynamicMethod;
}

What createMethod do is it simply generates a dynamic method and uses the pixels generated strings as the name for three instructions. The third one is easy to guess, a “Ret”.
The generated dynamicMethod does some arithmetic on six constants and compares the result with 0x45534554(ESET). However there are two arithmetic operations that we had to guess. I of course didn't guess them … I bruteforced them!

The first one is “Mul” and the second is “Add” and the third that I already guessed “Ret”.

And now we can get the dynamicMethod returns true.

Now, just one last thing before finishing the analysis of this file. Class_10_Object.Function_Boolean_2 sends an STATUS_COMPLETE message with ID 0xFF01 through the pipe indicating that the verification has been passed.

Now that all we need to know about PunchCardReader. Sure we can generate valid punch cards but that will be the job of the PuncherMachine!


PuncherMachine

The PuncherMachine uses the same anti-debug as the PunchCardReader. Let’s have a look on Class_18 which gets loaded during the initialization through Onload Event Handler.

Class_18

public Class_18(Class_3 a_0)
{
this.Field_6 = false;
this.Field_7 = a_0;
try
{
this.Field_10 = new ArrayList();
this.Field_8 = new ManualResetEvent(true);
byte[] numArray = this.Function_3();
if (numArray != null)
{
File.WriteAllBytes(Class_25.Function_130(), numArray);
using (MD5 mD5 = MD5.Create())
{
this.Field_3 = mD5.ComputeHash(numArray);
}
}
else if (this.Field_3 == null)
{
this.Procedure_8();
}
Assembly assembly = this.Function_4();
if (assembly == null)
{
this.Procedure_8();
}
else
{
this.Field_4 = assembly;
}
}
catch (Exception exception)
{
this.Procedure_8();
}
}


this.Function_3 simply requests two files 0xFF01 and 0xFF02 and returns 0xFF01 file bytes. 0xFF02 contains the hash “95eceaa118dd081119e26be1c44da2cb” while 0xFF01 doesn't exist in the DLL. In case 0xFF01 exists it will be written to PunchCard.bmp and the md5 hash will be calculated and saved in Field_3 instead of 0xFF02 hash.

At the start of the Puncher Machine it asks for punch card, by selecting the punch card we got from stage 2 we get an invalid punch card message as the md5 hash of the file isn't equal to “95eceaa118dd081119e26be1c44da2cb” and there is no 0xFF01 file.
Okay no problem, we will add the PunchCard.bmp from stage two as the file 0xFF01 but first we have to do couple of things to get it working.
First we have to set all the pixels to 0xFF, this step is for later punching. Second we need to encrypt the bmp with the key “3cc021f8bc623ec0f5450c55418ba120”.
Just after the end of the last file in the DLL we will write the file ID 0xFF01 then the file size and finally paste the encrypted bmp. The modified DLL is attached.

Function_4 requests the file 0xFF04, which represents an assembly, gets loaded and decrypted with the key “3cc021f8bc623ec0f5450c55418ba120”.



Now we have a new window, it requests two calibration codes lets jump to the procedure that handles the inputs.

Procedure_9

private void Procedure_9(object sender, EventArgs e)
{
if (this.Field_14.Function_2().Function_2(this.Field_9.Text, this.Field_8.Text))
{
this.Field_3.Visible = true;
this.Field_13.Visible = false;
this.Field_2.Visible = false;
this.Field_4.Visible = true;
this.Field_5.Visible = true;
this.Field_6.Visible = false;
this.Field_14.Function_7();
}
else
{
MessageBox.Show(Class_25.Function_28(), Class_25.Function_27());//Calibration Error // Error!
}
}


Function_2 gets called with the two calibration codes as parameters and the return value decides whether to show Calibration Error or not. So let's jump into Function_2.

public bool Function_2(string string_0, string string_1)
{
int num = 0;
ulong num1 = 0L;
bool flag = false;
string str;
List<uint> nums = new List<uint>();
num = 0;
while (num < string_0.Length)
{
try
{
nums.Add(Convert.ToUInt32(string_0.Substring(num, 8), 16));//Instructions Hashes
}
catch (Exception exception1)
{ 
}
num = num + 8; 
}
if (string_1.Length <= this.Field_12.Count)
{
if (string_1.Length != 0)
{
try
{
Class_6.Class_8<ulong, string> class8 = this.Function_4(nums.ToArray()); //Pushes Instruction Hashes and get the Dynamic Method
this.Procedure_3();
this.Field_15 = new Hashtable();
num = 0;
while (num < this.Field_12.Count)
{
Class_6.Class_8<ulong, string> class81 = class8;
if (num < string_1.Length)
{
string str1 = this.Field_12[num].ToString();
char string1 = string_1[num];
str = string.Concat(str1, string1.ToString());
}
else
{
str = this.Field_12[num].ToString();
}
num1 = class81(str);
if (!this.Field_14.ContainsKey(num1))
{
flag = false;
return flag;
}
this.Field_15.Add(this.Field_12[num], this.Field_14[num1]);//Field_15 is the HashTable used to generate the PunchCard .. Same as PunchCardReader HashTable
num++;
}
flag = true;
return flag;
}
catch (Exception exception)
{
flag = false;
}
return flag;
}
return false;
}
else
{
return false;
}
}

Each eight characters of the first calibration codes are converted to Uint32, that will be used then as instructions hashes ... We will get to that later.

Function_4 invokes DynMethodFactory.CreateMethod from the assembly with ID 0xFF04.


If we had a quick look at CreateMethod we will find that it hashes the OpCode names at the following code

for (int i = 0; i < (int)opCodeArray.Length; i++)
{
OpCode opCode = opCodeArray[i];
hashtables.Add(oneParameter(opCode.Name), opCode);
}

Then just like before, our calibration code is used to generate two instructions.

NOP
Ldc_I8, 2AAAAAAAAAAAAB67
Stloc_0
Ldc_I4_0
Stloc_1
Br_S Label
Label1:
Nop
Ldloc_0 //2AAAAAAAAAAAAB67
XXX 
Ldloc_1
Get_Chars
Conv_u8
Add
Stloc_0
Ldloc_0
Ldc_I8, 2AAAAAAAAAAAAB6F
YYY
Stloc_0
Nop
Ldloc_1
Ldc_I4_1
Add
Stloc_1
label:
Ldloc_1
Ldarg_0
GetLength
Clt
Stloc_3
Ldloc_3
Brtrue_S label1
Ldloc_0
Stloc_2
br_S label2
label2:
Ldloc_2
Ret

Hmm let’s guess the first instruction. Our mysterious instruction, zero pushed into the stack then a call to Get_Chars. So our mysterious instruction must be pushing the argument into the stack. So XXX will be Ldarg_0 which corresponds to the hash 0364abe7.

Apparently the above IL code does some arithmetic on each byte of the supplied string then returns the result. YYY is just after pushing a constant so it likely to be an arithmetic operation, but which one?
It was kind of hard for me to guess the arithmetic instruction at this stage as I don't know any of the expected output results, so for now let's move on.

Back to Function_2, Each character of calibration code two gets joined to its index and supplied to the DynamicMethod.
For example if our calibration code is XYZ then 0X will be supplied to the DynaimcMethod then 1Y and so on.

A new hash table, Field_15, gets its values built from Field_14 hash table and the return of the DynamicMethod, and its keys depend on Field_12 and the index.
That hash table will be used later to punch the cards, so it must be identical to the one in the PunchCardReader!

If we took a look on the values of Field_14 we will find it is very divergent so It must not have been generated using a sub or add instruction. I guessed it might be a “Mul” then I bruteforced the first character of the calibration code till a value that lies within Filed_14 is generated and luckily I found the first one, an “I”.
Bruteforcing the rest of the code and this appeared “Infant Jesus of Prague”


Great! So our first calibration code will be “0364abe72d29c96c” and the second is “Infant Jesus of Prague”


Now one last thing, Generate the long waited punch cards by entering the following:


mul
add
ret

Now the Punch Card Reader verification has been passed!

No comments:

Post a Comment