Buffer Overflow, Memory Leaks and Valgrind

Pretečenie pamäte, zásobník a hromada, úniky v pamäti, ich odhaľovanie pomocou nástroja valgrind a ako im predchádzať.

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
    printf("Enter password to Arena: ");
    scanf("%s", password);

    // evaluate the password
    if(strcmp(password, PASSWORD) == 0) {
        pass = true;
    }

    // allow/forbid access
    if(pass == false) {
        printf (">> Security breach detected. Calling police!\n");
    }else {
        printf (">> Welcome to Arena, master Manager!\n");
    }
}

Ú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á, ale dopustili sme sa v ňom vážnej chyby - ak totiž zadáme heslo dlhšie, ako 8 znakov, úspešne sa do systému dostaneme.

Môže za to náš neošetrený vstup, ktorý spôsobil pretečenie v pamäti (z angl. Buffer overflow). Keď sme zadali vstup dlhší ako 7 znakov, funkcia scanf() začala zapisovať za pamäť vyhradenú pre vstupný buffer, pretože si neoveruje, či sa načítaný vstup do cieľového umiestnenia naozaj zmestí.

V tomto kroku sa na tento problém pozrieme bližšie.

Úloha

Vypíšte v programe hodnotu premennej pass vo forme ASCII znaku spolu s jej hodnotou a overte svoju implementáciu.

To, či nás zabezpečenie systému Aréna pustí ďalej alebo nie, závisí od hodnoty v premennej pass. Preto sa na ňu pozrieme bližšie. V prípade zadania správneho vstupu (ktorý bude kratší, ako 8 znakov), bude výstup vyzerať takto:

'' (0)

Úloha

Spustite program znova a zadajte nesprávne heslo, ktoré bude v tomto prípade dlhšie ako 8 znakov, napr. generator.

Prečo je to tak?

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.

+---+---+---+---+---+---+---+---+---+----+
| g | e | n | e | r | a | t | o | r | \0 |
+---+---+---+---+---+---+---+---+---+----+
^                               ^
| password                      | pass

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:

  1. pridaním voľby pri preklade, a
  2. 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

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.

Ú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().

UP!

V tomto kroku vytvoríte funkciu upper(), ktorá vytvorí kópiu reťazca, v ktorej budú všetky písmená veľké. Pomocou funkcie sa zoznámime s dynamickou alokáciou pamäte a neskôr ju budeme používať 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é.

Ako riešenie použijeme túto veľmi naivnú a veľmi zlú implementáciu:

char* upper(const char* text){
   char result[strlen(text)];

   for(int i = 0; i < strlen(text); i++){
       result[i] = toupper(text[i]);
   }

   result[strlen(text) + 1] = '\0';

   return result;
}

Úloha

Overte správnosť svojej implementácie zavolaním funkcie upper() vo funkcii main() napr. na reťazci HeLLo w0rld!.

Podoba použitia funkcie môže vyzerať napríklad takto:

int main(){
   char* up = upper("HeLLo w0rld!");
   printf("upper(\"HeLLo w0rld!\") = \"%s\"\n", up);
}

Úloha

Program preložte a spustite.

Ak sa pokúsite program preložiť, dôjde ku chybe:

$ make upper
gcc -std=c11 -Wall -Werror    upper.c  -lm -o upper
upper.c: In function ‘upper’:
upper.c:14:11: error: function returns address of local variable [-Werror=return-local-addr]
   14 |    return result;
      |           ^~~~~~
cc1: all warnings being treated as errors
make: *** [<builtin>: upper] Error 1

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

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.

Conditional Jump

  • pouzit malloc miesto callocu
  • nenastavovat terminatora rucne pri vytvarani, ale spolieaht sa, ze tam automaticky bude pri rezertovani pamate
  • chyba nastane pri volani funkcie printf(), ktora terminator ocakava (ale nebol explicitne zapisany)
  • riesenie - calloc()

Neporiadok

  • uvolnenie pamate po skonceni
  • double free problem
  • NULL after free()

Additional Tasks

  1. Častou chybou pri programovaní je pomýlenie sa pri indexe o 1 známy pod menom Off by One Problem alebo Fence-Post Problem. Pozrite sa na nasledujúci kód a pokúste sa v ňom identifikovať chyby bez toho, aby ste ho spustili pomocou programu valgrind. Ten spustite až potom, keď vám budú jasné dve chyby, ktoré sa v ňom nachádzajú. Následne pomocou výstupu programu valgrind sa pokúste identifikovať miesta vzniku jednotlivých problémov.

    #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]);
    }
  2. 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++)
              a[i] = 42;
    }

Ďalšie zdroje