Keygen 2: adrianbn's lincrackme3

Mar 20, 2018

Link: http://crackmes.cf/users/adrianbn/lincrackme3/ (binary)

The description for this binary refers to antidebugging techniques, and suggests building a keygen for it rather than just bypassing the check.

# ./lincrackme3-32
 **********************************************************************
 * Welcome to lincrackme3! This is a simple linux crackme. The goal   *
 * is to understand the key checking method and write your own keygen *
 * for it. Keep an eye on antidebugging techniques, because thats the *
 * reason for this crackmes, knowing them and the way to bypass them. *
 * Key format: XXXX-YYYY-WWWW-ZZZZ with all numbers. Good luck!       *
 * @author: Adrian adrianbn[at]gmail.com http://securityetalii.es     *
 * ********************************************************************

Enter the key, you fool: 1234-1234-1234-1234
Wrong key! I've seen monkeys smarter than you...

If we run it under ltrace, we see a call to ptrace and an error message:

ptrace(0, 0, 1, 0)                               = 0xffffffff
puts("Debugger detected. Bye!\n")                = 24

As usual, this is an easy fix:

080485d9         call       j_ptrace
080485de         test       eax, eax
080485e0         jns        loc_80485fa

The call instruction takes 5 bytes, plenty of space to insert XOR EAX, EAX followed by NOPs. After changing [E8 86 FE FF FF] to [31 C0 90 90 90], Hopper sees it as:

080485d9         xor        eax, eax
080485db         nop
080485dc         nop
080485dd         nop
080485de         test       eax, eax

We save the binary and continue. sub_804862c is interesting. It reads the input character by character, validating that it contains only digits and hyphens:

08048643         cmp        byte [ebp+var_9], 0xa   ; compare to '\n'
08048647         je         loc_8048688             ;   -> end of buffer
08048649         cmp        byte [ebp+var_9], 0x0   ; null byte
0804864d         je         loc_8048688             ;   -> end of buffer
0804864f         cmp        byte [ebp+var_9], 0x2d  ; '-'
08048653         je         loc_804867c             ;   -> continue
08048655         cmp        byte [ebp+var_9], 0x2f  ; compare to '/' which is just before '0'
08048659         jle        loc_8048661             ;   -> error
0804865b         cmp        byte [ebp+var_9], 0x39  ; compare to '9'
0804865f         jle        loc_8048668             ;   -> continue

After the call to this procedure, we verify that we got 0x10 (16) characters right, and print an error message otherwise:

080487de         cmp        dword [esp+0xe0+var_B4], 0x10
080487e3         je         loc_80487fd
080487e5         mov        dword [esp+0xe0+var_E0], aWrongKeyFormat  
080487ec         call       j_puts

Next, a trick comparison:

080487fd         mov        dword [esp+0xe0+var_D8], 0x10            ; argument "__n" for method j_strncmp, CODE XREF=sub_80486b4+303
08048805         mov        dword [esp+0xe0+var_DC], aItsNotThatEasy ; argument "__s2" for method j_strncmp, "It's not that easy, dude"
0804880d         lea        eax, dword [esp+0xe0+var_24]
08048814         mov        dword [esp+0xe0+var_E0], eax             ; argument "__s1" for method j_strncmp
08048817         call       j_strncmp

This call to strcmp would be obvious in ltrace, but its return value is not even used so it’s just there to fool us.

A few local variables are set up, and then follows a very short call:

08048841         call       sub_80486b4+403
08048846         jmp        0x8a18da3

The call is to 0x08048847, which is one byte after what Hopper decoded as a JMP. We need to change it to data and then code again to reveal the real instructions:

08048841         call       sub_80486b4+403 ; jumps to 0x08048847
08048846         db         0xe9
08048847         pop        eax
08048848         add        eax, 0x9d
0804884d         push       eax
0804884e         ret

We ret back to 0x80488e3, where a function pointer is read and called. At this point, ebx is 0x080485B4, which we use in a call:

080488e3         mov        ebx, dword [esp+40]
080488e7         call       ebx

0x080485B4 is where we broke the ptrace call, we’ll pass through without failing. We return from the previous two calls and end up in 0x08048850. A variable at esp+0x24 is initialized to zero, before we jump to the end of the block and compare it to 3: this is some sort of counter.

08048852         mov        dword [esp+0x24], 0x0
0804885a         jmp        sub_804884f+135
[...]
080488d6         cmp        dword [esp+0x24], 0x3
080488db         jle        sub_804884f+13

In between, we successively load the integer values for our input digits, and add them into multiple local variables. First with the integer value of input[counter] into esp+0x36 (let’s call it var_36). So as the counter is incremented, the value of var_36 will be the sum of counter[0] to counter[3] included:

0804885c         mov        eax, dword [esp+0x24]
08048860         movzx      eax, byte [esp+eax+0xbc]
08048868         cbw
0804886a         add        ax, word [esp+0x36]
0804886f         sub        eax, 0x30       ; 0x30 is ascii for '0'
08048872         mov        word [esp+0x36], ax

Then input[counter+4] is added to esp+0x34 (var_34):

08048877         mov        eax, dword [esp+0x24]
0804887b         add        eax, 0x4
0804887e         movzx      eax, byte [esp+eax+0xbc]
08048886         cbw
08048888         add        ax, word [esp+0x34]
0804888d         sub        eax, 0x30
08048890         mov        word [esp+0x34], ax

Then input[counter+8] to esp+0x32 (var_32):

08048895         mov        eax, dword [esp+0x24]
08048899         add        eax, 0x8
0804889c         movzx      eax, byte [esp+eax+0xbc]
080488a4         cbw
080488a6         add        ax, word [esp+0x32]
080488ab         sub        eax, 0x30
080488ae         mov        word [esp+0x32], ax

And finally input[counter+12] to esp+0x30 (var_30):

080488b3         mov        eax, dword [esp+0x24]
080488b7         add        eax, 0xc
080488ba         movzx      eax, byte [esp+eax+0xbc]
080488c2         cbw
080488c4         add        ax, word [esp+0x30]
080488c9         sub        eax, 0x30
080488cc         mov        word [esp+0x30], ax

We do this 4 times, from counter = 0 to 3. We then jump to a checksum verification block at 0x080488ef. The 4 checksums are involved, computing ecx = var_36+var_34, eax = (var_32 + var_30) * 2, and comparing the two:

080488ef         movzx      edx, word [esp+0x36]
080488f4         movzx      eax, word [esp+0x34]
080488f9         lea        ecx, dword [edx+eax]
080488fc         movzx      edx, word [esp+0x32]
08048901         movzx      eax, word [esp+0x30]
08048906         lea        eax, dword [edx+eax]
08048909         add        eax, eax
0804890b         cmp        ecx, eax
0804890d         jne        verify_checksum+92 ; assuming a failure jump

Then we need var_34 > var_32 or we’d fail:

0804890f         movzx      eax, word [esp+0x34]
08048914         cmp        ax, word [esp+0x32]
08048919         jbe        verify_checksum+92

We continue with a check on whether (var_36 + var_30) & 1 == 0:

0804891b         movzx      edx, word [esp+0x36]
08048920         movzx      eax, word [esp+0x30]
08048925         lea        eax, dword [edx+eax]
08048928         and        eax, 0x1
0804892b         test       al, al
0804892d         jne        verify_checksum+92

Followed by the requirements that var_36 > 5 and <= 0x18:

0804892f         cmp        word [esp+0x36], 0x5
08048935         jbe        verify_checksum+92    ; jump if below or equal
08048937         cmp        word [esp+0x36], 0x18
0804893d         ja         verify_checksum+92    ; jump if above

Finally, var_30 must be odd:

0804893f         movzx      eax, word [esp+0x30]
08048944         and        eax, 0x1
08048947         test       eax, eax
08048949         jne        verify_checksum+151
; right here is verify_checksum + 92, the failure message:

We initialize a variable at esp+0x20 to zero and loop until it’s 0x31:

0804894b         mov        dword [esp+0x20], 0x0
08048953         jmp        verify_checksum+132
08048955         mov        eax, dword [esp+0x20]
08048959         movzx      eax, byte [esp+eax+0x89]
08048961         movsx      eax, al
08048964         xor        al, 0xe9
08048966         mov        dword [esp], eax
08048969         call       j_putchar
0804896e         add        dword [esp+0x20], 0x1
08048973         cmp        dword [esp+0x20], 0x31
08048978         jle        verify_checksum+102

The address at esp+0x89 was initialized above, pointing to 0x8048d84. We XOR 0x31 bytes at that address with 0xe9, to produce the following message: “Wrong key! I’ve seen monkeys smarter than you…\n”

If we had jumped to verify_checksum+151:

08048986         mov        dword [esp+0x1c], 0x0 
0804898e         jmp        sub_8048990+27 ; just below
[...]
sub_8048990:
08048990         mov        eax, dword [esp+28]
08048994         movzx      eax, byte [esp+eax+56]
08048999         movsx      eax, al
0804899c         xor        al, 0xf9
0804899e         mov        dword [esp+0], eax    ; argument "__c" for method j_putchar
080489a1         call       j_putchar             ; putchar
080489a6         add        dword [esp+28], 0x1
080489ab         cmp        dword [esp+28], 0x4f
080489b0         jle        sub_8048990  

The loop decrypts a string using XOR 0xf9, eventually printing: “You got it, congratz. Have you tried to keygen me? Next crackme soon ^_^\n”.

To recap the rules:

  • the format is AAAA-BBBB-CCCC-DDDD where A,B,C,D are digits
  • var_36 is the sum of the A digits
  • var_34 is the sum of the B digits
  • var_32 is the sum of the C digits
  • var_30 is the sum of the D digits
  • var_36 + var_34 == 2 * (var_32 +var_30)
  • var_34 > var_32
  • var_36 + var_30 % 2 == 0
  • 5 < var_36 <= 24
  • var_30 % 2 == 1

A simple keygen gives us a random serial to try:

$ ./keygen.py
4630-6843-8020-1204

$ ./lincrackme3-32
 **********************************************************************
 * Welcome to lincrackme3! This is a simple linux crackme. The goal   *
 * is to understand the key checking method and write your own keygen *
 * for it. Keep an eye on antidebugging techniques, because thats the *
 * reason for this crackmes, knowing them and the way to bypass them. *
 * Key format: XXXX-YYYY-WWWW-ZZZZ with all numbers. Good luck!       *
 * @author: Adrian adrianbn[at]gmail.com http://securityetalii.es     *
 * ********************************************************************

Enter the key, you fool: 4630-6843-8020-1204
Good! You got it, congratz. Have you tried to keygen me? Next crackme soon ^_^