GCC, Trampoline, et code auto-modifiable

Aux temps héroïques, le code auto-modifiable était fréquent. Ça consiste à écrire du code qui se modifie lui-même pendant l’exécution, pour faire des micro-optimisations qui tuent (en fait, surtout gagner une indirection par ci par là, mais à l’époque c’était pas négligeable).

Mais en fait, ça n’a pas complètement disparu : GCC, dans un cas précis, en génère d’ailleurs !

Fonctions imbriquées en C

Une fonction imbriquée en C est une fonction définie à l’intérieur d’une autre fonction. A quoi est-ce que ça sert ? Parfois, pour rendre votre code plus clair, vous définissez des fonctions qui ne sont utiles que dans une autre fonction. Il est donc débile de la rendre accessible depuis toutes les autres fonctions ! GNU CC vous autorise à écrire le code suivant (c’est une extension de GCC, ce n’est pas dans la norme C99) :

 int f1() { 	int f2() 	{ 		[...] 	} 	[...] 	f2(); 	[...] } 

C’est bien joli, mais ça pose un problème. Regardez cet exemple (qui compile, n’hésitez pas à essayer) :

 void f0(void (*f)()); long f1 (void) { 	long i = 0; 	void f2(void) 	{ 		i++; 	} 	f0(f2); 	return i; } void f0(void (*f)()) { 	(*f)(); } int main() { 	return f1(); } 

C’est tout à fait légal : f1 passe un pointeur vers f2 à f0, qui se charge d’appeler la fonction en paramètre. Le problème est que f2 modifie i. Or i est une variable locale de f1 (allouée sur la pile par f1). La distance entre ESP dans f2 et les variables locales de f1 sur la pile n’est pas connue : ici, f0 appelle directement f2, mais il pourrait appeler blop(f2) qui lui même appelerait f2. Il faut trouver un moyen de passer à f2 l’adresse du contexte de sa fonction parente.

Trampolines

Un papier (PDF) évoque ce problème pour le C++ (où les closures sont encore plus pratiques qu’en C, notamment lorsqu’on utilise des itérateurs). Il propose une solution élégante utilisant un trampoline. Dans l’article, il démontre la solution avec de l’assembleur 68000. Voici le code ci-dessus compilé pour x86, sans optimisation histoire de garder le code le plus proche possible du C.

 	.file	"t5.c" 	.text 	.type	f2.0, @function f2.0: 	pushl	%ebp 	movl	%esp, %ebp 	subl	$4, %esp 	movl	%ecx, -4(%ebp) 	movl	-4(%ebp), %ecx 	incl	-4(%ecx) 	leave 	ret 	.size	f2.0, .-f2.0 .globl f1 	.type	f1, @function f1: 	pushl	%ebp 	movl	%esp, %ebp 	subl	$40, %esp 	leal	-24(%ebp), %eax 	addl	$0, %eax 	andl	$-1, %eax 	movl	$f2.0, %ecx 	leal	10(%eax), %edx 	subl	%edx, %ecx 	movl	%ecx, %edx 	movb	$-71, (%eax) 	leal	-8(%ebp), %ecx 	movl	%ecx, 1(%eax) 	movb	$-23, 5(%eax) 	movl	%edx, 6(%eax) 	movl	$0, -12(%ebp) 	leal	-24(%ebp), %eax 	addl	$0, %eax 	andl	$-1, %eax 	movl	%eax, (%esp) 	call	f0 	movl	-12(%ebp), %eax 	leave 	ret 	.size	f1, .-f1 .globl f0 	.type	f0, @function f0: 	pushl	%ebp 	movl	%esp, %ebp 	subl	$8, %esp 	movl	8(%ebp), %eax 	call	*%eax 	leave 	ret 	.size	f0, .-f0 .globl main 	.type	main, @function main: 	pushl	%ebp 	movl	%esp, %ebp 	subl	$8, %esp 	andl	$-16, %esp 	movl	$0, %eax 	subl	%eax, %esp 	call	f1 	leave 	ret 	.size	main, .-main 	.section	.note.GNU-stack,"x",@progbits 	.ident	"GCC: (GNU) 3.3.5 (Debian 1:3.3.5-8)" 

Un trampoline, c’est un petit bout de code qui va mettre dans registre (ici ECX, je ne sais pas quelle est la convention exactement) l’adresse de l’environnement de la fonction parente. Dans le code de f2, on voit qu’on incrémente -4(%ecx) pour incrémenter i. Ce petit bout de code est généré sur la pile par f1 avant d’appeler f0. Le code qui génère le code est entre “subl $40, %esp” et “call f0”, en gros. Et mon débogueur me dit que le code généré ressemble à :

 Dump of assembler code from 0xbffff930 to 0xbffffa30:     0xbffff930:     mov    $0xbffff940,%ecx     0xbffff935:     jmp    0x8048354  

Quand f0 fera son call, il appellera une adresse de la pile sans le savoir. Et quand f2 retournera, elle retournera directement dans f0.

C’est trop beau, il doit bien y avoir un problème qqpart …

Hé oui. La pile exécutable, c’est un problème : c’est la base de l’exploitation de beaucoup de trous de sécurité. Du coup, des systèmes essayent de rendre la pile non exécutable. C’est le cas de OpenBSD avec W^X, ou de Linux avec Exec-shield (PDF). Comment faire pour continuer à autoriser les trampolines, alors ? Et bien c’est tout simple. Sous linux, l’en-tête ELF des binaires contient un flag indiquant si ce programme a besoin que la pile soit exécutable. Avec l’exécutable du code de tout à l’heure :

 ***lucas@blop:~% readelf -l t5 | grep STACK   STACK          0x000000 0x00000000 0x00000000 0x00000 0x00000 RWE 0x4 

Tandis qu’avec un autre exécutable (notez le RW au lieu du RWE) :

 ***lucas@blop:~% readelf -l t3 | grep STACK   STACK          0x000000 0x00000000 0x00000000 0x00000 0x00000 RW  0x4 

C’est un flag ajouté par GCC.

Bravo si vous avez lu jusque là ;)