Buffer Overflow, Memory Leaks and Valgrind
Pretečenie pamäte, úniky v pamäti, ich odhaľovanie pomocou nástroja valgrind a predchádzanie im
Video pre cvičenie
About
Na dnešnom cvičení si vysvetlíme riziko, ktoré predstavuje pretečenie pamäte a povieme si, ako možným problémom predchádzať.
Jazyk C nám poskytuje niekoľko funkcií zo štandardnej
knižnice stdlib.h
, pomocou ktorých vieme pracovať s pamäťou
dynamicky. To znamená, že si sami vieme v programe vypýtať, koľko bytov
pamäte pre naše potreby potrebujeme vyhradiť. Tieto funkcie však
nekontrolujú to, či s pamäťou aj pracujeme tak, ako sme si ju
rezervovali - či náhodou nečítame z miesta, ktoré nemáme vyhradené alebo
naopak nezapisujeme na miesto, ktoré nám nepatrí.
Valgrind je nástroj, ktorý vám pomôže prípadné úniky v pamäti identifikovať.
Objectives
- Porozumieť problému pretečenia pamäte a bezpečnostným rizikám s ním spojených.
- Porozumieť problému únikov v pamäti.
- Naučiť sa využívať nástroj
valgrind
na odhaľovanie týchto problémov.
Postup
Securing Arena
V prvom kroku zatiaľ o veľa nepôjde. Napíšeme krátky program, ktorý slúži na overenie prihlásenia používateľa Manager do systému Arena.
Úloha
Vytvorte súbor secret.c
a skopírujte do neho nasledujúci
kód.
#include <stdio.h>
#include <string.h>
#include <stdbool.h>
#define PASSWORD "secret"
int main() {
char pass = false;
char password[8];
// get the password from user
("Enter password to Arena: ");
printf("%s", password);
scanf
// evaluate the password
if(strcmp(password, PASSWORD) == 0) {
= true;
pass }
// allow/forbid access
if(pass == false) {
(">> Security breach detected. Calling police!\n");
printf }else {
(">> Welcome to Arena, master Manager!\n");
printf }
}
Úloha
Program preložte.
Tentokrát program neprekladajte príkazom make
, ale
preložte ho nasledujúcim príkazom:
$ gcc -std=c11 -Werror -Wall -fno-stack-protector -g secret.c -lm -o secret
Úloha
Overte správnosť fungovania programu zadaním správneho a nesprávneho hesla.
Ak zadáte správne heslo secret
, program pozdraví
Manager-a správou
>> Welcome to Arena, master Manager!
V prípade zadania nesprávneho hesla (napr. hello
)
program vypíše správu
>> Security breach detected. Calling police!
Program teda podľa očakávania správne rozhodne o tom, či povolí prístup do systému alebo nie.
Hacking Arena
V tomto kroku sa to už však začne komplikovať. Síce sme overili, že program funguje, ako má, dopustili sme sa v ňom však vážnej chyby, ktorá vedie ku problému s názvom Buffer Overflow.
Úloha
Zadajte heslo, ktoré bude mať dĺžku viac ako 8 znakov.
Vyskúšajte zadať napr. heslo generator
. Výsledkom bude,
že sme sa úspešne dostali do systému aj napriek tomu, že sme nezadali
správne heslo. A to rozhodne nie je správanie, ktoré sme chceli.
Úloha
Vypíšte v programe hodnotu premennej pass
vo forme ASCII
znaku spolu s jej hodnotou.
Čo program vypísal? Viete prečo?
Poznámka
Môže za to náš neošetrený vstup, ktorý spôsobil pretečenie v pamäti.
Keď sme zadali vstup dlhší ako 7 znakov (netreba zabudnúť, že jeden bajt
je vyhradený na ukončenie reťazca pomocou terminátora
'\0'
), funkcia scanf()
začala zapisovať za
pamäť vyhradenú pre vstupný buffer. Tomuto problému hovoríme
pretečenie pamäte (z angl. Buffer
overflow). Na mieste v pamäti, ktorú sme začali prepisovať,
bola uložená hodnota premennej pass
a dôsledkom pretečenia
bola jej hodnota zmenená.
Úloha
Spustite program s heslom generator
a zdôvodnite
vypísaný výstup.
Prečo je to tak?
Poznámka
Pri zadaní reťazca generator
sa na výstupe zobrazí:
'r' (114)
Najprv sme totiž naplnili jednorozmerné pole znakov s názvom
password
, do ktorého sa uložilo prvých 8 znakov zo
vstupu. Konkrétne sa jedná o znaky generato
. A keďže
premenné sú v pamäti uložené za sebou, písmeno r
sa uloží
do premennej pass
, ktoré sa vypíše na obrazovku.
Fixing Arena
V tomto kroku sa pozrieme na to, ako Arénu opraviť, aby sa do nej nemohol prihlásiť len tak hocikto. Riešiť tento problém budeme dvoma spôsobmi:
- pridaním voľby pri preklade, a
- pomocou bezpečnejších funkcií.
Úloha
Preložte program znova použitím voľby -fstack-protector
a zadajte opäť heslo generator
.
Tentokrát preložíme program nasledovným príkazom:
$ gcc -std=c11 -Wall -Werror -fstack-protector -g secret.c -lm -o secret
Pri preklade programu sme vypli tzv. stack-protector. Ak by
bol zapnutý, nepodarilo by sa nám prepísať premennú check
a
program by skončil s výpisom:
$ ./secret
Enter password to Arena: generator
'' ()
>> Security breach detected. Calling police!
*** stack smashing detected ***: terminated
Aborted
Poznámka
Existujú rôzne ďalšie ochranné mechanizmy. Šikovný hacker ich však dokáže obísť. Tieto mechanizmy nemusia byť však implementované všade alebo pri určitých nepravdepodobných okolnostiach nemusia fungovať správne. Najdôležitejšie však je, že neopravujú chybu programu, iba sa snažia minimalizovať jej následky. My ako softvéroví inžinieri sa ale snažíme tieto chyby odstraňovať.
Identifikujme základné chyby, ktoré sme urobili:
využili sme funkciu gets()
. Vhodnejšie je využiť funkciu
fgets()
, umožňujúcu špecifikovať maximálny počet znakov,
ktoré sa načítajú do vyrovnávacej pamäte.
Upozornenie
Funkcia gets()
, tak ako nás správne upozornil prekladač,
je nebezpečná a označená ako zastaralá
(deprecated).
Takto označené funkcie by sa nemali používať, keďže boli nahradené a neskôr budú odstránené. Problém tejto funkcie spočíva v tom, že nekontroluje veľkosť buffera, do ktorého zapisuje vstup od používateľa a bude pokračovať v zapisovaní za pamäť pre neho vyhradenú.
Úloha
Vo funkcii scanf()
využite modifikátor
width
na obmedzenie množstva znakov, ktoré funkcia zo
vstupu môže načítať.
Formátovací
reťazec funkcie scanf()
vyzerá nasledovne:
%[*][width][modifiers]type
Modifikátor width
špecifikuje maximálny počet znakov,
ktoré môžu byť zo vstupu prečítané.
Úloha
Nahraďte funkciu scanf()
bezpečnejšou funkciou
fgets()
.
Poznámka
Pokiaľ potrebujeme načítať formátovaný vstup, je vhodné pomocou
fgets()
načítať celý riadok a následne pomocou
sscanf()
vybrať jednotlivé položky.
Upozornenie
Veľkosť výrovnávacej pamäte nekontrolujú ani funkcie
scanf()
, strcpy()
a strcat()
.
Vhodnejšie je využiť ich alternatívy: fgets()
,
strncpy()
a strncat()
.
Poznámka
Častou chybou je ignorovanie návratovej honoty funkcie. Je to síce práca navyše, dokáže nás ale zachrániť pred mnohými vážnymi problémami.
UP!
V tomto kroku vytvoríte funkciu upper()
, ktorá vytvorí
kópiu reťazca, v ktorej budú všetky písmená veľké. Túto funkciu
použijeme na prezentovanie nástroja valgrind
.
Úloha
Vytvorte funkciu upper(const char* text)
, ktorá vráti
referenciu na kópiu reťazca text
, v ktorom budú všetky
písmená veľké.
Poznámka
Na prevod písmen na veľké vám vie pomôcť makro
toupper()
z knižnice ctype.h
.
Úloha
Overte správnosť svojej implementácie na reťazcoch
HeLLo
, world
a H3ll0 w0rld!
.
Pokiaľ ste postupovali správne, funkcia zabezpečí transformáciu každého písmena na veľké, pokiaľ je možné z neho veľké písmeno spraviť.
Introduction to Valgrind
V tomto kroku sa zoznámite s nástrojom valgrind. Naučíte sa ho spustiť s vašim programom a naučíte sa rozumieť výstupu, ktorý produkuje.
Úloha
Spustite program pomocou príkazu valgrind
a analyzujte
výsledok.
Program spustite v tvare:
$ valgrind ./upper
Po jeho spustení budete vidieť podobný výstup ako nasledovný:
valgrind ./upper
==12417== Memcheck, a memory error detector
==12417== Copyright (C) 2002-2013, and GNU GPL'd, by Julian Seward et al.
==12417== Using Valgrind-3.10.1 and LibVEX; rerun with -h for copyright info
==12417== Command: ./upper
==12417==
hello world! is HELLO WORLD!
==12417==
==12417== HEAP SUMMARY:
==12417== in use at exit: 13 bytes in 1 blocks
==12417== total heap usage: 1 allocs, 0 frees, 13 bytes allocated
==12417==
==12417== LEAK SUMMARY:
==12417== definitely lost: 13 bytes in 1 blocks
==12417== indirectly lost: 0 bytes in 0 blocks
==12417== possibly lost: 0 bytes in 0 blocks
==12417== still reachable: 0 bytes in 0 blocks
==12417== suppressed: 0 bytes in 0 blocks
==12417== Rerun with --leak-check=full to see details of leaked memory
==12417==
==12417== For counts of detected and suppressed errors, rerun with: -v
==12417== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
Číslo 12417 predstavuje identifikačné číslo procesu (tzv.
PID - Process ID). Nás však budú zaujímať informácie v
dvoch častiach identifikovaných ako HEAP SUMMARY
a
LEAK SUMMARY
.
Význam niektorých prípadov je nasledovný:
“Still reachable” - Referencia na blok v pamäti je stále k dispozícii. Tento prípad je veľmi častý, a aj keď nie je považovaný za kritickú chybu, programátor by mal pred ukončením programu zabezpečiť uvoľnenie takéhoto bloku pamäti.
“Definitely lost” - V pamäti zostal vyhradený blok, ale neexistuje naň žiadna referencia. Takýto blok je označený ako stratený (z angl. “lost”), pretože programátor tento blok už nedokáže uvoľniť pred ukončením programu, keďže jeho adresu už nemá.
Poznámka
Kompletný zoznam a význam jednotlivých chýb, ktoré valgrind identifikuje, môžete nájsť na stránkach jeho návodu.
V jednej aj druhej časti sa dozvedáme, že po skončení (in use at
exit) ostalo v pamäti aj naďalej 13 bytov
. To je
spôsobené tým, že po skončení funkčnosti nášho programu sme neuvoľnili
pamäť, ktorú sme si rezervovali.
Úloha
Opravte vzniknutý problém tak, aby po skončení programu k výpisu uvedenej chyby nedošlo.
Poznámka
Tento problém vznikol neuvoľnením vyhradenej pamäte pred skončením programu. Aj keď sa nejedná o chybu programu, je dobrým zvykom pridelenú pamäť pred skončením programu uvoľniť. Vyhnete sa napríklad vyčerpaniu pamäti v prípade, ak by program bol spustený veľmi dlho a neustále by dochádzalo ku vyhradzovaniu ďalšej pamäti.
Invalid Read/Write!
Častým zdrojom chýb je prístup k údajom v pamäti, ktorú nemáme
vyhradenú - či už z nej chceme čítať alebo do nej chceme zapisovať.
Pomocou nástroja valgrind
však vieme identifikovať aj
takéto problémy.
Úloha
Demonštrujte príklad identifikovania čítania z pamäte, ktorá nie je vyhradená pre váš program.
Na identifikáciu tohto problému nám poslúži nasledujúci fragment kódu:
#include <stdio.h>
#include <stdlib.h>
int main(){
int size = 4;
int* array = (int*)malloc(size * sizeof(int));
("%dth value is %d\n", size, array[size]);
printfreturn 0;
}
Aj napriek tomu, že preklad prebehne bez problémov,
valgrind
identifikuje chybu pomerne jasne:
==21181== Memcheck, a memory error detector
==21181== Copyright (C) 2002-2013, and GNU GPL'd, by Julian Seward et al.
==21181== Using Valgrind-3.10.1 and LibVEX; rerun with -h for copyright info
==21181== Command: ./main
==21181==
==21181== Invalid read of size 4
==21181== at 0x4007BD: main (main.c:21)
==21181== Address 0x54fa050 is 0 bytes after a block of size 16 alloc'd
==21181== at 0x4C29BCF: malloc (in /usr/lib64/valgrind/vgpreload_memcheck-amd64-linux.so)
==21181== by 0x4007A4: main (main.c:19)
==21181==
4th value is 0
==21181==
==21181== HEAP SUMMARY:
==21181== in use at exit: 16 bytes in 1 blocks
==21181== total heap usage: 1 allocs, 0 frees, 16 bytes allocated
==21181==
==21181== LEAK SUMMARY:
==21181== definitely lost: 16 bytes in 1 blocks
==21181== indirectly lost: 0 bytes in 0 blocks
==21181== possibly lost: 0 bytes in 0 blocks
==21181== still reachable: 0 bytes in 0 blocks
==21181== suppressed: 0 bytes in 0 blocks
==21181== Rerun with --leak-check=full to see details of leaked memory
==21181==
==21181== For counts of detected and suppressed errors, rerun with: -v
==21181== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)
Nástroj valgrind
teda identifikoval nesprávne čítanie
4
bytov priamo za vytvoreným poľom (0
bytov za
alokovaným blokom o veľkosti 16
bytov). Ak je zapnutý pri
preklade aj prepínač -g
, valgrind
napíše aj
názov súboru a číslo riadku, kde ku chybe došlo (v tomto prípade súbor
main.c
na riadku 21
).
Úloha
Identifikujte problém, opravte ho a overte správnosť vašej implementácie.
Úloha
Demonštrujte príklad identifikovania zápisu do pamäte, ktorá nie je vyhradená pre váš program.
Na identifikáciu tohto problému nám poslúži nasledujúci fragment kódu:
#include <stdio.h>
#include <stdlib.h>
int main(){
int size = 4;
int* array = (int*)malloc(size * sizeof(int));
[size] = 99;
array("%dth value is %d\n", size, array[size]);
printfreturn 0;
}
Aj napriek tomu, že preklad prebehne bez problémov,
valgrind
identifikuje chybu pomerne jasne:
==22321== Memcheck, a memory error detector
==22321== Copyright (C) 2002-2013, and GNU GPL'd, by Julian Seward et al.
==22321== Using Valgrind-3.10.1 and LibVEX; rerun with -h for copyright info
==22321== Command: ./main
==22321==
==22321== Invalid write of size 4
==22321== at 0x4007BD: main (main.c:20)
==22321== Address 0x54fa050 is 0 bytes after a block of size 16 alloc'd
==22321== at 0x4C29BCF: malloc (in /usr/lib64/valgrind/vgpreload_memcheck-amd64-linux.so)
==22321== by 0x4007A4: main (main.c:19)
==22321==
==22321== Invalid read of size 4
==22321== at 0x4007D7: main (main.c:22)
==22321== Address 0x54fa050 is 0 bytes after a block of size 16 alloc'd
==22321== at 0x4C29BCF: malloc (in /usr/lib64/valgrind/vgpreload_memcheck-amd64-linux.so)
==22321== by 0x4007A4: main (main.c:19)
==22321==
4th value is 99
==22321==
==22321== HEAP SUMMARY:
==22321== in use at exit: 16 bytes in 1 blocks
==22321== total heap usage: 1 allocs, 0 frees, 16 bytes allocated
==22321==
==22321== LEAK SUMMARY:
==22321== definitely lost: 16 bytes in 1 blocks
==22321== indirectly lost: 0 bytes in 0 blocks
==22321== possibly lost: 0 bytes in 0 blocks
==22321== still reachable: 0 bytes in 0 blocks
==22321== suppressed: 0 bytes in 0 blocks
==22321== Rerun with --leak-check=full to see details of leaked memory
==22321==
==22321== For counts of detected and suppressed errors, rerun with: -v
==22321== ERROR SUMMARY: 2 errors from 2 contexts (suppressed: 0 from 0)
Nástroj valgrind
teda identifikoval nesprávny zápis
4
bytov priamo za vytvoreným poľom (0
bytov za
alokovaným blokom o veľkosti 16
bytov). Ak je zapnutý pri
preklade aj prepínač -g
, valgrind
napíše aj
názov súboru a číslo riadku, kde ku chybe došlo (v tomto prípade súbor
main.c
na riadku 20
).
Úloha
Identifikujte problém, opravte ho a overte správnosť vašej implementácie.
Additional Tasks
Prečítajte si, ako vám môže Valgrind pomôcť pri odhaľovaní pretečenia statických a globálnych polí na tejto stránke. Vyskúšajte ho na tomto jednuchom príklade:
int main() { int i, a[10]; for (i = 0; i <= 10; i++) [i] = 42; a}