Crackmes.de – zaas's Old_KeygenMe_2010
- October 28, 2014
- reverse engineering
- crackmes
- no comments
You can find the cracke here. It has a rating of “2 – Needs a little brain (or luck)”.
An Exceptional Path
I used IDA Pro to solve this crackme. First I searched for the good boy message by looking at the strings, see Figure . There is one reference to the good boy message “Well done!”, see Figure .
A simple check of the stack variable [ebp-0B0h]
, which I renamed to code_valid
, decides whether we failed (code_valid
is False), or succeeded (name_valid
is True):
.text:00401766 cmp dword ptr [ebp+name_valid], 0
Two paths lead up to the check. The left path in Figure ends with the following lines:
.text:00401726 mov eax, [ebp+var_44] .text:00401729 xor edx, edx .text:0040172B div [ebp+var_28] .text:0040172E mov [ebp+var_A4], eax .text:00401734 mov [ebp+name_valid], 0 .text:0040173E mov [ebp+ms_exc.registration.TryLevel], 0FFFFFFFFh .text:00401745 jmp short loc_401766 ; check_flag should NOT be zero
In Line .text:00401734
the variable name_valid
is set to False
, so this path can’t be it. The right path looks better:
.text:0040174D loc_40174D: ; DATA XREF: .rdata:stru_405118o .text:0040174D mov esp, [ebp+ms_exc.old_esp] ; Exception handler 0 for function 401310 .text:00401750 mov eax, [ebp+name_valid] .text:00401756 and eax, 1 .text:00401759 mov [ebp+name_valid], eax .text:0040175F mov [ebp+ms_exc.registration.TryLevel], 0FFFFFFFFh
Line .text:00401759
sets name_valid
to True. The problem with this block is that seemingly no path, i.e., arrows in IDA’s graph view, lead to to those lines. That is because it is an “Exception handler 0 for function 401310” as IDA Pro recognized and commented in line .text:0040174D
. The exception handler takes care of all exception thrown inside the first level try block inside the routine 401310
. We find the start of this try block a couple of lines backwards from the good boy check:
.text:00401686 mov [ebp+ms_exc.registration.TryLevel], 0 ; start of try block
The try block ends here:
.text:0040173E mov [ebp+ms_exc.registration.TryLevel], 0FFFFFFFFh
To get to the good boy message, we need to trigger an exception inside the try block.
Valid Names
Let’s see what happens with the name and code after we hit OK
. Going back a few block inside the subroutine 401310
we find the characteristic lines that read the values of the two input boxes::
.text:00401639 push 32h ; cchMax .text:0040163B lea edx, [ebp+name] .text:00401641 push edx ; lpString .text:00401642 push 1 ; nIDDlgItem .text:00401644 mov eax, [ebp+hWndParent] .text:00401647 push eax ; hDlg .text:00401648 call ds:GetDlgItemTextA .text:0040164E mov [ebp+name_length], eax ; name_length .text:00401654 push 32h ; cchMax .text:00401656 lea ecx, [ebp+code] .text:0040165C push ecx ; lpString .text:0040165D push 2 ; nIDDlgItem .text:0040165F mov edx, [ebp+hWndParent] .text:00401662 push edx ; hDlg .text:00401663 call ds:GetDlgItemTextA .text:00401669 mov [ebp+code_length], eax ; code_length .text:0040166F cmp [ebp+name_length], 0 .text:00401676 jz short loc_401681 .text:00401678 cmp [ebp+code_length], 0
This snippet fetches the content of both input boxes and checks if they contain text. If they do, the code enters the try
-block (so any exception thrown from now on gives us the good boy message)::
.text:00401686 ; --------------------------------------------------------------------------- .text:00401686 .text:00401686 loc_401686: ; CODE XREF: sub_401310+36Fj .text:00401686 mov [ebp+ms_exc.registration.TryLevel], 0 ; start of try block .text:0040168D cmp [ebp+name_length], 4 .text:00401694 jb short loc_40169F .text:00401696 cmp [ebp+code_length], 4 .text:0040169D jnb short loc_4016B1 .text:0040169F .text:0040169F loc_40169F: ; CODE XREF: sub_401310+384j .text:0040169F xor eax, eax .text:004016A1 cmp [ebp+name_valid], 0 .text:004016A8 setz al .text:004016AB mov [ebp+name_valid], eax ; check_flag = 0 .text:004016B1 .text:004016B1 loc_4016B1: ; CODE XREF: sub_401310+38Dj .text:004016B1 mov [ebp+i], 0 .text:004016BB jmp short loc_4016CC .text:004016BD ; ---------------------------------------------------------------------------
The code checks if the length of the name
field is at least 4 characters. If not, it sets the variable [ebp+name_valid]
to False
. Otherwise, the variable stays at True
as set in line 40162F
::
.text:0040162F mov [ebp+name_valid], 1
If the name has at least four characters we enter a loop::
.text:004016B1 loc_4016B1: ; CODE XREF: sub_401310+38Dj .text:004016B1 mov [ebp+i], 0 .text:004016BB jmp short loc_4016CC .text:004016BD ; --------------------------------------------------------------------------- .text:004016BD .text:004016BD loc_4016BD: ; CODE XREF: sub_401310+3EEj .text:004016BD mov ecx, [ebp+i] ; increment .text:004016C3 add ecx, 1 .text:004016C6 mov [ebp+i], ecx .text:004016CC .text:004016CC loc_4016CC: ; CODE XREF: sub_401310+3ABj .text:004016CC mov edx, [ebp+i] .text:004016D2 cmp edx, [ebp+name_length] .text:004016D8 jnb short loc_401700 .text:004016DA mov eax, [ebp+i] .text:004016E0 mov cl, [ebp+eax+name] ; name[i] .text:004016E7 push ecx .text:004016E8 call sub_4019A0 .text:004016ED add esp, 4 .text:004016F0 mov edx, [ebp+name_valid] .text:004016F6 and edx, eax .text:004016F8 mov [ebp+name_valid], edx ; if not sub_4019A0(c): check_flag = False .text:004016FE jmp short loc_4016BD ; increment
This code snippet iterates over all characters in name
. It calls a routine sub_4019A0
for all characters in name
, and updates the name_valid
flag based on the return value of sub_4019A0
:
name_valid = name_valid && sub_4019A0(name[i])
So sub_4019A0
is most likely a check for valid characters. If one of the characters in name is invalid, the flag name_valid
becomes False (and stays False). The routine sub_4019A0
is::
.text:004019A0 sub_4019A0 proc near ; CODE XREF: sub_401310+3D8p .text:004019A0 .text:004019A0 character = byte ptr 4 .text:004019A0 .text:004019A0 c = al .text:004019A0 mov c, [esp+character] .text:004019A4 cmp c, '/' .text:004019A6 jle short no_numbers ; jump if below numbers .text:004019A8 cmp c, ':' .text:004019AA jl short loc_4019BC ; jump if number .text:004019AC .text:004019AC no_numbers: ; CODE XREF: sub_4019A0+6j .text:004019AC cmp c, 'A' .text:004019AE jl short loc_4019B4 ; jump if not letter .text:004019B0 cmp c, 'Z' .text:004019B2 jle short loc_4019BC ; jump if capital letter .text:004019B4 .text:004019B4 loc_4019B4: ; CODE XREF: sub_4019A0+Ej .text:004019B4 cmp c, 'a' .text:004019B6 jl short loc_4019C2 ; jump if special .text:004019B8 cmp c, 'z' .text:004019BA jg short loc_4019C2 ; jump if lower case letter .text:004019BC .text:004019BC loc_4019BC: ; CODE XREF: sub_4019A0+Aj .text:004019BC ; sub_4019A0+12j .text:004019BC mov eax, 1 ; return True .text:004019C1 retn .text:004019C2 ; --------------------------------------------------------------------------- .text:004019C2 .text:004019C2 loc_4019C2: ; CODE XREF: sub_4019A0+16j .text:004019C2 ; sub_4019A0+1Aj .text:004019C2 xor eax, eax ; return False .text:004019C4 retn .text:004019C4 sub_4019A0 endp
The routine checks if the character is one of the following:
- a digit
- an uppercase letter
- a lowercase letter
In other words, this is the C function isalnum
. After checking all characters in name we get to::
.text:00401700 cmp [ebp+name_valid], 0 .text:00401707 jz short loc_401726 ; if name invalid -> failed
If the name has at least four characters, and all characters of the name are alpha numeric, then the flag name_valid
is still True and we continue, otherwise we jump to loc_401726
and the bad boy message is shown.
The Key Validation
If the name is valid, the following lines are executed::
.text:00401709 lea eax, [ebp+code] .text:0040170F push eax .text:00401710 lea ecx, [ebp+name] .text:00401716 push ecx .text:00401717 lea edx, [ebp+var_28] .text:0040171A push edx .text:0040171B call sub_401960 ; ecx = code .text:00401720 add esp, 0Ch .text:00401723 mov [ebp+var_44], eax
They boil down to:
var_44 = sub_401960(var_28, name, code)
The routine sub_401960 calculates a value based on the name
and code
. The first argument of the function var_28
was initialized to 0 before and will probably hold a second return value of sub_401960
(besides the one in
):
.text:00401353 mov [ebp+var_28], 0
So let’s have a look at sub_401960
::
.text:00401960 sub_401960 proc near ; CODE XREF: sub_401310+40Bp .text:00401960 .text:00401960 result = dword ptr 4 .text:00401960 name = dword ptr 8 .text:00401960 code = dword ptr 0Ch .text:00401960 .text:00401960 mov ecx, [esp+code] .text:00401964 mov edx, [esp+result] ; starts at 0 .text:00401968 push ebx .text:00401969 push ebp .text:0040196A push esi .text:0040196B mov esi, [esp+0Ch+name] ; esi = name .text:0040196F push edi .text:00401970 xor eax, eax ; sum=0 .text:00401972 sub esi, ecx .text:00401974 mov edi, 4 ; repeat four times .text:00401979 .text:00401979 loc_401979: ; CODE XREF: sub_401960+2Cj .text:00401979 movsx ebx, byte ptr [esi+ecx] ; name[i] .text:0040197D mov ebp, [edx] ; result .text:0040197F add eax, ebx ; sum = sum + name[i] .text:00401981 movsx ebx, byte ptr [ecx] ; code[i] .text:00401984 add ebx, eax ; ebx = sum + code[i] .text:00401986 add ebp, ebx ; result = result + ebx .text:00401988 inc ecx .text:00401989 dec edi .text:0040198A mov [edx], ebp .text:0040198C jnz short loc_401979 ; name[i] .text:0040198E mov ecx, added_to_weighted_sum .text:00401994 mov esi, ebp .text:00401996 add esi, ecx ; add constant .text:00401998 pop edi .text:00401999 mov [edx], esi .text:0040199B pop esi .text:0040199C pop ebp .text:0040199D pop ebx .text:0040199E retn .text:0040199E sub_401960 endp
The code boils down to the following pseudocode:
FUNCTION sub_401960(int* result, char* name, char* code) rv = 0 FOR i = 0 TO 3 rv += name[i] result += rv + code[i] END FOR result += added_to_weighted_sum RETURN rv END
Let ni, ci be the ith character of the name and code respectively, and let C be the constant added_to_weighted_sum
, then the above code calculates:
$$ \begin{aligned} result &= \left(\sum_{i=0}^3 c_i \right) + 4 n_3 + 3 n_2 + 2 n_1 + n_0 + C \\ rv &= \sum_{i=0}^3 n_i \end{aligned} $$
The return value of sub_401960
is stored in [ebp+rv]
.
.text:0040171B call sub_401960 ; ecx = code .text:00401720 add esp, 0Ch .text:00401723 mov [ebp+rv], eax
Next follow the last lines of our try block::
.text:00401726 loc_401726: ; CODE XREF: sub_401310+3F7j .text:00401726 mov eax, [ebp+rv] .text:00401729 xor edx, edx .text:0040172B div [ebp+var_28] .text:0040172E mov [ebp+var_A4], eax .text:00401734 mov [ebp+code_valid], 0 .text:0040173E mov [ebp+ms_exc.registration.TryLevel], 0FFFFFFFFh
Finally we’ve got an instruction that can throw an exception (the division by zero exception):
.text:0040172B div [ebp+var_28]
The div
statement divides what is in edx:eax
by var_28
. edx
is set to zero, and eax
holds the return value of sub_401960
. We don’t care about these values, because a division by zero exception only occurs when the divisor is zero, regardless of the dividend. The divisor var_28
was the first argument passed to sub_401960
, i.e., the result
of the routine sub_401960
. If a name/code pair leads to result
being 0, an exception is thrown and we solved the crackme. With the mathematical notation introduced before this means:
$$ result = \left(\sum_{i=0}^3 c_i \right) + 4 n_3 + 3 n_2 + 2 n_1 + n_0 + C \stackrel{!}{=} 0 $$
What is the value of C? If you check the value of added_to_weighted_sum
with a debugger it is 0 as set in this line::
.data:00408570 added_to_weighted_sum dd 0 ; DATA XREF: .text:0040112Ar
So how can we get the variable result
to become zero? The values of n to n3 are alpha numeric ASCII codes and therefore greater than 0. The values of ci are the ASCII codes of the code and also positive. So with C being zero there is no way to get the sum in result
to zero. We need to find a way to change C aka added_to_weighted_sum
.
A Secret Key Combination
To see how we can change added_to_weighted_sum
let’s check the references to this variable. Outside of sub_401960
the only other references are inside the following code snippet:
.text:00401102 cmp pressed_shift_3_before, 0 .text:00401109 jnz short loc_40113C .text:0040110B cmp dword ptr [ebp+pressed_key], '3' ; 3 pressed .text:0040110F jnz short loc_40113C .text:00401111 push 10h .text:00401113 call ds:GetKeyState .text:00401119 movsx eax, ax .text:0040111C test eax, eax .text:0040111E jge short loc_40113C .text:00401120 mov pressed_shift_3_before, 1 .text:0040112A mov ecx, added_to_weighted_sum .text:00401130 sub ecx, 586h .text:00401136 mov added_to_weighted_sum, ecx
The whole snippet is inside the callback that registers key presses. I renamed some variables to make clearer what the snippet does. It boils down to this:
IF NOT pressed_shift_3_before THEN IF pressed_key == '3' THEN key_state = GetKeyState() IF key_state != 0 THEN pressed_shift_3_before = True added_to_weighted_sum = -0x586 END IF END IF END IF
This means that the first time we press key 3
together with Shift
(which will give a non zero KeyState
), the value added_to_weighted_sum
is set to -586h. On an US keyboard layout this means our code needs to contain the #
character. We can enter this character at any point before hitting OK
, we can also enter it more than once. As long as there is the letter #
somewhere in our code, the constant added_to_weighted_sum
, i.e., C, becomes -0x586h.
The Keygenerator
We know two things about our code now. First of, it needs to contain Shift+3
(the hash character on US keyboards, the star *
on Swiss keyboards and the §
on German keyboards). Secondly, the following equation must hold:
$$ \left(\sum_{i=0}^3 c_i\right) + 4 n_3 + 3 n_2 + 2 n_1 + n_0 + C \stackrel{!}{=} 0 $$
So with C = − 586h we have:
$$ \left(\sum_{i=0}^3 c_i\right) \stackrel{!}{=} 586h - 4 n_3 - 3 n_2 - 2 n_1 - n_0 $$
This condition can be met by many different codes for each name. Here is a simple keygenerator that picks four characters that satisfy the sum, then adds the #
character to trigger the code to set the added_to_weighted_sum
alias C value::
import string import argparse def keygen(name): code_sum = 0x586 for i in range(4): code_sum -= (4-i)*ord(name[i]) nice_ascii = string.ascii_letters + string.digits nice_ascii_nr = [ord(c) for c in nice_ascii] code_list = 4*[0] for i in range(3): avg = (code_sum - sum(code_list)) // (4-i) code_list[i] = min(nice_ascii_nr, key=lambda x: abs(x-avg)) code_list[3] = code_sum - sum(code_list) code = "".join([chr(c) for c in code_list] ) return code parser = argparse.ArgumentParser("Keygen for Old_KeygenMe.exe") parser.add_argument("name") args = parser.parse_args() if len(args.name) < 4: print("Name must have at least 4 characters") quit() code = keygen(args.name) print("enter the following code: {}".format(code)) print("next enter SHIFT+3 and hit OK") print("-> so on US keyboards enter: {}".format(code+"#"))
Here’s a test::
> keygen.py sheldon enter the following code: SSSS next enter SHIFT+3 and hit OK -> so on US keyboards enter: SSSS#