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!'