Crackmes.de – adamziaja's crackme1
- August 5, 2014
- reverse engineering
- crackmes
- no comments
This crackme is rated “Difficulty: 1 – Very easy, for newbies”. I’ll show how to solve it mostly with debugging and trial and error. Start by running the program:
$ ./crackme1 username: Peter username must be between 8 and 12! username: Big Head serial number: 1234 WRONG!
Open the binary in IDA (the freeware version will do just fine). Next look for the string WRONG!
in the strings subview and jump to the referencing location pressing x
:
00401380 cvtsi2sd xmm0, eax 00401384 movsd xmm1, [rbp+var_2F8] 0040138C ucomisd xmm0, xmm1 00401390 jp short loc_4013B6 00401392 ucomisd xmm0, xmm1 00401396 jnz short loc_4013B6 00401398 mov esi, offset aSNOk ; "s/n OK!" 0040139D mov edi, offset _ZSt4cout@@GLIBCXX_3_4 004013A2 call __ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc 004013A7 mov esi, offset __ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_ 004013AC mov rdi, rax 004013AF call __ZNSolsEPFRSoS_E 004013B4 jmp short loc_4013D2 004013B6 ; --------------------------------------------------------------------------- 004013B6 004013B6 loc_4013B6: ; CODE XREF: main+303j 004013B6 ; main+309j 004013B6 mov esi, offset aSNWrong ; "s/n WRONG!
In line 00401392
the SSE registers xmm0
and xmm1
are compared. If they are not equal, then the next line (jnz short loc_4013B6
) causes the program to jump to loc_4013B6
which prints the badboy message WRONG!
. If, on the other hand, the two registers are equal, then the flow continues with line 00401398
which prints OK!
. So we are looking for inputs that lead to xmm0 = xmm1
in line 0x401396
.
Next, open the executable in a 64 debugger. I used GDB on a 64bit Linux host. First, set a breakpoint at line 0x401396
:
$ gdb ./crackme1 -silent Reading symbols from ./crackme1...done. (gdb) break *0x401396 Breakpoint 1 at 0x401396: file main.cpp, line 42.
Now run the code and enter a username and serial.
(gdb) run Starting program: /home/phreak/privat/clones/crackmes/crackme1 username: 01234567 serial number: 771 Breakpoint 1, 0x0000000000401396 in main () at main.cpp:42 42 main.cpp: No such file or directory.
The execution stops right at jnz short loc_4013B6
(before actually executing the line). Let’s print xmm0
and xmm1
:
(gdb) p $xmm0 $1 = {v4_float = {5.62949953e+14, 16.8905296, 0, 3.57331108e-43}, v2_double = {48495051, 5.4110892669614444e-312}, v16_int8 = {0, 0, 0, 88, -50, 31, -121, 65, 0, 0, 0, 0, -1, 0, 0, 0}, v8_int16 = {0, 22528, 8142, 16775, 0, 0, 255, 0}, v4_int32 = { 1476395008, 1099374542, 0, 255}, v2_int64 = {4721777705421373440, 1095216660480}, uint128 = 0x000000ff0000000041871fce58000000} (gdb) p $xmm1 $2 = {v4_float = {0, 4.25292969, 0, 0}, v2_double = {771, 0}, v16_int8 = {0, 0, 0, 0, 0, 24, -120, 64, 0, 0, 0, 0, 0, 0, 0, 0}, v8_int16 = {0, 0, 6144, 16520, 0, 0, 0, 0}, v4_int32 = {0, 1082660864, 0, 0}, v2_int64 = {4649993003539103744, 0}, uint128 = 4649993003539103744}
The instruction to compare the two SSE registers is ucomisd
, which compares the *low double-precision floating-point values*, i.e., the first value in v2_double
in the above output:
xmm0 = 48495051 xmm1 = 771
We see that the serial was copied to xmm1
, while xmm0
is probably based on the username. We see that xmm0
contains the number 48, 49, 50, 51. Those are the ASCII codes for the characters “0”, “1”, “2”, and “3” respectively. So the we can formulate the following first assumption for the validation algorithm:
Version 1
Concatenate the ASCII codes of the letters of the username and take the first 8 bytes of the result:
Or in Python:
def keygen_version1(username): key = '' for u in username: key += str(ord(u)) return key[:8]
Let’s run the code again within gdb and enter a different username. Note that the value of xmm0
comes from eax
in line 0x00401380
, so we can also check the value of eax
:
username: abcdefgh serial number: 979899100 Breakpoint 1, 0x0000000000401396 in main () at main.cpp:42 42 main.cpp: No such file or directory. (gdb) p $eax $1 = 97669968
Interesting! So version 1 of the keygen is obviously not correct. While 97 and 99 are as expected (ASCII codes of “a” and “c”), the two other numbers 66 and 68 are exactly 32 lower than the codes for “b” and “d”. Subtracting 32 from the ASCII codes of the lower case characters gives the upper case version. So the crackme1 is probably converting the even character to upper case. Run the same experiment with an all uppercase username:
username: ABCDEFGH serial number: 979899100 Breakpoint 1, 0x0000000000401396 in main () at main.cpp:42 42 main.cpp: No such file or directory. (gdb) p $eax $1 = 97669968
From this we can see that the code is actually also converting the odd characters to lower-case. This leads to version 2 of the keygen algorithm:
Version 2
Convert every odd character of the username to lower case, and every
even character to upper case. Then concatenate the ASCII codes of the
letters and take the first 8 bytes of the result
In Python:
def keygen_version2(username): for i, u in enumerate(username): if i%2: key += str(ord(u.upper())) else: key += str(ord(u.lower())) return key[:8]
Now let’s test usernames that are longer than 8 characters to see what happens:
username: abcdefgh --> serial number: 97669968 username: abcdefghi --> serial number: 66996810 username: abcdefghij --> serial number: 99681017 username: abcdefghijk --> serial number: 68101701 username: abcdefghijkl --> serial number: 10170103
So the serial number always has 8 digits, but those are no longer the first 8 bytes of the concatenation for longer usernames. Doing some more testing we find that the algorithms takes:
- bytes 0-7 for 8 character usernames
- bytes 2-9 for 9 character usernames
- bytes 4-11 for 10 character usernames
- bytes 6-13 for 11 character usernames
- bytes 8-15 for 12 character usernames
- bytes (c-8)*2 – (c-8)*2+7 for
character usernames
So the final keygen algorithm is:
Working Keygen
def generate_key(username): if not 8 <= len(username) <= 12: print("username must be between 8 and 12 characters.") quit() key = '' for i, u in enumerate(username): if i%2: key += str(ord(u.upper())) else: key += str(ord(u.lower())) return int(key[2*(len(username)-8):][0:8]) if __name__=="__main__": parser = argparse.ArgumentParser("crackme1 keygen") parser.add_argument("username") args = parser.parse_args() print(generate_key(args.username))