Ein PDF ist kein Dokument, das Sie einfach öffnen. Es ist ein kleines Programm, das Sie ausführen. Jede eingebettete Schriftart ist ein stapelbasierter Interpreter, der auf Charstrings wartet. Jedes Bild ist ein Decoder, der mit Breiten-, Höhen- und Bittiefenfeldern gefüttert wird, die die Datei gewählt hat, und jeder Stream kommt in Filter verpackt an, deren Parameter die Datei festgelegt hat. Keine dieser Zahlen gehört Ihnen. Sie stammen von demjenigen, der die Datei erstellt hat - bei realer Arbeit beispielsweise die Rechnung eines Kunden oder ein Anhang von einem unbekannten Absender. Die Decoder, die diese Bytes in Pixel und Glyphen umwandeln, sind die Angriffsfläche, und ein Parser, der seinen Eingaben vertraut, ist nur eine fehlerhafte Datei von einem Absturz oder Schlimmerem entfernt
PDFlibPas hat einen Härtungsprozess durchlaufen, bei dem der gesamte Decodierungspfad als feindselig behandelt wurde - über die Schriftprogramme (TrueType, Type1, CFF und die CMap-Tabellen), die Bilddecoder (PNG, GIF, TIFF, JBIG2 sowie CCITT Gruppe 3 und Gruppe 4) und die Stream-Filter (LZW, ASCII85 und die Flate-Prädiktoren). Im Folgenden werden fünf geschlossene Schwachstellenklassen beschrieben, die jeweils in dem spezifischen Delphi-Verhalten begründet liegen, das sie ermöglichte. Sie sind in aktuellen Versionen behoben, und dieselben Muster treten in jedem Pascal-Code auf, der nicht vertrauenswürdige Eingaben parst
Ein Ganzzahlüberlauf, der Ihnen einen zu kleinen Puffer übergibt
Der klassische Speichersicherheitsfehler in einem Bilddecoder ist ein Produkt der Abmessungen, das überläuft. Ein Decoder liest Breite, Höhe, Komponentenanzahl und Bittiefe, multipliziert diese zur Dimensionierung seiner Ausgabe, weist entsprechend viele Bytes zu und schreibt dann das Bild in seinen tatsächlichen Abmessungen. Wenn die Multiplikation in 32-Bit-Arithmetik durchgeführt wird, kann das Produkt zu einem kleinen Wert überlaufen, selbst wenn jeder einzelne faktor in einem vernünftigen Bereich liegt. Die Zuweisung ist zwar erfolgreich, fällt aber viel zu klein aus, und die Decodierung läuft über das Ende hinaus. Dies ist CWE-190 (Ganzzahlüberlauf), was einen Schritt später zu einem Heap-Out-of-Bounds-Schreibvorgang (CWE-787) führt
Der gemeinsame Bildpfad hat bereits jede Dimension auf 65535 begrenzt; die eigenständigen Decoder haben diese Begrenzung nicht alle übernommen. Ein Ausdruck für die Zeilen-Bytes mal Höhe wie ByteCount * FHeight oder ein Pro-Pixel-Ausdruck wie FWidth * Components * BitDepth ist in Delphi ein 32-Bit-Produkt, wenn beide Operanden 32-Bit-Ganzzahlen sind - unabhängig davon, wie breit die Variable ist, der Sie das Ergebnis zuweisen. Eine Breite und eine Höhe von 60000 sind für einen großen Scan jeweils plausibel, aber ihr Produkt in Bytes übersteigt den vorzeichenbehafteten 32-Bit-Bereich, und die Länge fällt klein aus. Dieselbe Falle steckte im ZLib-Prädiktor-Stride: BitsPerComponent * Colors * Columns
Die Behebung besteht darin, mindestens einen Operanden zu Int64 zu machen, sodass der gesamte Ausdruck in 64-Bit ausgewertet wird. Anschließend wird das Ergebnis mit MaxInt verglichen und die Datei abgelehnt, bevor wieder verkleinert wird, um SetLength aufzurufen
// Reject before allocating, not after writing.
// Evaluate the product in Int64 so it cannot wrap at 32 bits.
RowBytes := (Int64(FWidth) * Components * BitDepth + 7) div 8;
if (RowBytes <= 0) or (RowBytes * FHeight > MaxInt) then
Exit; // hostile or unsupportable dimensions; refuse the image
SetLength(Buffer, RowBytes * FHeight);
Was dies zu einem Delphi-Problem und nicht zu einem allgemeinen Problem macht, ist das stille Verkleinern. Das Zuweisen eines zu breiten Ausdrucks an ein 32-Bit-Ziel ist eine legale Konvertierung, vor der der Compiler standardmäßig nicht warnt, und die Bereichsprüfung erfasst keinen Überlauf, der stattfindet, bevor der Wert überhaupt als Index verwendet wird. Belässt man das Produkt bei 32 Bit, liefert die Sprache stillschweigend eine Länge, die darüber hinwegtäuscht, wie viel Speicher die Decodierung gleich berühren wird
Ein Feldtyp, der das Auslösen einer Schutzbarriere unmöglich macht
Eine TIFF-Datei ist eine Kette von Bilddateiverzeichnissen (Image File Directories, IFDs), von denen jedes den Byte-Offset des nächsten trägt. Eine bösartige Datei kann diese Kette auf sich selbst zurückverweisen lassen, und ein Reader, der sie ohne Abbruchbedingung durchläuft, läuft ewig. Das ist CWE-835, eine unendliche Schleife, die durch vom Angreifer kontrollierte Eingaben gesteuert wird. Die Verteidigung dagegen ist ein Zähler, der stoppt, sobald er eine Grenze überschreitet, die keine legitime Datei erreichen würde
Der Seitenzähler war als Word deklariert, das in Delphi Werte von 0 bis 65535 aufnehmen kann. Die Schleife trug eine Abbruchbedingung der Form „Stoppe, wenn die Seitenzahl 65535 überschreitet". Dies liest sich als korrekt, bis man bemerkt, dass der Operand und der Schwellenwert dieselbe Obergrenze teilen. Ein Word kann niemals größer als 65535 sein, sodass der Vergleich strukturell immer falsch ist: Wenn der Zähler 65535 erreicht, setzt der nächste Inkrementierungsschritt ihn auf 0 zurück. Der Schutz sieht nie einen Wert über der Obergrenze, und eine kreisende IFD-Kette hält den Reader in einer Endlosschleife
Die Behebung bestand darin, das Feld zu erweitern, damit die Schutzbarriere einen Wert ausdrücken kann, den der Zähler tatsächlich halten kann. Da TPDFTIFF.FPageCount als Integer deklariert wurde, wird derselbe Vergleich FPageCount > 65535 erreichbar, die Schleife bricht ab, und die öffentliche Eigenschaft PageCount änderte ihren Typ passend, ohne einen Aufrufer zu beeinträchtigen. Wann immer eine Grenzprüfung die Form Value > MaxValueOfType(Value) hat und der Operand bereits genau auf dieses Maximum typisiert ist, die Bedingung ist ein konstantes Falsch: Erweitern Sie den Typ oder prüfen Sie auf Gleichheit mit dem Maximum, damit sie auslösen kann
Bereichsprüfung auf einem Hot-Path deaktiviert
Bei aktivierter Bereichsprüfung fügt Delphi bei jedem Array- und Stringindex eine Grenzprüfung ein. Das macht den Unterschied aus, ob ein außerhalb des Bereichs liegender Index einen abfangbaren ERangeError auslöst oder ob derselbe Index Speicher liest oder schreibt, der nicht zur Struktur gehört. Hot-Paths deaktivieren dies manchmal mit einer lokalen {$R-}-Direktive, was so lange vertretbar ist, wie die Indizes vertrauenswürdig sind
Der Listen-Accessor, auf den sich die Schriftarten-Interpreter stützen, TPDFlibStringList.Get, ist genau ein solcher Pfad. Unter Windows wird er mit deaktivierter Bereichsprüfung kompiliert und indiziert seinen Backing-Store direkt, sodass ein ungültiger Index kein Fehler, sondern ein direkter Speicherzugriff ist. Das ist in Ordnung, wenn der Index immer gültig ist. Es ist nicht mehr in Ordnung innerhalb eines CFF- oder Type2-Charstring-Interpreters, wo der Index aus der Datei stammen kann. Ein Charstring, der einen Operanden von einem leeren Stack holt, erzeugt einen Index von minus eins; ein um eins verschobener Glyphenbezeichner gegenüber der Glyphenanzahl indiziert einen Platz hinter dem Ende. Bei ausgeschalteter Bereichsprüfung werden beide zu einem echten Out-of-Bounds-Zugriff anstelle einer abfangbaren Ausnahme. Da die Plätze referenzgezählte AnsiString-Werte halten, kann ein Fehlzugriff auch den Referenzzähler eines Strings beschädigen
Die Härtung hat die Bereichsprüfung für den Hot-Path nicht wieder eingeschaltet. Sie hat die Indizes stattdessen vorab nachweisbar gültig gemacht: Bevor der Interpreter das oberste Element des Operanden-Stacks nimmt, prüft er, ob der Stack nicht leer ist, und jeder Indexschutz wurde als striktes „kleiner als" gegenüber der Anzahl geschrieben statt als ein „kleiner oder gleich", das den Off-by-One-Fehler zulässt. Die Direktive verlagert die Verantwortung für Grenzwerte vom Compiler auf Sie, und die entfernte Validierung muss an jedem Einstiegspunkt von Hand wieder eingeführt werden
Unbegrenzte Rekursion in einem Charstring-Interpreter
Ein Type2-Charstring kann eine Subroutine aufrufen, und eine Subroutine ist selbst ein Charstring, der eine andere aufrufen kann. So ermöglichen es die lokalen und globalen Subroutinen-Aufrufoperatoren der Datei zu entscheiden, wie tief es geht. Eine Subroutine, die sich selbst direkt oder über einen Zyklus aufruft, rekursiert endlos, bis der native Stack erschöpft ist und der Prozess stirbt. Das ist CWE-674, unkontrollierte Rekursion
Der Type1-Interpreter schützte sich bereits davor. Er trug einen Aufruftiefenzähler und eine Obergrenze, PLType1MaxCallDepth, und weigerte sich, tiefer abzusteigen, was das Tiefenlimit widerspiegelt, das die Type1-Spezifikation selbst nennt. Der später hinzugefügte und strukturell ähnliche Type2-Interpreter trug diesen Schutz nicht, und eine handgefertigte Schriftart mit einer Subroutine, die ihre eigene Nummer aufruft, läuft direkt durch die fehlende Prüfung in einen Stack-Überlauf
// The shape of the Type1 guard the Type2 path was missing.
// Track depth across nested calls and refuse to recurse past it.
Inc(CallDepth);
if CallDepth > PLType1MaxCallDepth then
Exit; // hostile self-referential subroutine; stop descending
// ... interpret the subroutine, then Dec(CallDepth) on the way out
Die Behebung bestand darin, dem Type2-Pfad dieselbe begrenzte Tiefe zu geben, die sein Type1-Geschwister bereits hatte. Jeder rekursive Abstieg über eine vom Angreifer kontrollierte Struktur (ob Schriftart-Subroutinen, ein verschachteltes Array oder eine Kreuzreferenzkette) benötigt eine Tiefenbegrenzung, die die Eingabe nicht aushebeln kann
Nicht initialisierter Speicher, der in die Ausgabe sickert
Die subtilste Schwachstelle schleuste Heap-Inhalte in entschlüsselte Ausgaben ein, und die Ursache ist eine Eigenschaft von SetLength, die man leicht vergisst. Wenn Sie einen AnsiString mit SetLength vergrößern, Delphi weist die Bytes zu, nullt sie jedoch nicht. Die neue Region enthält also das, was sich zuvor in diesem Heap-Speicher befand. Wenn jedes Byte anschließend geschrieben wird, spielt dies keine Rolle. Wenn ein Pfad jedoch einen Teil des Puffers unbeschrieben lässt und ihn dann als Daten zurückgibt, wandern diese veralteten Bytes mit dem Ergebnis ab. Das ist CWE-457 (Verwendung von nicht initialisiertem Speicher), und wenn das Ergebnis eine Vertrauensgrenze überschreitet, wird es zu einem Informationsleck
Der AES-CBC-Entschlüsselungspfad stieß genau auf dieses Problem. Der Ausgabepuffer wurde mit SetLength dimensioniert, und der Entschlüsseler verarbeitete den Chiffretext blockweise mit jeweils 16 Byte. Wenn die Länge des Chiffretexts kein Vielfaches von 16 war (eine Länge, die ein Angreifer wählen kann), wurde der abschließende Teilblock nie geschrieben. So behielten diese letzten Bytes den Heap-Inhalt, den SetLength hinterlassen hatte, und der Puffer wurde als entschlüsselter Klartext eines Dokumentobjekts zurückgegeben. Die Abhilfe besteht aus zwei Schutzmaßnahmen, von denen keine allein ausreicht: Der Einstiegspunkt für die Entschlüsselung lehnt nun jeden Chiffretext ab, dessen Länge kein Vielfaches der Blockgröße ist, und als zusätzliche Absicherung wird die Ausgabe vor der Verwendung mit FillChar gelöscht. So gibt jeder Pfad, der das Schreiben in einen Bereich versäumt, Nullen anstelle von Heap-Rückständen zurück
Was Ihnen nach dem Durchgang bleibt
Die fünf Schwachstellen sind unterschiedliche Fehler, aber sie ähneln sich. Eine Ganzzahlbreite, die ein produkt überlaufen lässt, ein Feldtyp, der eine Prüfung auf ein konstantes Falsch festlegt, eine deaktivierte Bereichsprüfung, bei der die Indizes nicht mehr sicher waren, eine Rekursion ohne Boden und ein Puffer, den die Sprache nicht nullen wollte. In jedem einzelner Fall tat Delphi genau das, was es definiert, weil die Sprache bietet Ihnen Arithmetik, die überläuft, stilles Verkleinern, abschaltbare Bereichsprüfungen, Rekursion ohne eingebautes Limit und Speicherzuweisungen, die nicht initialisiert werden. Das ist der Vertrag, und ein Pascal-Parser erfüllt ihn, indem er vier Dinge an jeder von der Datei kontrollierten Grenze von Hand verwaltet: Ganzzahlbreite, Bereichsprüfung, Rekursionstiefe und Pufferinitialisierung
Diese Schwachstellen sind in aktuellen PDFlibPas-Versionen behoben, der Engine für Delphi und C++Builder. Wenn Ihre Arbeit auch die Frage betrifft, wie eine Datei geschützt zu sein beansprucht, decken die begleitenden Notizen zur Prüfung von Verschlüsselung und Berechtigungen und zum PDF/A- und PDF/UA-Preflight die Analyseseite desselben Parsers ab. All dies wird innerhalb der PDFlibPas Delphi PDF Library zusammen mit den APIs zum Laden, Rendern und Signieren ausgeliefert, die an anderer Stelle auf diesem Blog behandelt werden