Zoom Icon

Invisible Lookup Table Crackme

From UIC

WarrantyVoider's "The secret of the invisible lookup table" crackme

Contents


Invisible Lookup Table Crackme
Author: ZaiRoN
Email: zaironcrk(at)hotmail(dot)com
Website: ...
Date: 14/06/2006 (dd/mm/yyyy)
Level: Slightly hard
Language: English Flag English.gif
Comments: ...


Video allegato


Introduction

Nice crackme but unfortunately without a tutorial, only a keygen made by jB. The crackme is really nice so a tutorial should be written; that's why I decided to write it. To reverse engineer the crackme you have to use a ring0 debugger because a driver is part of it, this is not totally right because sometimes you can do all the work without a ring0 debugger; I'll show you that a little home made program would suffice. What I'm going to use is one of my personal tool slightly modified for the crackme requirements. The original tool is a system call hook detector (which it simply lists all the system calls trying to figure out which one is hooked or not (nothing special, internet is full of this kind of programs)) added with some features like the possibility to see the disasm of a function (I use my personal disasm engine) and something else you'll see later. Yes, you'll *see* because I decided to explain my work using a video... something unusual but I hope you'll like it.


Tools

  • Ring0 debugger


Essay

As always my analysis starts looking at the file using Ida. One of the strings inside 'Strings' window captures my attention: "You rule! Please write a tutorial.". Let's see the code around the string:

.text:004012C0 lea edx, [esp+418h+Buffer]
.text:004012C4 push edx // lpBuffer
.text:004012C5 push 200h // nBufferLength
.text:004012CA call ds:GetTempPathA // Get the location of the temp dir
.text:004012D0 mov eax, [esp+418h+arg_4] // eax = serial you have typed
.text:004012D7 push eax
.text:004012D8 lea ecx, [esp+41Ch+Buffer] // temp dir
.text:004012DC push ecx
.text:004012DD lea edx, [esp+420h+FileName]
.text:004012E4 push offset str->Sl00kuptabl3_08x // "%sl00KuPtAbl3.%08X" <- how to format
// the string
.text:004012E9 push edx // LPSTR
.text:004012EA call ds:wsprintfA // Format the string with: <temp_dir>"l00KuPtAbl3."<serial>
.text:004012F0 add esp, 10h
.text:004012F3 push 0
.text:004012F5 push FILE_ATTRIBUTE_HIDDEN
.text:004012F7 push OPEN_EXISTING // OPEN_EXISTING... The file already exist...!?!
.text:004012F9 push 0
.text:004012FB push FILE_SHARE_READ
.text:004012FD push GENERIC_READ // GENERIC_READ flag, the file exist...!?!
.text:00401302 lea eax, [esp+430h+FileName]
.text:00401309 push eax // lpFileName
.text:0040130A call ds:CreateFileA // Open the file
.text:00401310 cmp eax, ebp // Compare between file handle and
// an *unknown* value
.text:00401312 jnz short loc_401331 // If equals we are registered
.text:00401314 push 0
.text:00401316 push 0
.text:00401318 push offset str->YouRulePleaseWriteATutorial_ // "You rule! Please write a
// tutorial."

Obviously, to find out all the informations I have used a debugger; Ollydbg works fine now. At this point I already have some questions in my mind. Who creates the file? The file named "l00KuPtAbl3" is not on my hard disk but the crackme wants to read something from it. I checked all the crackme trying to find a CreateFile call with CREATE_NEW or CREATE_ALWAYS flag without luck. Maybe there are some crypted parts... Hmmm, let's see the rest of the code, especially what is the value that is compared with CreateFile's return value (Instruction 401310) and where it comes from:

.text:00401270 mov dl, [eax] // eax -> current char of the username
.text:00401272 add eax, 1
.text:00401275 test dl, dl
.text:00401277 jnz short loc_401270 // 401270/401277 used to find length of username
.text:00401279 sub eax, edi // eax = len(username)
.text:0040127B mov edx, 0
.text:00401280 mov edi, eax
.text:00401282 mov [esp+418h+var_408], edx
.text:00401286 jz short loc_4012C0
.text:00401288 jmp short loc_401290
.text:00401290 movsx edx, byte ptr [edx+esi] // esi -> username[i] (i is a generic
// counter from 0 to strlen(username))
.text:00401294 xor eax, eax
.text:00401296 movsx ebx, byte ptr [eax+esi] // ebx = username[j] (j is a generic
// counter from 0 to strlen(username))
.text:0040129A add ecx, edx // Add username[i] to ecx
.text:0040129C add ebx, edx // ebx = username[j] + username[j]
.text:0040129E add ebx, ebx // Double ebx
.text:004012A0 add ecx, ecx // Double ecx
.text:004012A2 xor ecx, ebx // A simple xor
.text:004012A4 add eax, 1
.text:004012A7 add ebp, ecx // ebp stores the result of the above operations
.text:004012A9 cmp eax, edi
.text:004012AB jb short loc_401296 // 401296/4012AB for len(username) times
.text:004012AD mov edx, [esp+418h+var_408]
.text:004012B1 add edx, 1
.text:004012B4 cmp edx, edi
.text:004012B6 mov [esp+418h+var_408], edx
.text:004012BA jb short loc_401290 // 401290/4012BA for len(username) times

Some basic operations over the username, the value I'm looking for comes from the name's letters. I'm in front of a dilemma: 1. the file doesn't exist but CreateFile returns a handle (don't know if valid or not...) 2. the handle of the opened file should be equal to a value obtained from the name How is it possible??? The rules are pretty clear: "Write a keygen". An impossible crackme? No!

FhF shows me the way
I have to admit I spent some time trying to figure out what's really going on without luck. Only after some tries I came out with a good idea: what about to check if the crackme hooks a function? To find out the trick I used my system call hook detector.

Ok, time to play the video. To *synchronize* the tutorial with the video I'll use a time information, [start_time - end_time]. Below the time information I'll explain what is happened from start_time to end_time.

[00:00 - 00:13]
I always test crackme/worm/malware under a clean virtual machine, it means it's everything normal and untouched but as you can see from the video something is changed, there is a sys file with a suspicous name "ZLFBEQY" which is hooking something. You can read the address of the function, which is FA09C310. I'm almost sure the crackme made the hook but a closer look it's necessary. FhF has a disasm engine inside so the best thing to do is to disasm the function trying to figure out something interesting.

[00:14 - 00:29]
Clicking on the name I can dump the module (a very crude dump...) or disasm it directly from memory. I choose to disasm the module. The program doesn't disasm all the module but only some bytes, at the moment it ends when a 'RET' or an invalid instruction is found. In this case the disasm starts from address FA1C310 and ends at FA09C391 because the next instruction is an invalid instruction. You can disasm any address you like using the 'Disam from' box. The disasm view is the classical view containing informations about address, bytes, instruction and note. The disasm view reveals calls (in this case RtlUnicodeStringToAnsiString and strstr) and strings ("l00KuPtAbl3"). This last string is really interesting because now I'm sure, the crackme made the hook!

Code analysis
[00:30 - 00:46]
For the moment I can skip the first lines focusing on instruction at FA09C32D:

FA09C32D E8 CE FF FF FF CALL FA09C300

The call is used many times inside the crackme and it's something used for making our stepping session a little bit hard. From the current disasm view I can't see the content of the function because it has not been disasmed yet; a double click over the instruction and the function is disasmed:

FA09C300 58 POP EAX
FA09C301 40 INC EAX
FA09C302 FF E0 JMP EAX

As you can see the routine pops the return address from the stack, add 1 to the popped value and jump to the address. The routine forces the ret to return_address+1, and I think it's only used to obfuscate the code. So, I have:

FA09C32D E8 CE FF FF FF CALL FA09C300 // The call will lead me at FA09C333
FA09C332 54 PUSH ESP // Useless byte/instruction
FA09C333 FA CLI // Here!!!
FA09C334 F7 DC NEG ESP
FA09C336 F7 DC NEG ESP
FA09C338 FB STI
FA09C339 85 F6 TEST ESI, ESI // esi is the value returned by
// RtlUnicodeStringToAnsiString
FA09C33B 0F 8C 0B 01 00 00 JL FA09C44C // Jump if esi != STATUS_SUCCESS
FA09C341 E8 BE FF FF FF CALL FA09C304 // Routine similar to the previous 'call FA09C300'

[00:47 - 01:11]
At the beginning of the function (00:22) there's a call to RtlUnicodeStringToAnsiString, I don't know anything about it but if something goes in the wrong way I jump to FA09C44C:

FA09C44C E8 AF FE FF FF CALL 0xFA09C300
FA09C451 60 PUSHAD // Not executed
FA09C452 FA CLI
FA09C453 F7 DC NEG ESP
FA09C455 F7 DC NEG ESP
FA09C457 FB STI
FA09C458 FF 75 30 PUSH DWORD PTR [EBP+30h]
FA09C45B FF 75 2C PUSH DWORD PTR [EBP+2Ch]
FA09C45E FF 75 28 PUSH DWORD PTR [EBP+28h]
FA09C461 FF 75 24 PUSH DWORD PTR [EBP+24h]
FA09C464 FF 75 20 PUSH DWORD PTR [EBP+20h]
FA09C467 FF 75 1C PUSH DWORD PTR [EBP+1Ch]
FA09C46A FF 75 18 PUSH DWORD PTR [EBP+18h]
FA09C46D FF 75 14 PUSH DWORD PTR [EBP+14h]
FA09C470 FF 75 10 PUSH DWORD PTR [EBP+10h]
FA09C473 FF 75 0C PUSH DWORD PTR [EBP+0Ch]
FA09C476 FF 75 08 PUSH DWORD PTR [EBP+08h] // The resulted handle to file object
FA09C479 FF 15 14 17 16 FA CALL DWORD PTR [0xFA09C714] // NtCreateFile
FA09C47F 5F POP EDI
FA09C480 5E POP ESI
FA09C481 5B POP EBX
FA09C482 C9 LEAVE
FA09C483 C2 2C 00 RET 0x002C // Back to ring3 code

The above code performs some final operations and if I reach this part of code something wrong happened. It's pretty easy to understand what it does and I only made a single comment at FA09C476 because I'll use it later, remember it!

[01:12 - 01:33]
Call FA09C304 is another function used to obfuscate the code:

FA09C304 58 POP EAX // Pop the return address
FA09C305 40 INC EAX // Add 1 to the address
FA09C306 FA CLI
FA09C307 F7 DC NEG ESP
FA09C309 F7 DC NEG ESP
FA09C30B FB STI
FA09C30C 40 INC EAX // Again, add 1 to the address
FA09C30D FF E0 JMP EAX // Jump to eax

The routine forces the ret to return_address+2 which is -in this case- FA09C348. As you can see the address is not visible from the disasm view because there is an instruction at FA09C347 and another one at FA09C349; I can make it visible btw. A right-click over an instruction opens a little menu containing the items: - "Copy selected lines": the item speaks itself, it copies the selected line(s) into the clipboard - "Undefine...": make all the instructions, from the selected instruction, undefined showing a single byte for every lines of the disasm view. Ida users should be familiar with this command. - "Disasm...": disasm the undefined instructions. - "Spy stack!": I'll show you later the meaning of this item. To make the instruction visible I need to do an undefine operation followed by a disasm operation. Look at the video, the result is in front of our face:

FA09C341 E8 BE FF FF FF CALL FA09C304 // Jump down to FA09C348
FA09C346 54 PUSH ESP // Useless instruction
FA09C347 34 // Useless byte
FA09C348 0F B7 45 F4 MOVZX EAX, WORD PTR [EBP-0Ch]
FA09C34C 8B 4D F8 MOV ECX, DWORD PTR [EBP-08h]
FA09C34F 80 24 08 00 AND BYTE PTR [ECX+EAX], 00h
FA09C353 E8 A8 FF FF FF CALL 0xFA161300
FA09C358 12 // Useless byte
FA09C359 FA CLI
FA09C35A F7 DC NEG ESP
FA09C35C F7 DC NEG ESP
FA09C35E FB STI
FA09C35F 66 83 7D F4 14 CMP WORD PTR [EBP-0Ch], 14h // [ebp-0C] points to the parameter
// of RtlUnicodeString...
FA09C364 0F 82 D8 00 00 00 JB 0xFA161442 // it's a check for the length of the
// first field of the structure

[01:34 - 01:59]
A check over the RtlUnicodeStringToAnsiString parameter. RtlUnicodeStringToAnsiString converts a unicode string into an ansi string. The destination string has the following structure:

typedef struct _STRING {
USHORT Length; // Length in bytes of the Buffer
USHORT MaximumLength; // Maximum length in bytes of Buffer
PCHAR Buffer; // The buffer...
} ANSI_STRING *PANSI_STRING;

Knowing the structure doesn't help me to much because I can't access the buffer and check what it's hiding. I can only say that the crackme checks the length of the buffer, nothing more. To gain more informations I need to explore the next instructions. Where will the jump bring me? A double click over the 'jb' instruction will lead me to the right place.

FA09C442 8D 45 F4 LEA EAX, DWORD PTR [EBP-0Ch]
FA09C445 50 PUSH EAX
FA09C446 FF 15 00 16 16 FA CALL DWORD PTR [0xFA161600] // RtlFreeAnsiString
FA09C44C E8 AF FE FF FF CALL 0xFA09C300
... // I already know what happens here, it's the end of the function

ANSI_STRING->Length field should be atleast 0x14 bytes but it's impossible to know what the ANSI_STRING->Buffer contains...

[02:00 - 02:47]
I have to analyse the *good boy* of the conditional jump:

FA09C370 68 00 17 16 FA PUSH 0xFA161700 // str2 = "l00KuPtAbl3."
FA09C375 FF 75 F8 PUSH DWORD PTR [EBP-08h] // str1 = !?!
FA09C378 FF 15 04 16 16 FA CALL DWORD PTR [0xFA161604] // strstr, returns a pointer to the
// 1° occurrence of str2 in str1
FA09C37E 8B D8 MOV EBX, EAX // or NULL if no match is found
FA09C380 85 DB TEST EBX, EBX
FA09C382 59 POP ECX
FA09C383 59 POP ECX
FA09C384 0F 84 B8 00 00 00 JE 0xFA161442 // If no match is found we jump down

Function strstr scans 1° string for the first occurrence of the 2° string. The 1° string is the one pushed at FA09C375 and for the moment I don't know what is it while the 2° string is "l00KuPtAbl3.". Do you remember the items inside the pop up menu? One of them is named 'Spy stack!' and it's time to use it. This feature is used when you need to know which kind of string is on the top of the stack at a certain address. FhF patches the original code of the program adding a call to DbgPrint which will print out the value I wanted to know. Supposing to insert 12345678 in the serial box here is what Dbgview prints: "Spy: \??\C:\DOCUME~1\massy\IMPOST~1\Temp\l00KuPtAbl3.12345678" The video is self explanatory. I know the string and I can disable the spy feature now. strstr returns a pointer to the serial. I already know the code at FA161442 and I can go on with the normal flow

[02:48 - 04:13]

FA09C38A 33 F6 XOR ESI, ESI
FA09C38C E8 73 FF FF FF CALL 0xFA161304
FA09C391 E8 // Not executed
FA09C392 42 // Not executed
FA09C393 33 FF XOR EDI, EDI
FA09C395 E8 66 FF FF FF CALL 0xFA161300
FA09C39A 95 XCHG EAX, EBP // Not executed
FA09C39B 8A 44 1F 0C MOV AL, BYTE PTR [EBX+EDI+0Ch] // ebx points to "l00KuPtAbl3.12345678"
// (pointer returned by strstr) and ebx+0xC points to "12345678"
// (because strlen("l00KuPtAbl3.")=0xC)
FA09C39F 3C 30 CMP AL, 30h // Compare between the current serial's
// character and 0x30
FA09C3A1 7C 1B JL 0xFA1613BE // Jump down if lower, current
// character not in 0x30-0x39 range
FA09C3A3 3C 39 CMP AL, 39h // Compare between the current serial's
// character and 0x39
FA09C3A5 7F 17 JG 0xFA1613BE // Jump down if greater, current
// character not in 0x30-0x39 range
FA09C3A7 83 C6 FD ADD ESI, -03h // Here if current serial's character
// is inside 0x30-0x39 range
FA09C3AA 0F BE C0 MOVSX EAX, AL
FA09C3AD C1 E6 04 SHL ESI, 04h
FA09C3B0 03 F0 ADD ESI, EAX // Operations from +3A7 to +3B0 are
// used to convert the number expressed in byte to the number expressed with a single digit.
// Example: supposing to have 0x38 I'll get: shl((0-3), 4) + 0x38 = 8. This is what happens
// the first time; the next time, supposing to have 0x32, you'll get: shl((8-3), 4) + 0x32 = 82.
FA09C3B2 89 75 FC MOV DWORD PTR [EBP-04h], ESI // Save the result
FA09C3B5 E8 4A FF FF FF CALL 0xFA161304
FA09C3BA 56 // Not executed
FA09C3BB 31 // Not executed
FA09C3BC EB 1C JMP SHORT0xFA1613DA // The character was in 0x30-0x39
// range, pass to the next
FA09C3BE 3C 41 CMP AL, 41h // Compare between the current serial's
// character and 0x41
FA09C3C0 7C 74 JL 0xFA161436 // Jump down if lower, current
// character not in 0x41-0x46 range
FA09C3C2 3C 46 CMP AL, 46h // Compare between the current serial's
// character and 0x46
FA09C3C4 7F 70 JG 0xFA161436 // Jump down if greater, current
// character not in 0x41-0x46 range
FA09C3C6 0F BE C0 MOVSX EAX, AL // /
FA09C3C9 C1 E6 04 SHL ESI, 04h // | Conversion similar to the
// previous, check yourself
FA09C3CC 8D 74 06 C9 LEA ESI, DWORD PTR [EAX+ESI-37h]// \
FA09C3D0 89 75 FC MOV DWORD PTR [EBP-04h], ESI // Save the result
FA09C3D3 E8 2C FF FF FF CALL 0xFA161304
FA09C3D8 76 14 JBE 0xFA1613EE // Not executed
FA09C3DA 47 INC EDI // Moves the counter to the
// next character
FA09C3DB 83 FF 08 CMP EDI, 08h // The routine checks 8 digits
FA09C3DE 72 B5 JB 0xFA161395 // Jump up...

Damn, another bug in my tool :p. Did you see it? Look at 03:33. It's not a big problem, only a little bug in the undefine function. Bug fixed but I didn't want to record everything once again, sorry. Back to the meaning of the routine. A simple routine used to convert the serial (8 digits at all in 0..9/A..F range) from string to dword saving the result inside the dword pointed by [ebp-4]. Once again the code is obfuscated using the same calls but it's pretty easy to understand it once you have figure out the right sequence of instructions.

[04:14 - 05:02]

FA09C3E0 E8 1B FF FF FF CALL 0xFA161300
FA09C3E5 83 // Not executed
FA09C3E6 FA CLI
FA09C3E7 F7 DC NEG ESP
FA09C3E9 F7 DC NEG ESP
FA09C3EB FB STI
FA09C3EC 8B 55 FC MOV EDX, DWORD PTR [EBP-04h] // The serial in dword format
FA09C3EF 81 F2 3A 42 37 8D XOR EDX, 0x8D37423A // serial = serial ^ 0x8D37423A
FA09C3F5 C1 CA 05 ROR EDX, 05h // serial = serial >> 5
FA09C3F8 E8 03 FF FF FF CALL 0xFA161300
FA09C3FD 19 // Not executed
FA09C3FE 0F CA BSWAP EDX // bswap(serial)
FA09C400 C1 CA 15 ROR EDX, 15h // serial = serial >> 0x15
FA09C403 E8 FC FE FF FF CALL 0xFA161304
FA09C408 35 // Not executed
FA09C409 47 // Not executed
FA09C40A 81 C2 C0 73 5A 0F ADD EDX, 0x0F5A73C0 // serial = serial + 0xF5A73C0
FA09C410 89 55 FC MOV DWORD PTR [EBP-04h], EDX // Store the result
FA09C413 E8 E8 FE FF FF CALL 0xFA17B300
FA09C418 64 // Not executed
FA09C419 8B 45 08 MOV EAX, DWORD PTR [EBP+08h] // [ebp+8] was used at FA09C476.
// Remember? It's the last parameter of NtCreateFile, where the handle will be saved
FA09C41C 8B 4D FC MOV ECX, DWORD PTR [EBP-04h] // The computed value
FA09C41F 89 08 MOV DWORD PTR [EAX], ECX // The computed value will be
// returned to our program!!!
...
FA09C434 EB 49 JMP SHORT 0xA93BD47F // Jump at the end of the function
// going back to the protection routine

I finally understand why the call to CreateFile returns a handle, even if the file doesn't exist. The returned value is obtained by the simple operations:

serial = serial ^ 0x8D37423A
serial = serial >> 5
bswap(serial)
serial = serial >> 15
serial = serial + 0xF5A73C0

So, this is the scenario: the name is used to compute a value (call it valueFrom_Name) which is compared with the value returned as a fake file handle (call it valueFromSerial) from the driver; if equals you are registered. All the instructions used to obtain valueFromSerial are reversable so the only thing to do is to compute valueFromName, put it inside the reversed instructions and read the right serial. Valid combination: ZaiRoN - A996DEDB

Cheers, ZaiRoN


Note Finali

Tutorial ends here. You don't have FhF but you can follow the tutorial using a ring0 debugger, I used FhF because sometimes a better approach to a target is given by non standard tools. Yes, you can't write a new program for every crackme but if you have your own tools you can combine them together obtaining something usefull in a very short time. In this case I had the system call hook detector and the disasm engine; putting them together I got FhF. It was born just for joke but I think it might help me sometimes. If you think it can help you in some ways let me know :) Thanks to WarrantyVoider for this nice crackme!


Disclaimer

I documenti qui pubblicati sono da considerarsi pubblici e liberamente distribuibili, a patto che se ne citi la fonte di provenienza. Tutti i documenti presenti su queste pagine sono stati scritti esclusivamente a scopo di ricerca, nessuna di queste analisi è stata fatta per fini commerciali, o dietro alcun tipo di compenso. I documenti pubblicati presentano delle analisi puramente teoriche della struttura di un programma, in nessun caso il software è stato realmente disassemblato o modificato; ogni corrispondenza presente tra i documenti pubblicati e le istruzioni del software oggetto dell'analisi, è da ritenersi puramente casuale. Tutti i documenti vengono inviati in forma anonima ed automaticamente pubblicati, i diritti di tali opere appartengono esclusivamente al firmatario del documento (se presente), in nessun caso il gestore di questo sito, o del server su cui risiede, può essere ritenuto responsabile dei contenuti qui presenti, oltretutto il gestore del sito non è in grado di risalire all'identità del mittente dei documenti. Tutti i documenti ed i file di questo sito non presentano alcun tipo di garanzia, pertanto ne è sconsigliata a tutti la lettura o l'esecuzione, lo staff non si assume alcuna responsabilità per quanto riguarda l'uso improprio di tali documenti e/o file, è doveroso aggiungere che ogni riferimento a fatti cose o persone è da considerarsi PURAMENTE casuale. Tutti coloro che potrebbero ritenersi moralmente offesi dai contenuti di queste pagine, sono tenuti ad uscire immediatamente da questo sito.

Vogliamo inoltre ricordare che il Reverse Engineering è uno strumento tecnologico di grande potenza ed importanza, senza di esso non sarebbe possibile creare antivirus, scoprire funzioni malevole e non dichiarate all'interno di un programma di pubblico utilizzo. Non sarebbe possibile scoprire, in assenza di un sistema sicuro per il controllo dell'integrità, se il "tal" programma è realmente quello che l'utente ha scelto di installare ed eseguire, né sarebbe possibile continuare lo sviluppo di quei programmi (o l'utilizzo di quelle periferiche) ritenuti obsoleti e non più supportati dalle fonti ufficiali.