11 KiB
title | date | author | tags | categories | |||||||
---|---|---|---|---|---|---|---|---|---|---|---|
Sécurité logicielle : TD8 Utiliser GDB | 2023-04-07 |
|
|
|
Partie 1
Commençons par récupérer l'ensemble des éléments de x
à savoir
- son contenu (une adresse)
- le contenu pointé cette adresse
- son adresse
(gdb) p x
$1 = (int *) 0xffffd690
(gdb) p *x
$2 = 2
(gdb) p &x
$3 = (int **) 0xffffd680
Regardons maintenant le contenu de la pile et retrouvons les différents élements:
0xffffd6b8 0xf7ffcff4
0xffffd6b4 0x00000070
0xffffd6b0 0x00000000
0xffffd6ac 0xf7c23295
0xffffd6a8 0x00000000
0xffffd6a4 0xffffd6c0
0xffffd6a0 0x00000001
0xffffd69c 0x00000001
0xffffd698 0x08049198
0xffffd694 0xffffd6a8
0xffffd690 0x00000002 <- La valeur de x (son adresse sur la pile)
0xffffd68c ... 0xf7fc14a0
0xffffd688 arg3 0xf7c1ca2f
0xffffd684 arg2 0xf7fd98cb
0xffffd680 arg1 0xffffd690 <- L'adresse de x (&x)
0xffffd67c ret@ 0x0804917b
0xffffd678 bp sp 0xffffd694
La backtrace de f()
:
(gdb) bt
#0 f (x=0xffffd690) at stack.c:4
#1 0x0804917b in g (y=1) at stack.c:9
#2 0x08049198 in main () at stack.c:13
Adresse de retour dans pframe:
...
0xffffd680 arg1 0xffffd690
0xffffd67c ret@ 0x0804917b <- adresse de retour de f() (vers g())
0xffffd678 bp sp 0xffffd694
Après un up
, voici les adresses demandées:
Breakpoint 1, f (x=0xffffd690) at stack.c:4
4 return *x+1;
(gdb) up
#1 0x0804917b in g (y=1) at stack.c:9
9 return f(&z);
(gdb) p y
$1 = 1
(gdb) p&y
$2 = (int *) 0xffffd69c
(gdb) p z
$3 = 2
(gdb) p &z
$4 = (int *) 0xffffd690
(gdb)
Partie 2
Lançons gdb
sur optim.02 et posons notre point d'arrêt sur f()
Breakpoint 1, f (x=2) at optim.c:4
4 return x+1;
(gdb) bt
#0 f (x=2) at optim.c:4
#1 0x080491c1 in g (y=1) at optim.c:9
#2 0x08049068 in main () at optim.c:15
(gdb) up
#1 0x080491c1 in g (y=1) at optim.c:9
9 int a = f(z);
(gdb) p z
$1 = 2
(gdb) p &z
Can't take address of "z" which isn't an lvalue.
(gdb)
Effectivement gdb
ne peut nous afficher la valeur.
Après avoir désassemblé la fonction g()
, nous pouvons effectivement voir que
z
n'existe pas en tant que tel: il est directement mis sur la pile.
0x080491b4 <+4>: mov 0x10(%esp),%eax
0x080491b8 <+8>: add $0x1,%eax <- z
0x080491bb <+11>: push %eax
Lorsqu'on essaye d'afficher a
, gdb nous affiche que cette variable a été
optimisée
(gdb) p a
$2 = <optimized out>
En observant le code assembleur, ici encore la variable a
n'existe que sur la
pile dans %eax
. Le programme passe ensuite %eax
dans %ebx
pour le passer à
la fonction printf
...
0x080491c5 <+21>: mov %eax,%ebx
0x080491c7 <+23>: push $0x804a008
0x080491cc <+28>: call 0x8049040 <printf@plt>
Dans la version optimisée du programme, le compilateur a donc réduit au maximum la création de variables.
La backtrace complète, a
est aussi <optimized out>
:
(gdb) bt full
#0 f (x=2) at optim.c:4
No locals.
#1 0x080491c1 in g (y=1) at optim.c:9
z = 2
a = <optimized out>
#2 0x08049068 in main () at optim.c:15
Partie 3
Nous lançons puis plaçons un point d'arrêt sur main()
:
(gdb) b main
Breakpoint 1 at 0x804918a: file modif.c, line 12.
(gdb) r
Breakpoint 1, main () at modif.c:12
12 int b = 1;
(gdb) wa a
Watchpoint 2: a
Après avoir continué l'exécution de notre programme, gdb
s'arrête lors de la
modification de a
:
[...]
(gdb) c
Continuing.
Watchpoint 2: a
Old value = -134474120
New value = 2
main () at modif.c:14
14 int c = 3;
Nous pouvons maintenant afficher %eip
et désassembler:
(gdb) p $eip
$2 = (void (*)()) 0x8049198 <main+31>
(gdb) disassemble
Dump of assembler code for function main:
0x08049179 <+0>: lea 0x4(%esp),%ecx
0x0804917d <+4>: and $0xfffffff0,%esp
0x08049180 <+7>: push -0x4(%ecx)
0x08049183 <+10>: push %ebp
0x08049184 <+11>: mov %esp,%ebp
0x08049186 <+13>: push %ecx
0x08049187 <+14>: sub $0x14,%esp
0x0804918a <+17>: movl $0x1,-0xc(%ebp)
0x08049191 <+24>: movl $0x2,-0x14(%ebp) <- initialisation de a
=> 0x08049198 <+31>: movl $0x3,-0x10(%ebp) <- %eip
...
On peut demander à gdb de calculer l'adresse de %ebp -14
et de la comparer
avec l'adresse de a
:
(gdb) p $ebp - 0x14
$8 = (void *) 0xffffd694
(gdb) p &a
$9 = (int *) 0xffffd694
Demandons à gdb
de nous afficher le contenu de cette case mémoire:
#0 main () at modif.c:14
(gdb) p/x *(int*)($ebp - 0x14)
$8 = 0x2
Continuons maintenant l'exécution
(gdb) c
Continuing.
Watchpoint 2: a
Old value = 2
New value = 3
f (x=0xffffd694) at modif.c:5
On peu observer l'incrémentation de a
dans la fonction f()
:
(gdb) disassemble
Dump of assembler code for function f:
0x08049156 <+0>: push %ebp
0x08049157 <+1>: mov %esp,%ebp
0x08049159 <+3>: mov 0x8(%ebp),%eax
0x0804915c <+6>: mov (%eax),%eax
0x0804915e <+8>: lea 0x1(%eax),%edx <-incrémentation de a
0x08049161 <+11>: mov 0x8(%ebp),%eax
0x08049164 <+14>: mov %edx,(%eax) <- a est placé dans %eax
=> 0x08049166 <+16>: nop
0x08049167 <+17>: pop %ebp
0x08049168 <+18>: ret
End of assembler dump.
La variable a
est incrémentée en utilisant lea
, le résultat de cette
opération est placé dans %ebx
avant d'être positionnée dans %eax
. C'est ce
qui cause l'arrêt de gdb.
Après avoir relancé le programme, nous obtenons l'adresse de a
:
Breakpoint 1, main () at modif.c:12
12 int b = 1;
(gdb) p &a
$1 = (int *) 0xffffd694
Effectivement en positionnant un watch
sur l'adresse de a
, on obtient le
même comportement:
(gdb) wa *(int *)0xffffd694
Hardware watchpoint 2: *(int *)0xffffd694
(gdb) c
Continuing.
Hardware watchpoint 2: *(int *)0xffffd694
Old value = -134474120
New value = 2
main () at modif.c:14
14 int c = 3;
Partie 4
Exécution du programme modif2
:
./modif2
0xffe7d18c:1234567890 0xffe7d188:1234567680 0xffe7d17c
On voit bien que le second entier est différent du premier. Exécutons le
programme avec gdb
en posant un watchpoint sur les deux entiers. Pour ce
faire nous allons poser un breackpoint sur main()
, exécuter pas à pas pour
repérer l'initialisation des deux entiers et poser nos watchpoint dessus:
(gdb) b main
Breakpoint 1 at 0x80491ad: file modif2.c, line 13.
(gdb) r
[...]
13 int b = 1234567890;
(gdb) n
14 int c = 1234567890;
(gdb) wa b
Hardware watchpoint 2: b
(gdb) wa c
Hardware watchpoint 3: c
Nous voyons biens que ces entiers ont été initialisée avec 1234567890.
Continuons l'exécution du programme, nous pouvons alors voir que la variable c
est modifiée dans memset-sse2.S
:
[...]
Old value = 1234567890
New value = 1234567680
__memset_sse2_rep () at ../sysdeps/i386/i686/multiarch/memset-sse2-rep.S:173
En désassemblant,, nous pouvons effectivement voir que nous sommes au beau
milieu de la fonction memset
:
(gdb) disass
Dump of assembler code for function __memset_sse2_rep:
[...]
0xf7d779fc <+108>: mov %eax,-0xd(%edx)
0xf7d779ff <+111>: mov %eax,-0x9(%edx)
0xf7d77a02 <+114>: mov %eax,-0x5(%edx)
0xf7d77a05 <+117>: mov %al,-0x1(%edx)
=> 0xf7d77a08 <+120>: mov 0x8(%esp),%eax
0xf7d77a0c <+124>: pop %ebx
0xf7d77a0d <+125>: ret
[...]
End of assembler dump.
Maintenant remontons dans la fonction appelante:
(gdb) up
#1 0x0804917d in f (x=0xffffd67c "", n=13) at modif2.c:5
5 <c code that we don't see>
(gdb) disassemble
Dump of assembler code for function f:
0x08049166 <+0>: push %ebp
0x08049167 <+1>: mov %esp,%ebp
0x08049169 <+3>: sub $0x8,%esp
0x0804916c <+6>: mov 0xc(%ebp),%eax
0x0804916f <+9>: sub $0x4,%esp
0x08049172 <+12>: push %eax
0x08049173 <+13>: push $0x0
0x08049175 <+15>: push 0x8(%ebp)
0x08049178 <+18>: call 0x8049050 <memset@plt>
=> 0x0804917d <+23>: add $0x10,%esp
0x08049180 <+26>: nop
0x08049181 <+27>: leave
0x08049182 <+28>: ret
End of assembler dump.
Nous allons maintenant relancer notre exécution en posant un point d'arrêt sur
l'adresse 0x08049178
.
Lançons maintenant notre exécution:
(gdb) b * 0x08049178
Breakpoint 1 at 0x8049178: file modif2.c, line 5.
(gdb) r
[...]
Breakpoint 1, 0x08049178 in f (x=0xffffd67c "Hello, you!", n=13) at modif2.c:5
(gdb) p &x[0]
$3 = 0xffffd67c "Hello, you!"
(gdb) up
(gdb) up
(gdb) p &c
$5 = (int *) 0xffffd688
(gdb) p &b
$6 = (int *) 0xffffd68c
Maintenant que nous avons les adresses, intéressons nous à la fonction memset
.
Sa signature est memset(void s, int c, size_t n)
, elle remplie n
élément de
la zone mémoire s
avec c
.
D'après gdb
, voici l'appel de cette fonction dans f()
:
memset(x, '\0', n);
où x
est un pointeur vers a
et n
est égal à 13.
Adresse | Variable | Memset |
---|---|---|
... | ... | ... |
0xffffd68c | b | |
0xffffd68b | c | |
0xffffd68a | c | |
0xffffd689 | c | |
0xffffd688 | c | memset[12] |
0xffffd687 | a[11] | memset[11] |
0xffffd686 | a[10] | memset[10] |
... | ... | ... |
0xffffd67e | a[2] | memset[2] |
0xffffd67d | a[1] | memset[1] |
0xffffd67c | a[0] | memset[0] |
Comme nous pouvons le voir sur le tableau, le memset
écrase le bit de poids
faible de notre variable c
. Nous avons la cause de sa modification!
Le calcul est simple : adresse de a
+ 12 bits = 0xfffd67c + 0xc = 0xfffd688
,
notre memset
empiète bien sur c
.
Le bug est simple le développeur a ajouter un à sizeof(a)
surement pour
prendre en compte le caractère /0
, or cette opération est effectuée dès
l'initialisation de notre constante :
(gdb) frame 2
#2 0x080491de in main () at modif2.c:16
16 g(a, sizeof(a) + 1);
Partie 5
Effectivement, lors de l'exécution du programme avec Valgring, celui-ci reporte une erreur:
[...]
==4666== Invalid write of size 1
==4666== at 0x4049FF0: memset (in /usr/libexec/valgrind/vgpreload_memcheck-x86-linux.so)
==4666== by 0x804919C: f (modif3.c:6)
==4666== by 0x80491B6: g (modif3.c:10)
==4666== by 0x80491ED: main (modif3.c:15)
==4666== Address 0x428a02c is 0 bytes after a block of size 4 alloc'd
==4666== at 0x4040660: malloc (in /usr/libexec/valgrind/vgpreload_memcheck-x86-linux.so)
==4666== by 0x4101315: strdup (strdup.c:42)
==4666== by 0x80491D9: main (modif3.c:14)
[...]
Après avoir lancé Valgrind avec le paramètre --vgdb-error=1
et lancé gdb
avec target remote | /usr/bin/vgdb --pid=<pid>
, nous pouvons observer la même
erreur que pour la partie précédente:
(gdb) bt
#0 0x04049ff0 in _vgr20210ZZ_libcZdsoZa_memset () from /usr/libexec/valgrind/vgpreload_memcheck-x86-linux.so
#1 0x0804919d in f (x=0x428a028 "", n=5) at modif3.c:6
#2 0x080491b7 in g (y=0x428a028 "", n=5) at modif3.c:10
#3 0x080491ee in main () at modif3.c:15
(gdb) frame 3
#3 0x080491ee in main () at modif3.c:15
15 g(a, sizeof(a)+1);
Un a été ajouté à sizeof(a)
alors qu'il ne faut pas.