Crackme 14: geyslan’s crackme.02.32

Link: http://crackmes.cf/users/geyslan/crackme.02.32/ (binary)

$ ./crackme.02.32
Please tell me my password: 1234
No! No! No! No! Try again.

Hopper finds no strings, no symbols. strings tells us that there are probably some protections in place:

$ strings ./crackme.02.32
[...]
I'm sorry GDB! You are not allowed!
Tracing is not allowed... Bye!
Please tell me my password:
The password is correct!
Congratulations!!!
No! No! No! No! Try again.

The strings are mostly around 0x08048858.

GDB does not recognize the executable, which seems to be corrupted:

$ file ./crackme.02.32
crackme.02.32: ELF 32-bit LSB executable, Intel 80386, (SYSV), too many section (65535)

$ gdb ./crackme.02.32
[...]
"./crackme.02.32": not in executable format: File format not recognized

Hopper shows a reference in EntryPoint to 0x80486fd, which is recognized as data but that we can switch to code by pressing ‘c’. Immediately, we see references to “Please tell me my password: “, “The password is correct!\nCongratulations!!!\n”, and “No! No! No! No! Try again.\n”. The code at 0x08048768 performs the comparison:

08048768         test       eax, eax
0804876a         jne        EntryPoint+787

If we were to trivially patch this test by replacing test eax, eax with xor eax, eax, it would defeat it. Just like in crackme 13, we can replace 0x85 0xC0 with 0x31 0xC0 and save it as a new binary.

$ ./crackme.02.xortest
Please tell me my password: hello
The password is correct!
Congratulations!!!

That was a bit too easy, so let’s dig into the binary and see what it’s actually doing.

Let’s start with renaming 0x8048410 as printf since it’s called with string parameters:

0804871f         mov        dword [esp], aPleaseTellMeMy    ; "Please tell me my password: "
08048726         call       0x8048410
[...]
08048785         mov        dword [esp], aThePasswordIsC    ; "The password is correct!\\nCongratulations!!!\\n"
0804878c         call       0x8048410
[...]
080487ac         mov        dword [esp], aNoNoNoNoTryAga    ; "No! No! No! No! Try again.\\n"
080487b3         call       0x8048410

Let’s also try to fix the number of sections, with the hope that GDB will load the file. Wikipedia has a description of the ELF header, listing e_shentsize (the size of the section entry) at offset 0x2E. We can observe that the 2 bytes at that offset are 0xFF 0xFF, matching our 65535 sections from file. readelf confirms the corrupt header:

$ readelf -a crackme.02.header
ELF Header:
    Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
    Class:                             ELF32
    Data:                              2's complement, little endian
    Version:                           1 (current)
    OS/ABI:                            UNIX - System V
    ABI Version:                       0
    Type:                              EXEC (Executable file)
    Machine:                           Intel 80386
    Version:                           0xffffffff
    Entry point address:               0x8048480
    Start of program headers:          52 (bytes into file)
    Start of section headers:          4294967295 (bytes into file)
    Flags:                             0x0
    Size of this header:               65535 (bytes)
    Size of program headers:           32 (bytes)
    Number of program headers:         8
    Size of section headers:           10240 (bytes)
    Number of section headers:         10496
    Section header string table index: 65535 <corrupt: out of range>
readelf: Warning: The e_shentsize field in the ELF header is larger than the size of an ELF section header
readelf: Error: Reading 0x6680000 bytes extends past end of file for section headers
readelf: Error: Section headers are not available!

“Size of this header” refers to e_ehsize at offset 0x28, it’s usually 52 for a 32-bit executable. We can replace the two bytes there with 0x34 0x00. Next, section headers. They should be 0x28 bytes so we can replace the 0xFF 0xFF at offset 0x2E with 0x28 0x00. Let’s also reset e_shoff to zero, as well as e_shnum and e_shstrndx. readelf now reports a clean header (even if we might be missing a couple of things) when we save a new binary:

$ readelf -a ./crackme.02.header
ELF Header:
    Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
    Class:                             ELF32
    Data:                              2's complement, little endian
    Version:                           1 (current)
    OS/ABI:                            UNIX - System V
    ABI Version:                       0
    Type:                              EXEC (Executable file)
    Machine:                           Intel 80386
    Version:                           0xffffffff
    Entry point address:               0x8048480
    Start of program headers:          52 (bytes into file)
    Start of section headers:          0 (bytes into file)
    Flags:                             0x0
    Size of this header:               52 (bytes)
    Size of program headers:           32 (bytes)
    Number of program headers:         8
    Size of section headers:           40 (bytes)
    Number of section headers:         0
    Section header string table index: 0

There are no sections in this file.

There are no sections to group in this file.

GDB loads it too:

$ gdb ./crackme.02.header
[...]
gdb$ r
Starting program: ./crackme.02.header
Tracing is not allowed... Bye!
[Inferior 1 (process 106) exited with code 01]

Interestingly, strace shows a call to ptrace(PTRACE_TRACEME) which returns -1 EPERM (Operation not permitted) before the program exist. We’ll need to patch this if we want to debug the program.

The tracing error message is referenced from 0x08048613:

08048613         test       eax, eax
08048615         jns        EntryPoint+431
08048617         mov        dword [esp], aTracingIsNotAl           ; "Tracing is not allowed... Bye!"
0804861e         call       printf+16

We can patch it and XOR eax before the JNS as in earlier crackmes, saving a new binary.

After reading the password, esp+0x15 contains our input (ending with a new line). A static buffer is also loaded before calling a procedure:

08048754         mov        dword [esp+4], 0x8049bb8
0804875c         lea        eax, dword [esp+0x15]
08048760         mov        dword [esp], eax
08048763         call       EntryPoint+554

0x8049bb8 contains: [0xf7, 0xf8, 0xf1, 0xf4, 0xf1, 0xf8, 0xb3, 0xfc, 0xfc, 0x00]. This is not a readable string.

The loop that follows goes over our input and calls 0x08048631 on each character before comparing the values to the array above. The code is slightly obfuscated with a few bytes inserted throughout the code, likely to confuse tools and prevent disassembly:

08048631         push       ebp
08048632         mov        ebp, esp
08048634         sub        esp, 0x4
08048637         mov        eax, dword [ebp+8]
0804863a         mov        byte [ebp-4], al
0804863d         movsx      eax, byte [ebp-4]
08048641         mov        dword [dword_8049c08], eax
08048646         jmp        EntryPoint+458
08048648         db  0xc9 ; '.'                         ; garbage
08048649         db  0x35 ; '5'                         ; garbage
0804864a         push       edx
0804864b         mov        edx, dword [dword_8049c08]
08048651         jmp        EntryPoint+469
08048653         db  0x08 ; '.'                         ; garbage
08048654         db  0x5a ; 'Z'                         ; garbage
08048655         or         edx, 0x90                   ; OR 0x90 on each byte
0804865b         mov        dword [dword_8049c08], edx
08048661         pop        edx
08048662         mov        eax, dword [dword_8049c08]
08048667         leave 

We can reverse the OR by doing an AND ~0x90 on the array found above:

>>> ''.join(map(lambda c: chr(c & ~0x90), [0xf7, 0xf8, 0xf1, 0xf4, 0xf1, 0xf8, 0xb3, 0xfc, 0xfc]))
'ghadah#ll'

Let’s try it out:

$ ./crackme.02.32
Please tell me my password: ghadah#ll
The password is correct!
Congratulations!!!