mardi 10 décembre 2013

Un autre trick anti-gdb

gdb ouvre des files descriptor lorsqu'il est démarré.
Il devient donc très simple de détecter gdb depuis un binaire:
mitsurugi@mitsu:~/gdb$ cat close3.c
#include <stdio.h>
#include <stdlib.h>

cint main(void) {
   if (close(3)==0) {
      printf("Debugger detected, Bye\n");
      exit(0);
   }
   printf("Je suis dans le main\n");
}
mitsurugi@mitsu:~/gdb$

et c'est tout:
mitsurugi@mitsu:~/gdb$ gdb -n -q ./close3
Reading symbols from /home/mitsurugi/gdb/close3...done.
(gdb) r
Starting program: /home/mitsurugi/gdb/./close3 

Debugger detected, Bye
[Inferior 1 (process 15890) exited normally]
(gdb) q
mitsurugi@mitsu:~/gdb$ ./close3
Je suis dans le main
mitsurugi@mitsu:~/gdb$

Simple et efficace.

Ca se contourne tout aussi facilement, il existe plein de doc sur le sujet.

Le problème avec cette méthode, c'est que n'importe quel reverser la voit immédiatement. On pourrait imaginer des crackmes plus fins avec une détection silencieuse du genre:
mitsurugi@mitsu:~/gdb$ cat close3.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

char etat[32] = "gentil";

int main(void) {
   if (close(3)==0) {
      strcpy(etat,"mechant");
   }
   printf("Je suis dans le main\n");
   printf("Je suis %s.\n", etat);
}

Ce qui donne:
mitsurugi@mitsu:~/gdb$ ./close3
Je suis dans le main
Je suis gentil.
mitsurugi@mitsu:~/gdb$ gdb -n -q ./close3
Reading symbols from /home/mitsurugi/gdb/close3...done.
(gdb) r
Starting program: /home/mitsurugi/gdb/./close3
Je suis dans le main
Je suis mechant.
[Inferior 1 (process 16571) exited with code 021]
(gdb) q
mitsurugi@mitsu:~/gdb$


Cet exemple n'est pas très représentatif, mais on peut imaginer un changement de magic number, un appel à une fonction qui ne se fait pas ou ce genre de choses.

jeudi 5 décembre 2013

linux anti debug trick

Sous linux, tous les outils de décompilation s'attendent à avoir des binaires bien formés.
Prenons l'exemple du binaire crackme.01.32 de crackmes.de. L'antidebug est simple à réaliser et très efficace comme nous allons voir: la partie section header du binaire est erronée. Un hexdump le montre bien:
mitsurugi@mitsu:~/crackmes$ hexdump -C crackme.01.32 | head -4
00000000  7f 45 4c 46 01 01 01 00  00 00 00 00 00 00 00 00  |.ELF............|
00000010  02 00 03 00 ff ff ff ff  b0 84 04 08 34 00 00 00  |............4...|
00000020  ff ff ff ff 00 00 00 00  ff ff 20 00 08 00 ff ff  |.......... .....|
00000030  ff ff ff ff 06 00 00 00  34 00 00 00 34 80 04 08  |........4...4...|
mitsurugi@mitsu:~/crackmes$

Les sections headers sont remplacées par des ff ff, voir le post précédent avec le grand tableau de l'elf101.

Et c'est suffisant pour mettre hors jeu les debuggers classiques sous linux que j'ai pu essayer:
mitsurugi@mitsu:~/crackmes$ objdump -d crackme.01.32
objdump: crackme.01.32: File format not recognized
mitsurugi@mitsu:~/crackmes$ gdb -q crackme.01.32
"/home/mitsurugi/crackmes/crackme.01.32": not in executable format: Format de fichier non reconnu
(gdb) q
mitsurugi@mitsu:~/crackmes$ readelf -s crackme.01.32
readelf: Error: Unable to seek to 0xffffffff for section headers
readelf: Error: Unable to seek to 0xffffffff for section headers
mitsurugi@mitsu:~/crackmes$


Les debuggers spécialisés chargent le binaire, mais semblent avoir des problèmes par la suite. edb a crashé assez vite, radare2 charge le binaire, mais ne fait aucun mapping par la suite: tous octets du binaires sont simplement plaqué linéairement à partir de l'adresse 0x08048000. J'ai ouvert un bug chez radare2 du coup.

En fait, seul IDA semble bien s'en sortir. Le post de blog http://em386.blogspot.de/2006/10/elf-no-section-header-no-problem.html qui date de 2006 arrive exactement à la même conclusion.

Je trouve quand même ça fou que tous les debuggers s’appuient sur des sections qui ne sont pas utilisés à l'exécution. Cela signifie qu'il doit être possible de duper ces debuggers/reversers en forgeant des en-têtes de sections bien choisis. Comme le dit le post de blog plus haut, gdb est un peu dédié à l'analyse de code dont on a les sources, donc c'est normal qu'il ne soit pas robuste aux binaires mal fichus, mais c'est quand même dommage qu'il ne sache pas s'en sortir sans section headers.

J'ai envoyé une solution à crackmes.de en anglais en donnant une méthode sans IDA pour trouver la solution. Ca m'a permis de bien disséquer de l'ELF, mais c'est relativement long.

Mitsurugi

Solution:

Launch the binary to begin:
mitsurugi@mitsu:~/crackmes$ ./crackme.01.32
Please tell me my password: password
No! No! No! No! Try again.
mitsurugi@mitsu:~/crackmes$ gdb -q ./crackme.01.32
"/home/mitsurugi/crackmes/./crackme.01.32": not in executable format: File format not recognized
gdb$ q
mitsurugi@mitsu:~/crackmes$


At a first glance we can see an antidebug trick by wiping the section header:
mitsurugi@mitsu:~/crackmes$ readelf -s crackme.01.32
readelf: Error: Unable to seek to 0xffffffff for section headers
readelf: Error: Unable to seek to 0xffffffff for section headers
mitsurugi@mitsu:~/crackmes$


objdump fails at analyzing it, and gdb too. If we strings the binary we can also see:
mitsurugi@mitsu:~/crackmes$ strings -n10 crackme.01.32
/lib/ld-linux.so.2
_IO_stdin_used
__libc_start_main
__gmon_start__
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.
GCC: (GNU) 4.8.0
.note.ABI-tag
.note.gnu.build-id
.gnu.version
.gnu.version_r
.eh_frame_hdr
.init_array
.fini_array
mitsurugi@mitsu:~/crackmes$

The message about GDB let us assume that there are some other anti-gdb tricks, ptrace maybe:
mitsurugi@mitsu:~/crackmes$ strings crackme.01.32 | grep ptrace
ptrace
mitsurugi@mitsu:~/crackmes$


So it's time to disass it.We know that section headers are not usefull at all when running a binary, but program header are mandatory. Let's see what we have:
mitsurugi@mitsu:~/crackmes$ readelf -l crackme.01.32
readelf: Error: Unable to seek to 0xffffffff for section headers
readelf: Error: Unable to seek to 0xffffffff for section headers

Elf file type is EXEC (Executable file)
Entry point 0x80484b0
There are 8 program headers, starting at offset 52

Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  PHDR           0x000034 0x08048034 0x08048034 0x00100 0x00100 R E 0x4
  INTERP         0x000134 0x08048134 0x08048134 0x00013 0x00013 R   0x1
      [Requesting program interpreter: /lib/ld-linux.so.2]
  LOAD           0x000000 0x08048000 0x08048000 0x00a28 0x00a28 R E 0x1000
  LOAD           0x000a28 0x08049a28 0x08049a28 0x00140 0x00180 RW  0x1000
  DYNAMIC        0x000a38 0x08049a38 0x08049a38 0x000e8 0x000e8 RW  0x4
  NOTE           0x000148 0x08048148 0x08048148 0x00044 0x00044 R   0x4
  GNU_EH_FRAME   0x0008d4 0x080488d4 0x080488d4 0x00044 0x00044 R   0x4
  GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RW  0x4
mitsurugi@mitsu:~/crackmes$

So we can map some memory addresses with file offset.
First LOAD section:
From 0x08048000 to 0x08048a28, it's a linear mapping with the file from offset 0. But for bytes next, we have to make a gap of 0x1000.
Second LOAD section:
From 0x08049a28 to 0x08049a28+0x180 we have to map offset from files 0xa2 to 0xa28+0x140..
The DYNAMIC section, NOTE, GNU_EH_FRAME are not used for solving this crackme.

And now, with the help of ndisasm, we can solve the challenge by starting at entry point:
mitsurugi@mitsu:~/crackmes$ ndisasm -au -o 0x08048000 -e 0x4b0 crackme.01.32 | head -14
08048000  31ED              xor ebp,ebp
08048002  5E                pop esi
08048003  89E1              mov ecx,esp
08048005  83E4F0            and esp,byte -0x10
08048008  50                push eax
08048009  54                push esp
0804800A  52                push edx
0804800B  6800880408        push dword 0x8048800
08048010  6890870408        push dword 0x8048790
08048015  51                push ecx
08048016  56                push esi
08048017  68C8860408        push dword 0x80486c8
0804801C  E89FFFFFFF        call dword 0x8047fc0
08048021  F4                hlt
mitsurugi@mitsu:~/crackmes$

This looks like pretty usual stuff. The 0x080486c8 should be the address of the main function.

Before tracing main, let's find some offset of some strings to help:
at 0x86a : Please tell me my password:
at 0x888 : The password is correct! Congratulations!!!
at 0x8b5 : No! No! No! No! Try again.


Now we try to find those references in the main function, because we know that somewhere, it should asks for a password, so we should see those addresses pushed on the stack and the function printf or puts called.
mitsurugi@mitsu:~/crackmes$ ndisasm -au -o 0x080486c8 -e 0x6c8 crackme.01.32 | head -41
080486C8  55                push ebp
080486C9  89E5              mov ebp,esp
080486CB  83E4F0            and esp,byte -0x10
080486CE  83EC20            sub esp,byte +0x20
080486D1  A1A09B0408        mov eax,[0x8049ba0]
080486D6  8944240C          mov [esp+0xc],eax
080486DA  C74424081C000000  mov dword [esp+0x8],0x1c
080486E2  C744240401000000  mov dword [esp+0x4],0x1
080486EA  C704246A880408    mov dword [esp],0x804886a 

address of "Please tell me my password"
080486F1  E83AFDFFFF        call dword 0x8048430

It should be puts function
080486F6  A1809B0408        mov eax,[0x8049b80]
080486FB  89442408          mov [esp+0x8],eax
080486FF  C744240407000000  mov dword [esp+0x4],0x7
08048707  8D442418          lea eax,[esp+0x18]
0804870B  890424            mov [esp],eax
0804870E  E8FDFCFFFF        call dword 0x8048410
08048713  8D442418          lea eax,[esp+0x18]
08048717  890424            mov [esp],eax
0804871A  E823FFFFFF        call dword 0x8048642
0804871F  C7442404609B0408  mov dword [esp+0x4],0x8049b60
08048727  8D442418          lea eax,[esp+0x18]
0804872B  890424            mov [esp],eax
0804872E  E842FFFFFF        call dword 0x8048675
08048733  85C0              test eax,eax
08048735  7527              jnz 0x804875e

goes to loose func
08048737  A1A09B0408        mov eax,[0x8049ba0]
0804873C  8944240C          mov [esp+0xc],eax
08048740  C74424082C000000  mov dword [esp+0x8],0x2c
08048748  C744240401000000  mov dword [esp+0x4],0x1
08048750  C7042488880408    mov dword [esp],0x8048888

address of winning string
08048757  E8D4FCFFFF        call dword 0x8048430

puts function again
0804875C  EB25              jmp short 0x8048783
0804875E  A1A09B0408        mov eax,[0x8049ba0]
08048763  8944240C          mov [esp+0xc],eax
08048767  C74424081B000000  mov dword [esp+0x8],0x1b
0804876F  C744240401000000  mov dword [esp+0x4],0x1
08048777  C70424B5880408    mov dword [esp],0x80488b5

address of loose message
0804877E  E8ADFCFFFF        call dword 0x8048430

and puts function again
08048783  B800000000        mov eax,0x0
08048788  C9                leave
08048789  C3                ret
mitsurugi@mitsu:~/crackmes$


Things becomes clearer now. Everything important is here:
080486EA  C704246A880408    mov dword [esp],0x804886a | Please tell me my password
080486F1  E83AFDFFFF        call dword 0x8048430  | should be puts function
080486F6  A1809B0408        mov eax,[0x8049b80]
080486FB  89442408          mov [esp+0x8],eax
080486FF  C744240407000000  mov dword [esp+0x4],0x7
08048707  8D442418          lea eax,[esp+0x18]
0804870B  890424            mov [esp],eax
0804870E  E8FDFCFFFF        call dword 0x8048410

A function is called, it would be logical that gets is called to retrieve the password entered by the user.

08048713  8D442418          lea eax,[esp+0x18]
08048717  890424            mov [esp],eax
0804871A  E823FFFFFF        call dword 0x8048642

Now it's interesting, what this function does? Password validation? Password transformation? Other?

0804871F  C7442404609B0408  mov dword [esp+0x4],0x8049b60

Another point of interest. What we have at this address?

08048727  8D442418          lea eax,[esp+0x18]
0804872B  890424            mov [esp],eax
0804872E  E842FFFFFF        call dword 0x8048675

Another function? Validation of password?

08048733  85C0              test eax,eax

Makes sense: if validation function return something different than 0, go loose.
08048735  7527              jnz 0x804875e

Yep, it goes to loose function

So, we have to check addresses:
-0x08048410 : is it gets?
-0x08048642 : what happens there?
-0x08049b60 : what is there?
-0x08048675 : is it a validation function? How does it works?

Let's go:
mitsurugi@mitsu:~/crackmes$ ndisasm -au -o 0x08048410 -e 0x410 crackme.01.32 | head -3
08048410  FF25309B0408      jmp dword [dword 0x8049b30]
08048416  6800000000        push dword 0x0
0804841B  E9E0FFFFFF        jmp dword 0x8048400
mitsurugi@mitsu:~/crackmes$

Ok, typically a GOT entry. I assume it's puts function.

next one
mitsurugi@mitsu:~/crackmes$ ndisasm -au -o 0x08048642 -e 0x642 crackme.01.32 | head -19
08048642  55                push ebp
08048643  89E5              mov ebp,esp
08048645  83EC10            sub esp,byte +0x10
08048648  C745FC00000000    mov dword [ebp-0x4],0x0
0804864F  EB1C              jmp short 0x804866d
08048651  8B55FC            mov edx,[ebp-0x4]
08048654  8B4508            mov eax,[ebp+0x8]
08048657  01C2              add edx,eax
08048659  8B4DFC            mov ecx,[ebp-0x4]
0804865C  8B4508            mov eax,[ebp+0x8]
0804865F  01C8              add eax,ecx
08048661  0FB600            movzx eax,byte [eax]
08048664  83F06C            xor eax,byte +0x6c

Doing a simple XOR with value 0x6c
08048667  8802              mov [edx],al
08048669  8345FC01          add dword [ebp-0x4],byte +0x1
0804866D  837DFC05          cmp dword [ebp-0x4],byte +0x5

Looping 6 times.
08048671  7EDE              jng 0x8048651
08048673  C9                leave
08048674  C3                ret
mitsurugi@mitsu:~/crackmes$

So this function just XOR with 0x6c the 6 first chars of the input.

Next one is 0x08049b60, we must take care of the address. It's 0x08049b60, so it's in the second load section. and the second load section is loaded with an offset of 0x0849000. It means that in the file, the 0x08049b60 are located at 0xb60. Let's see with hexdump what we have:
00000b60  1b 04 15 02 5c 18 00 00  47 43 43 3a 20 28 47 4e  |....\...GCC: (GN|
00000b70  55 29 20 34 2e 38 2e 30  00 00 2e 73 68 73 74 72  |U) 4.8.0...shstr|
6 bytes with a null at the end.

Now it's time for a big guess: the 0x08048675 is a simple validation function like strcmp.

If I resume how the binary works:
1/ It asks for a password
2/ It XORs the 6 first chars of the password with 0x6c
3/ It checks if the results is 0x1b 0x04 0x15 0X02 0x5c 0x18
(If it doesn't work, it means that we have to go deeper and disassemble 0x08048675 to learn what's going on there.)

We can easily calculate:
mitsurugi@mitsu:~/crackmes$ cat sol.py
#! /usr/bin/python
v=''
for p in '\x1b\x04\x15\x02\x5c\x18':
    v += chr((ord(p)^0x6c))
print v
mitsurugi@mitsu:~/crackmes$ ./sol.py
whyn0t
mitsurugi@mitsu:~/crackmes$


This results let think that all assumptions were rights, we can easily verify:
mitsurugi@mitsu:~/crackmes$ ./crackme.01.32
Please tell me my password: whyn0t
The password is correct!
Congratulations!!!
mitsurugi@mitsu:~/crackmes$