Buffer Overflow: Når Hukommelsen Løber Over Kanten
Buffer overflow er en af de ældste og mest klassiske sårbarheder inden for computersikkerhed. Trods sin høje alder er den stadig yderst relevant i dag – den har gjort det muligt at udnyt alt fra 1988’s Morris Worm til moderne exploits mod indlejrede systemer. I denne artikel dykker vi ned i, hvad en buffer overflow faktisk er, hvordan angribere udnytter den, og hvad man som udvikler og sikkerhedsprofessionel kan gøre for at forhindre det.
1. Hvad er en Buffer?
Inden vi kan forstå overflowet, skal vi forstå selve bufferen.
En buffer er simpelthen et reserveret, sammenhængende stykke hukommelse, der bruges til midlertidigt at gemme data. Tænk på det som en kasse med en fast størrelse: Kassen er sat til at rumme præcis 10 bogstaver. Når et program kører, allokerer det (reserverer) buffere på enten:
- Stakken (Stack): Midlertidig, automatisk allokeret hukommelse til lokale variabler i funktioner. Stakken vokser “nedad” i hukommelsen og er lynhurtig at arbejde med.
- Heapen (Heap): Dynamisk allokeret hukommelse (via
malloc(),newosv.), der håndteres manuelt af udvikleren eller garbage collectoren. Heapen er mere fleksibel, men langsommere.
2. Hvad er et Buffer Overflow?
Et buffer overflow (eller buffer overrun) opstår, når et program skriver mere data ind i en buffer, end bufferen er allokeret til at rumme. Det overskydende data “løber over” og overskriver tilstødende hukommelse.
Her er et simpelt C-eksempel, der illustrerer problemet:
#include <stdio.h>
#include <string.h>
void autentificer(char *input) {
char buffer[8]; // En buffer til præcis 8 tegn
strcpy(buffer, input); // FARLIGT: Kopierer uden at tjekke størrelse!
printf("Modtaget: %s\n", buffer);
}
int main() {
char bruger_input[64];
printf("Indtast dit brugernavn: ");
gets(bruger_input); // FARLIGT: gets() tjekker heller ikke størrelse!
autentificer(bruger_input);
return 0;
}
Hvad sker der, hvis brugeren taster en streng på mere end 8 tegn? strcpy() kopierer data blindt, indtil den rammer et null-tegn (\0), helt uden at tjekke om destinationsbufferen er stor nok. De overskydende bytes ender i den hukommelse, der befinder sig efter buffer på stakken – og det er her, det bliver farligt.
3. Stakkens Opbygning og Return Address
For at forstå, hvorfor et buffer overflow er farligt og ikke blot en crash-fejl, skal vi forstå, hvordan stakken er organiseret.
Når en funktion kaldes i et program, lægger CPU’en et stack frame på stakken, der typisk indeholder:
- Lokale variabler (herunder vores sårbare
buffer) - Saved Base Pointer (SBP): Gemmer stedet vi skal vende tilbage til i den kaldende funktion.
- Return Address (RET): Den hukommelsesadresse, som CPU’en skal springe til (returnere til), efter den nuværende funktion er færdig.
Høj adresse
┌─────────────────────┐
│ Argumenter │
├─────────────────────┤
│ Return Address │ <-- Her vil angriberen skrive
├─────────────────────┤
│ Saved Base Pointer │
├─────────────────────┤
│ Lokal variabel │
├─────────────────────┤
│ buffer[0..7] │ <-- Her starter vores buffer
└─────────────────────┘
Lav adresse
Eftersom buffer ligger lavere i hukommelsen end Return Address, og data skrives opad mod højere adresser, kan et overflow skrive helt op til og overtage Return Address!
Scenariet: Angriberen kontrollerer Return Address
- Angriberen sender et input, der er præcis langt nok til at fylde bufferen, overskrive SBP, og derefter erstatte Return Address med sin egen adresse.
- Den adresse, angriberen sætter ind, peger typisk på en shellcode – et lille, ondsindet maskinsprog-program, som angriberen selv har placeret et sted i hukommelsen (f.eks. i selve bufferen!).
- Når
autentificer()-funktionen afslutter og CPU’en læser Return Address for at vende tilbage, springer den i stedet direkte ind i angriberens shellcode. - Shellcode kan derefter give angriberen et kommandolinjeskald (
/bin/sh) med de rettigheder, som det sårbare program kørte med. Kørte programmet som root, har angriberen nu fuld kontrol over systemet.
4. Typer af Buffer Overflow
Der skelnes primært mellem to varianter:
Stack-based Buffer Overflow
Den klassiske og mest kendte type, som beskrevet ovenfor. Angrebet retter sig mod Return Address på stakken. Meget direkte og relativt simpelt at udnytte i klassiske, ubeskyttede programmer.
Heap-based Buffer Overflow
Et heap overflow overskriver data på den dynamiske heap frem for stakken. Her er der normalt ikke en direkte Return Address at overskrive, men angriberen kan:
- Overskrive metadata i heap-strukturen (f.eks. chunk-headers i
glibc’smalloc), som kan føre til vilkårlig skrivning til hukommelsen. - Overskrive andre datastrukturer på heapen, som f.eks. funktionspointere, der efterfølgende bruges af programmet.
Heap overflows er generelt sværere at udnytte, men er stadig en alvorlig sårbarhedskategori (se f.eks. sårbarhederne i Internet Explorer’s jscript.dll over årtier).
5. Forsvar og Moderne Beskyttelsesmekanismer
Moderne operativsystemer og compilere implementerer adskillige mekanismer til at gøre buffer overflows sværere at udnytte:
Stack Canaries
Compileren (f.eks. GCC med -fstack-protector) indsætter en canary-værdi – et tilfældigt tal – direkte på stakken mellem de lokale variabler og Return Address. Før en funktion returnerer, tjekker programmet om canary-værdien er uændret. Har et overflow fundet sted, vil canary-værdien næsten altid være korrupt, og programmet terminerer straks med en fejl i stedet for at lade angriberen styre Return Address.
┌─────────────────────┐
│ Return Address │
├─────────────────────┤
│ Saved Base Pointer │
├─────────────────────┤
│ CANARY (random) │ <-- Overflow ødelægger denne
├─────────────────────┤
│ buffer[0..7] │
└─────────────────────┘
Address Space Layout Randomization (ASLR)
ASLR er en OS-funktion (understøttet af Linux, Windows og macOS) der randomiserer startadressen på stakken, heapen og biblioteker hver gang et program startes. Formålet er at gøre det ekstremt svært for en angriber at forudsige, præcis hvor i hukommelsen angriberes shellcode befinder sig, og dermed hvilken adresse de skal skrive ind som Return Address.
Data Execution Prevention (DEP) / No-eXecute (NX)
DEP, også kaldt NX (No-eXecute) eller XD (Execute Disable) bit, er en hardware/OS-funktion, der markerer hukommelsesregioner som enten eksekverbare (kode) eller skrivbare (data), men ikke begge dele. Stakken og heapen markeres som “data” og kan dermed ikke eksekveres direkte. Det forhindrer den klassiske tilgang, hvor angriberen placerede sin shellcode i bufferen og sprang til den.
Angribere har svaret igen med teknikker som Return-Oriented Programming (ROP), hvor de i stedet kæder eksisterende kode-fragmenter (kaldet “gadgets”) i selve programmet og dets biblioteker sammen for at udføre ondsindet logik – uden nogensinde at køre ny kode.
Sikre Biblioteksfunktioner
En stor del af problemet skyldes brugen af usikre C-funktioner, der ikke tjekker bufferlængde. Løsningen er at erstatte dem med sikre alternativer:
| Usikker funktion | Sikker alternativ |
|---|---|
strcpy() | strncpy() eller strlcpy() |
strcat() | strncat() eller strlcat() |
gets() | fgets() |
sprintf() | snprintf() |
scanf("%s") | scanf("%Ns", ...) med maksimal bredde |
Brug af Memory-Safe Programmeringssprog
Det ultimative forsvar er at vælge et sprog, der i sin natur forhindrer buffer overflows. Rust, Go, Java og Python udfører alle automatisk bounds-checking – dvs. verificerer at et array-indeks er inden for gyldig grænse, inden de skriver data. Koden crasher kontrolleret (med en undtagelse eller panic) frem for at overskrive vilkårlig hukommelse.
6. Et Rigtigt Eksempel: Morris Worm (1988)
Det første store, dokumenterede udnyttelse af et buffer overflow var Morris Worm fra 1988 – det første store “internet-angreb” i historien. Ormen udnyttede bl.a. en buffer overflow-fejl i fingerd-dæmonen på BSD Unix. Ved at sende et overlangt input kunne den overskrive Return Address og afvikle vilkårlig kode på ofrets maskine. Ormen inficerede anslået 6.000 maskiner (ca. 10% af det dengang eksisterende internet), forårsagede nedbrud og kostede millioner af dollars i oprydning.
Trods dette er klassen af sårbarheder 38 år senere stadig relevant i C og C++ kodebasen, der driver alt fra firmware i routere til styresystemers kerner.
7. Konklusion
Buffer overflow er ikke blot en akademisk sårbarhed i en lærebog. Det er en fundamentalt defekt programmeringsmodel, hvor konsekvensen af en enkelt manglende grænsecheck kan være fuldstændig systemkompromittering. Som sikkerhedsprofessionel er det essentielt at forstå angrebet fra grunden – fra stakkens opbygning til Return Address-overskrivning – for at kunne evaluere forsvar korrekt.
Det moderne forsvar er lag på lag: stack canaries, ASLR, NX/DEP, sikre biblioteksfunktioner og i stigende grad memory-safe sprog. Ingen af disse er en sølvkugle alene, men tilsammen gør de angreb markant sværere og mere upålidelige.
Kilder
- OWASP: Buffer Overflow – OWASPs grundlæggende beskrivelse af buffer overflow-sårbarheder og kategorierne.
- NIST: Understanding Buffer Overflow Vulnerabilities – NISTʼs tekniske gennemgang af overflowklassen.
- Aleph One: Smashing The Stack For Fun And Profit – Det klassiske, originale essay fra Phrack Magazine (1996), der systematisk forklarer stack-based buffer overflows.
- Microsoft MSRC: Data Execution Prevention – Microsoftʼs dokumentation af DEP.
- The Rust Programming Language: Memory Safety – Forklaring af Rusts ownership-model, der eliminerer buffer overflows per design.
> Quiz: Test din viden
1. Hvilken hukommelsesadresse overskrives ofte i et klassisk stack overflow-angreb?
2. Hvilken beskyttelse indsætter en canary-værdi mellem buffer og kontrol-data?
3. Hvad betyder DEP/NX i praksis for stak og heap?
4. Hvilket programmeringssprog nævnes som memory-safe alternativ i artiklen?