16 KiB
title | date | tags | author | categories | ||||||
---|---|---|---|---|---|---|---|---|---|---|
Sécurité logicielle : TD7 analyse statique / dynamique | 2023-03-10 |
|
|
|
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()
pourd
- dans la fonction
g()
pourc
(référencé souse
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)
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)
[...]
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:
./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 même espace qu'elle occupe en mémoire. Et il n'a pas
changé.
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 :
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)
Valgrind ne se plaint pas et pour cause: nous se sommes toujours pas en présence d'un problème de fuite de mémoire.
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:
// extract bit 0 to check for parity of the number of arguments
if (argc & 1 == 0) {
// ...
Elle sera toujours fausse. La correction est simple:
// 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:
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
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
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:
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
:
Device_Entry *entry = calloc(1, sizeof(entry));
Alors que l'instruction suivante alloue nue zone mémoire de la taille de
Device_Entry
:
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).
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:
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.