I’ve finally managed to pull my act together and solve a crackme. It was a really simple one (its author rated it 1/10 on the difficulty scale), but I feel it was adequate to my skills. The purpose of this crackme was creating a general key generator, not modifying the executable to accept any key. I’m going to explain how I accomplished that. The only tool I’ve used was OllyDbg.
Warning: it will be all pretty basic stuff, so if you’re an experienced reverse engineer, you’ll be bored out of your mind.
First of all, this crackme can be downloaded from crackmes.us. If you’re going to try to solve it by yourself, I suggest you avoid reading the rest of this post, as it’ll just spoil the fun.
When we run the program, we’ll see a simple window with two editboxes and three buttons, as can be seen in Picture 1. What’s interesting for us is code executed when we press the Enter button. There are many ways to find place in the executable where this button is handled — I chose what was, in my opinion, the easiest way: trace usage of error messages. To achieve that, I clicked the Enter button without entering any username or registration number beforehand. Just as expected, I was rewarded with an error message.
Now I needed to find that string in the executable. I had no idea whether it was a wide string or an ansi string, but luck was on my side and it was easy enough to spot at the very top of the data segment (at the address 40308Ch):
Having the address, I could try to find all occurences of it being pushed onto the stack (in order to call procedure creating the error messagebox) and put breakpoints on them.
Thankfully, there’s only one occurence, and I am further ensured about that when execution indeed breaks on my breakpoint.
CPU Disasm Address Hex dump Command Comments 00401590 /. 6A FF PUSH -1 00401592 |. 68 B81C4000 PUSH 00401CB8 ; Entry point 00401597 |. 64:A1 0000000 MOV EAX,DWORD PTR FS:[0] 0040159D |. 50 PUSH EAX 0040159E |. 64:8925 00000 MOV DWORD PTR FS:[0],ESP ; Installs SE handler 401CB8 004015A5 |. 83EC 10 SUB ESP,10 004015A8 |. 53 PUSH EBX 004015A9 |. 57 PUSH EDI 004015AA |. 8BF9 MOV EDI,ECX 004015AC |. 8D4C24 08 LEA ECX,[LOCAL.6] 004015B0 |. E8 C5030000 CALL <JMP.&MFC42.#540> ; Jump to MFC42.#540 004015B5 |. 33DB XOR EBX,EBX 004015B7 |. 8D4C24 10 LEA ECX,[ESP+10] 004015BB |. 895C24 20 MOV DWORD PTR SS:[ESP+20],EBX 004015BF |. E8 B6030000 CALL <JMP.&MFC42.#540> ; Jump to MFC42.#540 004015C4 |. 8D4C24 0C LEA ECX,[ESP+0C] 004015C8 |. C64424 20 01 MOV BYTE PTR SS:[ESP+20],1 004015CD |. E8 A8030000 CALL <JMP.&MFC42.#540> ; Jump to MFC42.#540 004015D2 |. 8D4424 08 LEA EAX,[ESP+8] 004015D6 |. 8D8F A0000000 LEA ECX,[EDI+0A0] 004015DC |. 50 PUSH EAX 004015DD |. C64424 24 02 MOV BYTE PTR SS:[ESP+24],2 004015E2 |. E8 BD030000 CALL <JMP.&MFC42.#3874> ; Jump to MFC42.#3874 004015E7 |. 8B4424 08 MOV EAX,DWORD PTR SS:[ESP+8] 004015EB |. 8B48 F8 MOV ECX,DWORD PTR DS:[EAX-8] 004015EE |. 83F9 0A CMP ECX,0A 004015F1 |. 7D 0C JGE SHORT 004015FF 004015F3 |. 53 PUSH EBX 004015F4 |. 53 PUSH EBX 004015F5 |. 68 8C304000 PUSH OFFSET 0040308C ; ASCII "Your Username must be at least 10 characters" 004015FA |. E9 8E000000 JMP 0040168D 004015FF |> 56 PUSH ESI 00401600 |. 33F6 XOR ESI,ESI 00401602 |. 49 DEC ECX 00401603 |. 85C9 TEST ECX,ECX 00401605 |. 7E 42 JLE SHORT 00401649 |
Thanks to Olly, finding the entry point is trivial (just look up), so all that’s left is finding the actual code needed to recreate and reverse whatever algorithm’s used. Again, it’s not a hard task, considering we can see CMP ECX,0A just four lines above our error message. After following the jump below(JGE SHORT 004015FF), we’ll be granted with view of loop that probably takes care of hashing the username (we can safely ignore jump 00401605 |. /7E 42 JLE SHORT 00401649 because there’s no chance it’ll ever going to occur).
CPU Disasm Address Hex dump Command Comments 00401607 |> /8A0C06 /MOV CL,BYTE PTR DS:[EAX+ESI] 0040160A |. |66:0FBE4406 0 |MOVSX AX,BYTE PTR DS:[EAX+ESI+1] 00401610 |. |66:0FBEC9 |MOVSX CX,CL 00401614 |. |0FAFC1 |IMUL EAX,ECX 00401617 |. |25 FFFF0000 |AND EAX,0000FFFF 0040161C |. |25 1F000080 |AND EAX,8000001F 00401621 |. |79 05 |JNS SHORT 00401628 00401623 |. |48 |DEC EAX 00401624 |. |83C8 E0 |OR EAX,FFFFFFE0 00401627 |. |40 |INC EAX 00401628 |> |04 41 |ADD AL,41 0040162A |. |8D4C24 10 |LEA ECX,[ESP+10] 0040162E |. |884424 18 |MOV BYTE PTR SS:[ESP+18],AL 00401632 |. |8B5424 18 |MOV EDX,DWORD PTR SS:[ESP+18] 00401636 |. |52 |PUSH EDX 00401637 |. |E8 62030000 |CALL <JMP.&MFC42.#940> ; Jump to MFC42.#940 0040163C |. |8B4424 0C |MOV EAX,DWORD PTR SS:[ESP+0C] 00401640 |. |46 |INC ESI 00401641 |. |8B48 F8 |MOV ECX,DWORD PTR DS:[EAX-8] 00401644 |. |49 |DEC ECX 00401645 |. |3BF1 |CMP ESI,ECX 00401647 |.^\7C BE \JL SHORT 00401607 |
Except for the MFC call, everything is trivial in this loop. Fortunately, the only thing MFC42.#940 function does is appending DL to the output string. With that knowledge we can write down what happens in above loop using human understandable language.
For ESI = 0 to length-1 :
- Move values from username+ESI and username+ESI+1th character to CX and AX, preserving their signs
- Perform signed integer multiplication over EAX and ECX
- Perform and operation on EAX and FFFFh
- Increment AL by 41h
- Append AL to the output string
I’ve ignored 00401621 |. |79 05 |JNS SHORT 00401628 because its AND EAX,8000001Fh will never be signed and thereby jump will always be taken.
We can now implement name hashing, C++ is perfect for that task. I’m going to use an union to abstract over a register:
namespace assembly{ typedef union { uint64_t rx; uint32_t ex; uint16_t x; struct {uint8_t l,h;}b; } Register; } |
And here’s the implementation of the name hashing loop:
std::string hash_name(std::string const & name) { using namespace assembly; Register a,c; auto MaxSize = name.size(); if(MaxSize-- < 10) throw "Username must be at least 10 characters long"; std::string hash; for(auto i = 0u; i < MaxSize; ++i){ c.b.l = name[i]; a.x = name[i+1]; (int16_t&)c.x = (int8_t)c.b.l; (int32_t&)a.ex *= (int32_t)c.ex; a.ex &= 0xFFFF; a.ex &= 0x1F; a.b.l += 0x41; hash += a.b.l; } return hash; } |
We can now check if my implementation is correct (it is), by comparing its output to program’s output. They should be identical. With that done we can look further… and see that the only operation separating us from “Good job cracker!Now make a keygen” messagebox creation is a call to mbscmp.
CPU Disasm Address Hex dump Command Comments 00401649 |> \8D4424 14 LEA EAX,[ESP+14] 0040164D |. 8D4F 60 LEA ECX,[EDI+60] 00401650 |. 50 PUSH EAX 00401651 |. E8 4E030000 CALL <JMP.&MFC42.#3874> ; Jump to MFC42.#3874 00401656 |. 8B4424 14 MOV EAX,DWORD PTR SS:[ESP+14] 0040165A |. 5E POP ESI 0040165B |. 3958 F8 CMP DWORD PTR DS:[EAX-8],EBX 0040165E |. 75 09 JNE SHORT 00401669 00401660 |. 53 PUSH EBX 00401661 |. 53 PUSH EBX 00401662 |. 68 64304000 PUSH OFFSET 00403064 ; ASCII "You have to enter a Registration Number" 00401667 |. EB 24 JMP SHORT 0040168D 00401669 |> 8B4C24 0C MOV ECX,DWORD PTR SS:[ESP+0C] 0040166D |. 50 PUSH EAX ; /string2 0040166E |. 51 PUSH ECX ; |string1 0040166F |. FF15 D0214000 CALL DWORD PTR DS:[<&MSVCRT._mbscmp>] ; \MSVCRT._mbscmp 00401675 |. 83C4 08 ADD ESP,8 00401678 |. 85C0 TEST EAX,EAX 0040167A |. 53 PUSH EBX 0040167B |. 75 09 JNE SHORT 00401686 0040167D |. 6A 40 PUSH 40 0040167F |. 68 40304000 PUSH OFFSET 00403040 ; ASCII "Good job cracker! Now make a keygen" 00401684 |. EB 07 JMP SHORT 0040168D |
A quick foray into google will tell us (if we didn’t guess it already) that it’s a string comparing function. What’s more, once we step to it, we can see that it’s comparing hashed username with key inserted by user, so our reproduction of name hashing loop is already a working keygen. Probably that’s the reason why this crackme was rated only 1/10.
At this point, we already have a working keygen, so the crackme should be considered solved. Have fun, here’s a sample username with key:
krzaqrocks
GU[RSONBR
After entering these we should be rewarded with success message.