Crackme 12: ELF, anti-debug

Link: https://www.root-me.org/en/Challenges/Cracking/ELF-Anti-debug (binary)

The binary is very small and has no symbols, it was likely handcrafted in assembly. Right from the start, we find strange code. The initial JMP lands on the first MOV, which sets eax to 0x30, ebx to 5, and ecx to 0x80480e2 before calling int 0x80. This interrupt triggers a system call. The system call table describes 0x30 as sys_signal, where ebx is the signal number (here 5 is SIGABRT) and ecx is the signal handler. Once the handler is set up, we jump to loc_8048077 which calls int3 and would cause a debugger to stop.

EntryPoint:
08048060      jmp     loc_8048063
08048062      db      0xe8 ; '.'

loc_8048063:
08048063      mov     eax, 0x30
08048068      mov     ebx, 0x5
0804806d      mov     ecx, 0x80480e2
08048072      int     0x80
08048074      jmp     loc_8048077
08048076      into

loc_8048077:
08048077      int3 

If we go over these instructions step by step, we end up at 0x80480e2 which the Hopper disassembly recognized as the middle of an instruction. We need to re-interpret the bits there, which gives us some more interesting code.

We initialize eax to 0x8048104, which is right below the instructions disassembled by Hopper. A tangled loop goes over these bytes:

080480e2      mov     eax, 0x8048104
080480e7      jmp     sub_80480da+39          ; this is 08048101 below
080480e9      cmp     eax, 0x80482e8          ; 0x80482e8 is the end of the program
080480ee      je      sub_80480da+41          ; if we've reached it, jump to the ret below
080480f0      jmp     sub_80480da+25          ; otherwise go to the XOR at 080480f3
080480f2      db      0xe8
080480f3      xor     dword [eax], 0x8048fc1  ; we XOR the block of code pointed by eax with this constant
080480f9      add     eax, 0x4                ; then move forward 4 bytes
080480fc      jmp     sub_80480da+37          ; jump immediately below
080480fe      jmp     sub_80480da+17          ; and jump back to the CMP at 080480e9.
08048100      db      0xe8 ; '.'
08048101      jmp     sub_80480da+15          ; we jump back up to 080480e9
08048103      ret 

This code is self-modifying! Everything from 0x08048104 until the end is XOR’d dword by dword with the constant 0x8048fc1, before we return. We can write a small program to XOR this section and replace it before saving the output to a new binary. I used Hex Fiend for this, but any hex editor will do.

When we disassemble it again, some relevant code is revealed, and we finally see some plaintext strings:

08048100      call    0xc3c867f0
08048105      add     dword [eax], eax
08048107      add     byte [eax], al
08048109      mov     ecx, aEnterThePasswo ; "Enter the password: "
0804810e      mov     edx, 0x14
08048113      call    sub_80481cd

There is also a first loop that transforms an array at 0x8048251, until a null byte is found:

sub_8048138:
08048138      mov     eax, 0x8048251

loc_804813d:
0804813d      cmp     byte [eax], 0x0
08048140      je      loc_8048148

08048142      xor     byte [eax], 0xfc
08048145      inc     eax
08048146      jmp     loc_804813d ; jumps back up

A verification loop is also visible, after the transformation. It compares the XOR’d array above with a constant array, and expect matching values:

sub_8048149:
08048149      mov     eax, 0x8048251
0804814e      mov     ebx, 0x80482d1

loc_8048153:
08048153      mov     cl, byte [eax]
08048155      cmp     cl, byte [ebx]
08048157      jne     loc_8048162

08048159      cmp     cl, 0x0
0804815c      je      loc_804817b

0804815e      inc     eax
0804815f      inc     ebx
08048160      jmp     loc_8048153 ; a few lines above.

The bytes at 0x080482d1 are: [A5 CF 9D B4 DD 88 B4 95 AF 95 AF 88 B4 CF 97 B9 85 DD 00]. So we need (input XOR 0xFC) to match these values. Note that our loop stops when cl is zero.

In Python:

>>> ''.join(map(lambda i: chr(i ^ 0xfc), [0xA5, 0xCF, 0x9D, 0xB4, 0xDD, 0x88, 0xB4, 0x95, 0xAF, 0x95, 0xAF, 0x88, 0xB4, 0xCF, 0x97, 0xB9, 0x85, 0xDD]))
'Y3aH!tHiSiStH3kEy!'