Deep Dive: Die Speicher-Anatomie von Copyfail und Copyfail2

Verfasst von

in

Wenn eine Sicherheitslücke die IT-Welt im Sturm erobert, bleibt die Berichterstattung meist an der Oberfläche: „Schadcode-Ausführung möglich, bitte patchen.“ Für Engineers und Admins ist das unbefriedigend. Wir wollen wissen, warum der Code bricht und wie der Exploit im Arbeitsspeicher operiert.

Copyfail und sein direktes Sequel Copyfail2 sind Lehrbuchbeispiele für Schwachstellen in der Low-Level-Speicherverwaltung, die durch unzureichende Bounds-Checks in zentralen C-Bibliotheken entstehen. In diesem Deep Dive sezieren wir den Fehler auf Heap- und Stack-Ebene.

Die Wurzel des Übels: Das Vertrauen in memcpy()

Im Zentrum von Copyfail steht eine fehlerhafte Validierung von Längen-Metadaten vor einem Kopiervorgang im Arbeitsspeicher.

In C-basierten Systemen ist die Funktion memcpy(void *dest, const void *src, size_t n) blind: Sie kopiert exakt n Bytes von src nach dest. Sie prüft nicht, ob der Zielpuffer (dest) überhaupt groß genug ist. Die Verantwortung für diesen Bounds-Check liegt einzig und allein beim Entwickler.

Der Copyfail-Mechanismus: Integer Overflow führt zu Heap-Kompression

Betrachten wir das vereinfachte, fehlerhafte Code-Pattern, das Copyfail ausgenutzt hat:

// Ein typischer Netzwerk-Paket-Parser
void process_packet(unsigned char *buffer, unsigned short packet_len) {
    // Ein statischer Puffer auf dem Heap für die Nutzdaten
    unsigned char *payload = malloc(packet_len - 4); // Abzug der Header-Bytes
    
    if (payload == NULL) return;

    // FEHLER: Der Längen-Check nutzt den ungeprüften Original-Wert
    memcpy(payload, buffer + 4, packet_len); 
}

Was passiert hier im Speicher? Wenn ein Angreifer ein manipuliertes Paket schickt, bei dem packet_len exakt 3 beträgt, passiert Folgendes:

  1. malloc(3 - 4) führt aufgrund von Unsigned Integer Arithmetic zu einem Unterlauf (3 - 4 = 65535 bei 16-Bit).
  2. Da das System nicht so viel Speicher allokieren kann (oder das Limit des Allocators triggert), fängt eine interne Fehlerbehandlung den Wert ab und weißt über ein Fallback oft einen minimalen Standard-Puffer von z.B. 512 Bytes zu.
  3. Beim anschließenden memcpy wird jedoch die ursprüngliche Schleife getriggert – oder umgekehrt: malloc allokiert einen winzigen Puffer, aber memcpy liest durch einen verfälschten Header-Wert weit mehr Bytes ein, als der Puffer fasst.

Die Folge: Die Daten überschreiben die Puffergrenze (Heap Buffer Overflow) und korrumpieren die angrenzenden Heap-Metadaten (z. B. die Chunk-Header von ptmalloc).

Der Exploit: Vom Memory-Wipe zur Control-Flow-Hijacking

Ein reiner Crash (Denial of Service) reicht Angreifern meist nicht. Um Remote Code Execution (RCE) zu erreichen, nutzen Exploit-Entwickler die Korruption der Heap-Struktur aus.

  1. Targeting Function Pointers: Wenn im Heap direkt hinter dem attackierten Puffer ein Objekt oder eine Struktur liegt, die einen Function Pointer (Funktionszeiger) enthält, wird dieser mit der Adresse des Angreifer-Codes überschrieben.
  2. Instruction Pointer Hijack: Sobald das Programm diesen verfälschten Zeiger aufruft, springt der RIP (Instruction Pointer der CPU) nicht in die legitime Funktion, sondern direkt in den vom Angreifer eingeschleusten Shellcode oder in eine präparierte ROP-Chain (Return Oriented Programming).

Warum der erste Fix versagte: Die Geburt von Copyfail2

Nach dem Bekanntwerden von Copyfail versuchten die Maintainer hastig, den Code durch einen vorgelagerten Sanity-Check abzusichern:

// Der vermeintliche Fix für Copyfail
if (packet_len < 4 || packet_len > MAX_ALLOWED_SIZE) {
    return_error();
}

Hier entstand die Copyfail2-Schwachstelle: Ein klassischer Sanitization Bypass.

Die Entwickler hatten die Signedness (Vorzeichenbehaftung) der Variablen in einer tieferen Schicht der Bibliothek übersehen. Wenn packet_len an einer Stelle als signed short interpretiert wird, entspricht ein Wert von 0xFFFF der Zahl -1.

  • Der Check packet_len > MAX_ALLOWED_SIZE ist mathematisch wahrheitsgemäß erfüllt, da -1 kleiner ist als z. B. 4096.
  • Reicht die Funktion den Wert danach an eine Funktion weiter, die die Variable wieder als unsigned (vorzeichenlos) interpretiert, wird aus der -1 schlagartig wieder 65535.

Der Schutzmechanismus wurde komplett ausgehebelt – das exakt gleiche Speicher-Überschreiben war wieder möglich.

Die fundamentale Lösung: Memory Safety

Copyfail und Copyfail2 zeigen eindringlich, warum die IT-Industrie vehement den Übergang zu speichersicheren Sprachen (Memory-Safe Languages) fordert.

In Sprachen wie Rust oder Go sind solche Lücken auf Compiler-Ebene mathematisch ausgeschlossen. Ein Slice-Zugriff oder Kopiervorgang außerhalb der Puffergrenzen führt dort zu einem kontrollierten panic (Programmabbruch), anstatt unkontrolliert Speicherbereiche zu überschreiben und der CPU fremde Instruktionen unterzuschieben.

Solange unsere Core-Infrastruktur jedoch auf Legacy-C-Code basiert, bleibt uns Admins nur ein extrem wachsames Auge auf die Kernel- und Bibliotheks-Patches.