Der Kontext

Man sieht "Perl" und "libFuzzer" selten in einem Satz. Perl ist eine leistungsstarke, ausgereifte Sprache, die für ihre Textverarbeitungsfähigkeiten, extreme Flexibilität und hohe Informationsdichte bekannt ist. Da sie interpretiert wird, verwenden viele Anwendungen Module, die in C geschrieben sind für leistungskritische Aufgaben (unter Verwendung der XS-Schnittstelle von Perl). Wie wir alle wissen, ist C-Code (leider) ein bevorzugtes Ziel für Pufferüberlauffehler, insbesondere in Parsern.

Das machte mich neugierig, als ich über das Modul JSON::XS stolperte. Wie schwer wäre es, einen modernen C/C++-Fuzzer wie libFuzzer auf die C-Bestandteile anzuwenden?

Es stellte sich heraus, dass dies nicht nur möglich war, sondern auch zur Entdeckung eines Heap-Buffer-Overflows führte, der durch eine speziell gestaltete Eingabezeichenfolge ausgelöst werden konnte.

Im Folgenden beschreibe ich, wie ich es gemacht habe.

Das Setup: Brückenschlag zwischen C und Perl

Die größte Herausforderung ist direkt ersichtlich: libFuzzer ist ein C/C++-Tool. Es funktioniert, indem es wiederholt eine einzige Funktion, LLVMFuzzerTestOneInput(), aufruft und ihr mutierte Daten zur Verfügung stellt. Mein Ziel, JSON::XS, ist ein Perl-Modul.

Mein erster Gedanke war, einfach die zugrunde liegende C-Implementierung (die Datei XS.xs) direkt zu testen. Nach Durchsicht des C-Codes wurde jedoch klar, dass dies nicht funktionieren würde. Der Code ist stark mit der Perl-Umgebung verflochten. Er enthält Perl-Header (perl.h, EXTERN.h) und ruft viele Perl-Funktionen auf.

Das Entfernen dieser Abhängigkeiten, um eine eigenständige C-Funktion zu erstellen, wäre schwierig und fehleranfällig. Der Testfall würde sich von der ursprünglichen Implementierung unterscheiden, wodurch möglicherweise echte Fehler verdeckt oder neue eingeführt würden.

Daher entschied ich mich, das Modul innerhalb eines laufenden Perl-Interpreters zu testen. Das bedeutet, dass meine Implementierung nicht nur das Modul, sondern auch den gesamten Perl-Interpreter einbetten und im Grunde genommen in eine C-Funktion einbinden muss.

Dieser sogenannte Fuzz-Harness ist ein kleines C-Programm, das in diesem Fall drei Aufgaben erfüllt:

  1. Initialisiert einen eingebetteten Perl-Interpreter.
  2. Lädt ein einfaches Perl-Wrapper-Skript zur Fehlerbehandlung.
  3. Akzeptiert Rohdaten von libFuzzer und leitet sie an die zu testende Perl-Funktion weiter.

Bevor ich jedoch beginnen konnte, war ein zusätzlicher Schritt erforderlich.

Schritt 1: Der Perl-Wrapper zur Fehlerbehandlung

Warum benötige ich einen "zusätzlichen Schritt" mit einem Perl-Wrapper?

Der Fuzzer generiert Tausende von Eingaben, von denen die meisten ungültiges JSON sind. Die Funktion decode_json würde für diese Eingaben korrekt einen Fehler (eine Ausnahme) melden. Wenn ich diese Ausnahmen nicht behandle, unterbrechen sie den Testlauf und der Fuzzer kann nicht fortfahren. Mich interessieren nur Abstürze aufgrund von Speicherverletzungen, nicht einfache Parsing-Fehler.

Um dieses Problem zu lösen, habe ich ein kleines Perl-Skript (json_eval.pl) verwendet, das den Aufruf in einen eval-Block einbindet. Dieser fungiert als einfacher Ausnahmehandler und unterdrückt die erwarteten Fehler.

use JSON::XS;
sub do_json {
    eval {
        decode_json("@_");
    } or do {
        return;
    };
}

Dieses Skript stellt eine neue, sichere Funktion namens do_json bereit, die ich aus dem C-Harness aufrufen kann.

Schritt 2: Das C-Harness zum Einbetten von Perl

Nun zum Hauptprogramm in C. Zunächst muss der eingebettete Perl-Interpreter initialisiert und die Bereinigung sichergestellt werden. Ich verwende die Hilfsfunktionen __attribute__((constructor)) und __attribute__((destructor)), damit dies automatisch und nur einmal beim Start ausgeführt wird.

Dieser Code (basierend auf der perlembed-Dokumentation) weist einen Interpreter zu, analysiert das Skript json_eval.pl und führt es aus. Die Funktion xs_init ist gemäß der Dokumentation definiert und hier nicht aufgeführt.

#include <EXTERN.h>
#include <perl.h>

EXTERN_C void xs_init (pTHX);

static PerlInterpreter *my_perl;

// Wird einmal beim Start des Programms aufgerufen.
__attribute__((constructor))
static void init_perl_fuzzer() {
    char *my_argv[] = {"", "json_eval.pl"};
    my_perl = perl_alloc();
    perl_construct(my_perl);
    perl_parse(my_perl, xs_init, 2, my_argv, (char **) NULL);
    perl_run(my_perl);
}

// Wird einmal beim Beenden des Programms aufgerufen.
__attribute__((destructor))
static void destroy_perl_fuzzer() {
    perl_destruct(my_perl);
    perl_free(my_perl);
}

Als Nächstes benötigte ich eine C-Funktion, um die Perl-Funktion do_json aufzurufen. Dazu muss die Perl-C-API verwendet werden, um den Stack einzurichten, die JSON-Zeichenkette als Argument zu übergeben und den Aufruf auszuführen.

static void PerlJson(const char *const json, const STRLEN len) {
    dSP;
    ENTER;
    SAVETMPS;
    PUSHMARK(SP);
    // Fuzzer-Daten auf den Perl-Stack schieben
    XPUSHs(sv_2mortal(newSVpvn(json, len)));
    PUTBACK;
    // Die Funktion "do_json" aufrufen, Ergebnis verwerfen (G_EVAL)
    call_pv("do_json", G_EVAL);
    SPAGAIN;
    PUTBACK;
    FREETMPS;
    LEAVE;
}

Schließlich habe ich die von libFuzzer benötigte Funktion LLVMFuzzerTestOneInput implementiert. Diese ist jetzt recht einfach: Sie schiebt die Daten aus dem Fuzzer direkt in meine PerlJson- Funktion.

int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) {
    char *json = (char *) Data;
    PerlJson(json, Size);
    return 0;
}

Kompilieren und Instrumentieren

Ich war fast fertig, dachte ich. Der finale Schritt bestand darin, mein Harness zu kompilieren und zu verknüpfen. Die richtigen Compiler-Flags für eine eingebettete Perl-Anwendung zu finden, ist knifflig, aber Perl hat dafür ein Dienstprogramm, das mir 90 % der Arbeit abnimmt:

perl -MExtUtils::Embed -e 'print(ccopts(), ldopts())'

Ich habe die Fuzzer-Flags von Clang (-fsanitize=fuzzer,address) hinzugefügt und den Harness kompiliert.

Als ich es zum ersten Mal ausführte, stieß ich jedoch auf ein großes Problem: Die Leistung war miserabel. Der Fuzzer lief unglaublich schnell, aber er untersuchte den Zielcode überhaupt nicht.

Der Grund: Der C-Harness wurde mit Instrumentierung kompiliert, aber der Perl-Interpreter selbst und das JSON::XS-Modul (der Code, den ich eigentlich testen wollte) nicht. Der Fuzzer arbeitete im Wesentlichen "blind", ohne Feedback zur Codeabdeckung vom Ziel.

Um dies zu beheben, habe ich Perl selbst und das Modul mit denselben Fuzzer-Instrumentierungsflags neu kompiliert.

Nachdem ich die Dokumentation zum Erstellen von Perl durchgesehen hatte, habe ich es wie folgt konfiguriert:

./Configure -de -Dcc=clang -Dusedevel -Doptimize=-g -O1 \
        -Accflags=-fsanitize=fuzzer-no-link,address \
        -Aldflags=-fsanitize=fuzzer-no-link,address

Nach der Kompilierung und Installation dieses benutzerdefinierten Perls habe ich JSON::XS neu installiert. Da die Compiler-Flags in der Perl-Installation gespeichert sind, wurde das Modul automatisch mit derselben Instrumentierung kompiliert.

Nun war der gesamte Stack (libFuzzer, Test-Harness, Perl-Interpreter und der JSON::XS-C-Code) vollständig instrumentiert. Die Fuzzing-Leistung stieg sofort an, und der Fuzzer konnte den Zielcode effektiv untersuchen.

Die Entdeckung

Nachdem die Einrichtung und Kompilierung abgeschlossen war und einige generische JSON-Dateien als Korpus verwendet wurden, war der Start des Fuzzers ganz einfach:

./fuzzer -jobs=10 -workers=10 corpus

Nach weniger als einer Stunde fand der Fuzzer einen Absturz.

Ausgabe des AdressSanitizers für den beobachteten Pufferüberlauf

AddressSanitizer (ASan) erkannte einen Heap-Pufferüberlauf. Der Fuzzer hatte eine bestimmte Kombination von Bytes generiert, die, wenn sie an decode_json übergeben wurden, dazu führten, dass der zugrunde liegende C-Code Speicher las, den er nicht hätte lesen dürfen.

libFuzzer speicherte die genaue Absturzeingabe hilfreich in einer Datei, sodass ich den Absturz leicht reproduzieren und die Schwachstelle bestätigen konnte. Da für diese Schwachstelle keine Voraussetzungen erforderlich sind, können Anwendungen Verfügbarkeitsrisiken ausgesetzt sein, wenn sie JSON-Eingaben akzeptieren und einen anfälligen JSON-Parser verwenden.

Wenn wir uns die gemeldete Zeile ansehen, finden wir eine Float-Parsing-Funktion mit der folgenden while-Schleife:

while (((U8)*s - '0') < 10)
    ++s;

Die while-Schleife ist so konzipiert, dass sie eine Gleitkommazahl überspringt. Sie liest die Zeichenfolgen durch, überprüft jedes Zeichen, ob es eine Ziffer ist, und erhöht den Zeiger, bis sie ein Nicht-Ziffernzeichen findet. Da die Schleife den Zahlenbereich nicht korrekt überprüft, kann der Lesezugriff die Zeichenkette überschreiten. Daher besteht die wichtigste Korrektur darin, zu überprüfen, ob das Zeichen tatsächlich zwischen 0 und 9 liegt.

Zeitplan der Offenlegung

Die Sicherheitslücke wurde in JSON::XS Version 4.04 behoben. Das CPAN-Sicherheitsteam reagierte sehr schnell und koordinierte umgehend die Behebung des Problems und die Verteilung des Patches. Es stellte außerdem fest, dass zwei weitere Module von derselben Sicherheitslücke betroffen sind. Da die Codebasis mit anderen Projekten geteilt wurde, wurde der Patch zusätzlich auf die Module JSON-SIMD und Cpanel::JSON::XS angewendet. Die Autoren der Module reagierten nach der ersten Kontaktaufnahme ebenfalls sehr schnell und veröffentlichten eine neue Version ihrer Module mit dem Patch.

  1. 15.07.2025: Meldung der Schwachstelle an den Modulentwickler.
  2. 03.09.2025: Meldung der Sicherheitslücke an das CPAN-Sicherheitsteam.
  3. 04.09.2025: Sicherheitslücke vom CPAN-Team und den Modulautoren bestätigt und Patches vorbereitet.
  4. 08.09.2025: Patches veröffentlicht und Distributionen benachrichtigt.

Kontakt

Wir unterstützen Ihr Unternehmen bei der Konfiguration und dem Testen Ihrer Software.