Solution-borismilner-4N006135
Level 0
Introduction
This level is the first level of a series of 4 crackmes available here.
Code analysis
This is very easy. It displays Password : at offset 0x4013F2 and waits for a user input at offset 0x401404.
Then the secret password (expected one) is saved to ESI at offset 0x40140C and the user input is saved to EDI at offset 0x401412. These 2 values are comared at offset 0x401418. If they don't match, the code jumps (at offset 0x40141A) to the bad boy.
.text:004013E0 ; int __cdecl main(int argc, const char **argv, const char **envp)
.text:004013E0 public _main
.text:004013E0 _main proc near
.text:004013E0 push offset __data_start__ ; "\nCrackme - Level 0 - by 60Ô15\n-------"...
.text:004013E5 call _printf
.text:004013EA add esp, 4
.text:004013ED push offset password_prompt ; "Password : "
.text:004013F2 call _printf
.text:004013F7 add esp, 4
.text:004013FA push offset password_guess
.text:004013FF push offset string_format ; "%20s"
.text:00401404 call _scanf
.text:00401409 add esp, 8
.text:0040140C mov esi, secret_password ; defined as 'Easy' at offset 0x409049
.text:00401412 mov edi, ds:password_guess
.text:00401418 cmp esi, edi
.text:0040141A jnz short incorrect_guess
.text:0040141C push offset good_job ; "\nGood Job !\n"
.text:00401421 call _printf
.text:00401426 add esp, 4
.text:00401429 retn
.text:0040142A ; ---------------------------------------------------------------------------
.text:0040142A
.text:0040142A incorrect_guess:
.text:0040142A push offset incorrect_password ; "\nNope, try again !\n"
.text:0040142F call _printf
.text:00401434 add esp, 4
.text:00401437 retn
.text:00401437 _main endp
Now, in IDA-Pro, by double clicking on secret_password, it jumps to offset 0x409049 where we can see:
.data:00409049 secret_password dd 79736145h
This corresponds to the expected password in ASCII and can be decoded as follows:
$ python >>> '79736145'.decode('hex')[::-1] 'Easy'
The password is Easy:
C:\crackmes>level-0.exe Crackme - Level 0 - by 60Ô15 ---------------------------- Password : Easy Good Job !
Level 1
Introduction
Description
This is level 2 of a series of 4 challenges available here.
Code overview
The layout is as follows:
Code analysis
Initialization
This block of code asks for a username at offset 0x401404 and a password at offset 0x401423. The username and password are resectively saved at memory locations 0x40D020 and 0x40D035 and the username is saved to eax at offset 0x40142D.
.text:004013E0 ; int __cdecl main(int argc, const char **argv, const char **envp)
.text:004013E0 public _main
.text:004013E0 _main proc near
.text:004013E0 push offset __data_start__ ; "\nCrackme - Level 1 - by 60Ô15\n-------"...
.text:004013E5 call _printf
.text:004013EA add esp, 4
.text:004013ED push offset username_prompt ; "Username : "
.text:004013F2 call _printf
.text:004013F7 add esp, 4
.text:004013FA push offset username ; offset 0x40D020
.text:004013FF push offset scanf_string_format ; "%20s"
.text:00401404 call _scanf
.text:00401409 add esp, 8
.text:0040140C push offset password_prompt ; "Password : "
.text:00401411 call _printf
.text:00401416 add esp, 4
.text:00401419 push offset password ; offset 0x40D035
.text:0040141E push offset scanf_integer_format ; "%d"
.text:00401423 call _scanf
.text:00401428 add esp, 8
.text:0040142B xor ecx, ecx
.text:0040142D mov eax, offset username
.text:00401432 dec eax
Transformation
This block of code is performing a transforming based on the username:
.text:00401433 keep_going:
.text:00401433 inc eax ; eax += 1
.text:00401434 movsx ebx, byte ptr [eax] ; ebx = username[i]
.text:00401437 add ecx, ebx ; ecx += username[i]
.text:00401439 cmp byte ptr [eax], 0 ; if not last character...
.text:0040143C jnz short keep_go ; continue loop
Remember that the username was saved to EAX at the initialization (see previous section) and EAX was decresed by 1 (dec eax) at offset 0x401432. The block of code above is looping over all characters in the username and accumulates the values in ecx. At the end of the loop, ecx is the sum of all characters of the username, which could be explained in python as follows:
>>> s = "aldeid" >>> ecx = 0 >>> for i in s: ... ecx += ord(i) ... print "%s\t%s\t%s" % (i, hex(ord(i)), hex(ecx)) ... a 0x61 0x61 l 0x6c 0xcd d 0x64 0x131 e 0x65 0x196 i 0x69 0x1ff d 0x64 0x263
Comparison
As shown on the below extract, the password provided by the user is compared to ECX at offset 0x40143E. If it is not equal, the code jumps to the bad boy, else, to the good boy.
.text:0040143E cmp ecx, ds:password
.text:00401444 jnz short wrong_password
.text:00401446 push offset good_job ; "\nGood job !\n"
.text:0040144B call _printf
.text:00401450 add esp, 4
.text:00401453 retn
.text:00401454 ; ---------------------------------------------------------------------------
.text:00401454
.text:00401454 wrong_password:
.text:00401454 push offset try_again ; "Nope, try again !\n"
.text:00401459 call _printf
.text:0040145E add esp, 4
.text:00401461 retn
Solution
As explained previously, the password should be equal to the sum of all characters of the username. Here is how you can write a very simple keygen in python:
#!/usr/bin/env python
import sys
def make_serial(username):
ecx = 0
for i in username:
ecx += ord(i)
return ecx
if __name__ == '__main__':
if len(sys.argv) < 2:
print "Usage: %s <username>" % sys.argv[0]
sys.exit(1)
username = sys.argv[1]
if len(username) > 20:
print "[ERROR] Username should be 20 chars max"
sys.exit(2)
print "Serial: %d" % make_serial(username)
Here are some usage examples:
keygen | check |
---|---|
$ ./keygen.py aldeid Serial: 611 |
C:\crackmes>level-1.exe Crackme - Level 1 - by 60Ô15 ---------------------------- Username : aldeid Password : 611 Good job ! |
$ ./keygen.py abcdefghij Serial: 1015 |
C:\crackmes>level-1.exe Crackme - Level 1 - by 60Ô15 ---------------------------- Username : abcdefghij Password : 1015 Good job ! |
$ ./keygen.py abcdefghijklmnopqrst Serial: 2130 |
C:\crackmes>level-1.exe Crackme - Level 1 - by 60Ô15 ---------------------------- Username : abcdefghijklmnopqrst Password : 2130 Good job ! |
Level 2
Introduction
Description
This crackme is the 3rd challenge of a series of 4 challenges available here.
When run, the program displays a User Id and prompts a password:
C:\crackmes>level-2.exe Crackme - Level 2 - by 60Ô15 ---------------------------- User Id : 3719630010 Password : abcd Nope, try again !
Code overview
Code analysis
User Id
The program starts by building a 0x20 long array (variable user_id) with 0x4F:
.text:004013E0 public _main
.text:004013E0 _main proc near
.text:004013E0 mov edi, offset user_id ; mem loc 0x40D020
.text:004013E5 mov ecx, 20h ; size 0x20
.text:004013EA mov al, 4Fh ; items = 0x4F
.text:004013EC rep stosb ; fill 32 bytes with 0x4F starting from 0x40D020
Then, the value of the Read Time Stamp Counter (RDTSC) is gathered via the rdtsc intruction at offset 0x401401 and displayed as the User Id.
.text:004013EE inc edi
.text:004013EF mov edi, 0
.text:004013F4 push offset __data_start__ ; "\nCrackme - Level 2 - by 60Ô15\n-------"...
.text:004013F9 call _printf
.text:004013FE add esp, 4
.text:00401401 rdtsc
.text:00401403 mov ebx, eax
.text:00401405 push ebx ; ebx = rdtsc
.text:00401406 push offset public_message ; "User Id : %u\n"
.text:0040140B call _printf
.text:00401410 add esp, 8
The user_id array is moved to EDI at offset 0x401413:
.text:00401413 mov edi, offset user_id
Bit test
At offset 0x401418, a bit test is processed by the bt instruction. It will update the first byte of user_id if this bit is not 0. Example: suppose the rdtsc is 1297941641 (01001101010111010000010010001001 in binary). Here is how the test is performed:
ebx ┌─────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┐ │ bit │ 31 │ 30 │ 29 │ 28 │ 27 │ 26 │ 25 │ 24 │ 23 │ 22 │ 21 │ 20 │ 19 │ 18 │ 17 │ 16 │ 15 │ 14 │ 13 │ 12 │ 11 │ 10 │ 09 │ 08 │ 07 │ 06 │ 05 │ 04 │ 03 │ 02 │ 01 │ 00 │ ├─────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┤ │ val │ 0 │ 1 │ 0 │ 0 │ 1 │ 1 │ 0 │ 1 │ 0 │ 1 │ 0 │ 1 │ 1 │ 1 │ 0 │ 1 │ 0 │ 0 │ 0 │ 0 │ 0 │ 1 │ 0 │ 0 │ 1 │ 0 │ 0 │ 0 │ 1 │ 0 │ 0 │ 1 │ └─────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┘ │ ┌────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── bit #0 = 1 ◄──┘ │ user_id ▼ ┌──────────┬────────────────┬────────────────┬────────────────┬────────────────┐ │ 0x40D020 │ 2A 4F 4F 4F │ 4F 4F 4F 4F │ 4F 4F 4F 4F │ 4F 4F 4F 4F │ │ 0x40D030 │ 4F 4F 4F 4F │ 4F 4F 4F 4F │ 4F 4F 4F 4F │ 4F 4F 4F 4F │ └──────────┴────────────────┴────────────────┴────────────────┴────────────────┘
.text:00401413 mov edi, offset user_id ; edi points to 0x40D020
.text:00401418 bt ebx, 0 ; convert rdtsc (eax) to binary
.text:00401418 ; CF = bit 0 of binary value
.text:00401418 ; bin(rdtsc)[-1]
.text:0040141C jnb short zero_bit_set ; if CF==0, jump to zero_bit_set
.text:0040141E mov byte ptr [edi], 2Ah ; user_id[0] = 0x2A
rdtsc > 0xB16B00B5?
Then, a test is performed against the value of rdtsc. If it is lower or equal than 0xB16B00B5, the byte at 0x40D021 is overwritten with 0x2A.
.text:00401421 inc edi ; edi = 0x40D021
.text:00401422 cmp ebx, 0B16B00B5h ; \ if rdtsc > B16B00B5
.text:00401428 ja short above_b16b00b5h ; / take jump
.text:0040142A mov byte ptr [edi], 2Ah ; user_id[1] = 0x2A
Parity test
This block of code increments EDI. At offset 0x40142E, EDI equals 0x40D022, which, is equal to 10000001101000000100010 in binary. The parity test is performed at offset 0x40142E. The jump at offset 0x401430 will never be taken because the number of 1 in the low 8 bits of EDI is even, as depicted below:
┌──────────── parity test ──────────────┐ ┌─────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┐ │ bit │ 31 │ 30 │ 29 │ 28 │ 27 │ 26 │ 25 │ 24 │ 23 │ 22 │ 21 │ 20 │ 19 │ 18 │ 17 │ 16 │ 15 │ 14 │ 13 │ 12 │ 11 │ 10 │ 09 │ 08 │ 07 │ 06 │ 05 │ 04 │ 03 │ 02 │ 01 │ 00 │ ├─────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┤ │ val │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 1 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 1 │ 1 │ 0 │ 1 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 1 │ 0 │ 0 │ 0 │ 1 │ 0 │ └─────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┘ └──┬─┘ └──┬─┘ even number of 1 ◄───────┴───────────────────┘ => PF = 1
.text:0040142D inc edi ; edi = 0x40D022. PF = 1 (parity flag)
.text:0040142E jnp short no_parity ; jump will never be taken because PF = 1
.text:00401430 mov byte ptr [edi], 2Ah ; user_id[2] = 0x2A
0x401433 - 0x401464
Starting at offset 0x401434, byte #3 of the user_id array is overwriten with 0x2A. Then, the program will loop 28 times to update the remaining bytes of user_id by applying the following logic:
ebx = ebx >> 1 if i % 2 == 0: uid[i] = ebx % 0x1A + 0x41 else: uid[i] = ebx % 0x1A + 0x61
.text:00401433 inc edi
.text:00401434 mov byte ptr [edi], 2Ah ; user_id[3] = 0x2A
.text:00401437 mov ecx, 1Ch ; ecx = 28 (counter)
.text:0040143C
.text:0040143C get_byte:
.text:0040143C shr ebx, 1 ; ebx = ebx >> 1
.text:0040143E mov edx, 0 ; edx = 0 (clear dividend)
.text:00401443 mov eax, ebx ; eax = ebx
.text:00401445 mov esi, 1Ah ; esi = 0x1A
.text:0040144A div esi ; edx = eax % 0x1A
.text:0040144C test ecx, 1 ; \ will take the jump
.text:00401452 jz short add_97 ; / once every 2 loops (when ecx is even)
.text:00401454 add edx, 41h ; when ecx is odd
.text:00401454 ; edx += 0x41
.text:00401457 mov [edi], dl ; user_id[i] = dl
.text:00401457 ; with i starting from 3
.text:00401459 inc edi
.text:0040145A loop get_byte ; ecx -= 1
.text:0040145C jmp short go_on
.text:0040145E ; ---------------------------------------------------------------------------
.text:0040145E
.text:0040145E add_97:
.text:0040145E add edx, 61h ; when ecx is even
.text:0040145E ; edx += 0x61
.text:00401461 mov [edi], dl ; user_id[i] = dl
.text:00401461 ; with i starting from 3
.text:00401463 inc edi
.text:00401464 loop get_byte ; ecx -= 1
Password
The password is then prompted at offset 0x40147D:
.text:00401466 push offset private_prompt
.text:0040146B call _printf
.text:00401470 add esp, 4
.text:00401473 push offset password
.text:00401478 push offset scanf_format ; "%50s"
.text:0040147D call _scanf
.text:00401482 add esp, 8
.text:00401485 mov esi, offset user_id
.text:0040148A mov edi, offset password
Each byte of the provided password is then compared with the expected password and if the test fails for a given byte, the loop exits and goes to the bad boy.
.text:00401485 mov esi, offset user_id
.text:0040148A mov edi, offset password
.text:0040148F
.text:0040148F keep_comparing:
.text:0040148F cmp byte ptr [esi], 0
.text:00401492 jz short yepp
.text:00401494 mov al, [esi] ; al = user_id (expected password)
.text:00401496 cmp al, [edi] ; edi = provided password
.text:00401498 jnz short nope ; if user_id[i] != provided_password[i], goto badboy
.text:0040149A inc esi
.text:0040149B inc edi
.text:0040149C jmp short keep_comparing
.text:0040149E ; ---------------------------------------------------------------------------
.text:0040149E
.text:0040149E yepp:
.text:0040149E push offset good_job ; "\nGood job ! - Now please prepare a key"...
.text:004014A3 call _printf
.text:004014A8 add esp, 4
.text:004014AB retn
.text:004014AC ; ---------------------------------------------------------------------------
.text:004014AC
.text:004014AC nope:
.text:004014AC push offset try_again ; "\nNope, try again !\n"
.text:004014B1 call _printf
.text:004014B6 add esp, 4
.text:004014B9 retn
Solution
Keygen
Below is the keygen I wrote for this challenge. The assembly code has been left as comments to help understanding it.
#!/usr/bin/env python
import sys
"""
.text:004013E0 mov edi, offset user_id ; mem loc 0x40D020
.text:004013E5 mov ecx, 20h
.text:004013EA mov al, 4Fh
.text:004013EC rep stosb ; fill 32 bytes with 0x4F starting from 0x40D0
"""
uid = [0x4F, 0x4F, 0x4F, 0x4F, 0x4F, 0x4F, 0x4F, 0x4F,
0x4F, 0x4F, 0x4F, 0x4F, 0x4F, 0x4F, 0x4F, 0x4F,
0x4F, 0x4F, 0x4F, 0x4F, 0x4F, 0x4F, 0x4F, 0x4F,
0x4F, 0x4F, 0x4F, 0x4F, 0x4F, 0x4F, 0x4F, 0x4F]
def bit_test_0(n):
return int(bin(n)[-1])
def is_odd(n):
return bin(n)[-8:].count('1') % 2
def keygen(user_id):
"""
.text:004013EE inc edi
...
.text:00401418 bt ebx, 0 ; CF = bit 0 of binary value bin(rdtsc)[-1]
.text:0040141C jnb short zero_bit_set ; if CF==0, jump to zero_bit_set
.text:0040141E mov byte ptr [edi], 2Ah ; user_id[0] = 0x2A
"""
edi = 0x40D021
if bit_test_0(user_id) != 0:
uid[0] = 0x2A
"""
.text:00401421 inc edi
.text:00401422 cmp ebx, 0B16B00B5h ; \ if rdtsc > 0xB16B00B5
.text:00401428 ja short above_b16b00b5h ; / take jump
.text:0040142A mov byte ptr [edi], 2Ah ; user_id[1] = 0x2A
"""
edi += 1
if user_id <= 0xB16B00B5:
uid[1] = 0x2A
"""
.text:0040142D inc edi
.text:0040142E jnp short no_parity ; jump will never be taken
.text:00401430 mov byte ptr [edi], 2Ah ; user_id[2] = 0x2A
"""
edi += 1
if is_odd(edi) == 1:
uid[2] = 0x2A
"""
.text:00401433 inc edi
.text:00401434 mov byte ptr [edi], 2Ah ; user_id[3] = 0x2A
.text:00401437 mov ecx, 1Ch ; ecx = 0x1C
"""
uid[3] = 0x2A
ecx = 0x1C
"""
.text:0040143C shr ebx, 1 ; ebx = ebx >> 1
.text:0040143E mov edx, 0 ; edx = 0 (clear dividend)
.text:00401443 mov eax, ebx ; eax = ebx
.text:00401445 mov esi, 1Ah ; esi = 0x1A
.text:0040144A div esi ; edx = eax % 0x1A
.text:0040144C test ecx, 1 ; \ will take the jump
.text:00401452 jz short add_97 ; / once every 2 loops (when ecx is even)
.text:00401454 add edx, 41h ; when ecx is odd
.text:00401454 ; edx += 0x41
.text:00401457 mov [edi], dl ; user_id[i] = dl
.text:00401457 ; with i starting from 3
.text:00401459 inc edi
.text:0040145A loop get_byte ; ecx -= 1
.text:0040145C jmp short go_on
.text:0040145E ; ---------------------------------------------------------------------------
.text:0040145E
.text:0040145E add_97:
.text:0040145E add edx, 61h ; when ecx is even
.text:0040145E ; edx+=0x61
.text:00401461 mov [edi], dl ; user_id[i] = dl
.text:00401461 ; with i starting from 3
.text:00401463 inc edi
.text:00401464 loop get_byte ; ecx -= 1
"""
ebx = user_id
for i in range(3, 31):
ebx = ebx >> 1
if i % 2 == 0:
uid[i] = ebx % 0x1A + 0x41
else:
uid[i] = ebx % 0x1A + 0x61
return ''.join([chr(i) for i in uid])
if __name__ == '__main__':
if len(sys.argv) < 2:
print "Usage: %s <user_id>" % sys.argv[0]
sys.exit(1)
print keygen(int(sys.argv[1]))
Tests
program | keygen |
---|---|
C:\crackmes>level-2.exe Crackme - Level 2 - by 60Ô15 ---------------------------- User Id : 310539076 Password : O**iRiEcBaNtWyZzZmGdOuKsWlSjEcBO Good job ! - Now please prepare a keygen... |
C:\crackmes>python keygen.py 310539076 O**iRiEcBaNtWyZzZmGdOuKsWlSjEcBO |
Level 3
Introduction
Description
This crackme is the level 3 of a series of 4 challenges, available here. This level is very interesting because it manipulates flags to validate bits of the user input.
Overview
When run, the program asks for an input. If we enter letters, here is what we get:
C:\crackme>level-3.exe Crackme - Level 3 - by 60Ô15 ---------------------------- Guess : test Sorry, not it.
On the other hand, a different message is displayed when providing a numeric value:
C:\crackme>level-3.exe Crackme - Level 3 - by 60Ô15 ---------------------------- Guess : 1234 Nope, try again !
Below is an overview of the code:
Code Analysis
Initialization
This block of code is the initialization phase of the sub_4013E0 function. It validates that the user input is numeric. If not, it jumps to the first failing message at 0x40149B.
.text:004013E0 sub_4013E0 proc near
.text:004013E0 inc edi
.text:004013E1 mov edi, 0
.text:004013E6 push offset Format ; "\nCrackme - Level 3 - by 60Ô15\n-------"...
.text:004013EB call printf
.text:004013F0 add esp, 4
.text:004013F3 mov byte_409079, 64h
.text:004013FA push offset aGuess ; "Guess : "
.text:004013FF call printf
.text:00401404 add esp, 4
.text:00401407 push offset my_guess ; offset 0x40D020
.text:0040140C push offset asc_409078 ; "%"
.text:00401411 call scanf
.text:00401416 add esp, 8
.text:00401419 cmp eax, 0
.text:0040141C jz short loc_40149B ; my_guess should be numbers
Test 1
This test is checking that the number of 1 in the low 8 bits of the binary representation of the user input (my_guess) is even.
.text:0040141E mov eax, ds:my_guess
.text:00401423 xor eax, 0 ; affects Parity Flag (PF)
.text:00401426 pushf
.text:00401427 pop ebx
.text:00401428 bt ebx, 2 ; CF = Parity Flag (PF)
.text:0040142C jnb short FAIL ; jump to FAIL if CF = 0
.text:0040142C ; => bin(my_guess)[-8:].count('1') should be even
At offset 0x401423, the xor instruction will do nothing but update the Parity Flag (PF). It applies to the low 8 bits of eax, which has been set to the user input at offset 0x40141E.
At offset 0x401426, the FLAGS are pushed to the stack by the pushf instruction, as follows:
┌──────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┐ │ bit │ 15 │ 14 │ 13 │ 12 │ 11 │ 10 │ 09 │ 08 │ 07 │ 06 │ 05 │ 04 │ 03 │ 02 │ 01 │ 00 │ ├──────┼────┼────┼────┴────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┤ │ flag │ 0 │ NT │ IOPL │ OF │ DF │ IF │ TF │ SF │ ZF │ 0 │ AF │ 0 │ PF │ 1 │ CF │ └──────┴────┴────┴─────────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┘ │ │ │ │ │ │ │ │ │ │ │ │ │ │ └─ Carry flag │ │ │ │ │ │ │ │ │ │ │ │ │ └─ Reserved │ │ │ │ │ │ │ │ │ │ │ │ └─ Parity flag │ │ │ │ │ │ │ │ │ │ │ └─ Reserved │ │ │ │ │ │ │ │ │ │ └─ Adjust flag │ │ │ │ │ │ │ │ │ └─ Reserved │ │ │ │ │ │ │ │ └─ Zero flag │ │ │ │ │ │ │ └─ Sign flag │ │ │ │ │ │ └─ Trap flag │ │ │ │ │ └─ Interrupt enable flag │ │ │ │ └─ Direction flag │ │ │ └─ Overflow flag │ │ └─ I/O privilege level (286+ only), always 1 on 8086 and 186 │ └─ Nested task flag (286+ only), always 1 on 8086 and 186 └─ Reserved, always 1 on 8086 and 186, always 0 on later models
The instruction just after (offset 0x401427) is moving this value to ebx. At offset 0x401428, the bt instruction sets the Carry Flag (CF) to bit #2 (Parity Flag) of FLAGS. It jumps to FAIL if CF = 0.
To sum up, this block of code is checking that the following condition is satisfied:
The number of 1 in the low 8 bits of the binary representation of the user input should be even.
Test 2
.text:0040142E bt eax, 1Eh ; CF = bit #30 of my_guess
.text:00401432 pushf
.text:00401433 pop ebx ; ebx = CF
.text:00401434 bt ebx, 0
.text:00401438 jb short FAIL ; jump to FAIL if CF=1
.text:00401438 ; => bit #30 of my guess should be 0
At offset 0x40142E, bit #30 of the user input is checked. The value of FLAGS is pushed to the stack as explained previously (see test 1) and this time, bit 0 (Carry Flag) of FLAGS is used. It will jump to FAIL if it is set. We deduce that:
Bit #30 should be 0.
Test 3
.text:0040143A test eax, 1 ; my_guess & 0x1
.text:0040143F pushf
.text:00401440 pop ebx
.text:00401441 bt ebx, 6 ; CF = ZF
.text:00401445 jb short FAIL ; jump to FAIL if ZF = 1
.text:00401445 ; => bit #0 of my_guess should be 1
At offset 0x40143A, the test instruction is performing a logical AND with 0x1. It will return zero if bit #0 is not set to 1. The value of the Zero Flag (ZF) is then checked at offset 0x401441. In other terms:
Bit #0 of the user input should be set to 1
Test 4
.text:00401447 shl eax, 1 ; CF = Bit #31 of my_guess
.text:00401449 pushf
.text:0040144A pop ebx
.text:0040144B bt ebx, 0 ; CF is read
.text:0040144F jnb short FAIL ; Jump to FAIL if CF = 0
.text:0040144F ; => Bit #31 of my_guess should be 1
At offset 0x401447, the user input is shifted to the left (shl). The 1st bit that is shifted (bit #31 of the user input) is saved to the Carry Flag (CF) and is then read at offset 0x40144B (bit #0 of FLAGS). The code jumps to FAIL if CF = 0. In other terms:
Bit #31 of the user input should be equal to 1
Test 5
.text:00401451 add eax, 60000000h
.text:00401456 pushf
.text:00401457 pop ebx
.text:00401458 bt ebx, 0Bh ; CF = Overflow Flag (OF)
.text:0040145C jnb short FAIL ; Jump to FAIL if CF = 0
.text:0040145C ; my_guess should be > (0xFFFFFFFF-0x60000000)/2 = 1,342,177,279
Remember that the user input was shifted left by 1 in the previous test. This is like multiplying it by 2. At offset 0x401451, the value 0x60000000 is added and the Overflow Flag (OF) is tested at offset 0x401458. The maximum value the user input can be will be computed as follows:
2 * x + 0x60000000 = 0xffffffff x = (0xFFFFFFFF - 0x60000000) / 2 x = 1,342,177,279
This condition is useless since bits 31 and 30 should respectively be set to 1 and 0, as per previous tests. Hence:
minimum = 10000000000000000000000000000000 = 2,147,483,648
The minimum required by the previous tests is already above the minimum required by this test.
Test 6
.text:0040145E or eax, 20000000h
.text:00401463 and eax, 70000h ; eax should be 0 after these 2 transformations
.text:00401468 pushf
.text:00401469 pop ebx
.text:0040146A bt ebx, 6 ; CF = ZF
.text:0040146E jnb short FAIL ; Jump to FAIL if CF=0
.text:0040146E ; => Bit #28 should be 1
.text:0040146E ; => Bits #15-17 should be 0
The Zero Flag (ZF) is checked at offset 0x40146A and should be zero.
Considering the previous transformations and the ones applied at offsets 0x40145E and 0x401463, here is what we have:
┌──────────────────────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┐ │ │ 31 │ 30 │ 29 │ 28 │ 27 │ 26 │ 25 │ 24 │ 23 │ 22 │ 21 │ 20 │ 19 │ 18 │ 17 │ 16 │ 15 │ 14 │ 13 │ 12 │ 11 │ 10 │ 09 │ 08 │ 07 │ 06 │ 05 │ 04 │ 03 │ 02 │ 01 │ 00 │ ├──────────────────────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┤ │ eax │ 1 │ 0 │ x │ x │ x │ x │ x │ x │ x │ x │ x │ x │ x │ x │ x │ x │ x │ x │ x │ x │ x │ x │ x │ x │ x │ x │ x │ x │ x │ x │ x │ 1 │ │ shl eax,1 │ 0 │ x │ x │ x │ x │ x │ x │ x │ x │ x │ x │ x │ x │ x │ x │ x │ x │ x │ x │ x │ x │ x │ x │ x │ x │ x │ x │ x │ x │ x │ 1 │ 0 │ │ add eax, 0x600000000 │ 0 │ 1 │ 1 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ │ or eax, 0x20000000 │ 0 │ 0 │ 1 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ │ and eax, 0x70000 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 1 │ 1 │ 1 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ 0 │ └──────────────────────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┘ │ └───────┬──────┘ └── The OR test is without effect because it │ applies to bit #29 (after shl) and the bit └── To make the result 0, bits #16 to #18 (after shl) should #29 of 0x70000 is 0. Whatever value we be 0 because: have, it will never return 1 because: * 0 AND 1 = 0 (good) * 1 AND 0 = 0 (good) * 1 AND 1 = 1 (bad) * 0 AND 0 = 0 (good)
To sump up, bits #16 to #18 should be set to 0, once the shift left (shl) operation has been applied to the user input, which means that:
Bits #15-#17 of the user input should be 0.
Test 7
.text:00401470 mov eax, ds:my_guess ; eax = my_guess
.text:00401475 mov ebx, 0 ; ebx = 0
.text:0040147A mov ecx, 20h ; ecx = 32
.text:0040147F mov edx, 1Fh ; edx = 31
.text:00401484
.text:00401484 loc_401484:
.text:00401484 bt eax, edx ; CF = my_guess[i]
.text:00401487 jnb short loc_40148A ; jump if CF = 0
.text:00401489 inc ebx ; count number of '1' in binary
.text:00401489 ; representation of eax
.text:0040148A
.text:0040148A loc_40148A:
.text:0040148A dec edx ; edx-=1
.text:0040148B loop loc_401484 ; loop until ecx=0
.text:0040148D lea eax, my_guess+1 ; 0x40D021 contains byte #1 (bits #8-15) of user input
.text:00401493 mov eax, [eax]
.text:00401495 xor al, bl
.text:00401497 jnz short FAIL ; Nb of '1' in binary representation of my_guess
.text:00401497 ; should be equal to byte #1 of my_guess
At offset 0x401470, the user input is moved back to eax. Then, starting from offset 0x401484, there is a loop that is counting the number of 1 (saved in ebx) in the binary representation of the user input.
Then starting from offset 0x40148D, byte #1 of the user input (bits #8-15) is moved to eax. The XOR operation at offset 0x401495 is ensuring that this value equals the number of 1 obtained previously because x ^ x = 0.
Byte #1 (bits #8-15) of the user input should be equal to the total number of 1 in the binary representation of the user input.
Conditions and solution
Conditions
Below is a sum up of the conditions as seen in the previous tests:
Test | Description |
---|---|
Test 1 | The number of 1 in the low 8 bits of the binary representation of the user input should be even. |
Test 2 | bit #30 of the user input should be 0 |
Test 3 | bit #0 of the user input should be 1 |
Test 4 | Bit #31 of the user input should be 1 |
Test 5 | The user input should be > 1,342,177,279 |
Test 6 | Bits #15-17 of the user input should be 0 |
Test 7 | The number of '1' in the binary representation of the user input should be equal to byte #1 of the user input |
Valid codes
|
|
Level 4
Level 4 is available here
Comments
Keywords: assembly x86 reverse-engineering crackme borismilner 4N006135 crackmes.de