Crackme 11: ELF, Random

Link: https://www.root-me.org/en/Challenges/Cracking/ELF-Random-Crackme (binary) This crackme is significantly more complex than the previous ones:

flow

A quick look shows a protection against debuggers using ptrace(0,0,1,0) < 0. Let’s start by breaking it so that we can make progress:

loc_8048ad3:
mov        dword [esp+0x10124+var_10118], 0x0 ; CODE XREF=main+517
mov        dword [esp+0x10124+var_1011C], 0x1
mov        dword [esp+0x10124+var_10120], 0x0
mov        dword [esp+0x10124+var_10124], 0x0 ; argument "__request" for method j_ptrace
call       j_ptrace     ; ptrace
test       eax, eax
jns        loc_8048b2c

We change the JNS (0x79) into a JMP (0xEB) and save the binary. We can now debug it:

Reading symbols from ./Crackme.dbg...done.
gdb$ r
Starting program: ./Crackme.dbg

    ** Bienvenue dans ce challenge de cracking **

[+] Password :hello

[!]Access Denied !
[-] Try again

[Inferior 1 (process 27) exited with code 0377]
--------------------------------------------------------------------------[regs]

With this new binary, let’s take a look at the initial setup in loc_80488f3. There’s first a memset of 0xffff bytes to zero, then a call to time(0) (the current time), the result of which is passed to srand. We then call getpid and store it in ebp - 0x100a0:

lea        edx, dword [ebp-0x1007b] ; CODE XREF=main+79
mov        eax, 0xffff
mov        dword [esp+0x10120+var_10118], eax ; argument "__n" for method j_memset
mov        dword [esp+0x10120+var_1011C], 0x0 ; argument "__c" for method j_memset
mov        dword [esp+0x10120+var_10120], edx ; argument "__s" for method j_memset
call       j_memset     ; memset
mov        dword [ebp-0x1009c], 0x0
mov        dword [esp+0x10120+var_10120], 0x0 ; argument "__timer" for method j_time
call       j_time       ; time
mov        dword [esp+0x10120+var_10120], eax ; argument "__seed" for method j_srand
call       j_srand      ; srand
call       j_getpid     ; getpid
mov        dword [ebp-0x100a0], eax
call       j_rand       ; rand

The return value of rand is used in a series of numeric calculations, some involving floating-point arithmetic. It’s pretty complex so let’s not get into it in details. This is followed by a call to sprintf that builds a string with the pattern “%lld%s”, with a reference to “_VQLGE_TQPTYD_KJTIV_”. Let’s run through it a couple of times, and see what comes out of sprintf:

$ ltrace ./Crackme.dbg 2>&1 | grep sprintf
sprintf("5884452_VQLGE_TQPTYD_KJTIV_", "%lld%s", 5884452, nil) = 27
$ ltrace ./Crackme.dbg 2>&1 | grep sprintf
sprintf("2008856_VQLGE_TQPTYD_KJTIV_", "%lld%s", 2008856, nil) = 27
$ ltrace ./Crackme.dbg 2>&1 | grep sprintf
sprintf("653161_VQLGE_TQPTYD_KJTIV_", "%lld%s", 653161, nil) = 26

As these examples show, the random value is eventually transformed into an integer that gets prepended to “_VQLGE_TQPTYD_KJTIV_”. This is followed by a call to malloc(0xffff), and then malloc(0x15). We strcmp the formatted string with our input and store the result in ebp-0x7c before storing their respective lengths in two other local variables:

lea        eax, dword [ebp-0x1007b] ; CODE XREF=sub_8048b20+100
mov        dword [esp+0x10124+var_10120], eax ; argument "__s2" for method j_strcmp
lea        eax, dword [ebp-0x7c]
mov        dword [esp+0x10124+var_10124], eax ; argument "__s1" for method j_strcmp
call       j_strcmp     ; strcmp
mov        dword [ebp-0x10080], eax
lea        eax, dword [ebp-0x7c]
mov        dword [esp+0x10124+var_10124], eax ; argument "__s" for method j_strlen
call       j_strlen     ; strlen
mov        dword [ebp-0x10088], eax
lea        eax, dword [ebp-0x1007b]
mov        dword [esp+0x10124+var_10124], eax ; argument "__s" for method j_strlen
call       j_strlen     ; strlen
mov        dword [ebp-0x10084], eax

If the two lengths are different, we jump to an error message:

mov        eax, dword [ebp-0x10088]
cmp        eax, dword [ebp-0x10084]
je         loc_8048c50 ; Access Denied...

If the result of strcmp was less than or equal to zero, we also jump to a failure message (see reference to $ebp-0x10080 above):

loc_8048c50:
cmp        dword [ebp-0x10080], 0x0 ; CODE XREF=sub_8048b20+250
jle        loc_8048c8d ; Access denied

Following these branches, we notice that the strcmp needs to have returned zero, and that the lengths need to match as well (this check is redundant):

mov        eax, dword [ebp-0x10088]
cmp        eax, dword [ebp-0x10084]
jne        loc_8048ded

To crack tis program, we’ll need to predict the value of this random string, or make it predictable. We can patch the binary again, removing the call to rand() and replacing its value with zero. Here is the call instruction and the offset of the next one:

0804893b         call       j_rand                                              ; rand
08048940         mov        ecx, eax

We have 5 bytes between the two. We can insert xor eax, eax which is [0x31, 0xc0] and add 3 NOPs [0x90, 0x90, 0x90]. We now have, with addresses:

0804893b         xor        eax, eax
0804893d         nop 
0804893e         nop 
0804893f         nop 
08048940         mov        ecx, eax

We need to do the same to getpid and return zero there too. Instead of:

08048930         call       j_getpid                                            ; getpid
08048935         mov        dword [ebp-0x100a0], eax

We’ll edit 08048930 and replace 5 bytes there too. Here’s the hex view:

Let’s save the binary again and try it a few times under ltrace:

$ ltrace ./Crackme.norand 2>&1 | grep sprintf
sprintf("0_VQLGE_TQPTYD_KJTIV_", "%lld%s", 0, nil) = 21
$/# ltrace ./Crackme.norand 2>&1 | grep sprintf
sprintf("0_VQLGE_TQPTYD_KJTIV_", "%lld%s", 0, nil) = 21
$ ltrace ./Crackme.norand 2>&1 | grep sprintf
sprintf("0_VQLGE_TQPTYD_KJTIV_", "%lld%s", 0, nil) = 21

Our string is now predictable, and we can provide the same value as input:

$ ./Crackme.norand

    ** Bienvenue dans ce challenge de cracking **

[+] Password :0_VQLGE_TQPTYD_KJTIV_

[+]Good password
[+] Clee de validation du crack-me : _VQLG1160_VTEPI_AVTG_3093_