Exploitation via DTORS et facteur de mitigation dans gcc 4.4
La réécriture de la table DTORS est une technique connue pour exploiter un programme compilé avec GCC. En gros, DTORS permet d'appeler des fonctions à la fin du programme. Par exemple en ajoutant le code suivant, la fonction stop() sera appelée après l'exécution de la fonction main :
void stop() __attribute__ ((destructor));
void stop() {
printf("THE END\n");
}
La liste des fonctions à appeler après l'exécution du programme est placée dans la section DTORS, qui est accessible en écriture et qui peut donc être réécrite. Pour plus de détails sur cette technique, consulter cet article.
En tentant de mettre cette technique en œuvre, j'ai découvert que cela ne fonctionnait pas lorsqu'aucune fonction n'avait été déclarée comme destructor dans le code (contrairement à ce qui est dit dans la première édition de The Art of Exploitation et dans l'article cité précédemment). Une vérification du code assembleur nous explique pourquoi.
La commande nm nous permet de connaitre l'emplacement de la section DTORS :
$ nm no_dtors
080496a8 D __DTOR_END__
080496a4 d __DTOR_LIST__
Maintenant, examinons le contenu de la fonction __do_global_dtors_aux qui s'occupe d'appeler les destructeurs obtenu grâce à la commande objdump -d no_dtors.
08048440 <__do_global_dtors_aux>:
8048440: 55 push %ebp
8048441: 89 e5 mov %esp,%ebp
8048443: 53 push %ebx
8048444: 83 ec 04 sub $0x4,%esp
8048447: 80 3d bc 97 04 08 00 cmpb $0x0,0x80497bc
804844e: 75 3f jne 804848f <__do_global_dtors_aux+0x4f>
8048450: a1 c0 97 04 08 mov 0x80497c0,%eax
8048455: bb a8 96 04 08 mov $0x80496a8,%ebx
804845a: 81 eb a4 96 04 08 sub $0x80496a4,%ebx
8048460: c1 fb 02 sar $0x2,%ebx
8048463: 83 eb 01 sub $0x1,%ebx
8048466: 39 d8 cmp %ebx,%eax
8048468: 73 1e jae 8048488 <__do_global_dtors_aux+0x48>
804846a: 8d b6 00 00 00 00 lea 0x0(%esi),%esi
8048470: 83 c0 01 add $0x1,%eax
8048473: a3 c0 97 04 08 mov %eax,0x80497c0
8048478: ff 14 85 a4 96 04 08 call *0x80496a4(,%eax,4)
804847f: a1 c0 97 04 08 mov 0x80497c0,%eax
8048484: 39 d8 cmp %ebx,%eax
8048486: 72 e8 jb 8048470 <__do_global_dtors_aux+0x30>
8048488: c6 05 bc 97 04 08 01 movb $0x1,0x80497bc
804848f: 83 c4 04 add $0x4,%esp
8048492: 5b pop %ebx
8048493: 5d pop %ebp
8048494: c3 ret
8048495: 8d 74 26 00 lea 0x0(%esi,%eiz,1),%esi
8048499: 8d bc 27 00 00 00 00 lea 0x0(%edi,%eiz,1),%edi
Le code est assez long, j'ai mis la section intéressante en bleu. On remarque que les adresses de début et de fin de DTORS sont manipulées directement. En fait, l'adresse de début est soustraite à l'adresse de fin pour déterminer la longueur de la section. Les opérations suivantes (sar et sub) servent en fait à déterminer si la longueur de la section est inférieure ou égale à 4 octets. Si tel est le cas, l'exécution saute à 0x08048488 ce qui fait que le contenu de la section n'est pas inspectée et il n'y a pas d'appel fonctions (via le call en rouge).
Le programme utilisé comme exemple n'avait pas de fonction destructor est n'est donc pas exploitable. Si nous ajoutons une fonction destructor au code, nous obtenons le résultat suivant :
8048455: bb ac 96 04 08 mov $0x80496ac,%ebx
804845a: 81 eb a4 96 04 08 sub $0x80496a4,%ebx
Dans ce cas, la longueur de la section est 8 octets ce qui fait que les adresses qui s'y trouve sont appelées.
J'ai l'impression que le comportement généré par GCC a été changée dans les dernières versions pour limiter les possibilités d'exploitation, la plupart des programmes ne faisant pas appel à des fonctions destructors. Je n'ai par contre pas vérifié l'historique du code source de GCC pour en être sûr.