Join the discord

ESET - Malware analyst challenge

31 Jul, 2018 00:00
I finally decided to try this one.
Ok I lied. I tried it like, 2 years ago but never finished it, so the time has come.



crackme.exe


As always, I started the executable to see what's going on:


Next step was to load it in IDA's decompiler and fiddle with it a bit to get the code logic.
After renaming few of the variables and function calls, the decompiled code became pretty clear:
main(), IDA decompileint __cdecl main(int argc, const char **argv, const char **envp) {
   // First anti-debugger protection
   if (IsDebuggerPresent())
      ExitProcess(0);
   NumberOfCharsWritten = 0;
   XOR_decryptA(strMsgEnterCode, 0x1F, 0x25, 3);
   lenMsgEnterCode = strlen(strMsgEnterCode);
   WriteConsoleA(hConsoleOutput, strMsgEnterCode, lenMsgEnterCode, &NumberOfCharsWritten, 0);
   XOR_decryptA(strMsgEnterCode, 0x1F, 0x25, 3);
   NumberOfCharsRead = 0;
   ReadConsoleA(hConsoleInput, &u0, 0xAu, &NumberOfCharsRead, 0);
   execStart = GetTickCount();
   
   // First user code verification
   v22 = u6 + u7 == 0xCD
       && u5 + u8 == 0xC9
       && u3 + u6 + u7 == 0x13A
       && u5 + u8 + u4 + u9 == 0x16F
       && u0 + u1 == 0xC2
       && u9 + u8 + u7 + u6 + u5 + u4 + u3 + u2 + u1 + u0 == 0x39B;
   if (v22) {
      // Second anti-debugger protection
      if (*(_BYTE *)(__readfsdword(0x30u) + 2))
         ExitProcess(0);
      // Third anti-debugger protection
      execEnd = GetTickCount();
      if ( execEnd - execStart > 0x64 )
         ExitProcess(0);

      // Second user code verification
      if (crc((int)&u0, 0xAu) == 0x1928F914) {
         // Correct code
         XOR_decryptB(strMsgCongrats, 0x100, &u0, NumberOfCharsRead, 2);
         lenMsgCongrats = strlen(strMsgCongrats);
         WriteConsoleA(hConsoleOutput, strMsgCongrats, lenMsgCongrats, &NumberOfCharsWritten, 0);
         XOR_decryptB(strMsgCongrats, 0x100, &u0, NumberOfCharsRead, 2);
      } else {
         // if the second verification fails, it prints a hint message
         XOR_decryptA(strMsgHint, 0x6E, 0x12, 5);
         lenMsgHint = strlen(strMsgHint);
         WriteConsoleA(hConsoleOutput, strMsgHint, lenMsgHint, &NumberOfCharsWritten, 0);
         XOR_decryptA(strMsgHint, 0x6E, 0x12, 5);
      }
   } else {
      // Wrong code
      XOR_decryptA(strMsgWrongCode, 0x11, 0x16, 7);
      lenMsgWrongCode = strlen(strMsgWrongCode);
      WriteConsoleA(hConsoleOutput, strMsgWrongCode, lenMsgWrongCode, &NumberOfCharsWritten, 0);
      XOR_decryptA(strMsgWrongCode, 0x11, 0x16, 7);
   }
   return 0;
}
Pretty simple.

First of all, there are three anti-debugging protections:
- using the boring IsDebuggerPresent()
- checking the PEB.BeingDebugged flag, where IDA decompiled it as __readfsdword(0x30)+2)
- execution timeout kill switch made with two GetTickCount() surrounding the first password verification

The used strings are decrypted by two XOR based algorithms that I called XOR_decryptA():
XOR_decryptA(), python implementationdef XOR_decryptA(data, key, key_mod):
   result = ""
   for b in data:
      result += chr(b ^ key & 0xFF)
      key += key_mod

   return result;

and XOR_decryptB():
XOR_decryptB(), python implementationdef XOR_decryptB(data, key, key_mod):
   result = ""
   for i,b in enumerate(data):
      result += chr(b ^ (key[i%len(key)] + key_mod) & 0xFF)

   return result;

While XOR_decryptB() uses the correct password I don't have it yet for a key, XOR_decryptA() is using hardcoded values for key and key modifier, so I decrypted the three messages using this algorithm.
The "Please enter valid password" message and the "Wrong password!" are not interesting, but the third one that is getting printed if the first verification passes is:
Hint message!Good work. Little help:
char[8] = 85
char[0] + char[2] = 128
char[4] - char[7] = -50
char[6] + char[9] = 219
Nice hint, that will get handy in a minute.

So let's see the first password verification as a set of rules, where u0 to u9 are the characters in the user entered password:
First code verificationu6 + u7 == 205
u5 + u8 == 201
u3 + u6 + u7 == 314
u4 + u5 + u8 + u9 == 367
u0 + u1 == 194
u0 + u1 + u2 + u3 + u4 + u5 + u6 + u7 + u8 + u9 == 923
These and the hint I got are enough to build a valid password.
Right from the start I can easily find u3, by subtracting 205 (u6+u7) from 314 (u3+u6+u7) and get 109 or the character 'm'.

In a similar way I can find u2:
Calculating u2923 (sum of all characters)
-
194 (u0+u1)
-
314 (u3+u6+u7)
-
367 (u4+u5+u8+u9)
And get 48 or the character '0'

From here I can calculate valid pairs for u6 and u7, u5 and u8, u4 and u9 and u0 and u1 and get a code like "D~0m(KO~~~" that will pass the first check:

Now I got the hint without cheating and can add these rules to find more valid characters.
u8 is 85 - 'U', and I can find u5 by subtracting 201 - 85 = 116 : 't'

I already found u2 to be 48, and from the hint u0 should be 128 - 48 = 80 : 'P'

So if u0 is 80, then u1 should be 194 - 80 = 114 : 'r'

Let's sum up what I found so far:
valid codeu0 =  80 'P'
u1 = 114 'r'
u2 =  48 '0'
u3 = 109 'm'
u4 = ?
u5 = 116 't'
u6 = ?
u7 = ?
u8 =  85 'U'
u9 = ?
So I found 5 out of 9 and only four characters left.
These four characters however are entangled together, but I can bruteforce them pretty easy.

First, I can get the par sum of u4+u9 by subtracting u5+u8 from the u4+u5+u8+u9 == 367 equation and get 367 - 201 = 166.

My idea is to first find all valid pairing characters for u4+u9 and u6+u7.
From the result sets I will match the rules given me in the hint - u4-u7 == -50 and u6+u9 == 219.

This way the bruteforcing algorithm should take a lesser time, since I'm using optimized list of characters, instead of blindly trying them all.

In the end, I wrapped all this into a simple script that will generate the valid password for me:
Password generatorcorrect_code = [0] * 10;

# finding u3
# (u3 + u6 + u7) - (u6 + u7)
correct_code[3] = 314 - 205 # 'm'

# finding u2
# (u0 + u1 + u2 + u3 + u4 + u5 + u6 + u7 + u8 + u9) - (u3 + u4 + u5 + u6 + u7 + u8 + u9) = (u0 + u1 + u2)
# (u0 + u1 + u2) - (u0 + u1) = u2
correct_code[2] = 242 - 194 # '0'

# after using the hint, we can populate four more characters
correct_code[8] = 85 # 'U'

# finding u5
# (u5 + u8) - u8
correct_code[5] = 201 - 85 # 't'

# finding u0
# (u0 + u2) - u2
correct_code[0] = 128 - 48 # 'P'

# finding u1
# (u0 + u1) - u0
correct_code[1] = 194 - 80 # 'r'

# calculate pairs for u6+u7 and u4+u9
pairs = [[]] * 2
for a in range(0x20, 0x7F):
   for b in range(0x20, 0x7F):
      if (a+b) == 205:         # [u6, u7]
         pairs[0].append([a, b])
      if (a+b) == 367 - 201:   # [u4, u9]
         pairs[1].append([a, b])

# pick the correct pairs by using the hint
for p0 in pairs[0]:      # [u6, u7]
   for p1 in pairs[1]:   # [u4, u9]
      if (p1[0] - p0[1] == -50) and (p0[0] + p1[1] == 219):
         correct_code[4] = p1[0]
         correct_code[6] = p0[0]
         correct_code[7] = p0[1]
         correct_code[9] = p1[1]

print("Correct code: "+"".join([chr(i) for i in correct_code]))

Running my script gave me the correct password "Pr0m3theUs", and I can verify it, by passing it through the crc() function used as second stage verification:
crc() verification function# oh Python...
ror = lambda val, r_bits, max_bits=32: \
    ((val & (2**max_bits-1)) >> r_bits%max_bits) | \
    (val << (max_bits-(r_bits%max_bits)) & (2**max_bits-1))

def crc(data):
   result = 0
   for b in data:
      result = ror(result, 9) ^ b

   return result 

The checksum of "Pr0m3theUs" matches 0x1928F914, but that apparently is still not the end of this challenge:


I need to apply the decryption algorithm over an unreferenced data, and that's easy to find since there was a rather suspicious block of data right after the message I got:


Tweaking the buffer address and providing the correct password now gave me the final answer:


Second stage, here I come!




EsetCrackme2015.exe and EsetCrackme2015.dll


This one has a GUI with three password authentications and surprisingly no "bad pass" messages at all:


After loading the executable in IDA and decompiling it, it seems it's only a DLL loader:
EsetCrackme2015.exe main procedure, IDA decompileHMODULE __stdcall start(int a1, int a2, int a3, int a4) {
   HMODULE result; // eax
   unsigned int v5; // kr00_4
   char v6; // [esp+0h] [ebp-106h]
   char v7; // [esp+1h] [ebp-105h]
   CHAR Filename; // [esp+2h] [ebp-104h]

   // Only one running instance is allowed
   CreateMutexA(0, 1, "EsetCrackme2015");
   if ( GetLastError() == 183 )
      return (HMODULE)MessageBoxA(0, "Application already launched ... ", "Error", 0x30u);

   // build the dll path by replacing the "exe" extension with "dll"
   GetModuleFileNameA(0, &Filename, 0x104u);
   v5 = strlen(&Filename);
   *(&v7 + v5) = 'l';
   *(_WORD *)(&v6 + v5 - 1) = 'ld';
   
   // load the DLL
   result = LoadLibraryA(&Filename);
   dword_40102C = (int)result;    // this one might be important later
   if (!result)
      result = (HMODULE)MessageBoxA(0, &Filename, "Missing DLL file", 0x30u);
   return result;
}

Even though there's just this code in the main(), there's a lot of other functions with lots of XOR and ROL/ROR instructions in them, so I assume they are decryption helper functions that the DLL will probably use later.

Moving on with the DLL loded in IDA, the lack of resolved calls (except DllEntryPoint) makes me think it's encrypted entirely.
So, I took the DllEntryPoint() code out, and put some self explanatory comments:
DLL DllEntryPoint(), x86dbg10000226 | 83 7C 24 08 01           | cmp dword ptr ss:[esp+8],1         ;/ fdwReason == DLL_PROCESS_ATTACH
1000022B | 0F 85 B2 00 00 00        | jne esetcrackme2015.100002E3       ;\ 
10000231 | 64 A1 30 00 00 00        | mov eax,dword ptr fs:[30]          ;/ PEB
10000237 | 8B 40 0C                 | mov eax,dword ptr ds:[eax+C]       ;| PEB.Ldr
1000023A | 8B 40 14                 | mov eax,dword ptr ds:[eax+14]      ;\ PEB_LDR_DATA.InInitializationOrderModuleList
1000023D | 56                       | push esi                           
1000023E | 8B F0                    | mov esi,eax                        
10000240 | 85 C0                    | test eax,eax                       
10000242 | 0F 84 9A 00 00 00        | je esetcrackme2015.100002E2        
10000248 | 53                       | push ebx                           
10000249 | 57                       | push edi                           
1000024A | 8B 48 28                 | mov ecx,dword ptr ds:[eax+28]      ;/ LDR_DATA_TABLE_ENTRY.BaseDllName
1000024D | 85 C9                    | test ecx,ecx                       ;\
1000024F | 0F 84 8B 00 00 00        | je esetcrackme2015.100002E0        
10000255 | 66 83 39 00              | cmp word ptr ds:[ecx],0            
10000259 | BF C5 9D 1C 81           | mov edi,811C9DC5                   ;/ FNV-1a hashing start
1000025E | 74 2B                    | je esetcrackme2015.1000028B        ;|
10000260 | 8A 11                    | mov dl,byte ptr ds:[ecx]           ;|
10000262 | 83 C1 02                 | add ecx,2                          ;|
10000265 | 8D 5A 9F                 | lea ebx,dword ptr ds:[edx-61]      ;|
10000268 | 80 FB 19                 | cmp bl,19                          ;|/ strtolower implementation
1000026B | 77 03                    | ja esetcrackme2015.10000270        ;||
1000026D | 80 C2 E0                 | add dl,E0                          ;|\
10000270 | 0F BE D2                 | movsx edx,dl                       ;|
10000273 | 33 D7                    | xor edx,edi                        ;|
10000275 | 69 D2 93 01 00 01        | imul edx,edx,1000193               ;| FNV prime
1000027B | 66 83 39 00              | cmp word ptr ds:[ecx],0            ;|
1000027F | 8B FA                    | mov edi,edx                        ;|
10000281 | 75 DD                    | jne esetcrackme2015.10000260       ;\ FNV-1a hashing end
10000283 | 81 FF 66 68 70 FC        | cmp edi,FC706866                   ; Hash verification
10000289 | 74 0A                    | je esetcrackme2015.10000295        
1000028B | 8B 36                    | mov esi,dword ptr ds:[esi]         
1000028D | 8B 06                    | mov eax,dword ptr ds:[esi]         
1000028F | 85 C0                    | test eax,eax                       
10000291 | 75 B7                    | jne esetcrackme2015.1000024A       
10000293 | EB 4B                    | jmp esetcrackme2015.100002E0       
10000295 | 8B 40 10                 | mov eax,dword ptr ds:[eax+10]      ;/ ImageBase of EsetCrackme2015.exe
10000298 | 85 C0                    | test eax,eax                       ;|
1000029A | 74 44                    | je esetcrackme2015.100002E0        ;|
1000029C | B9 00 10 00 00           | mov ecx,1000                       ;| offset to the first PE section
100002A1 | BA 01 01 01 01           | mov edx,1010101                    ;| DWORD modifier
100002A6 | 8B 34 01                 | mov esi,dword ptr ds:[ecx+eax]     ;|/ ImageBase+0x1000, to move to the first PE section
100002A9 | 03 F2                    | add esi,edx                        ;||/ add the modifier value to the current value
100002AB | 81 FE 06 15 13 FB        | cmp esi,FB131506                   ;||| check the first DWORD
100002B1 | 75 1C                    | jne esetcrackme2015.100002CF       ;||\
100002B3 | 8B 74 01 04              | mov esi,dword ptr ds:[ecx+eax+4]   ;||/ check the second DWORD
100002B7 | 03 F2                    | add esi,edx                        ;|||
100002B9 | 81 FE DF 6A C1 20        | cmp esi,20C16ADF                   ;|||
100002BF | 75 0E                    | jne esetcrackme2015.100002CF       ;||\
100002C1 | 8B 74 01 08              | mov esi,dword ptr ds:[ecx+eax+8]   ;||/ check the third DWORD
100002C5 | 03 F2                    | add esi,edx                        ;|||
100002C7 | 81 FE A2 60 33 C4        | cmp esi,C43360A2                   ;|||
100002CD | 74 0B                    | je esetcrackme2015.100002DA        ;||\
100002CF | 41                       | inc ecx                            ;|| i++
100002D0 | 81 F9 00 2F 00 00        | cmp ecx,2F00                       ;|| egghunt is limited to the first 0x2F00 bytes
100002D6 | 72 CE                    | jb esetcrackme2015.100002A6        ;\\
100002D8 | EB 06                    | jmp esetcrackme2015.100002E0       
100002DA | 8D 44 01 0C              | lea eax,dword ptr ds:[ecx+eax+C]   ;/ Procedure EP
100002DE | FF D0                    | call eax                           ;\
100002E0 | 5F                       | pop edi                            
100002E1 | 5B                       | pop ebx                            
100002E2 | 5E                       | pop esi                            
100002E3 | 33 C0                    | xor eax,eax                        
100002E5 | 40                       | inc eax                            
100002E6 | C2 0C 00                 | ret C                              
Long story short:
- From the PEB the main executable is located, using its name's FVN-1a hash.
- An egghunting routine is executed, looking for these three DWORD values 0xFB131506, 0x20C16ADF and 0xC43360A2.
- If found, the fourth DWORD is the entry point of the function that is about to get executed.

I'm thinking this code might be used again, so I made a IDAPython implementation, just in case I need it later:
Egghunter, IDAPythonimport idaapi

def locate_procedure(offset_code, modifier, address_space, egg):
   space_start = idaapi.get_imagebase() + offset_code
   space_end = space_start + address_space

   for offset in range(space_start, space_end):
      if idaapi.get_dword(offset) + modifier == egg[0] and \
         idaapi.get_dword(offset+4) + modifier == egg[1] and \
         idaapi.get_dword(offset+8) + modifier == egg[2]:
   
         print("Procedure found at address %08X"%(offset+0x0C))
         return True

   print("Procedure not found.")
   return False

locate_procedure(0x1000, 0x01010101, 0x2F00, [0xFB131506, 0x20C16ADF, 0xC43360A2])

This leads me to address 00401E9F that I renamed to @DLL_main() inside the main executable:
@DLL_main(), IDA disassembly.text:00401E9F @DLL_main      proc near
.text:00401E9F                 push    edi
.text:00401EA0                 xor     edi, edi
.text:00401EA2                 cmp     dword_401030, edi
.text:00401EA8                 jz      short loc_401ED9
.text:00401EAA                 push    esi
.text:00401EAB                 call    @imp_GetKernel32Handle
.text:00401EB0                 mov     esi, eax
.text:00401EB2                 push    2FA62CA8h             ; "Sleep" FVN-1a hash
.text:00401EB7                 call    @imp_GetProcAddress   ; Sleep
.text:00401EBC                 push    edi
.text:00401EBD                 push    edi
.text:00401EBE                 push    eax                   ; Sleep handle
.text:00401EBF                 push    offset @ThreadMain
.text:00401EC4                 push    edi
.text:00401EC5                 push    edi
.text:00401EC6                 push    60AC7E39h             ; "CreateThread" FVN-1a hash
.text:00401ECB                 mov     dword_401030, edi
.text:00401ED1                 call    @imp_GetProcAddress   ; CreateThread
.text:00401ED6                 call    eax
.text:00401ED8                 pop     esi
.text:00401ED9                 pop     edi
.text:00401EDA                 retn
.text:00401EDA @DLL_main      endp

The FVN-1a hashing is again used to locate the Kernel32.dll handle and as a checksum verification in a GetProcAddress() implementation.
I wrote myself a implementation of the FVN-1a hashing and in the xrefs of @imp_GetProcAddress populated what API function each call resolves:

The first xref is a wrapper of @imp_GetProcAddress(), that takes the FVN-1a hash from a parameter so it probably pulls API functions from other libraries.

Something interesting that worth mentioning here is that the GetProcAddress() implementation doesn't return the EP of the called function, but EP+1.
This way, when I set a breakpoint on the exact entry point so the debugger change the first instruction to int3, that breakpoint will never get triggered.
It will also mess up any hooks attached on EP.

If you are wondering why they can get away with skipping the first instruction without crashing the code, check out the Sleep() function for example:
Sleep() entry point, x86dbg755410FF | 8B FF                    | mov edi,edi            ; Original entry point
75541101 | 55                       | push ebp               ; Entry point + 1
75541102 | 8B EC                    | mov ebp,esp
75541104 | 5D                       | pop ebp
75541105 | EB D7                    | jmp <kernel32.Sleep>
As you can see, the first instruction does nothing, so skipping it is perfectly fine.
I'll keep that in mind if I have to put breakpoints on function entries.

Now, back to the code, where CreateThread() executes @ThreadMain():
@ThreadMain(), IDA decompileint __stdcall ThreadMain(void (__stdcall *arg1)(signed int)) {
   int v1; // eax

   // backtrace from dword_401030, to locate the DLL ImageBase
   // the ImageBase is originally set at dword_40102C by the LoadLibrary()
   while(*(&dword_401030 - 1) == 0xCCCCCCCC)
      arg1(0xC0);                                // Sleep(0xC0)
   v1 = *(&dword_401030 - 1);
   if (v1)
      sub_40213B(v1);                            // pass the DLL's ImageBase as only parameter
   return 0;
}
Not much, so moving to sub_40213B().

The code is quite long, so I did some static analysis first and here's what I found.
First a zero Event (meaning lpEventAttributes, bManualReset, bInitialState and lpName are all set to NULL) and a new thread of sub_401F13() are created.
Since I already know what API functions @imp_GetProcAddress() resolves, I noticed that the thread is using CreateNamedPipeA(), FlushFileBuffers() and DisconnectNamedPipe(), so I assume this is a interprocess communicator thread. I'll leave this thread for later and just set a breakpoint at its EP for now.

Next, few rather interesting calls are called. Two egg hunters are executed, that try to locate certain data from the DLL's memory.
According to the xrefs, this egg hunt procedure is used in multiple places, and because I prefer to be ahead of the things, I wrote a simple IDAPython egg hunt implementation:
@locateBundleData(), IDAPythonimport idaapi

def find_eggs():
   addr_start = idaapi.get_imagebase()

   addr_end = idaapi.get_dword(addr_start + 0x3C)
   addr_end = addr_start + idaapi.get_dword(addr_end + addr_start + 0x50)

   while addr_start < addr_end:
      addr_start += idaapi.get_dword(addr_start + 2) + 6;

      print("Egg: %04X; size: %08X; address: %08X"%(idaapi.get_word(addr_start), idaapi.get_dword(addr_start + 2), addr_start+6))

This code gave me the following table of detected chunks:
IDsizeaddressIDsizeaddressIDsizeaddressIDsizeaddress
0x00010x000000C3100002260x01030x000002A91000314C0x01550x0001303E100858FC0xFF020x0000002010098CEB
0x00020x00000019100002EF0x01040x000000C5100033FB0xAA020x00000009100989400xFF040x00002A0010098D11
0x00030x00000020100002000x01510x00011E00100034C60xAA060x000000481009894F0xFF050x000024001009B717
0x00040x000000201000030E0x01520x00008E8E100152CC0xBB010x0000007B1009899D0xC8000x0000001E1009DB1D
0x01010x00000E00100003340x01530x0005DAC81001E1600xBB020x0000001110098A1E
0x01020x0000200C1000113A0x01540x00009CC81007BC2E0xFF000x000002B010098A35

The two chunks egg hunted here are with IDs 0x0003 and 0x0101.

Chunk 0x0003 is 0x20 bytes and it got XOR decrypted using the MS-DOS compatibility error message, from the Kernel32.dll standard header - "!This program cannot be run in D" as key.
Decrypting it produces the string "SXJyZW4lMjBpc3QlMjBtZW5zY2hsaWNo", that is then used as key in a ECB mode AES call, that decrypts the second egghunted chunk - 0x0101.

The decrypted 0x0101 chunk is a fully functional MZ-PE image, that is parsed and executed in the end of the sub_40213B() procedure.

I ended up writing a dumper in IDAPython, that will also AES decrypt the chunks I need:
dumpEgg() method, IDAPythondef dumpEgg(egg_id):
   addr_start = idaapi.get_imagebase()

   addr_end = idaapi.get_dword(addr_start + 0x3C)
   addr_end = addr_start + idaapi.get_dword(addr_end + addr_start + 0x50)

   egg_found = False   
   while addr_start < addr_end:
      if egg_id == idaapi.get_word(addr_start):
         egg_found = True
         break;
      addr_start += idaapi.get_dword(addr_start + 2) + 6;

   if egg_found == True:
      data_address = addr_start+6
      data_size = idaapi.get_dword(addr_start + 2)
      data = idaapi.get_bytes(data_address, data_size)

      print("Found egg: %04X; size: %08X; address: %08X"%(egg_id, data_size, data_address))

      cipher = AES.new("SXJyZW4lMjBpc3QlMjBtZW5zY2hsaWNo", AES.MODE_ECB)

      filename = AskFile(1, "*.bin", "Output file name")
      fh = open(filename, "wb")
      fh.write(cipher.decrypt(data))
      fh.close()

The file itself is a DLL, but the DllEntryPoint() method contains just a return TRUE;.
Because of that, a EP finder function is called to locate the method that in a ways, represents a main() in a regular executable.
This newly decrypted DLL code is executed from the regular EsetCrackme2015.dll memory so the addressing between the dumped and the in-memory code differs a bit.
I could rearrange the virtual addressing and match x86dbg to IDA, but the decrypted DLL has only 20 procedures, and just by static analysis I already figured out half of them.

So why bother?

Moving to the EP that I named @main(), this whole DLL looks like a user message handler in a GUI app:
Egg 0x0101 @main() procedure.text:100009A9 @main           proc near
.text:100009A9                 push    ebp
.text:100009AA                 mov     ebp, esp
.text:100009AC                 sub     esp, 104h
.text:100009B2                 push    esi
.text:100009B3                 mov     esi, ecx
.text:100009B5                 mov     dword_10000BC0, esi
.text:100009BB                 call    sub_10000732              ;/ perform some flag checks
.text:100009C0                 test    al, al                    ;|
.text:100009C2                 jz      short loc_100009CE        ;\ if check failed, jump to loc_100009CE
; do something
.text:100009C9                 jmp     loc_10000BA5              ; jump to @main() exit
.text:100009CE                 call    sub_10000752              ;/ perform another flag check
.text:100009D3                 test    al, al                    ;|
.text:100009D5                 jz      short loc_100009E1        ;\ if check failed, jump to loc_100009E1
; do something
.text:100009DC                 jmp     loc_10000BA5              ; jump to @main() exit
; more checks and more code
.text:10000BA5                 pop     esi
.text:10000BA6                 leave
.text:10000BA7                 retn
.text:10000BA7 @main           endp                              ; procedure exit
and on further examination, the message loop itself is located in EsetCrackme2015.exe here:
Message loop.text:0040225D                 call    @locatePayloadEP          ; locate @main() EP
.text:00402262                 mov     eax, [edi+406h]
.text:00402268                 add     esp, 24h
.text:0040226B                 mov     [ebp+var_4], eax
.text:0040226E                 jmp     short loc_4022C7          ; start of message loop
.text:00402270                 pushaw
.text:00402272                 mov     ecx, dword_402400
.text:00402278                 call    [ebp+var_4]               ; execute @main()
.text:0040227B                 movzx   eax, ax
.text:0040227E                 mov     [ebp+var_8], eax
.text:00402281                 popaw
; more code
.text:004022C7                 mov     esi, dword_402400
.text:004022CD                 cmp     [esi+108h], bl
.text:004022D3                 jz      short loc_402270          ; end of message loop
With that information, I started tracing the code and each call to the @main().

At its first run, it egg hunts three eggs - 0x0102 - a AES encrypted shellcode, 0x0103 and 0x0104 that are both binary data taken as-is.
The second run executes that shellcode, by passing the decrypted MZ-PE image in 0x0151, the 0x0103 binary data and the filepath to svchost.exe as arguments.

Now it's pretty obvious that the decrypted MZ-PE from 0x0151 will be injected in a instance of svchost.exe, but this shellcode turned out to be way cooler than that.

The shellcode is actually a virtual machine, executing custom ROM-like executable file format with its own bytecode, that those 0x0103 and 0x0104 contain.
I did a vast exploration of the format and its logic and was able to reverse engineer it, pretty much completely:
Format of the ROM imagesstruct BYTECODE_ROM {
    WORD    signature;      // 0x1337 - non encrypted; anything else is encrypted
    DWORD   offset_EP;      // Offset to entry point
    DWORD   offset_DATA;    // Offset to data section
    DWORD   size_image;     // Size of ROM image
    DWORD   reserved;       // NULL
    byte    loader[];       // Bootloader code, that decrypt the Code and Data sections
    byte    code[];         // Code section
    byte    data[];         // Data section
}
I also reverse engineered a large portion of the virtual machine itself and even sat on my ass and wrote a simple and rather shitty disassembler, that you can download here.

So, passing 0x0103 through my disassembler, gives me this pseudo-assembly:
0x0103 ROM, loader stub00000000 | 01 70 20            MOV DWORD R0, DWORD R7     ; R0 = data
00000003 | 01 01 01            MOV DWORD R1, BYTE &R0     ; R1 = data[0] (signature[0])
00000006 | 0A 00 12 01         ADD BYTE R0, 0x01          ; *data++
0000000A | 01 02 01            MOV DWORD R2, BYTE &R0     ; R2 = data[0] (signature[1])
0000000D | 0A 00 12 01         ADD BYTE R0, 0x01          ; *data++
00000011 | 01 03 21            MOV DWORD R3, DWORD &R0    ; R3 = offset_EP
00000014 | 01 80 20            MOV DWORD R0, DWORD R8     ; R0 = data length
00000017 | 0A 70 18            ADD DWORD R0, DWORD R7     ; R0 += data base address
0000001A | 0A 73 18            ADD DWORD R3, DWORD R7     ; R3 += data base address
0000001D | 01 34 01            MOV DWORD R4, BYTE &R3     ;/ Decryption loop, R4 = byte from offset_EP
00000020 | 0A 14 08            XOR DWORD R4, DWORD R1     ;| XOR the byte with signature[0] byte
00000023 | 0A 21 18            ADD DWORD R1, DWORD R2     ;| ADD signature[1] to signature[0] byte
00000026 | 01 43 04            MOV BYTE &R3, BYTE R4      ;| replace the encrypted with the decrypted byte
00000029 | 0A 03 12 01         ADD BYTE R3, 0x01          ;| move to the next encrypted byte
0000002D | 06 03 28            CMP DWORD R3, DWORD R0     ;|
00000030 | 07 01 1D 00 00 00   JNB 0000001D               ;\
00000036 | 01 70 20            MOV DWORD R0, DWORD R7     ; R0 = data
00000039 | 01 00 06 37         MOV BYTE &R0, 0x37         ; data[0] = 0x37 (signature[0])
0000003D | 0A 00 12 01         ADD BYTE R0, 0x01          ; *data++
00000041 | 01 00 06 13         MOV BYTE &R0, 0x13         ; data[1] = 0x13 (signature[1])
00000045 | 00                  RET
Nice!
The loader code is using the same XOR_decryptA() algorithm from the first executable of this challenge.
Using the first two signature bytes as key and key_mod I can decrypt the code and data sections.
The decrypted code is a trivial CreateProcessA()->WriteProcessMemory()->ResumeThread(), but the custom virtual machine was pretty cool.

Also, having the disassembler, I can check the other ROM from egg 0x0104:
0x0104 ROM, code stub00000000 | 06 0C 02 00                       CMP DWORD R12, 0x00
00000004 | 07 01 10 00 00 00                 JZ 00000010
0000000A | 04 8B                             PUSH DWORD R11
0000000C | 04 8A                             PUSH DWORD R10
0000000E | 02 0C                             $0x02                        ; didn't bother reverse engineering this opcode
00000010 | 04 20 00                          PUSH 0x00                    ;/ hTemplateFile          = NULL
00000013 | 04 20 80                          PUSH 0x80                    ;| dwFlagsAndAttributes   = FILE_ATTRIBUTE_NORMAL
00000016 | 04 22 02                          PUSH 0x02                    ;| dwCreationDisposition  = CREATE_ALWAYS
00000019 | 04 20 00                          PUSH 0x00                    ;| lpSecurityAttributes   = NULL
0000001C | 04 20 00                          PUSH 0x00                    ;| dwShareMode            = 0
0000001F | 04 A0 00 00 00 40                 PUSH 0x40000000              ;| dwDesiredAccess        = GENERIC_WRITE
00000025 | 04 8D                             PUSH DWORD R13               ;| lpFileName
00000027 | 03 06 0B 00 00 00 00 00 00 00 00  CALL 0x00000000->&data_0     ;| CreateFileA
00000032 | 01 04 20                          MOV DWORD R4, DWORD R0       ;\ file handle
00000035 | 04 20 00                          PUSH 0x00                    ;/ lpOverlapped           = NULL
00000038 | 04 B2 0C 00 00 00                 PUSH &data_C                 ;| lpNumberOfBytesWritten = 0x00000000
0000003E | 04 8B                             PUSH DWORD R11               ;| nNumberOfBytesToWrite
00000040 | 04 8A                             PUSH DWORD R10               ;| lpBuffer
00000042 | 04 84                             PUSH DWORD R4                ;| hFile
00000044 | 03 09 0B 04 00 00 00 00 00 00 00  CALL 0x00000000->&data_4     ;\ WriteFile
0000004F | 04 84                             PUSH DWORD R4                ;/ hFile
00000051 | 03 0D 0B 08 00 00 00 00 00 00 00  CALL 0x00000000->&data_8     ;\ CloseHandle
0000005C | 00                                RET
Great, now I have pretty much analysed these tree eggs and I can call them with something more human-friendly and self explanatory like VirtualMachine_0x0102.bin, ProcInject_0x0103.rom and ProcDrop_0x0104.rom and for the MZ-PE in egg 0x0151 I can simply call it svchost_0x0151.exe

Cool. The memory injected svchost.exe is next, so I guess, I'll move there.




Memory injected svchost.exe (egg 0x0151)


When it comes to attaching debugger to a memory patched executables, I always patch the EP to EB FE (JMP 0) before it's getting written, then attach to the looped process and fix the EP bytes to their original values.

Let's see what's happening in here...
It's a dialogue based app written in Visual Studio, and right from the start I can see something familiar:

Executed on WM_INITDIALOG message, the same anti hook/breakpoint method, by skipping the entry point's MOV EDI,EDI instruction is used here for LoadLibraryA.

A thread is started, where using base64 to decode the required library names user32.dll and kernel32.dll, and implementation of GetProcAddress is resolving the handles to lstrcmpA, CallWindowProcA, FindWindowExA and SetWindowLongA.
All of them are again breakpoint/hook protected, by skipping their first instruction.

Instead of using something as simple as FNV checksum like before, here the authors used a slightly modified SHA1 to create hashes of the API names.
The modification is in the way the message is split into 4 byte chunks - instead of using big endian notation, this one uses little endian notation.

The APIs resolved are then used to attach a GWL_WNDPROC procedure to the EDIT fields, that points to a procedure I named @PROC_EDIT_Control() and that's all that this thread does.

The final step from the WM_INITDIALOG is involving the PIPE communication, that was originally initialized as a thread in the EsetCrackme2015.exe
The communication protocol is simple:
Client-Server communication protocolstruct COM_PROT {
   byte    mode;
   WORD    id;
   // depending on the mode value and the sender
   if (mode == 0x01) {
      DWORD   offset;       // Offset to egg size
      DWORD   offset;       // Offset to egg data
   } else if (mode == 0x02 || mode == 0x03) {
      DWORD   data_size;    // Hardcoded to 2
      WORD    data;         // Hardcoded to 0x4B4F ("OK")
   } else {
      // do nothing
   }
}
So, the client here is svchost.exe and the server is of course EsetCrackme2015.exe, the id is the egg ID and the mode defines what kind of data will be received from the client and what will be replayed by the server as explained in this table:
modeidmeaning
0x01Egg IDRequests a egg from EsetCrackme2015.dll, where the id parameter is the egg ID
0x02Egg IDSets flag with id to TRUE, and replays with "OK"
0x03Egg IDSets flag with id to FALSE, and replays with "OK"
0xXXEgg IDUnknown requests are just replayed with "OK"

There are two calls here, requesting for eggs 0xBB01 and 0xBB02, both being Key-XOR decrypted using "PIPE" as a key, and both are arrays of strings:
Decrypted eggs 0xBB01 and 0xBB02// 0xBB01
869B39E9F2DB16F2A771A3A38FF656E050BB1882
0F30181CF3A9857360A313DB95D5A169BED7CC37
0B6A1C6651D1EB5BD21DF5921261697AA1593B7E

// 0xBB02
RFV1aV4fQ1FydFxk

This finalizes the WM_INITDIALOG.

I set breakpoints on the three buttons inside the WM_COMMAND code space, let the application run and the dialogue window popped up on the screen.




Finding the first password


It's finally time to enter some keys and click some buttons, so let's grab some of the code, that handles the WM_COMMAND messages:
WM_COMMAND of the top button.text:00401853                 push    1001                   ;/ nIDDlgItem = top EDIT control
.text:00401858                 call    sub_401760             ;\
.text:0040185D                 add     esp, 4
.text:00401860                 push    offset String2         ;/ lpString2
.text:00401865                 push    offset szKey_1         ;| lpString1
.text:0040186A                 call    ds:lstrcmpA            ;\
.text:00401870                 test    eax, eax               ;/ on match, continue down
.text:00401872                 jnz     _WM_COMMAND            ;\ otherwise exit from the WM_COMMAND code
.text:00401878                 mov     esi, ds:GetDlgItem     ;/
.text:0040187E                 push    eax                    ;| bEnable = FALSE
.text:0040187F                 mov     eax, hDlg              ;|
.text:00401884                 push    1                      ;|/ nIDDlgItem = top BUTTON Control
.text:00401886                 push    eax                    ;|| hDlg
.text:00401887                 call    esi                    ;|\ GetDlgItem
.text:00401889                 mov     edi, ds:EnableWindow   ;|
.text:0040188F                 push    eax                    ;| hWnd
.text:00401890                 call    edi                    ;\ EnableWindow
.text:00401892                 mov     ecx, hDlg              ;/
.text:00401898                 push    0                      ;| bEnable = FALSE
.text:0040189A                 push    1001                   ;|/ nIDDlgItem = top EDIT Control
.text:0040189F                 push    ecx                    ;|| hDlg
.text:004018A0                 call    esi                    ;|\ GetDlgItem
.text:004018A2                 push    eax                    ;| hWnd
.text:004018A3                 call    edi                    ;\ EnableWindow
.text:004018A5                 push    5                      ;/
.text:004018A7                 lea     edx, [ebp+ThreadId]    ;|
.text:004018AA                 push    edx                    ;|
.text:004018AB                 push    0BB01h                 ;| Egg ID 0xBB01
.text:004018B0                 jmp     loc_40198B             ;\ jumps to PIPE request
Seems simple enough. A function at sub_401760 is doing some magic, then lstrcmpA compares something and if there's a match, the top edit field and button gets disabled (meaning that the password was correct).

Inside sub_401760 there's a call to GetWindowTextA which is expected, but remember that GWL_WNDPROC procedure that got attached to the EDIT fields?
Because of it, every time GetWindowTextA is called, that procedure @PROC_EDIT_Control will be executed.
I traced it and it also turn out to be pretty simple.

First, the user entered password, or in my case "123123123" gets Base64 encoded to "MTIzMTIzMTIz".
Then, from every second byte, a 0x01 is subtracted, so from "MTIzMTIzMTIz" my Base64 password becomes "MSIyMSIyMSIy".
Finally this string is compared with the decrypted from the 0xBB02 egg - "RFV1aV4fQ1FydFxk".

After that the control is returned back to sub_401760, where the original password I entered "123123123" gets SHA1 hashed (again using the little endian modification) and formatted into a upper case ASCII string.
So, the lstrcmpA at VA 0040186A compares my password's SHA1 hash and 869B39E9F2DB16F2A771A3A38FF656E050BB1882 - the first string from egg 0xBB01.

To get the valid password, I'll have to fix the base64 key then just decode it, so I wrote myself a lazy snippet to do that for me:
Modified base64 decoderimport base64

code = bytearray("RFV1aV4fQ1FydFxk", "utf-8")

for i in range(1, len(code), 2):
   code[i] += 1

print(base64.b64decode(code).decode("utf-8"))
And that gave me the first code that is "Devin Castle"

The password got accepted and a final pipe send-receive with mode=2 and id=0xBB01 was sent, flagging the success.




Finding the second password


The WM_COMMAND code for the second button is practically the same.
However, because of the event that got flagged by solving the first code, the control to the @main loop from egg 0x0101 is returned, and I'll have to check how the second verification will be prepared.
Time to unveil more code of the main loop, this time with comments about the things I've learned so far:
Egg 0x0101 @main() procedure.text:100009A9                 push    ebp
.text:100009AA                 mov     ebp, esp
.text:100009AC                 sub     esp, 104h
.text:100009B2                 push    esi
.text:100009B3                 mov     esi, ecx
.text:100009B5                 mov     dword_10000BC0, esi
.text:100009BB                 call    sub_10000732
.text:100009C0                 test    al, al
.text:100009C2                 jz      short loc_100009CE
.text:100009C4                 call    @PrepareVMAndROMs             ; egghunts the virtual machine shellcode - 0x0102, 0x0103 and 0x0104
.text:100009C9                 jmp     loc_10000BA5
.text:100009CE                 call    sub_10000752
.text:100009D3                 test    al, al
.text:100009D5                 jz      short loc_100009E1
.text:100009D7                 call    @releaseBuffers
.text:100009DC                 jmp     loc_10000BA5
.text:100009E1                 push    0BB01h                        ;/ This is the flag that got set
.text:100009E6                 call    @checkFlag                    ;| after solving the first code
.text:100009EB                 pop     ecx                           ;|
.text:100009EC                 test    al, al                        ;|
.text:100009EE                 jz      loc_10000A75                  ;\
.text:100009F4                 push    0BB02h                        ;/ This should be the flag for the second code
.text:100009F9                 call    @checkFlag                    ;|
.text:100009FE                 pop     ecx                           ;|
.text:100009FF                 test    al, al                        ;|
.text:10000A01                 jz      short loc_10000A75            ;\
.text:10000A03                 push    0BB03h                        ;/ And the flag for third code
.text:10000A08                 call    @checkFlag                    ;|
.text:10000A0D                 pop     ecx                           ;|
.text:10000A0E                 test    al, al                        ;|
.text:10000A10                 jz      short loc_10000A75            ;\
.text:10000A12                 push    0FF01h                        ;/ The final boss flag
.text:10000A17                 call    @checkFlag                    ;|
.text:10000A1C                 pop     ecx                           ;|
.text:10000A1D                 test    al, al                        ;|
.text:10000A1F                 jz      short loc_10000A75            ;\
.text:10000A21                 push    offset szUser32               ;/ "user32.dll"
.text:10000A26                 push    53B2070Fh                     ;| FNV checksum of "LoadLibraryA"
.text:10000A2B                 call    dword ptr [esi+111h]          ;| GetProcAddress()
.text:10000A31                 call    eax                           ;\ LoadLibraryA("user32.dll")
.text:10000A33                 push    40h                           ;/
.text:10000A35                 push    offset szInfo                 ;| "Info"
.text:10000A3A                 push    offset szThatsAll             ;| "Thats all. Congratulations!"
.text:10000A3F                 push    0                             ;|
.text:10000A41                 push    offset szMessageBoxA          ;| "MessageBoxA"
.text:10000A46                 push    eax                           ;\ ... more code that calls MessageBoxA()

I'm obviously aiming at that MessageBoxA, and to get there I'll need to pass the four flag checks.
By solving the first password I already got the first flag 0xBB01, so moving on.

During the first run, this code send me to the svchost.exe process injector, that initialized the application.
This code is skipped now, and a file dropper is executed, again using the virtual machine, but this time with the second ROM file ProcDrop_0x0104.rom.
The dropped file is named drv.zip and after the AES decryption, it contains the data from egg 0x0152.

I'm running this crackme from my Desktop, and the zip file is dropped there, so let's see what this archive contains:

and the INSTALL.ME contents:
INSTALL.MEinstall me as legacy driver (@WIN7: Device Manager->Action->Add legacy hardware)

Reverse engineering drivers can be tricky, so before installing this one, I'd like to get familiar with it first and do some static analysis in IDA.
The DriverEntry is pretty simple:
crackme_drv.sys, DriverEntry() IDA decompileNTSTATUS __stdcall DriverEntry(PDRIVER_OBJECT DriverObject, PCUNICODE_STRING SourceString) {
   byte_4074F0 = 0;
   byte_4074F1 = 0;
   KeQuerySystemTime((PLARGE_INTEGER)&CurrentTime);
   alloc_result = IoAllocateDriverObjectExtension(DriverObject, DriverEntry, 0xCu, &DriverObjectExtension);
   if ( alloc_result < 0 )
      return alloc_result;
   *(_WORD *)DriverObjectExtension = SourceString->Length;
   *((_WORD *)DriverObjectExtension + 1) = SourceString->MaximumLength + 2;
   v3 = ExAllocatePoolWithTag(PagedPool, *((unsigned __int16 *)DriverObjectExtension + 1), '1maR');   // "Ram1"
   *((_DWORD *)DriverObjectExtension + 1) = v3;
   if ( !*((_DWORD *)DriverObjectExtension + 1) )
      return 0xC000009A;
   RtlCopyUnicodeString((PUNICODE_STRING)DriverObjectExtension, SourceString);
   *((_DWORD *)DriverObjectExtension + 2) = 0;
   DriverObject->MajorFunction[IRP_MJ_CREATE] = (PDRIVER_DISPATCH)@Proc_CREATE_CLOSE;             // IRP_MJ_CREATE
   DriverObject->MajorFunction[IRP_MJ_CLOSE] = (PDRIVER_DISPATCH)@Proc_CREATE_CLOSE;              // IRP_MJ_CLOSE
   DriverObject->MajorFunction[IRP_MJ_READ] = (PDRIVER_DISPATCH)@Proc_READ_WRITE;                 // IRP_MJ_READ
   DriverObject->MajorFunction[IRP_MJ_WRITE] = (PDRIVER_DISPATCH)@Proc_READ_WRITE;                // IRP_MJ_WRITE
   DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = (PDRIVER_DISPATCH)@Proc_DEVICE_CONTROL;   // IRP_MJ_DEVICE_CONTROL
   DriverObject->MajorFunction[IRP_MJ_PNP] = (PDRIVER_DISPATCH)@Proc_PNP;                         // IRP_MJ_PNP
   DriverObject->MajorFunction[IRP_MJ_POWER] = (PDRIVER_DISPATCH)@Proc_POWER;                     // IRP_MJ_POWER
   DriverObject->MajorFunction[IRP_MJ_SYSTEM_CONTROL] = (PDRIVER_DISPATCH)@Proc_SYSTEM_CONTROL;   // IRP_MJ_SYSTEM_CONTROL
   DriverObject->DriverExtension->AddDevice = AddDevice;
   DriverObject->DriverUnload = DriverUnload;
   return 0;
}
Inside AddDevice a IO device with name "\Device\45736574" (45736574 being "Eset" in HEX) is created and judging by two memory blocks with Tags "Ram1" and "Ram2" and a routine that constructs a FAT16 drive, it's obvious that the driver is creating a virtual disk device.
By design, the disk constructor can either create a FAT12 or FAT16 drive, but because of its size - 0x1E00000 (30MB), it will always create a FAT16 one.

I've pulled out the boot sector structure in a separate table for reference:
offsetsizedatadecription
0x00003EB 3C 903 bytes of entry point instruction: JMP XX; NOP
0x0003845 73 65 74 52 61 6D 20OEM name - "EsetRam "
0x000B200 02Bytes per sector - 512
0x000D102Sectors per cluster - 2
0x000E201 00Reserved sector count - 256
0x0010101Number of file allocation tables - 1
0x0011200 02Maximum number root directory entries - 512
0x0013200 F0Total sectors - 61440
0x00151F8Media descriptor - Fixed disk
0x0016278 00Sectors per File Allocation Table
0x0018220 00Sectors per track - 32
0x001A202 00Number of heads - 2
0x001C400 00 00 00Count of hidden sectors - 0
0x0020400 00 00 00Total sectors - 0
0x0024100Physical drive number - 0
0x0025100Flags - 0
0x0026129Extended boot signature - 0x29
0x0027478 56 34 12Volume serial number - 0x12345678
0x002B116B 73 69 44 6D 61 52 20 20 20 20Volume label - "ksiDmaR "
0x0036846 41 54 31 36 20 20 20File-system type - "FAT16 "
0x003E44800...Bootstrap code - all nulls
0x01FE255 AABoot sector signature

The driver didn't populate any entries to this virtual drive yet, but started two threads - both opening pipe "\DosDevices\Pipe\EsetCrackmePipe" link, which is of course the interprocess com-pipe used to egg hunt data from the EsetCrackme2015.dll.

These two threads pull out eggs 0xAA02 and 0xAA06.
So far the encryptions used on egghunted data were either XOR or AES, but here a RC4 encryption with key "3531_4ever" is used:
Driver RC4 implementation with hardcoded keytext:00403190 @RC4            proc near               ; CODE XREF: @THREAD_Drop_0xAA02+27F↑p
; snip
.text:004031B5                 mov     [ebp+key], 33h            ;/ '3'
.text:004031B9                 mov     [ebp+var_F], 35h          ;| '5'
.text:004031BD                 mov     [ebp+var_E], 33h          ;| '3'
.text:004031C1                 mov     [ebp+var_D], 31h          ;| '1'
.text:004031C5                 mov     [ebp+var_C], 5Fh          ;| '_'
.text:004031C9                 mov     [ebp+var_B], 34h          ;| '4'
.text:004031CD                 mov     [ebp+var_A], 65h          ;| 'e'
.text:004031D1                 mov     [ebp+var_9], 76h          ;| 'v'
.text:004031D5                 mov     [ebp+var_8], 65h          ;| 'e'
.text:004031D9                 mov     [ebp+var_7], 72h          ;\ 'r'
.text:004031DD                 push    408h                      ;/ allocate space for the RC4 table
.text:004031E2                 push    0                         ;|
.text:004031E4                 call    ds:ExAllocatePool         ;|
.text:004031EA                 mov     [ebp+P], eax              ;\
.text:004031ED                 push    0Ah                       ;/ key length
.text:004031EF                 lea     eax, [ebp+key]            ;|
.text:004031F2                 push    eax                       ;| key
.text:004031F3                 mov     ecx, [ebp+P]              ;|
.text:004031F6                 push    ecx                       ;| RC4 table
.text:004031F7                 call    @RC4_prepare              ;\ build and shuffle the RC4 table
.text:004031FC                 mov     edx, [ebp+data_length]    ;/ data length
.text:004031FF                 push    edx                       ;|
.text:00403200                 mov     eax, [ebp+data]           ;| data
.text:00403203                 push    eax                       ;|
.text:00403204                 mov     ecx, [ebp+P]              ;|
.text:00403207                 push    ecx                       ;| RC4 table
.text:00403208                 call    @RC4_decrypt              ;\ decrypt the data
; snip
.text:00403226 @RC4            endp

Egg 0xAA02 got decrypted to the string "ESETConst" and the 0xAA06 seems to be some binary data with an structured pattern:
egg 0xAA0612 00 00 00 82 99 8F 92 11 9E 18 94 B1 8E 8F 11 16 9C 11 1A 16 9D A8 A1 A1 29 A8 A1 A1 29 A8 A1 A1 29 A8 A1
12 00 00 00 D0 FC F9 F7 63 ED 71 FA D6 AE E6 62 36 FB 63 7F 77 E9 21 54 78 A1 95 43 64 84 73 10 12 78 31 40
I'll get back to this unknown data in a minute.

Additionally two internal buffers are RC4 decrypted.
The first one is 0x21AC bytes long and, *surprise-surprise*, it is almost the same virtual machine code that I had to deal with before.
The second 0x2B5 bytes long buffer, again RC4 decrypted, is the ROM executable that the virtual machine runs.

And here's where the decrypted egg 0xAA06 comes into the whole picture.
The data of 0xAA06 is 0x48 bytes long, and it overwrites the end of the ROM data, starting at offset 0x26D, so these are "stolen bytes" of the ROM executable.

Finally a pipe request with mode=2 and id=0xAA10 is sent, so it flips the 0xAA10 flag and I'm again sent to the message loop, right after the point where the drv.zip file was dropped before.
Here lies this code, that is actually the end of the whole action loop:
@main(), final case.text:10000AFF                 mov     eax, 0AA10h                ;/ message ID is the 0xAA10 flag
.text:10000B04                 cmp     [esi+10Bh], ax             ;|
.text:10000B0B                 jnz     loc_10000BA2               ;\
.text:10000B11                 push    1                          ;/
.text:10000B13                 push    offset szPunchCardReader   ;| drop filename "PunchCardReader.exe"
.text:10000B18                 mov     eax, 154h                  ;| Egg ID 0x0154
.text:10000B1D                 call    @VM_FileDropper            ;\ drop the file
.text:10000B22                 push    1                          ;/
.text:10000B24                 push    offset szPuncherMachine    ;| drop filename "PuncherMachine.exe"
.text:10000B29                 mov     eax, 153h                  ;| Egg ID 0x0153
.text:10000B2E                 call    @VM_FileDropper            ;\ drop the file
.text:10000B33                 push    1                          ;/
.text:10000B35                 push    4                          ;| decrypted to "\\?\GLOBALROOT\Device\45736574\"
.text:10000B37                 call    @FindAndDecryptEgg         ;\ this is the device created by crackme_drv.sys
.text:10000B3C                 mov     esi, eax
; few loops append "PunchCard.bmp" to the string from above
; the final string is the fullpath "\\?\GLOBALROOT\Device\45736574\PunchCard.bmp"
.text:10000B77                 lea     eax, [ebp+var_104]         ;/
.text:10000B7D                 push    edi                        ;|
.text:10000B7E                 push    eax                        ;| filename "\\?\GLOBALROOT\Device\45736574\PunchCard.bmp"
.text:10000B7F                 mov     eax, 155h                  ;| egg id 0x0155
.text:10000B84                 call    @VM_FileDropper            ;\

So the two executables PunchCardReader.exe and PuncherMachine.exe are dropped from eggs 0x0154 and 0x0153 in the working folder and the third file - PunchCard.bmp, taken from egg 0x0155 is written on the virtual drive that the crackme_drv.sys created.

The BMP is actually encrypted, so the goal of that driver challenge should probably to decrypt it.

While I was doing my attempt to solve this part, at that point I continued with the two executables that got dropped here, solved them then came back to the driver.
For the sake of clarity of the article, I'm now continuing with the driver as I should do in the first place.

One of the reasons to leave the driver for later was that I couldn't get the idea of it, and of course, once I solved the punch card executables I started to understand what's going on here.

I already have decoded virtual machine shellcode, ROM image with fixed tail bytes, a string "ESETConst" and some binary data in a form of a bitmap image inside a RAM drive created by the driver.

There's a procedure that handles the IRP_MJ_READ and IRP_MJ_WRITE requests and this procedure is where I should continue my work here.
After some static analysis, it turned out the procedure handles IRP_MJ_WRITE as-is but on IRP_MJ_READ it modifies the requested data.

So, basically if I try to read the PunchCard.bmp from that drive, its contents should be returned in a modified form.
I had major issues with tracing that part of the code. Problem was that my breakpoints on the READ requests were never triggered.
In the end, it turned out that Windows was giving me cached data, so it never called that part of the code where my breakpoints were set.
To solve this issue I wrote a small piece of C code that will try to read the PunchCard.bmp by specifically request it as non-cached read:
Requesting non-cached PunchCard.bmp#include <windows.h>
#include <stdio.h>

int main() {
    char    filepath[] = "\\\\?\\GLOBALROOT\\Device\\45736574\\PunchCard.bmp";
    DWORD   rw;
    DWORD   flags[] = {FILE_ATTRIBUTE_NORMAL|FILE_FLAG_NO_BUFFERING, FILE_ATTRIBUTE_NORMAL};

    for(int i = 0; i < sizeof(flags)/sizeof(DWORD); i++) {
        HANDLE hFile = CreateFile(filepath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, flags[i], NULL);
        if (hFile == INVALID_HANDLE_VALUE) {
            printf("Unable to open file\n");
            break;
        }
        FlushFileBuffers(hFile);
        if (i == 1) {
            DWORD size = GetFileSize(hFile, NULL);
            byte *data = VirtualAlloc(0, size, MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE);
            ReadFile(hFile, data, size, &rw, NULL);
            VirtualFree(data, 0, MEM_RELEASE);
        }
        CloseHandle(hFile);
    }
    return 0;
}
I wish I could kick myself in the ass, because figuring that out took me more time than reverse engineering the next two executables. Doh!

Anyway, once I solved that issue, I can now set breakpoints that will trigger.

There's a small catch here. To trigger the code that will start the virtual machine image, three checks must be passed:
IRP_MJ_READ decrypt trigger.text:00402C6E                 mov     ecx, [ebp+IRP]
.text:00402C71                 mov     edx, [ecx+0Ch]            ;/ checks if the requested data is at offset 0x00013200
.text:00402C74                 cmp     edx, _0x00013200          ;| where the PunchCard.bmp is located
.text:00402C7A                 jb      short loc_402CD2          ;\
.text:00402C7C                 movzx   eax, _regkey_data         ;/ checks the _regkey_data flag is set to TRUE
.text:00402C83                 test    eax, eax                  ;|
.text:00402C85                 jz      short loc_402CD2          ;\
.text:00402C87                 movzx   ecx, _flag_0xAA06         ;/ checks the _flag_0xAA06 flag is set to TRUE
.text:00402C8E                 test    ecx, ecx                  ;|
.text:00402C90                 jz      short loc_402CD2          ;\
.text:00402C92                 mov     edx, [ebp+IRP]
.text:00402C95                 mov     eax, [edx+4]
.text:00402C98                 push    eax
.text:00402C99                 mov     ecx, [ebp+MDL_StartVa]
.text:00402C9C                 mov     edx, [ecx+2Ch]
.text:00402C9F                 mov     eax, [ebp+IRP]
.text:00402CA2                 add     edx, [eax+0Ch]
.text:00402CA5                 push    edx
.text:00402CA6                 mov     ecx, [ebp+arg_0]
.text:00402CA9                 push    ecx
.text:00402CAA                 call    @ROM_decryptor            ; executes the virtual machine code
The requested data is always located at offset 0x00013200 and _flag_0xAA06 is set to TRUE after the virtual machine shellcode and its ROM image were decrypted, so these two will pass.
However the _regkey_data is FALSE.
The reason is, this flag is set after successfully reading the string registry key "ESETConst" located at "\REGISTRY\MACHINE\SYSTEM\ControlSet001\services\Crackme_drv\EsetCrackme".
That's handled by a thread that constantly tries to read the value of that key.
Once I created that key and gave it some value, my breakpoint at VA 00402CAA got executed and I could finally start working on the ROM image.

This ROM image however is a bit different than the ones I previously encounter.
I'm not sure if that's intentional or it's a typo, but here the signature WORD was in little endian notation.
The second difference was in the ROM header structure, that was ordered differently:
Driver ROM image headerstruct BYTECODE_ROM {
    WORD    signature;      // little endian 0x1337 - non encrypted; anything else is encrypted
    DWORD   size_image;     // Size of ROM image
    DWORD   offset_EP;      // Offset to entry point
    DWORD   offset_DATA;    // Offset to data section
    DWORD   reserved;       // Set to 0x00000001
    byte    loader[];       // Bootloader code, that decrypt the Code and Data sections
    byte    code[];         // Code section
    byte    data[];         // Data section
}

Again using the ROM decompiler, I was able to get a easy to read pseudo assembly:
Driver ROM image00000000 | 06 0C 02 00                       CMP DWORD R12, 0x00        ;/ R12 = reg_key buffer
00000004 | 07 01 8A 00 00 00                 JZ 0000008A                ;\
0000000A | 06 0D 02 00                       CMP DWORD R13, 0x00        ;/ R13 = reg_key length
0000000E | 07 01 1E 01 00 00                 JZ 0000011E                ;\
00000014 | 01 C2 20                          MOV DWORD R2, DWORD R12    ;/ R2 = reg_key
00000017 | 01 13 23 64 01 00 00              MOV DWORD R3, &data_164    ;| R3 = rom_key2
0000001E | 01 C4 23 37 01 00 00              MOV DWORD R4, &data_137    ;\ R4 = key "ETSE", hardcoded in the ROM data section
00000025 | 01 25 20                          MOV DWORD R5, DWORD R2     ;/ calculate reg_key end address
00000028 | 0A D5 18                          ADD DWORD R5, DWORD R13    ;\ R5 = reg_key_end
0000002B | 01 3E 20                          MOV DWORD R14, DWORD R3    ;/ calculate data end address
0000002E | 01 E0 23 60 01 00 00              MOV DWORD R0, &data_160    ;|
00000035 | 0A 0E 19                          ADD DWORD R14, DWORD &R0   ;\
00000038 | 01 4F 20                          MOV DWORD R15, DWORD R4    ;/ calculate rom_key2 end address
0000003B | 0A 4F 12 04                       ADD BYTE R15, 0x04         ;\
0000003F | 06 52 18                          CMP DWORD R2, DWORD R5     ;/ rom_key1 iterator
00000042 | 07 01 4B 00 00 00                 JNZ 0000004B               ;|
00000048 | 01 C2 20                          MOV DWORD R2, DWORD R12    ;\
0000004B | 06 F4 18                          CMP DWORD R4, DWORD R15    ;/ rom_key2 iterator
0000004E | 07 01 5B 00 00 00                 JNZ 0000005B               ;|
00000054 | 01 54 23 37 01 00 00              MOV DWORD R4, &data_137    ;\ key "ETSE"
0000005B | 06 E3 08                          CMP DWORD R3, DWORD R14    ;/ loop limit
0000005E | 07 01 00 01 00 00                 JZ 00000100                ;\
00000064 | 01 20 01                          MOV DWORD R0, BYTE &R2     ;/ calculator
00000067 | 0A 30 01                          XOR BYTE R0, BYTE &R3      ;|
0000006A | 0A 10 12 01                       ADD BYTE R0, 0x01          ;|
0000006E | 0A 10 52 01                       ROL BYTE R0, 0x01          ;|
00000072 | 0A 40 01                          XOR BYTE R0, BYTE &R4      ;|
00000075 | 01 03 04                          MOV BYTE &R3, BYTE R0      ;\ rol(rom_key1[i] ^ data[i] + 1, 1) ^ rom_key2[i]
00000078 | 0A 12 12 01                       ADD BYTE R2, 0x01          ;/ rom_key1++
0000007C | 0A 13 12 01                       ADD BYTE R3, 0x01          ;| data++
00000080 | 0A 14 12 01                       ADD BYTE R4, 0x01          ;| rom_key2++
00000084 | 07 00 3F 00 00 00                 JMP 0000003F               ;\
0000008A | 01 A3 23 40 01 00 00              MOV DWORD R3, &data_140    ;/ data
00000091 | 01 14 23 37 01 00 00              MOV DWORD R4, &data_137    ;\ key "ETSE"
00000098 | 01 3E 20                          MOV DWORD R14, DWORD R3    ;/ calculate data end address
0000009B | 01 00 23 3C 01 00 00              MOV DWORD R0, &data_13C    ;|
000000A2 | 0A 0E 19                          ADD DWORD R14, DWORD &R0   ;\
000000A5 | 01 4F 20                          MOV DWORD R15, DWORD R4    ;/ calculate key end address
000000A8 | 0A 4F 12 04                       ADD BYTE R15, 0x04         ;\
000000AC | 06 F4 18                          CMP DWORD R4, DWORD R15    ;/ key iterator
000000AF | 07 01 BC 00 00 00                 JNZ 000000BC               ;|
000000B5 | 01 A4 23 37 01 00 00              MOV DWORD R4, &data_137    ;\ key "ETSE"
000000BC | 06 E3 08                          CMP DWORD R3, DWORD R14    ;/ loop limit
000000BF | 07 01 E8 00 00 00                 JZ 000000E8                ;\
000000C5 | 01 00 02 00                       MOV DWORD R0, 0x00         ;/
000000C9 | 0A 30 01                          XOR BYTE R0, BYTE &R3      ;|
000000CC | 0A 10 12 01                       ADD BYTE R0, 0x01          ;|
000000D0 | 0A 10 52 01                       ROL BYTE R0, 0x01          ;|
000000D4 | 0A 40 01                          XOR BYTE R0, BYTE &R4      ;| key[i]
000000D7 | 01 03 04                          MOV BYTE &R3, BYTE R0      ;\
000000DA | 0A 13 12 01                       ADD BYTE R3, 0x01          ;/ data++
000000DE | 0A 14 12 01                       ADD BYTE R4, 0x01          ;| key++
000000E2 | 07 00 AC 00 00 00                 JMP 000000AC               ;\
000000E8 | 01 F0 23 3C 01 00 00              MOV DWORD R0, &data_13C      
000000EF | 01 00 21                          MOV DWORD R0, DWORD &R0      
000000F2 | 04 80                             PUSH DWORD R0                
000000F4 | 04 B9 40 01 00 00                 PUSH &data_140               
000000FA | 07 00 12 01 00 00                 JMP 00000112                 
00000100 | 01 C0 23 60 01 00 00              MOV DWORD R0, &data_160    ;/ rom_key2 length
00000107 | 01 00 21                          MOV DWORD R0, DWORD &R0    ;\
0000010A | 04 80                             PUSH DWORD R0              ;/ rom_key2 length
0000010C | 04 B1 64 01 00 00                 PUSH &data_164             ;| rom_key2
00000112 | 04 8B                             PUSH DWORD R11             ;| data size
00000114 | 04 8A                             PUSH DWORD R10             ;| data
00000116 | 0D 47 0B 00 00 00 00 04           CALL data_0x04             ;\ Execute second stage of the same ROM image
0000011E | 00                                RET

This code works on two modes - generator and executor. The generator starts at offset 0000008A and I'm guessing it's left there as a hint.
The thing is, if I want to trigger that code, the string I got from "ESETConst" registry key must be NULL, but if this string is NULL, the _regkey_data will be FALSE and the whole virtual machine will never be executed.

In generator mode, between 000000AC and 000000E2 a key is decrypted from the second part of that ROM image tail that got overwritten.
So, this solves the mystery, why this data had that pattern-like look - the first DWORD is the length, followed by the encrypted data padded to 0x20 bytes.

Represented in python, this code will look like this:
Driver ROM image, key generatordef key_generate(data, key):
   code = ""
   for i, b in enumerate(data):
      b += 1
      b = rol(b, 1)		# 8 bit ROL implementation
      b ^= key[i%len(key)]
      code += "%c" % (b & 0xFF)
   return code

data = b"\x82\x99\x8F\x92\x11\x9E\x18\x94\xB1\x8E\x8F\x11\x16\x9C\x11\x1A\x16\x9D"
key = b"ETSE"
print(key_generate(data, key))

Running it gave me the string "Barbakan Krakowski", that hashed with SHA1 (little endian) produces the hash 0F30181CF3A9857360A313DB95D5A169BED7CC37.
This is great, because I just got the second password for the challenge!

At this point, in my initial attempt to solve this crackme I could leave the driver because I technically don't need the BMP image.
But let's not leave unfinished business, and do the whole thing.

I got a password, now what? Now comes the executor mode of the ROM image where the string from the "ESETConst" key is used to decrypt the second chunk of the ROM image tail.
The decryptor code converted into python looks like this:
Driver ROM image, key decryptordef key_decrypt(data, keyA, keyB):
   code = ""
   for i, b in enumerate(data):
      b ^= keyB[i%len(keyB)]
      b += 1
      b = rol(b, 1)		# 8 bit ROL implementation
      b ^= keyA[i%len(keyA)]
      code += "%02X " % (b & 0xFF)
   return code

data = b"\xD0\xFC\xF9\xF7\x63\xED\x71\xFA\xD6\xAE\xE6\x62\x36\xFB\x63\x7F\x77\xE9"
keyA = b"ETSE"
keyB = b"Barbakan Krakowski"
print(key_decrypt(data, keyA, keyB))

This time the result is non printable:
decrypted second key62 69 4A 68 43 5B 71 6E AA 99 78 4D F9 7F 79 5F 7F 57

I thought I'm doing something wrong, but looking at the rest of the ROM image code it seems I still be on the right track.

The ROM image ends with a CALL instruction to somewhere, and after reverse engineered it's opcode parser, it turned out to be a call to a second ROM image, nested inside the current one.
This one is with encrypted code, but since it uses the same encryption algorithm as the previous ROMs, I could easily decrypt and disassemble it:
Second ROM image00000000 | 0B 60 00 01                       MOV DWORD R0, MALLOC(0x0100)   ;/ Allocate space for the RC4 table
00000004 | 01 05 20                          MOV DWORD R5, DWORD R0         ;\
00000007 | 01 01 02 00                       MOV DWORD R1, 0x00             ;/ generate RC4 table
0000000B | 01 10 04                          MOV BYTE &R0, BYTE R1          ;| *table = i
0000000E | 0A 10 12 01                       ADD BYTE R0, 0x01              ;| *table++
00000012 | 0A 11 12 01                       ADD BYTE R1, 0x01              ;| i++
00000016 | 06 01 26 00 01                    CMP DWORD R1, 0x0100           ;| limit is 0x100
0000001B | 07 01 0B 00 00 00                 JNB 0000000B                   ;\ loop end
00000021 | 01 00 02 00                       MOV DWORD R0, 0x00             ;/ shuffle RC4 table
00000025 | 01 01 02 00                       MOV DWORD R1, 0x00             ;|
00000029 | 01 C4 20                          MOV DWORD R4, DWORD R12        ;| key
0000002C | 01 4E 20                          MOV DWORD R14, DWORD R4        ;|
0000002F | 0A DE 18                          ADD DWORD R14, DWORD R13       ;|
00000032 | 06 E4 18                          CMP DWORD R4, DWORD R14        ;|/ key iterator
00000035 | 07 01 3E 00 00 00                 JNZ 0000003E                   ;||
0000003B | 01 C4 20                          MOV DWORD R4, DWORD R12        ;|\
0000003E | 01 53 20                          MOV DWORD R3, DWORD R5         ;|
00000041 | 0A 03 18                          ADD DWORD R3, DWORD R0         ;|
00000044 | 0A 31 11                          ADD BYTE R1, BYTE &R3          ;|
00000047 | 0A 41 11                          ADD BYTE R1, BYTE &R4          ;|
0000004A | 0A 01 76 00 01                    DIV WORD R1, 0x0100            ;|
0000004F | 01 53 20                          MOV DWORD R3, DWORD R5         ;|
00000052 | 0A 03 18                          ADD DWORD R3, DWORD R0         ;|
00000055 | 01 5F 20                          MOV DWORD R15, DWORD R5        ;|
00000058 | 0A 1F 18                          ADD DWORD R15, DWORD R1        ;|
0000005B | 01 32 01                          MOV DWORD R2, BYTE &R3         ;|
0000005E | 01 F3 05                          MOV BYTE &R3, BYTE &R15        ;|
00000061 | 01 2F 04                          MOV BYTE &R15, BYTE R2         ;|
00000064 | 0A 10 12 01                       ADD BYTE R0, 0x01              ;| i++
00000068 | 0A 14 12 01                       ADD BYTE R4, 0x01              ;| *key++
0000006C | 06 00 26 00 01                    CMP DWORD R0, 0x0100           ;| loop limit is 0x100
00000071 | 07 01 32 00 00 00                 JNB 00000032                   ;\ loop end
00000077 | 01 00 02 00                       MOV DWORD R0, 0x00             ;/ decryptor routine
0000007B | 01 01 02 00                       MOV DWORD R1, 0x00             ;|
0000007F | 01 A3 20                          MOV DWORD R3, DWORD R10        ;|
00000082 | 01 34 20                          MOV DWORD R4, DWORD R3         ;|
00000085 | 0A B4 18                          ADD DWORD R4, DWORD R11        ;|
00000088 | 06 43 08                          CMP DWORD R3, DWORD R4         ;| loop start
0000008B | 07 01 DC 00 00 00                 JZ 000000DC                    ;|
00000091 | 0A 10 12 01                       ADD BYTE R0, 0x01              ;|
00000095 | 0A 00 76 00 01                    DIV WORD R0, 0x0100            ;|
0000009A | 01 52 20                          MOV DWORD R2, DWORD R5         ;|
0000009D | 0A 02 18                          ADD DWORD R2, DWORD R0         ;|
000000A0 | 0A 21 11                          ADD BYTE R1, BYTE &R2          ;|
000000A3 | 0A 01 76 00 01                    DIV WORD R1, 0x0100            ;|
000000A8 | 01 5E 20                          MOV DWORD R14, DWORD R5        ;|
000000AB | 0A 1E 18                          ADD DWORD R14, DWORD R1        ;|
000000AE | 01 2F 01                          MOV DWORD R15, BYTE &R2        ;|
000000B1 | 01 E2 05                          MOV BYTE &R2, BYTE &R14        ;|
000000B4 | 01 FE 04                          MOV BYTE &R14, BYTE R15        ;|
000000B7 | 01 0F 02 00                       MOV DWORD R15, 0x00            ;|
000000BB | 0A 2F 11                          ADD BYTE R15, BYTE &R2         ;|
000000BE | 0A EF 11                          ADD BYTE R15, BYTE &R14        ;|
000000C1 | 0A 0F 76 00 01                    DIV WORD R15, 0x0100           ;|
000000C6 | 0A 5F 18                          ADD DWORD R15, DWORD R5        ;|
000000C9 | 01 FF 01                          MOV DWORD R15, BYTE &R15       ;|
000000CC | 0A 3F 01                          XOR BYTE R15, BYTE &R3         ;|
000000CF | 01 F3 04                          MOV BYTE &R3, BYTE R15         ;|
000000D2 | 0A 13 12 01                       ADD BYTE R3, 0x01              ;|
000000D6 | 07 00 88 00 00 00                 JMP 00000088                   ;\ loop end
000000DC | 0C 85                             FREE(DWORD R5)                 ; free the RC4 table buffer
000000DE | 00                                RET
It's a RC4 implementation!
Alright! So, if my I'm correct, decrypting the BMP data with the binary key I got should give me something meaningful.

But it doesn't...

My second attempt was to directly use "Barbakan Krakowski", and this gave... some results:


I guess I'm expected to provide a carefully crafted registry key (keyB) to key_decrypt(), that will decrypt the first key from the ROM image (data) to "Barbakan Krakowski"?
Right, let's try to (badly, but oh well) implement that:
key_bruteforce()def key_bruteforce(data, keyA, keyB):
   code = ""
   for i, k in enumerate(data):
      for c in range(0x00, 0xFF):
         b = (c ^ k) + 1
         b = (rol(b, 1, 8) ^ keyA[i%len(keyA)]) & 0xFF
         if b == keyB[i]:
            code += "%c"%c
            break
   return code

data = b"\xD0\xFC\xF9\xF7\x63\xED\x71\xFA\xD6\xAE\xE6\x62\x36\xFB\x63\x7F\x77\xE9"
keyA = b"ETSE"
keyB = b"Barbakan Krakowski"
print(key_bruteforce(data, keyA, keyB))

Well I be damned! Running this code gave me "Reversing is great" as result.
Nice, I finally got the correct registry key that will decrypt the correct RC4 key that will decrypt the PunchCard.bmp file. What an inception, right?

And here's the twist. When I changed the registry key to "Reversing is great" and copied the (non-cached) bitmap I got the following result:

So, my method obvious had a problem, but I found that problem pretty fast.
It turned out that the bitmap is getting decrypted 0x200 bytes at a time, and because the key was 0x12 bytes long, it did overwrap badly, thus producing the noisy image from my first result.




Finding the third password, PuncherMachine.exe


This one is a .NET app:


It's obfuscated by something custom or customized to the point where de4dot wasn't able to even detect.
dnSpy is a great tool for reverse engineering these, and even has a real time debugger built in, so I used it to browse around the code and later debug it in real time.

The obfuscation is straight forward - messed up naming convention, switch based control-flow, encrypted string and all that jazz, but definitely doable even by hand.

I walked around the code few times to get a taste of it and started with the first namespace, because it looked curious and simple enough for a static analysis:
namespace <PrivateImplementationDetails>{1A66E280-0D40-43C7-9513-A7B89807BE0A}internal class EMPTY_NAME {

   private static string EMPTY_NAME(int A_0, int A_1, int A_2) {
      string @string = Encoding.UTF8.GetString(SHORTED_NAMESPACE.EMPTY_NAME.EMPTY_NAME, A_1, A_2);
      SHORTED_NAMESPACE.EMPTY_NAME.EMPTY_NAME[A_0] = @string;
      return @string;
   }

   public static string A() {
      return SHORTED_NAMESPACE.EMPTY_NAME.EMPTY_NAME[0] ?? SHORTED_NAMESPACE.EMPTY_NAME.EMPTY_NAME(0, 0, 36);
   }

   public static string a() {
      return SHORTED_NAMESPACE.EMPTY_NAME.EMPTY_NAME[1] ?? SHORTED_NAMESPACE.EMPTY_NAME.EMPTY_NAME(1, 36, 36);
   }

   public static string B() {
      return SHORTED_NAMESPACE.EMPTY_NAME.EMPTY_NAME[2] ?? SHORTED_NAMESPACE.EMPTY_NAME.EMPTY_NAME(2, 72, 15);
   }

   public static string b() {
      return SHORTED_NAMESPACE.EMPTY_NAME.EMPTY_NAME[3] ?? SHORTED_NAMESPACE.EMPTY_NAME.EMPTY_NAME(3, 87, 17);
   }

   public static string C() {
      return SHORTED_NAMESPACE.EMPTY_NAME.EMPTY_NAME[4] ?? SHORTED_NAMESPACE.EMPTY_NAME.EMPTY_NAME(4, 104, 22);
   }

   public static string c() {
      return SHORTED_NAMESPACE.EMPTY_NAME.EMPTY_NAME[5] ?? SHORTED_NAMESPACE.EMPTY_NAME.EMPTY_NAME(5, 126, 15);
   }

   // trimmed

   static EMPTY_NAME() {
      SHORTED_NAMESPACE.EMPTY_NAME.EMPTY_NAME = new byte[] { /* stripped for clarity */ };
      int num = 0;
      for (;;) {
         IL_2B:
         int num2 = -1099461478;
         for (;;) {
            switch (num2 ^ -1099461477) {
               case 0:
                  SHORTED_NAMESPACE.EMPTY_NAME.EMPTY_NAME[num] = (byte)((int)SHORTED_NAMESPACE.EMPTY_NAME.EMPTY_NAME[num] ^ num ^ 170);
                  num++;
                  num2 = -1099461479;
                  continue;
               case 1:
                  num2 = -1099461479;
                  continue;
               case 2:
                  num2 = ((num < SHORTED_NAMESPACE.EMPTY_NAME.EMPTY_NAME.Length) ? -1099461477 : -1099461480);
                  continue;
               case 4:
                  goto IL_2B;
            }
            return;
         }
      }
   }
}

The obfuscation is pretty clear here, and the static code basically does XOR decryption of the buffer in the beginning.
Those public methods do a lookup inside an array of strings, and either return its contents (if there's any), or call the main method, that acts as a substring() method of the decrypted buffer.
That's pretty simple, so I first decrypted the buffer, then extracted the parameters from the public functions, to split it into separate strings using this code here:
string decryptdata = [
   # encrypted buffer trimmed for clearance
]

entries = (
   # array of lists in (method, index, offset, length) format
)

for i, b in enumerate(data):
   data[i] = chr((b ^ i ^ 170) & 0xFF)

data = "".join(data)
for e in entries:
   print("[%d] %s() \"%s\""%(e[1], e[0], data[e[2]:e[2]+e[3]]))

Running it gave me this list of decrypted strings, that can be uses as reference table later:
imethoddataimethoddataimethoddata
0A()"3023912A-E3F8-4026-B6E1-3950992FAFE8"1a()"Another instance is already running."2B()"openFileDialog1"
3b()"Load puncher card"4C()"load_punch_card_button"5c()"Load punch card"
6D()"punch_card_pictureBox"7d()"dataToPunchTextBox"8E()"punchItButton"
9e()"Punchit!"10F()"calibrate_panel"11f()"calibrate_button"
12G()"Calibrate!"13g()"label2"14H()"Calibration code 2"
15h()"label1"16I()"Calibration code 1"17i()"calibrationcode2"
18J()"calibrationcode1"19j()"load_punch_card_panel"20K()"pictureBox1.Image"
21k()"pictureBox1"22L()"PuncherMachineForm"23l()"Puncher machine"
24M()"Punch error!"25m()"Error!"26N()"Calibration error!"
27n()"Invalid punch card!"28O()"001000000000"29o()"000100000000"
30P()"000010000000"31p()"000001000000"32Q()"000000100000"
33q()"000000010000"34R()"000000001000"35r()"000000000100"
36S()"000000000010"37s()"000000000001"38T()"100100000000"
39t()"100010000000"40U()"100001000000"41u()"100000100000"
42V()"100000010000"43v()"100000001000"44W()"100000000100"
45w()"100000000010"46X()"100000000001"47x()"010100000000"
48Y()"010010000000"49y()"010001000000"50Z()"010000100000"
51z()"010000010000"52aA()"010000001000"53aa()"010000000100"
54aB()"010000000010"55ab()"010000000001"56aC()"001100000000"
57ac()"001010000000"58aD()"001001000000"59ad()"001000100000"
60aE()"001000010000"61ae()"001000001000"62aF()"001000000100"
63af()"001000000010"64aG()"001000000001"65ag()"101100000000"
66aH()"101010000000"67ah()"101001000000"68aI()"101000100000"
69ai()"101000010000"70aJ()"101000001000"71aj()"101000000100"
72aK()"101000000010"73ak()"101000000001"74aL()"110100000000"
75al()"110010000000"76aM()"110001000000"77am()"110000100000"
78aN()"110000010000"79an()"110000001000"80aO()"110000000100"
81ao()"110000000010"82aP()"110000000001"83ap()"011010000000"
84aQ()"011001000000"85aq()"011000100000"86aR()"011000010000"
87ar()"011000001000"88aS()"011000000100"89@as()"011000000010"
90aT()"011000000001"91at()"000010000010"92aU()"000001000010"
93au()"000000100010"94aV()"000000010010"95av()"000000001010"
96aW()"000000000110"97aw()"100001000010"98aX()"100000100010"
99ax()"100000010010"100aY()"100000001010"101ay()"100000000110"
102aZ()"010001000010"103az()"010000100010"104BA()"010000010010"
105Ba()"010000001010"106BB()"001001000010"107Bb()"001000100010"
108BC()"001000010010"109Bc()"001000001010"110BD()"001000000110"
111Bd()"000000000000"112BE()"010000000000"113Be()"100000000000"
114BF()"DynMethod.DynMethodFactory"115Bf()"createMethod"116BG()"No data to punch!"
117Bg()"punch_card_{0}.bmp"118BH()"Can't save punchcard with name: {0}"119Bh()"Maximum line length exceeded on line nr.: {0}"
120BI()"Invalid character on line nr.: {0}"121Bi()"{0:x2}"122BJ()"Pipe communication failed!"
123Bj()"."124BK()"EsetCrackmePipe"125Bk()"Write timeout!"
126BL()"Read timeout!"127Bl()"Invalid argument"128BM()"punchcard.bmp"
129Bm()"resource{0}"130BN()"A.H"131Bn()"Computer_card_punch"
132BO()"fast"133Bo()"{ mi = "134BP()", attributes = "
135Bp()" }"136BQ()"{ Mi = "137Bq()", Attribute = "

When executed and during the initialization process, the app makes pipe requests to the main executable.
The requested eggs are AES decrypted using a key created in the following way:

- bytecode of private static void A.A()
- HEX representation of the string "8B5C690C-909B-4510-AB4C-15A0E19F73C0"
- bytecode of private static void a()
- HEX representation of the string "2E0666AC-9D68-4C16-85DD-7442AC607D81"
- bytecode of private static void A.A(object A_0)
- HEX representation of the string "B31431D2-B17E-404F-8F39-8356CC5D69EE"
- bytecode of private static byte[] A.B()
- HEX representation of the string "618EEF38-5CA5-4446-B432-62D48B5FD2F6"
- bytecode of protected void a.a()
- HEX representation of the string "2EB0079C-BD4E-47D5-9CF5-3B5EA72FC31E"

This data gets MD5 hashed then converted to lower case ASCII string to produces the AES key "3cc021f8bc623ec0f5450c55418ba120".
So, if I did deobfuscated the application, this key would be wrong therefore it will produce wrong decrypted data.
Good thing I didn't successfully deobfuscated it then, right?

This key is used to decrypt eggs 0xFF02 and 0xFF04.
Egg 0xFF02 is just a simple string "95eceaa118dd081119e26be1c44da2cb", but egg 0xFF04 turned out to be a fully functional DLL.

The app has only one button, and pressing it triggers yet another pipe request - pulling egg 0xFF00 that is 0x2B0 bytes of unknown binary data:
HEX dump of egg 0xFF001E F7 0F F7 12 DA 4B 68 4A 38 F4 2F A1 BD 84 76
F3 56 D8 68 2F A1 BD 04 E9 77 BC A1 BD 84 F6 12
AD A6 A0 DA 4B 68 2F 21 68 25 DA 68 2F A1 BD 04
1D 5F 13 F7 12 DA 4B E8 74 F9 4C 85 F6 12 DA CB
4A 88 86 13 DA 4B 68 2F 7D 62 15 F7 12 DA 4B 68
63 8A 36 BE 84 F6 12 DA A6 AD 1A F7 12 DA 4B 68

After that, a file open dialogue pops out, and since this is a puncher machine, feeding it with the PunchCard.bmp looks pretty natural.
Doing so shows where that hash string "95eceaa118dd081119e26be1c44da2cb" I got from egg 0xFF02 gets into play.
The file's MD5 checksum is compared against the hash from egg 0xFF02 and if they don't match, an error message is displayed.
In the end, it turned out that properly decrypting the BMP image was a good thing, even though patching the hashes before the comparison is also an option.

This leads me here:


Seems like I'll have to pass these two checks and that's where the DLL from egg 0xFF04 is thrown into action.

The DLL fortunately is not obfuscated and it even has it's original name "CalibrationDynMethod.dll".
It gets dynamically loaded and executed here:
Dynamic execution of CalibrationDynMethod.dllprivate b.B<ulong, string> A(uint[] A_1) {
   MethodInfo method = this.A.GetType("DynMethod.DynMethodFactory").GetMethod("createMethod");
   DynamicMethod dynamicMethod = (DynamicMethod)method.Invoke(null, new object[] { A_1 });
   return (b.B<ulong, string>)dynamicMethod.CreateDelegate(typeof(b.B<ulong, string>));
}

There's two methods inside I'm interested in, so let's see them in order of their execution:
CalibrationDynMethod.dll createMethod()public static DynamicMethod createMethod(uint[] instructionHashes) {
   OpCode[] array = (from x in 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>();
   DynMethodFactory.OneParameter<uint, string> oneParameter = DynMethodFactory.create_HashMethodForOpcodes();
   Hashtable hashtable = new Hashtable();
   foreach (OpCode opCode in array) {
      hashtable.Add(oneParameter(opCode.Name), opCode);
   }
   Type[] parameterTypes = new Type[] { typeof(string) };
   DynamicMethod dynamicMethod = new DynamicMethod("", typeof(ulong), parameterTypes);
   ILGenerator ilgenerator = dynamicMethod.GetILGenerator();
   ilgenerator.DeclareLocal(typeof(ulong), true);                                        // ulong loc0
   ilgenerator.DeclareLocal(typeof(int), true);                                          // int loc1
   ilgenerator.DeclareLocal(typeof(ulong), true);                                        // ulong loc2
   ilgenerator.DeclareLocal(typeof(bool), true);                                         // bool loc3
   Label label = ilgenerator.DefineLabel();
   Label label2 = ilgenerator.DefineLabel();
   Label label3 = ilgenerator.DefineLabel();
   IlParticlesEmitor il = new IlParticlesEmitor(ilgenerator);
   // add() = addILParticle()
   // emit() = new ILEmitParticle()
   il.add(emit(OpCodes.Nop, null, "IL_00000"));                                          // procedure start
   il.add(emit(OpCodes.Ldc_I8, 3074457345618258791L, "IL_00010"));                       // /
   il.add(emit(OpCodes.Stloc_0, null, "IL_000a0"));                                      // \ loc0 = 0x2AAAAAAAAAAAAB67
   il.add(emit(OpCodes.Ldc_I4_0, null, "IL_000b0"));                                     // /
   il.add(emit(OpCodes.Stloc_1, null, "IL_000c0"));                                      // \ loc1 = 0
   il.add(emit(OpCodes.Br_S, label, "IL_000d0"));                                        // jump to label
   il.add(emit(label2, null, "IL_000d9"));                                               // 
   il.add(emit(OpCodes.Nop, null, "IL_000f0"));                                          // 
   il.add(emit(OpCodes.Ldloc_0, null, "IL_00100"));                                      // / loc0
   try {                                                                                 // |/
      il.add(emit(hashtable[instructionHashes[0]], null, "IL_00110"));                   // || first stolen instruction
   } catch (Exception) { }                                                               // |\
   il.add(emit(OpCodes.Ldloc_1, null, "IL_00120"));                                      // |
   il.add(emit(OpCodes.Callvirt, typeof(string).GetMethod("get_Chars"), "IL_00130"));    // | get char from a string
   il.add(emit(OpCodes.Conv_U8, null, "IL_00180"));                                      // | convert to byte
   il.add(emit(OpCodes.Add, null, "IL_00190"));                                          // |
   il.add(emit(OpCodes.Stloc_0, null, "IL_001a0"));                                      // \ loc0 += ?[i]
   il.add(emit(OpCodes.Ldloc_0, null, "IL_001b0"));                                      // / loc0
   il.add(emit(OpCodes.Ldc_I8, 3074457345618258799L, "IL_001c0"));                       // |
   try {                                                                                 // |/
      il.add(emit(hashtable[instructionHashes[1]], null, "IL_00250"));                   // || second stolen instruction
   } catch (Exception) { }                                                               // |\
   il.add(emit(OpCodes.Stloc_0, null, "IL_00260"));                                      // \ loc0 ?= 0x2AAAAAAAAAAAAB6F
   il.add(emit(OpCodes.Nop, null, "IL_00270"));                                          // 
   il.add(emit(OpCodes.Ldloc_1, null, "IL_00280"));                                      // /
   il.add(emit(OpCodes.Ldc_I4_1, null, "IL_00290"));                                     // |
   il.add(emit(OpCodes.Add, null, "IL_002a0"));                                          // |
   il.add(emit(OpCodes.Stloc_1, null, "IL_002b0"));                                      // \ loc1++
   il.add(emit(label, null, "IL_002b9"));                                                // 
   il.add(emit(OpCodes.Ldloc_1, null, "IL_002c0"));                                      // / loc1
   il.add(emit(OpCodes.Ldarg_0, null, "IL_002d0"));                                      // | 
   il.add(emit(OpCodes.Callvirt, typeof(string).GetMethod("get_Length"), "IL_002e0"));   // | arg0.length()
   il.add(emit(OpCodes.Clt, null, "IL_00330"));                                          // |
   il.add(emit(OpCodes.Stloc_3, null, "IL_00350"));                                      // |
   il.add(emit(OpCodes.Ldloc_3, null, "IL_00360"));                                      // |
   il.add(emit(OpCodes.Brtrue_S, label2, "IL_00370"));                                   // \ for loop limit
   il.add(emit(OpCodes.Ldloc_0, null, "IL_00390"));                                      // /
   il.add(emit(OpCodes.Stloc_2, null, "IL_003a0"));                                      // \ loc2 = loc0
   il.add(emit(OpCodes.Br_S, label3, "IL_003b0"));                                       // / this is pointless
   il.add(emit(label3, null, "IL_003c9"));                                               // \
   il.add(emit(OpCodes.Ldloc_2, null, "IL_003d0"));                                      // /
   il.add(emit(OpCodes.Ret, null, "IL_003e0"));                                          // \ return loc2
   il.emitParticles();
   return dynamicMethod;
}
CIL shellcode, nice.

However, there's two stolen instructions that I'll have to populate somehow.
With the comments I added, I can safely assume that the first one should be pushing arg0 on the stack and the second one should be some arithmetic operation.

During my debug run, I found out that instructionHashes contains one item - 0x12312312, which is HEX representation of the first 8 bytes of the code I entered in the first field "123123123".
It took the first 8 characters as HEX, and after some more digging in the code I found that the next 8 characters from the first input field, will be taken as second HEX value in the instructionHashes.

These two values are used as keys for the hashtable, and here's where the second procedure from CalibrationDynMethod.dll is called:
CalibrationDynMethod.dll oneParameter()public static DynMethodFactory.OneParameter<uint, string> create_HashMethodForOpcodes() {
   Type[] parameterTypes = new Type[] { typeof(string) };
   DynamicMethod dynamicMethod = new DynamicMethod("", typeof(uint), parameterTypes);
   ILGenerator ilgenerator = dynamicMethod.GetILGenerator();
   ilgenerator.DeclareLocal(typeof(uint));
   ilgenerator.DeclareLocal(typeof(int));
   Label label = ilgenerator.DefineLabel();
   Label label2 = ilgenerator.DefineLabel();
   MethodInfo method = typeof(string).GetMethod(
      "get_Chars",
      BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic,
      null, new Type[] { typeof(int) }, null);
   MethodInfo method2 = typeof(string).GetMethod(
      "get_Length",
      BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic,
      null, new Type[0], null);
   ilgenerator.Emit(OpCodes.Ldc_I4_0);                    // /
   ilgenerator.Emit(OpCodes.Stloc_0);                     // \ loc0 = 0
   ilgenerator.Emit(OpCodes.Ldc_I4_0);                    // /
   ilgenerator.Emit(OpCodes.Stloc_1);                     // \ loc1 = 0
   ilgenerator.Emit(OpCodes.Br_S, label);                 //  jump to label
   ilgenerator.MarkLabel(label2);                         // / loop start
   ilgenerator.Emit(OpCodes.Ldloc_0);                     // |/
   ilgenerator.Emit(OpCodes.Ldarg_0);                     // ||
   ilgenerator.Emit(OpCodes.Ldloc_1);                     // ||
   ilgenerator.Emit(OpCodes.Callvirt, method);            // || get_Chars
   ilgenerator.Emit(OpCodes.Add);                         // ||
   ilgenerator.Emit(OpCodes.Stloc_0);                     // |\ loc0 += arg0[loc1]
   ilgenerator.Emit(OpCodes.Ldloc_0);                     // |/
   ilgenerator.Emit(OpCodes.Ldloc_0);                     // ||/
   ilgenerator.Emit(OpCodes.Ldc_I4_S, 10);                // |||
   ilgenerator.Emit(OpCodes.Shl);                         // ||\ loc0 << 10
   ilgenerator.Emit(OpCodes.Add);                         // ||
   ilgenerator.Emit(OpCodes.Stloc_0);                     // |\ loc0 += (loc0 << 10)
   ilgenerator.Emit(OpCodes.Ldloc_0);                     // |//
   ilgenerator.Emit(OpCodes.Ldloc_0);                     // |||
   ilgenerator.Emit(OpCodes.Ldc_I4_6);                    // |||
   ilgenerator.Emit(OpCodes.Shr_Un);                      // ||\ loc0 >> 6
   ilgenerator.Emit(OpCodes.Xor);                         // ||
   ilgenerator.Emit(OpCodes.Stloc_0);                     // |\ loc0 ^= (loc0 >> 6)
   ilgenerator.Emit(OpCodes.Ldloc_1);                     // |/
   ilgenerator.Emit(OpCodes.Ldc_I4_1);                    // ||
   ilgenerator.Emit(OpCodes.Add);                         // ||
   ilgenerator.Emit(OpCodes.Stloc_1);                     // |\ loc1++
   ilgenerator.MarkLabel(label);                          // |
   ilgenerator.Emit(OpCodes.Ldloc_1);                     // |/ loc1 in the iterator
   ilgenerator.Emit(OpCodes.Ldarg_0);                     // || arg0 = input string
   ilgenerator.Emit(OpCodes.Callvirt, method2);           // || get_Length
   ilgenerator.Emit(OpCodes.Blt_S, label2);               // \\ jump to loop start at label2
   ilgenerator.Emit(OpCodes.Ldloc_0);                     // //
   ilgenerator.Emit(OpCodes.Ldloc_0);                     // ||
   ilgenerator.Emit(OpCodes.Ldc_I4_3);                    // ||
   ilgenerator.Emit(OpCodes.Shl);                         // |\ loc0 << 3
   ilgenerator.Emit(OpCodes.Add);                         // |
   ilgenerator.Emit(OpCodes.Stloc_0);                     // \ loc0 += (loc0 << 3)
   ilgenerator.Emit(OpCodes.Ldloc_0);                     // //
   ilgenerator.Emit(OpCodes.Ldloc_0);                     // ||
   ilgenerator.Emit(OpCodes.Ldc_I4_S, 11);                // ||
   ilgenerator.Emit(OpCodes.Shr_Un);                      // |\ loc0 >> 11
   ilgenerator.Emit(OpCodes.Xor);                         // |
   ilgenerator.Emit(OpCodes.Stloc_0);                     // \ loc0 ^= (loc0 >> 11)
   ilgenerator.Emit(OpCodes.Ldloc_0);                     // //
   ilgenerator.Emit(OpCodes.Ldloc_0);                     // ||
   ilgenerator.Emit(OpCodes.Ldc_I4_S, 15);                // ||
   ilgenerator.Emit(OpCodes.Shl);                         // |\ loc0 << 15
   ilgenerator.Emit(OpCodes.Add);                         // |
   ilgenerator.Emit(OpCodes.Stloc_0);                     // \ loc0 += (loc0 << 15)
   ilgenerator.Emit(OpCodes.Ldloc_0);                     // /
   ilgenerator.Emit(OpCodes.Ret);                         // \ return loc0
   return (DynMethodFactory.OneParameter<uint, string>)dynamicMethod.CreateDelegate(typeof(DynMethodFactory.OneParameter<uint, string>));
}

This code receives a CIL instruction as string to creates a 32 bit hash out of it, and it can be used to calculate the key corresponding to a certain CIL instruction to fix the first shellcode.

The first instruction, I already know must be "ldarg.0" that will push arg0 onto stack, but I still don't know what the arithmetic operation for the second one should be.

The answer lies back in the main executable, where the method from the dynamically loaded DLL is used here:
User input validation// I've deobfuscated and renamed some of the variables for clarity

// Initialize the data needed for the validation process
public b(string A_1, byte[] A_2, Assembly A_3, b.A A_4) {
   // populate the charset list
   this.charset = new List<char>();
   this.charset.Add('0');
   this.charset.Add('1');
   this.charset.Add('2');
   this.charset.Add('3');
   this.charset.Add('4');
   // trimmed
   
   // the following binary strings were originally encrypted
   this.msg_values = new List<int>();
   this.msg_values.Add(Convert.ToInt32("001000000000", 2));
   this.msg_values.Add(Convert.ToInt32("000100000000", 2));
   this.msg_values.Add(Convert.ToInt32("000010000000", 2));
   this.msg_values.Add(Convert.ToInt32("000001000000", 2));
   this.msg_values.Add(Convert.ToInt32("000000100000", 2));
   // trimmed
}

// populates this.message hashtable, using this.msg_values
private void a() {
   // e.A() is pulling the egg 0xFF00 data
   E e = new E();
   byte[] value = e.A(0xFF00, global::A.A.A);

   this.message = new Hashtable();
   for (int i = 0; i < this.message.Count; i++) {
      this.message.Add(BitConverter.ToUInt64(value, i * 8), this.msg_values[i]);
   }
}

// Validator procedure
public bool A(string UserInputA, string UserInputB) {
   // Take the user input from the first field
   // and split it into two DWORDS stored at cil_instructions
   List<uint> cil_instructions = new List<uint>();
   for(int i = 0; i < UserInputA.Length; i += 8) {
      cil_instructions.Add(Convert.ToUInt32(UserInputA.Substring(i, 8), 16));
   }

   // This is where the DLL is loaded
   // b() will be the name of the dynamically initialized method from the DLL
   // and the passed parameter will fix the stolen instructions
   b.B<ulong, string> b = createMethod(cil_instructions.ToArray());

   // populates this.message global
   this.a();

   // final result hashtable
   result = new Hashtable();

   // Iterate through the parsed message data from egg 0xFF00
   for(int i = 0; i < this.message.Count; i++) {
      // b() is calling the createMethod() from the DLL
      ulong key = b((i < UserInputB.Length) ? (this.charset[i].ToString() + UserInputB[i].ToString()) : this.message[i].ToString());

      // key is the result of the createMethod() and is used as hashtable key for this.message
      if (!this.message.ContainsKey(key)) {
         break;
      }

      result.Add(this.charset[i], message[key]);
   }
}

This loop in A() basically validates both user inputs.
The first one is fixing the stolen CIL instruction, while the second one is used for the validation against the data from egg 0xFF00.

The variable I named this.charset contains the list of characters "0123456789ABCDEFGHIJKLMNOPQR/STUVWXYZabcdefghijklmnopqrstuvwxyz:#@'=".<(+|$*);,%_>? -&".

Each of these characters are concatenated in pairs with a character from the second user input, so for my second input "abcdef", this code will construct pairs like "0a", "1b", "2c", "3d" and so on.
Finally, these pairs are getting hashed by the code with the two stolen instructions, and if the result hash is found in the message hashtable, the user input character is considered as valid and stored inside the result hashtable.

With that information, I can find the needed arithmetic instruction from the CIL shellcode like this:
- the first character is the first character from the charset list and this is always '0'
- to this '0' I must append the first character from the second input field and this is what I'm trying to guess here
- the result should match the ulong representation of the first 8 bytes from egg 0xFF00, that is 0x684BDA12F70FF71E

I can easily bruteforce that, trying arithmetic operations until I get a match:
Finding the correct arithmetic operationfrom ctypes import c_uint, c_ulonglong

def brute(input_hash):
   charset = "0123456789ABCDEFGHIJKLMNOPQR/STUVWXYZabcdefghijklmnopqrstuvwxyz:#@'=\".<(+|$*);,%_>? -&"

   for c in charset:
      for o in ('+', '-', '^', '//', '*'):
         hash = 0x2AAAAAAAAAAAAB67
         for b in "0"+c:
            hash = c_ulonglong(hash + ord(b)).value
            # eval(), because like Steven Tyler, I also like living on the edge
            hash = c_ulonglong(eval("hash "+o+" 0x2AAAAAAAAAAAAB6F")).value

         if hash == input_hash:
            print("Found operation: %s"%o)

# 0x684BDA12F70FF71E is the checksum for the first character
brute(0x684BDA12F70FF71E)
If we ignore for a second the quality of the code above, the answer is multiplication.

I can now implement the whole thing onto a calibration codes solver:
Solution for the calibration codesfrom ctypes import c_uint, c_ulonglong

# calculate stolen instruction hash for calibration code 1
def calc_opcode_hash(opcode):

   code = 0
   for b in opcode:
      code += ord(b)
      code = c_uint(code + (code << 10)).value
      code = c_uint(code ^ (code >> 6)).value

   code = c_uint(code + (code << 3)).value
   code = c_uint(code ^ (code >> 11)).value
   code = c_uint(code + (code << 15)).value
	
   return code

# calculate char pair hash for calibration code 2
def calc_pair_hash(chunk):
   hash = 0x2AAAAAAAAAAAAB67
   for b in chunk:
      hash = c_ulonglong(hash + ord(b)).value
      hash = c_ulonglong(hash * 0x2AAAAAAAAAAAAB6F).value

   return hash

# the file contains the raw binary contents of egg 0xFF00
data = open("egg_0xFF00.bin", "rb").read()

# decrypt the message hidden inside egg 0xFF00
charset = "0123456789ABCDEFGHIJKLMNOPQR/STUVWXYZabcdefghijklmnopqrstuvwxyz:#@'=\".<(+|$*);,%_>? -&"
code = ""
for j, i in enumerate(range(0, len(data), 8)):
   pair_hash = int.from_bytes(data[i:i+8], byteorder='little', signed=False)

   for c in charset:
      if (calc_pair_hash(charset[j:j+1]+c) == pair_hash):
         code += c

print("Calibration code 1: %08X%08X"%(calc_opcode_hash("ldarg.0"), calc_opcode_hash("mul")))
print("Calibration code 2: %s"%code)

And the answers are:
Calibration codesCalibration code 1: 0364ABE72D29C96C
Calibration code 2: Infant Jesus of Prague

So I gave them a try:

Awesome! I don't know what to punch yet, but I think I'll find out in a minute.

Hashing "Infant Jesus of Prague" to SHA1 (little endian) produces the string "0B6A1C6651D1EB5BD21DF5921261697AA1593B7E" and this is the third password for the main app.
Filling it in the main app, flips the third flag 0xBB03 to TRUE and only one flag left to the "congrats" message.





Solving the last flag - PunchCardReader.exe


This one is also a .NET executable, with a minimalistic GUI:


Like the previous one, here's used the same obfuscation, so I'll use the know-how I got so far.
For a starter I decrypted the strings:
imethoddataimethoddataimethoddata
0A()"001000000000"1a()"000100000000"2B()"000010000000"
3b()"000001000000"4C()"000000100000"5c()"000000010000"
6D()"000000001000"7d()"000000000100"8E()"000000000010"
9e()"000000000001"10F()"100100000000"11f()"100010000000"
12G()"100001000000"13g()"100000100000"14H()"100000010000"
15h()"100000001000"16I()"100000000100"17i()"100000000010"
18J()"100000000001"19j()"010100000000"20K()"010010000000"
21k()"010001000000"22L()"010000100000"23l()"010000010000"
24M()"010000001000"25m()"010000000100"26N()"010000000010"
27n()"010000000001"28O()"001100000000"29o()"001010000000"
30P()"001001000000"31p()"001000100000"32Q()"001000010000"
33q()"001000001000"34R()"001000000100"35r()"001000000010"
36S()"001000000001"37s()"101100000000"38T()"101010000000"
39t()"101001000000"40U()"101000100000"41u()"101000010000"
42V()"101000001000"43v()"101000000100"44W()"101000000010"
45w()"101000000001"46X()"110100000000"47x()"110010000000"
48Y()"110001000000"49y()"110000100000"50Z()"110000010000"
51z()"110000001000"52aA()"110000000100"53aa()"110000000010"
54aB()"110000000001"55ab()"011010000000"56aC()"011001000000"
57ac()"011000100000"58aD()"011000010000"59ad()"011000001000"
60aE()"011000000100"61ae()"011000000010"62aF()"011000000001"
63af()"000010000010"64aG()"000001000010"65ag()"000000100010"
66aH()"000000010010"67ah()"000000001010"68aI()"000000000110"
69ai()"100001000010"70aJ()"100000100010"71aj()"100000010010"
72aK()"100000001010"73ak()"100000000110"74aL()"010001000010"
75al()"010000100010"76aM()"010000010010"77am()"010000001010"
78aN()"001001000010"79an()"001000100010"80aO()"001000010010"
81ao()"001000001010"82aP()"001000000110"83ap()"000000000000"
84aQ()"010000000000"85aq()"100000000000"86aR()"punch_card_{0}.bmp"
87ar()"Invalid picture format!"88aS()"{0:x2}"89@as()"Verification passed ..."
90aT()"Verification info"91at()"Verification failed ..."92aU()"Verification error!"
93au()"openFileDialog1"94aV()"Load puncher card"95av()"read_punch_card_button"
96aW()"Read punch cards"97aw()"ReaderForm"98aX()"Punch card reader"
99ax()"Pipe communication failed!"100aY()"."101ay()"EsetCrackmePipe"
102aZ()"Write timeout!"103az()"Read timeout!"104BA()"A.e"
105Ba()"C6513807-9F93-4A38-A422-9F1EF8F30E70"106BB()"Another instance is already running."107Bb()"DynMethod.DynMethodFactory"
108BC()"createMethod"109Bc()"{ mi = "110BD()", attributes = "
111Bd()" }"112BE()"{ Mi = "113Be()", Attribute = "

Again AES in ECB mode is using to decrypt egg data, and this time the key "a26d11dee294284f38db8a724c119d74" is derived the same way:

- bytecode of protected void c.A()
- HEX representation of the string "9C71FD9F-5147-4F34-B3C3-EFE37A950FEC"
- bytecode of private static void F.A()
- HEX representation of the string "387EF9CD-00B1-479C-9B5A-4E0B041F2007"
- bytecode of private static void F.a()
- HEX representation of the string "64FAC7B6-AE8B-4D65-89FF-F9EB457BA5C6"
- bytecode of private static void F.A(object A_0)
- HEX representation of the string "82C03C09-F8AC-485C-AE32-8C2E42898C3C"
- bytecode of private static byte[] F.B()
- HEX representation of the string "97C09DA9-A6E7-4A6D-819B-69B10456E731"

On initialization the egg 0xFF05 is requested and decrypted and what do you know, it's again a DLL file, originally named VerificationDynMethod.dll.
The DLL is used once the button is pressed, but before that, a set of punch cards is read here:
Deobfuscated bitmap readerpublic string[] A() {
   List<string> list = new List<string>();
   if (this.A) {
      return list.ToArray();
   }

   for (num5 = 0; num5 < 1000; num5++) {
      string text = string.Format("punch_card_{0}.bmp", Convert.ToString(num5).PadLeft(3, '0'));
      if (File.Exists(text)) {
         Bitmap bitmap = new Bitmap(text, true);
         if ((bitmap.PixelFormat == PixelFormat.Format4bppIndexed) && (bitmap.Width == 600) && (bitmap.Height == 259)) {
            BitmapData bitmapData = bitmap.LockBits(
               new Rectangle(0, 0, bitmap.Width, bitmap.Height),
               ImageLockMode.ReadOnly,
               bitmap.PixelFormat);
            array = new byte[bitmapData.Height * bitmapData.Stride];
            Marshal.Copy(bitmapData.Scan0, array, 0, array.Length);
            bitmap.UnlockBits(bitmapData);
            StringBuilder stringBuilder = new StringBuilder();
            for (num2 = 22; num2 < 582; num2 += 7) {
               num4 = 0;
               num6 = 1;
               for (num3 = 197; num3 > -7; num3 -= 17) {
                  if (this.A(array, num2, num3)) {
                     num4 |= num6;
                  }
                  num6 <<= 1;
               }
               if (this.A.ContainsKey(num4)) {
                  stringBuilder.Append(this.A[num4]);
               }
            }
            list.Add(stringBuilder.ToString().TrimEnd(new char[] {' '}));
            bitmap.Dispose();
         } else {
            throw new global::A.a("Invalid picture format!");
         }
      }
   }
   return list.ToArray();
}

From the code I can see it's trying to load up to 1000 punchcards with filenames punch_card_000.bmp to punch_card_999.bmp.
These punchcards are with dimensions 600px in width by 259px in height and they are in 4 bits per pixel format, or 16 colors.
The reading starts at x=22 and it repeats by adding 7 up to x=528. The simple calculation (582-22)/7 give us 80, which is the amount of characters a single punchcard can hold.

I already know most of that information from the PuncherMachine.exe anyway, so moving on.
The place where the DLL is used is here:
Validation processprotected void A(object A_1) {
   try {
      // array contains the contents of the provided punch cards (if any)
      string[] array = this.A.A();
	  // initialize the DLL
      f.A<bool> a = this.A(array);
	  // calls its function
      if (a()) {
         // verification passed
         ((A.A)A_1)(global::A.A.a.a, null);
      }
   } catch (Exception ex) {
      ((A.A)A_1)(global::A.A.a.B, ex);
   }
   this.A.Set();
}

I haven't provided any punch cards since I don't know yet what data should I punch and running it as-is rises an exception, so I took a look inside the DLL's code:
createMethod() startpublic static DynamicMethod createMethod(string[] instructions) {
   OpCode[] array = (from x in 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 hashtable = new Hashtable();
   foreach (OpCode opCode in array) {
   	hashtable.Add(opCode.Name, opCode);
   }
   Type[] parameterTypes = new Type[0];
   DynamicMethod dynamicMethod = new DynamicMethod("", typeof(bool), parameterTypes);
   ILGenerator ilgenerator = dynamicMethod.GetILGenerator();
   MethodInfo method = typeof(Encoding).GetMethod(
      "get_ASCII",
      BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic,
      null,
      new Type[0],
      null);
   MethodInfo method2 = typeof(Encoding).GetMethod(
      "GetBytes",
      BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic,
      null,
      new Type[] {
         typeof(string)
      },
      null);
   MethodInfo method3 = typeof(BitConverter).GetMethod(
      "ToUInt32",
      BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic,
      null,
      new Type[] {
         typeof(byte[]),
         typeof(int)
      },
      null);
   // Trimmed shellcode building
}

So, the complete CIL instruction set is taken here (skipping the break instruction) then addresses to the three methods get_ASCII, GetBytes and ToUInt32 are taken.
The interesting part is coming down, where a CIL shellcode is getting constructed:
createMethod() shellocde constructorilgenerator.DeclareLocal(typeof(uint));                      // local0
ilgenerator.DeclareLocal(typeof(uint));                      // local1
ilgenerator.DeclareLocal(typeof(uint));                      // local2
ilgenerator.DeclareLocal(typeof(uint));                      // local3
ilgenerator.DeclareLocal(typeof(uint));                      // local4
ilgenerator.DeclareLocal(typeof(uint));                      // local5
ilgenerator.DeclareLocal(typeof(byte[]));                    // local6
ilgenerator.DeclareLocal(typeof(uint));                      // local7
ilgenerator.DeclareLocal(typeof(bool));                      // local8
ilgenerator.DeclareLocal(typeof(bool));                      // local9
Label label = ilgenerator.DefineLabel();                     
Label label2 = ilgenerator.DefineLabel();                    
ilgenerator.Emit(OpCodes.Nop);                               // shellcode start
ilgenerator.Emit(OpCodes.Ldc_I4, 57005);                     // /
ilgenerator.Emit(OpCodes.Stloc_0);                           // \ loc0 = 0xDEAD
ilgenerator.Emit(OpCodes.Ldc_I4, 48879);                     // /
ilgenerator.Emit(OpCodes.Stloc_1);                           // \ loc1 = 0xBEEF
ilgenerator.Emit(OpCodes.Ldc_I4, 51966);                     // /
ilgenerator.Emit(OpCodes.Stloc_2);                           // \ loc2 = 0xCAFE
ilgenerator.Emit(OpCodes.Ldc_I4, 47806);                     // /
ilgenerator.Emit(OpCodes.Stloc_3);                           // \ loc3 = 0xBABE
ilgenerator.Emit(OpCodes.Ldc_I4, 64206);                     // /
ilgenerator.Emit(OpCodes.Stloc_S, 4);                        // \ loc4 = 0xFACE
ilgenerator.Emit(OpCodes.Ldloc_0);                           // load loc0 onto stack
ilgenerator.Emit(OpCodes.Ldloc_1);                           // load loc1 onto stack
try {
   ilgenerator.Emit((OpCode)hashtable[instructions[0]]);     // get first punchcard as instruction
} catch (Exception) { }
ilgenerator.Emit(OpCodes.Ldloc_2);                           // load loc2 onto stack
ilgenerator.Emit(OpCodes.Ldloc_3);                           // load loc3 onto stack
try {
   ilgenerator.Emit((OpCode)hashtable[instructions[1]]);     // get second punchcard as instruction
} catch (Exception) { }
ilgenerator.Emit(OpCodes.Xor);                               // / (loc0 ? loc1) ^ (loc2 ? loc3)
ilgenerator.Emit(OpCodes.Ldloc_S, 4);                        // | load loc4 onto stack
ilgenerator.Emit(OpCodes.Xor);                               // | (loc0 ? loc1) ^ (loc2 ? loc3) ^ loc4
ilgenerator.Emit(OpCodes.Ldc_I4, -229612108);                // | load 0xF25065B4 onto stack
ilgenerator.Emit(OpCodes.Xor);                               // | 
ilgenerator.Emit(OpCodes.Stloc_S, 5);                        // \ loc5 = (loc0 ? loc1) ^ (loc2 ? loc3) ^ loc4 ^ 0xF25065B4
ilgenerator.Emit(OpCodes.Call, method);                      // /reference to get_ASCII
ilgenerator.Emit(OpCodes.Ldstr, "ESET");                     // | "ESET"
ilgenerator.Emit(OpCodes.Callvirt, method2);                 // | GetBytes()
ilgenerator.Emit(OpCodes.Stloc_S, 6);                        // \ loc6 = GetBytes("ESET")
ilgenerator.Emit(OpCodes.Ldloc_S, 6);                        // / load loc6 onto stack
ilgenerator.Emit(OpCodes.Ldc_I4_0);                          // | load 0 onto stack
ilgenerator.Emit(OpCodes.Call, method3);                     // | ToUInt32()
ilgenerator.Emit(OpCodes.Stloc_S, 7);                        // \ loc7 = ToUInt32(loc6, 0)
ilgenerator.Emit(OpCodes.Ldloc_S, 5);                        // / load loc5 onto stack
ilgenerator.Emit(OpCodes.Ldloc_S, 7);                        // | load loc7 onto stack
ilgenerator.Emit(OpCodes.Ceq);                               // | if loc5 == loc7
ilgenerator.Emit(OpCodes.Ldc_I4_0);                          // | load 0 onto stack
ilgenerator.Emit(OpCodes.Ceq);                               // | (loc5 == loc7) == 0
ilgenerator.Emit(OpCodes.Stloc_S, 9);                        // \ loc9 = (loc5 == loc7) == 0
ilgenerator.Emit(OpCodes.Ldloc_S, 9);                        // / load loc9 onto stack
ilgenerator.Emit(OpCodes.Brtrue_S, label);                   // \ if loc9 == true; jump to label
ilgenerator.Emit(OpCodes.Ldc_I4_1);                          // / load 1 onto stack
ilgenerator.Emit(OpCodes.Stloc_S, 8);                        // \ loc8 = true
ilgenerator.Emit(OpCodes.Br_S, label2);                      // jump to label2
ilgenerator.MarkLabel(label);
ilgenerator.Emit(OpCodes.Ldc_I4_0);                          // / load 0 onto stack
ilgenerator.Emit(OpCodes.Stloc_S, 8);                        // \ loc8 = false
ilgenerator.Emit(OpCodes.Br_S, label2);                      // jump to label2
ilgenerator.MarkLabel(label2);
ilgenerator.Emit(OpCodes.Ldloc_S, 8);                        // load loc8 onto stack
try {
   ilgenerator.Emit((OpCode)hashtable[instructions[2]]);     // get third punchcard as instruction
} catch (Exception) { }
return dynamicMethod;                                        // return the built shellcode as a method

Alright. So, here's where the punchards are used to fix the stolen instruction of this shellcode and I need three of them.
The last one is pretty obvious, and it's "Ret" instruction, because every procedure should end with a return.

As most of the stack based languages, CIL also has this rule where you enter a method with empty stack and you always leave the method with empty stack for void procedures, or stack holding one value for procedures that returns result.
In this case, in order to keep the stack happy I should use some arithmetic operations, so in the end the stack contains only the return BOOL value from loc8.

Like in PuncherMachine.exe I bruteforced that:
Validation equation math operation bruteforcedef brute(input_hash):

   op = ('+', '-', '^', '//', '*')
   for o1 in op:
      for o2 in op:
         res_a = eval("0xDEAD "+o1+" 0xBEEF")
         res_b = eval("0xCAFE "+o2+" 0xBABE")

         hash = res_a ^ res_b ^ 0xFACE ^ 0xF25065B4
         if hash == input_hash:
            print("0xDEADu %s 0xBEEFu"%o1)
            print("0xCAFEu %s 0xBABEu"%o2)

# 0x54455345 is the DWORD representation of "ESET"
brute(0x54455345)

And this gave me the answer for the two stolen instructions:
validator stolen instructions0xDEADu * 0xBEEFu
0xCAFEu + 0xBABEu

So the stolen instructions are mul, add and ret.
Using the PuncherMachine.exe I punched myself those:






And pressed the "Read punch cards" button again:


Hooray! Challenge solved.

Comments

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

Guest 01 Feb, 2020 07:36
Nice write up. I believe the strtolower in DllMain is actually strtoupper though.
Guest 13 Mar, 2019 18:23
origen:
https://join.eset.com/en/open-positions/malware-analyst
https://join.eset.com/en/challenges/crack-me

about other, good job :)
Guest 01 Aug, 2018 13:26
w8 wut?
Guest 31 Jul, 2018 10:10
Allah
© nullsecurity.org 2011-2024 | legal | terms & rules | contacts