cours/content/secu_logicielle/td7-analyse_statique_dynamique/index.md

513 lines
17 KiB
Markdown

---
title: "Sécurité logicielle : TD7 analyse statique / dynamique"
date: 2023-03-10
tags: ["Valgrind", "gcc"]
author:
- Yorick Barbanneau
- Gwendal Aupee
categories: ["Sécurité logicielle", "TD"]
---
## Partie 1
### getpw
Le message d'avertissement de `gcc` est:
```
`getpw' function is dangerous and should not be used.
```
D'après le manuel de `getpw`, il n'y a pas de paramètre de taille, cette
fonction est donc sensible aux attaques par dépassement de tampon (*buffer
overflow*).
Nous avons diminué la taille de `buf` à 12 et observons une erreur de
segmentation lors de la fin du programme. Avec analyse de la pile par `pframe`
nous pouvons observer que le retour de `getpw()` écrase des élément au dessus de
lui-même sur l'adresse de retour.
Il pourrait être possible d'intégrer un *shellcode* dans le champ *gecos* de
notre utilisateur afin d'exploiter ce **dépassement de tampon**.
## mktemp
Cette fonction ne se charge pas de créer le fichier, elle défini juste un nom.
Il est alors possible pour un attaquant de créer le fichier avant celui du
programme en ratissant le plus large possible. Nous avions vu ce cas de figure
lors du premier TD sur les défis *Nebula*.
## Partie 2
### Question 1
Effectivement `cppcheck` retourne une erreur dans le code source entrainant une
fuite de mémoire :
```
cppcheck test-leak.c
Checking test-leak.c ...
test-leak.c:9:2: error: Memory leak: d [memleak]
return c;
^
```
Dasn le code source, les espaces mémoire doivent être désalloués :
* dans la fonction `f()` pour `d`
* dans la fonction `g()` pour `c` (référencé sous `e` dans cette dernière)
En terme de sécurité, une fuite mémoire permet à une attaque par **déni de
service**.
### Question 2
Ni `cppcheck` ni la version `asan` de l'exécutable ne remontent d'erreur. Seul
`valgrind` nous averti d'un espace mémoire accessible à la terminaison du
programme.
Valgrind nous donne des information sur la fuite de mémoire:
```
valgrind --leak-check=full --show-leak-kinds=all ./test-reachable-leak
==14383== Memcheck, a memory error detector
==14383== Copyright (C) 2002-2022, and GNU GPL'd, by Julian Seward et al.
==14383== Using Valgrind-3.19.0 and LibVEX; rerun with -h for copyright info
==14383== Command: ./test-reachable-leak
==14383==
==14383==
==14383== HEAP SUMMARY:
==14383== in use at exit: 10 bytes in 1 blocks
==14383== total heap usage: 1 allocs, 0 frees, 10 bytes allocated
==14383==
==14383== 10 bytes in 1 blocks are still reachable in loss record 1 of 1
==14383== at 0x48407B4: malloc (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==14383== by 0x109146: main (test-reachable-leak.c:6)
==14383==
==14383== LEAK SUMMARY:
==14383== definitely lost: 0 bytes in 0 blocks
==14383== indirectly lost: 0 bytes in 0 blocks
==14383== possibly lost: 0 bytes in 0 blocks
==14383== still reachable: 10 bytes in 1 blocks
==14383== suppressed: 0 bytes in 0 blocks
==14383==
==14383== For lists of detected and suppressed errors, rerun with: -s
==14383== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
```
En observant le code de notre programme nous pouvons voir que `*c` est un
**pointeur global** (dans le sens variable globale). Dans le `main()`, il prend
l'adresse d'une zone de mémoire créée avec `malloc`. **Cette zone mémoire n'est
pas libérée**, ainsi elle reste accessible à la fin du programme.
## Partie 3
Effectivement, `cppcheck` nous averti à propos d'une zone mémoire créée mais non
initialisées. La version `asan` ne fait aucun rapport mais *Valgrind* est lui un
peu plus locace :
```
[...]
Conditional jump or move depends on uninitialised value(s)
at 0x10917A: f (test-uninitialized.c:5)
by 0x1091D0: main (test-uninitialized.c:13)
[...]
Use --track-origins=yes to see where uninitialised values come from
[...]
```
Lors du check de `test-uninitialized-printf.c` *Valgrind* nous averti sur
l'usage de valeur non-initialisée dont dépendent des *jump* ou *move*
conditionnels. Cette erreur provient de la variable `c` créée par un `malloc`
mais non initialisée.
Avec l'option `--track-origins=yes` suggérée par sa première exécution,
*Valgrind* nous informe de l'origine de l'allocation:
```
[...]
==14921== Conditional jump or move depends on uninitialised value(s)
==14921== at 0x48BBEC7: __vfprintf_internal (vfprintf-process-arg.c:58)
==14921== by 0x48B14FA: printf (printf.c:33)
==14921== by 0x10917F: f (test-uninitialized-printf.c:5)
==14921== by 0x1091A9: main (test-uninitialized-printf.c:10)
==14921== Uninitialised value was created by a heap allocation
==14921== at 0x48407B4: malloc (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==14921== by 0x109194: main (test-uninitialized-printf.c:9)
[...]
```
Valgrind n'averti de ce problème que lors de la première utilisation de la
variable, ici dans `printf()`. C'est un comportement normal : c'est lors de
**l'accès en lecture de cette varible** que Valgrind peut déterminer cet accès
se fait alors que la variable n'est pas initialisée.
## Partie 4
Lors du lancement du programme `test-undefined`, nous pouvons constater que la
valeur afficher de notre variable `i` n'est pas celle attendue:
```bash
./test-undefined
-2147483648
```
Comme nous l'avons vu en cours, nous sommes en présence d'un cas non définis par
la norme C.
*Valgrind* lui de fait part d'aucun problème, il ne se soucis pas du type de la
variable `i` mais de l'espace qu'elle occupe en mémoire. Et il **n'a pas
changé**.
``` shell
valgrind ./test-undefined
==3381== Memcheck, a memory error detector
==3381== Copyright (C) 2002-2022, and GNU GPL'd, by Julian Seward et al.
==3381== Using Valgrind-3.19.0 and LibVEX; rerun with -h for copyright info
==3381== Command: ./test-undefined
==3381==
-2147483648
==3381==
==3381== HEAP SUMMARY:
==3381== in use at exit: 0 bytes in 0 blocks
==3381== total heap usage: 1 allocs, 1 frees, 1,024 bytes allocated
==3381==
==3381== All heap blocks were freed -- no leaks are possible
==3381==
==3381== For lists of detected and suppressed errors, rerun with: -s
==3381== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
```
La version *usan* indique effectivement un overflow de notre variable `i` :
```
./test-undefined.usan
test-undefined.c:7:3: runtime error: signed integer overflow: 2147483647 + 1 cannot be represented in type 'int'
-2147483648
```
L'option de compilation `-fsanitize=undefined` utilisée pour cette version de
l'exécutable ajoute des éléments pour vérifier les **comportements indéfinis**.
## Partie 5
### Question 1
`cppcheck` nous indique bien un *overrun* :
```bash
cppcheck test-overrun.c
Checking test-overrun.c ...
Array 'c[10]' accessed at index 10, which is out of bonds.[arrayIndexOutOfBounds]
c[10] = 1;
^
```
Le fichier en `.asan` nous indique un : *heap-buffer-overflow* avec plus de
détails (registres pc bp et sp, adresses, etc) :
```
/test-overrun.asan
=================================================================
==6036==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x60200000001a
at pc 0x55f0c2aa51cb bp 0x7ffc2aa141d0 sp 0x7ffc2aa141c8
WRITE of size 1 at 0x60200000001a thread T0
[...]
Shadow bytes around the buggy address:
0x0c047fff7fb0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c047fff7fc0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c047fff7fd0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c047fff7fe0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0c047fff7ff0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x0c047fff8000: fa fa 00[02]fa fa fa fa fa fa fa fa fa fa fa fa
0x0c047fff8010: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c047fff8020: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
[...]
```
L'élément du tableau `c[10]` est incorrect car `malloc(10)` correspond à un
tableau de 10 éléments avec des indices de **0 à 9** donc `c[10]` est
*out-of-bond*.
LA version `usan` ne reporte aucune erreur, tout comme *Valgrind*.
### Question 2
Pour cette version, *Valgrind* ne reporte aucune erreur. Il ne se rend pas
compte que l'accès en dehors de la variable dans le segment `data`. La version
`usan` de notre exécutable elle affiche des erreurs:
```
./test-overrun-data.usan
test-overrun-data.c:7:3: runtime error: index 10 out of bounds for type 'char [10]'
test-overrun-data.c:7:8: runtime error: store to address 0x55e89a7ca102
with insufficient space for an object of type 'char'
0x55e89a7ca102: note: pointer points here
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
^
test-overrun-data.c:8:3: runtime error: index -1 out of bounds for type 'char [10]'
```
`cppcheck` indique lui aussi deux erreurs pour `c[10]` et `c[-1]` :
```
cppcheck test-overrun-data.c
Checking test-overrun-data.c ...
test-overrun-data.c:7:3: error: Array 'c[10]' accessed at index 10, which is out of bounds. [arrayIndexOutOfBounds]
c[10] = 1;
^
test-overrun-data.c:8:3: error: Array 'c[10]' accessed at index -1, which is out of bounds. [negativeIndex]
c[-1] = 1;
^
```
La version `asan` ne reporte non plus aucune erreur avec la version de *GCC*
installée au *CREMI*. Cependant sur nos machines -- avec *Debian Unstable* comme
système -- la version `asan` reporte une erreur.
Dans la version installée au CREMI, l'*address sanitizer* inclus dans *GCC*,
l'option `-fno-common` n'est pas activée par défaut. Sans cette option il n'est
pas possible pour lui de détecter le problème : il ne sais définir les limites
des données globales non initialisés (stockées dans le segment *BSS*)
le module *Memcheck* de Valgrind ne détecte pas les erreurs de type
*out-of-bound* sur la pile, c'est une limitation documentée par l'équipe de
dévellopement :
> Unfortunately, Memcheck doesn't do bounds checking on global or stack arrays.
> We'd like to, but it's just not possible to do in a reasonable way that fits
> with how Memcheck works. Sorry.
[source](https://valgrind.org/docs/manual/faq.html#faq.overruns)
### Question 3
Dans la version `asan`, l'option de compilation `-fsanitize=address` active par
défaut l'option `asan-stack` chargée de vérifier les erreurs de type
`out-of-bounds` et `use-after-free`.
Lorsque nous exécutons `test-overrun-stack` avec *GDB*, après l'instruction
`c[-1] = 1` nous obtenons la pile suivante:
```
[...]
0x7fffffffe430 0x00007fffffffe520
0x7fffffffe428 ret@ 0x00007ffff7dfb18a
0x7fffffffe420 bp sp 0x0000000000000001 <- overflow de c[10]
0x7fffffffe418 0x00007ffff7ffdad0
0x7fffffffe410 0x0000010000000000 <- overlow de c[-1]
[...]
```
Avec l'aide le la commande `print`, nous pouvons alors vous les adresses de `c`
```
(gdb) p &c[0]
$4 = 0x7fffffffe416 ""
(gdb) p &c[9]
$5 = 0x7fffffffe41f ""
```
Ici `c[10]` passe dans le segment de pile au dessus, déclenchant l'erreur dans
notre exécutable `asan`.
## Partie 8
### Question 1
Le bug provient d'un problème de priorité : l'égalité est prioritaire sur tout
le reste dans la condition:
```c
// extract bit 0 to check for parity of the number of arguments
if (argc & 1 == 0) {
// ...
```
Elle sera toujours fausse. La correction est simple:
```c
// extract bit 0 to check for parity of the number of arguments
if ((argc & 1) == 0) {
// ...
```
### Question 2
C'est à la ligne 718 qu'il faut chercher l'erreur:
```c
while ((ret = xfs_bulkstat(fsfd,
&lastino, GRABSZ, &buf[0], &buflenout) == 0)) {
```
La parenthèse fermante définissant la priorité de l'affectation est mal placée,
la comparaison se transforme donc en affectation. Utiliser cette notation permet
d'affecter `ret` pendant la condition de la boucle, mais en contrepartie
**augmente le risque d'erreur**.
### Question 3
L'instruction `assert` est mal utilisée, le simple signe égal indique une
affectation et non une comparaison
```cpp
assert(n = int(*(p+1)), "just checkin'...");
```
La vérification sera toujours vrai, quoi qu'il arrive. Aucune erreur sera
remontée et le programme continuera son exécution alors **qu'il ne devrait
pas**.
`assert()` n'est utilisé que pour du *debug* dans le cas où la macro `NDEBUG`
n'est pas définie au moment de la compilation. La portée de cette erreur était
donc limitée.
### Question 4
L'outil d'analyse statique a détecté l'erreur car la méthode `screen->empty()`
retourne un booléen, or ici aucune valeur de retour est utilisée.
### Question 5
```c
for (i = 0; g_objArray[i] && i < MAX_WCOMP; i++) {
MultiMoveRelXY(g_objArray[i], x - center, deltay);
```
L'erreur d'accès est causée par `g_objArray[i]`, en effet le programme teste la
validité de la case mémoire avant la validité de `i`. Le programme accède à la
case mémoire avant de vérifier les limites.
Il est simple de fixer le problème: inverser les conditions comme ci-dessous:
```c
for (i = 0; i < MAX_WCOMP && g_objArray[i]; i++) {
MultiMoveRelXY(g_objArray[i], x - center, deltay);
```
## Partie 9
### Question 1
La portion de code est appelée *dead code* car quel que soit les conditions,
cette portion ne sera jamais atteinte. Ici, `decode` est définit à `false` et
jamais modifiée (l. 160), ainsi de la condition l. 174 sera toujours fausse et
donc les instructions dans la `if` jamais atteintes.
### Question 2
La valeur de `skip` est modifiée à la ligne 751, puis est suivie d'une
instruction `continue` qui relance un tour de boucle. La valeur de `skip` est
alors réinitialisée à *1* au début de cette boucle. La modification effectuée
alors est **totalement inutile**.
### Question 3
Le bloc d'instruction du `case` ligne 1240 n'est terminé ni par un `break` --
permettant de sortir du `switch` -- ni d'un `return`. Ainsi l'exécution du
programme continuer dans le `case` suivant (ou le `default`). Des portion de
code peuvent alors être exécutées dans des cas non prévus.
### Question 4
Ici le test de la boucle `uint x >= 0` est toujours vrai, on entre alors dans une
boucle infinie.
### Question 5
## Partie 10
### Question 1
### Question 2
`pos` pointe vers une variable locale a locale fonction `Texture::render()`.
Lors de la fin de cette dernière, alors `pos` pointera vers une zone mémoire qui
n'est plus disponible.
### Question 3
Dans la fonction `setParent( ExecutionNode parent)`, si `parent` est *NULL*
alors une erreur de type *Null pointer dereferences* se produit lors de
l'instruction `getContext().setParent(parent.getContext());`. Le développeur
teste cependant la validité du pointeur juste avant.
### Question 4
L'analyseur a détermine que `pos` ne pouvait pas être égal à 0 pour deux
raisons:
* la signature de la fonction `strchr` renvoie un pointeur, `p` contient donc
une adresse
* dans le cas ou aucun caractère correspondant n'est trouvé, le programme ne
rentre pas dans la boucle c'est la condition
`((pos = strchr(lastpos, '&')) != NULL)` qui le défini.
Si `pos` est défini, alors sont adresse est incrémentée de 1 (l. 1716), `pos`
n'est donc jamais égal à 0, cependant il est possible que la case mémoire
**pointée par `p` le soit**, indiquant une fin de chaine de caractères.
### Question 5
Là encore on est face à un problème entre le type pointeur (qui contient une
adresse, sont un `int`) et la zone mémoire vers la quelle il pointe.
L'instruction suivante alloue une zone mémoire égale à un `int`:
```c
Device_Entry *entry = calloc(1, sizeof(entry));
```
Alors que l'instruction suivante alloue nue zone mémoire de la taille de
`Device_Entry`:
```c
Device_Entry *entry = calloc(1, sizeof(*entry));
```
### Question 6
Lors de la première exécution de la boucle ` for (cur_module = wmodules;
cur_module; wmodules = next_module)`, le programme libère la zone mémoire
utilisée par la variable `cur_module` (l. 60). Cependant cette variable est
utilisée ensuite dans le test de la boucle `for`. Tous les modules ne seront
donc pas libérés (s'il y en a plus d'un).
```c
for (cur_module = wmodules; cur_module; wmodules = next_module) {
next_module = cur_module->next;
cur_module->context->destroy(cur_module->data);
free(cur_module);
}
```
Dans la correction proposée, `cur_module` est aussi utilisée dans la définition
de la boucle `for`, ainsi là aussi notre boucle ne fera pas plus d'un tour.
## Partie 11
### Question 1
La dernière fonction appelée par ne donne pas lieu à un test comme les
précédente, ainsi si tous les autres test réussissent mais pas ce dernier, alors
le résultat sera vrai.
Le développeur aura du garder le fonctionnement des élément précédent et ainsi
écrire:
```c
if (!checkPriv(fromDBbackend, QLatin1String("CheckPriv_Cleanup")) {
result = false;
}
```
### Question 2
`TAINTED_STRING` signifie qu'un ou plusieurs éléments externes influnce(nt) le
comportement de notre programme alors que ces éléments ne sont pas examinés.
Ici le code permet de lancer des commandes SQL contenues dans le fichier pointe
par `optFile` sans aucun contrôle. Un attaquant pourrait alors réaliser une
attaque par **injection SQL**.