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

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 aj niekoľko funkcií zo štandardnej knižnice stdlib.h, pomocou ktorých vieme pracovať s pamäťou dynamicky. Nekontroluje však 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

Postup

Lets Hack!

V prvom kroku zatiaľ o veľa nepôjde. Napíšeme krátky program, na ktorom si vysvetlíme pojem Buffer Overflow.

Úloha 1.1

Skopírujte si nasledujúci príklad do súboru secret.c. Nezabudnite priložiť potrebné hlavičkové súbory.

int main() {
  int check = 0;
  char buff[8];

  gets(buff);
  if(strcmp(buff, "pass") == 0) {
    check = 1;
  }

  if(check) {
    printf ("\n Som gROOT \n");
  }else {
    printf ("\n Volam policiu \n");
  }

  return 0;  
}

Program skompilujte príkazom:

gcc -fno-stack-protector -g -o secret secret.c

Úloha 1.2

Vyskúšajte funkčnosť programu zadaním hesla “pass” a “auto”.

Všimnite si, že program správne rozhodne o tom, či Vám má povoliť prístup.

Úloha 1.3

Teraz však vyskúšame zadať niečo, čo programátor neočakával. Čo sa stane, ak by sme napríklad zadali ako heslo autoooooooo ?

Poznámka

Prekvapený, však? Program nám umožnil prístup, napriek tomu, že sme zadali nesprávne heslo.

Úloha 1.4

Nechajte vypísať hodnotu premennej check.

Čo nám program vypísal? Viete prečo?

Poznámka

Môže za to náš neošetrený vstup, ktorý spôsobil pretečenie vyrovnávacej pamäte. Keď sme zadali vstup dlhší ako 4 znaky (jeden bajt je vyhradený na '\0'), funkcia gets() začala zapisovať za pamäť vyhradenú pre vstupný buffer. Vzniklo pretečenie výrovnávacej pamäte (tzv. Buffer overflow). Na mieste v pamäti, ktorú sme začali prepisovať, bola uložená hodnota premennej check a dôsledkom bola zmena jej hodnoty.

Úloha 1.5

Skúste zadať na vstup autoooooo, aký má výstup?

Teraz zadajte autooooooo. Všimnite si rozdiel pri výstupe.

Prečo je to tak?

Poznámka

Výstup pri autoooooo bolo číslo 111, čo zodpovedá ASCII hodnote znaku 'o' (prepísali sme prvých 8 najnižších bitov premennej typu int). Ak zadáme autooooooo prepíšeme 16 bitov, čo zodpovedá hodnote 28527.

Ako sa podobným chybám môžeme brániť?

Pri kompilácii 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:

stack smashing detected *: ./1 terminated
Aborted

Poznámka

Existujú rôzne ďalšie ochranné mechanizmy, šikovný hacker ich však dokáže obísť, nemusia byť všade implementované 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 1.6

Nahraďte funkciu gets() jej bezpečnou alternatívou fgets().

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.

Existuje technika nazvaná Defensive programming, ktorá sa snaží zlepšiť bezpečnosť a spoľahlivosť kódu. Makačom odporúčame prečítať 8. kapitolu knihy Code Complete 2.

UP!

Vytvoríte funkciu upper(), na ktorej si následne ukážeme použitie nástroja Valgrind.

Úloha 2.1

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 2.2

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 3.1

Spustite svoj kód pomocou príkazu valgrind a analyzujte výsledok.

Kód 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ý:

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 neuvolnili pamäť, ktorú sme si rezervovali.

Úloha 3.2

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ť.

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 4.1

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));
    printf("%dth value is %d\n", size, array[size]);
    return 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 4.2

Identifikujte problém, opravte ho a overte správnosť vašej implementácie.

Úloha 4.3

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));
    array[size] = 99;
    printf("%dth value is %d\n", size, array[size]);
    return 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 4.4

Identifikujte problém, opravte ho a overte správnosť vašej implementácie.

Additional Tasks

  1. 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:

Ďalšie zdroje