Zum Inhalt springen

Modernes Fuzzing in 2026: Snapshots, Frameworks und Fuzzing von dem, was du nicht konntest

· 13 min read · default
cybersecurityfuzzingvulnerability-researchreverse-engineeringtestingsecurity

Fuzzing hat ein Reputationsproblem: Viele Ingenieure stellen es sich immer noch vor als "werfe zufällige Bytes auf ein Programm, bis es abstürzt". Diese Beschreibung war grob gesagt genau im Jahr 1990 und ist seit einem Jahrzehnt irreführend. Modernes Fuzzing ist Coverage-gesteuerbar — der Fuzzer instrumentiert das Ziel, beobachtet, welche Code-Pfade jeder Input erreicht, und lenkt Mutation zu Inputs, die neue Gebiete erkunden. Diese Feedback-Schleife ist der Unterschied zwischen zufälliges Poked an einem Parser und seinem systematischen Gehen durch seinen State-Space, und es ist warum Fuzzing jetzt Tausende echte CVEs pro Jahr in Software findet, die von Experten überprüft wurde.

Die 2026-Landschaft ist jedoch weit über einem einzelnen Tool hinausgegangen, und die interessanten Entwicklungen handeln davon, Ziele zu erreichen, die konventionelles Fuzzing nicht konnte: Betriebssystem-Kernel, Code, der hinter teuerem Setup begraben ist, und Anwendungen, deren Input-Struktur Byte-Mutation besiegt. Diese Anleitung deckt diese Landschaft — die Coverage-gesteuer-Klassiker, syzkaller für Kernel, Snapchange für Snapshot-Fuzzing und LibAFL um den Fuzzer zu bauen, den dein Ziel braucht — plus cargo-fuzz und honggfuzz für alltägliche Arbeiten.

Warum Coverage-Leitung alles verändert hat

Der Mechanismus ist Verständnis wert, weil er erklärt, wofür Fuzzing gut ist und wo es stall ist. Ein Coverage-geleiteter Fuzzer kompiliert das Ziel mit Instrumentierung, die aufzeichnet, welche Kanten des Control-Flow-Graphen eine Ausführung berührte. Er unterhält einen Corpus von Inputs, und wenn ein mutierter Input eine Kante erreicht, die kein vorheriger Input erreichte, wird dieser Input als interessant beurteilt und zum Corpus hinzugefügt, um weiter mutiert zu werden. Über Millionen Iterationen sammelt der Corpus Inputs an, die zusammen tiefe, ungewöhnliche Pfade trainieren — einschließlich Pfade, die kein Mensch je einen Test schrieb.

Dies erzeugt Verhalten, das fast intelligent aussieht. Angesichts eines Corpus, das mit einem gültigen PNG gesät ist, wird ein Fuzzer die Chunk-Struktur entdecken, dann gültige Chunk-Typen, dann die Parsing-Branches für jeden Typ, progressiv Inputs bauen, die tiefer in den Decoder vordringen. Es versteht "nie" PNG; es behält einfach, was Coverage bewegte.

Das Corollar ist, wo Fuzzing stall: Hard Checks, es kann nicht vorbeiziehen. Ein Magic-Constant-Vergleich, eine Checksum oder eine kryptographische Signatur erstellt eine Wand — zufällige Mutation wird im Wesentlichen nie die rechten 8 Bytes produzieren, sodass alles hinter dieser Prüfung unerforschert bleibt. Die praktischen Antworten sind Säen des Corpus mit gültigen Inputs, Liefern eines Wörterbuchs von Magic-Werten oder Patchen von Checksums in einem Fuzz-Build. Ein Coverage-Plateau als "Ich habe eine Wand getroffen" zu erkennen, anstatt "Es gibt keine mehr Bugs", ist einer der nützlichsten Instinkte in dieser Arbeit.

Kernel-Fuzzing: syzkaller

Betriebssystem-Kernel sind ein feindselig Ziel für konventionelle Fuzzer. Der Input ist nicht eine Datei, sondern eine Sequenz von Syscalls mit interdependenten Argumenten — ein Datei-Deskriptor von open muss zu read fließen, und die meisten zufälligen Syscall-Sequenzen versagen sofort mit EINVAL. Crashes nehmen die ganze Machine aus anstelle eines Prozesses, und Coverage muss aus Kernel-Raum gesammelt werden.

syzkaller löst alle drei. Es beschreibt Syscalls in einer deklarativen Sprache (syzlang), sodass es plausible Sequenzen mit richtig-gelösten, interdependenten Argumenten generieren kann; es läuft Ziele in wegwerfbaren VMs, sodass Crashes überlebbar sind und automatisch gesammelt werden; und es nutzt KCOV für Kernel-Coverage-Rückmeldung plus KASAN, um Speicherfehler zu erfassen, die sonst stille Korruption wären. Googles syzbot läuft es kontinuierlich gegen Linux und hat Tausende von Bugs gemeldet.

Die Lektion verallgemeinert sich über Kernel hinaus: syzkaller funktioniert, weil jemand Wissen von der Input-Struktur in Beschreibungen kodierte. Wenn dein Ziel Input mit Grammatik hat, unterrichtet der Fuzzer dieser Grammatik schlägt Raw-Byte-Mutation um eine breite Marge aus. Die entsprechenden Kosten sind real — Erweiterung von syzlang für ein unter-getestetes Subsystem ist echte Arbeit, und es ist auch der höchste Wertbeitrag, den die meisten Menschen zu Kernel-Fuzzing machen können.

Snapshot-Fuzzing: vorbeiziehen am Setup

Die zweite Grenze sind Ziele, wo der interessante Code hinter teurer Initialisierung sitzt. Betrachte ein Datenbank-Query-Parser zu fuzzen: Jede Iteration würde den Server starten, Storage initialisieren, Authentifizieren und eine Sitzung etablieren müssen, bevor ein einzelner Query geparst wird. Bei vielleicht zehn Iterationen pro Sekunde ist Coverage-gesteuertes Fuzzing hoffnungslos — die Technik braucht Tausende.

Snapshot-Fuzzing invertiert dies. Du laifst das Ziel einmal zum Moment von Interesse, Snapshot den ganzen Machine State, und dann stelle diesen Snapshot für jede nachfolgende Iteration wieder her. Alle Setup-Kosten werden einmal bezahlt. Snapchange (von AWS) implementiert dies mit KVM: Du erfasst einen Snapshot mit QEMU, schreibe einen kleinen Rust-Harness, der beschreibt, wo Input zu injizieren und wann eine Iteration zu enden ist, und es spielt von diesem State mit sehr hohen Raten ab.

Dies entsperrt Kategorien, die früher unpraktisch waren: Stateful-Netzwerk-Protokolle genutzt mid-session, Code hinter Authentifizierung, Hypervisor und Kernel-Code und jede Anwendung mit schwerem Startup. Der Trade ist Aufwand — du schreibst einen Rust-Fuzzer anstelle eines Befehls zu laufen, und du musst das Ziel Memory-Layout gut genug verstehen, um Input richtig zu injizieren. Es ist eine Spezialisier-Technik, die zahlt sich genau ab, wenn die Alternative nicht das Ziel zu fuzzen überhaupt ist.

Frameworks: Baue den Fuzzer, den dein Ziel braucht

Die dritte Entwicklung ist philosophisch. AFL++ und libFuzzer sind ausgezeichnet bei dem, wofür sie entworfen wurden und holprig, wenn dein Ziel nicht passt — ein benutzerdefiniertes Binärprotokoll, ein emuliertes Firmware-Image, ein Input, das ein Baum anstelle eines Buffer ist. Historisch du gebogtest das Tool, normalerweise schlecht.

LibAFL, vom AFL++-Team, behandelt einen Fuzzer als zusammensetzbare Teile: Beobachter, die Daten aufzeichnen, Rückmeldungen, die Interessanth beurteilen, Mutatoren, die Inputs transformieren, Scheduler, Stufen und Executors. Du montierst die Kombination, die dein Ziel braucht, definierst dein eigenen Input-Typ und Mutatoren, wenn der Input strukturiert ist, wähle einen Executor (in-process, Forkserver, QEMU-Emulation, Frida-Instrumentierung, Snapshot) und erhalte Multi-Core-Skalierung kostenlos.

Der ehrliche Rahmen ist, dass dies ein größeres Engagement als das Laufen eines Tools ist, sodass die Sequenz wichtig ist: Starten Sie mit cargo-fuzz für Rust oder honggfuzz/AFL++ für natürliche Ziele und wechseln zu LibAFL nur, wenn du spezifisch artikulieren kannst, warum sie nicht passen. "Der Input ist ein Protokoll-State-Machine und Byte-Mutation produziert niemals eine gültige zweite Nachricht" ist so ein Grund; "Ich möchte, dass es schneller ist" normalerweise nicht.

Alltägliches Fuzzing, das Teams tatsächlich aufrechterhalten

Die meisten Wert, für die meisten Teams, kommt aus unattraktiven kontinuierliches Fuzzing von Parsern und nicht-vertrauenswürdigen Input-Handlern. cargo-fuzz macht dies fast reibungslos für Rust: Schreibe eine Funktion, die &[u8] nimmt (oder besser, ein getypter Wert via der arbitrary-Crate) und laufe einen Befehl. honggfuzz ist ähnlich einfach für Nativ-Code und fügt Hardware-basierte Coverage via Intel PT/BTS hinzu, was dir ermöglicht, Binärdateien zu fuzzen, die du nicht erneut kompilieren kannst — wertvoll für Closed-Source-Abhängigkeiten.

Zwei Praktiken trennen Teams, die Wert von Fuzzing bekommen, von Teams, die es nach einer Woche aufgeben. Erstens, Säe den Corpus mit echten gültigen Inputs; ein Fuzzer, der aus einem leeren Corpus startet, verbringt enorme Zeit damit, grundlegende Format-Gültigkeit umzugestalten, die du ihm hätte geben können. Zweitens, Laufe kontinuierlich und behandle Befunde als Tests: Konvertiere jeden minimierten Crash zu einem Regressions-Test, sodass er behoben bleibt, und lass den Corpus zwischen Läufen bestehen, sodass der Fortschritt sich ansammelt. Fuzzing ist nicht ein Nachmittag-Audit; es ist ein Hintergrund-Prozess, der weiterhin Dinge findet, wenn Code ändert.

Sanitizers verdienen eine Erwähnung, weil sie Wirksamkeit multiplizieren. AddressSanitizer verwandelt stumme Memory-Korruption in einen unmittelbaren, diagnostizierbaren Crash, und UBSan erfasst Undefined Behavior, das sonst als mysteriöse Miskompilation später erscheinen könnte. Fuzzing ohne Sanitizer findet nur die Bugs, die zufällig auf ihren eigenen abstürzen — typischerweise ein kleiner Bruchteil von dem, was tatsächlich da ist.

Triage: Die Arbeit, die anfängt, wenn der Crash eintrifft

Einen Crash zu finden ist der Anfang, und Teams unterschätzen routinmäßig den Aufwand zwischen "der Fuzzer stoppte" und "ein Entwickler kann dies beheben." Vier Schritte machen den Unterschied zwischen einem nützlichen Report und einem ignorierten.

Minimiere den Input. Ein Crashing-Input von einem Fuzzer ist typischerweise voll irrelevanter Bytes, die nur überlebte, weil nichts sie entfernte. Jeder ernst-gemeinter Fuzzer schifft einen Minimizer (cargo fuzz tmin, afl-tmin, syzkallers syz-repro), und das Laufen verwandelt ein 4KB-Blob in eine Handvoll Bytes, die den tatsächlichen Trigger isolieren. Dies wichtig enorm für den Entwickler, der es verstehen muss.

Dedupliziere. Ein Fuzzer, das über Nacht läuft, wird den gleichen Bug Dutzende Male durch verschiedene Inputs melden. Gruppierung nach Crash-Ort und Stack-Signatur verwandelt 200 Crashes in sechs unterschiedliche Bugs. Ohne diesen Schritt sieht Triage unmöglich teuer aus und Leute aufgeben.

Bewerte Ausnutzbarkeit, sorgfältig. Nicht jeder Crash ist eine Sicherheitslücke. Ein Null-Pointer-Dereference in einem Parser ist normalerweise eine Denial-of-Service; ein Heap-Buffer-Overflow mit Angreifer-kontrollierte Länge ist potenziell viel schlimmer. Sanitizer-Output hilft enorm hier — ASan sagt dir die Art von Speicherfehler, die Größen und beide Allocation und Access-Stacks. Widerstehe der Versuchung, alles Kritisches zu bezeichnen, weil ein Team, das aufgeblasen Schweregrade erhält, aufhört, den Reports zu vertrauen.

Konvertiere zu einem Regressions-Test. Der minimierte Input wird ein Unit-Test, der neben dem Fix committed ist. Das ist, was den Bug behoben hält und was Fuzzing über Zeit ansammeln macht, anstatt den gleichen Problemen nach einem Refaktor umzuentdecken.

Die Teams, die anhaltenden Wert von Fuzzing bekommen, sind die, die diese Pipeline einmal bauen, nicht die, die die meisten Crashes finden.

Wähle, wo du anfängst

Die Entscheidung folgt das Ziel. Für Rust-Code, verwende cargo-fuzz und verwende arbitrary um getypte APIs zu fuzzen, anstelle nur Byte-Parser. Für native Userspace-Binärdateien mit Quelle, honggfuzz oder AFL++ mit Sanitizer. Für Binärdateien ohne Quelle, sykkallers Hardware-Rückmeldung oder AFL++'s QEMU-Modus. Für OS-Kernel, syzkaller, und betrachte das Erweitern von syzlang für das Subsystem, das du magst. Für Code hinter teurem Setup oder Zustandsvollem Session, Snapchange oder ein Snapshot-fähig LibAFL-Konfiguration. Und für Inputs mit echter Struktur — Protokolle, ASTs, Dateiformate mit Grammatik — entweder ein Grammatik-bewusster Mutator oder ein benutzerdefinierten LibAFL-Fuzzer, weil Byte-Mutation früh Plateau wird.

Über alle hinweg, die gleiche Disziplin trifft an: Säe gut, läufe mit Sanitizer, läufe kontinuierlich, minimiere Crashes und konvertiere Befunde zu Regressions-Tests. Das Tool wirkt weniger als ob die Schleife hält laufend.

Das Fazit

Fuzzing in 2026 ist eine systematische Sicherheitslücken-Discovery-Disziplin, die auf Coverage-Rückmeldung gebaut ist, und seine Grenze ist das Erreichen von Zielen, die früher out-of-Scope waren. syzkaller fuzzt Kernel durch Encoding Syscall-Struktur und Laufen in wegwerfbaren VMs; Snapchange nutzt KVM-Snapshots um Code Fuzzing zu begraben, hinter teuerem Setup; LibAFL lässt dich einen Fuzzer zu einem ungewöhnlichen Ziel matched bauen, anstatt ein monolithisches Tool zu beugen; und cargo-fuzz und honggfuzz machen alltägliches kontinuierliches Fuzzing günstig genug zu tatsächlich aufrechterhalten. Starten Sie mit dem leichten Tool für deine Sprache, Säen Sie den Corpus mit echten Inputs, aktivieren Sie immer Sanitizer, behandeln Sie ein Coverage-Plateau als eine Wand zum Bauen vorbei, anstelle eines All-Clear und verwandeln jeden Crash zu einem Test.

Referenzen und Ressourcen

Tools

Hintergrund und Analyse

Zugehörige 1337skills Cheatsheets