Wenn Sie ein PDF signieren, denken Sie normalerweise, dass der Signierschlüssel etwas ist, das Sie kontrollieren. Er befindet sich in einer von Ihnen erstellten .pfx-Datei, die durch ein von Ihnen gewähltes Passwort geschützt ist. Der Code, der diese Datei liest, fühlt sich wie eine reine Datenleitung an, nicht wie eine Grenze. Diese Intuition ist falsch, sobald das Zertifikat nicht mehr Ihnen gehört. Ein Desktop-Tool, das den Benutzer eine beliebige .pfx-Datei auswählen lässt, ein Server, der ein hochgeladenes Zertifikat akzeptiert, oder ein Batch-Signierer, der Zertifikate über das Netzwerk erhält - sie alle übergeben vom Angreifer beeinflusste Bytes an einen Parser, bevor auch nur ein einziges Byte der Signatur erzeugt wird. Ein PKCS#12-Reader ist eine Angriffsfläche, genau wie ein Bilddecoder oder ein Schriftartenlader
Dieser Artikel beschreibt zwei echte Schwachstellen, die in diesem Reader vorhanden waren, beide auf dem Pfad zum Importieren eines Signierzertifikats. Keine davon ist exotisch. Beide beruhen auf derselben Ursache, die fast jeden Binär-Parser in Sprachen mit Ganzzahlen fester Breite betrifft: Einer Länge oder Anzahl aus der Datei wird einen Schritt zu weit vertraut. Eine führt zu einem Out-of-Bounds-Lesevorgang, die andere zu einem Prozess, der sich aufhängt, bis Sie ihn beenden
Wohin die Bytes reisen
Der Import einer .pfx-Datei zum Signieren eines Dokuments ist keine einzelne Operation, sondern eine kurze Pipeline, und jede Stufe parst Daten, die ein Angreifer geschrieben haben könnte. Der Container ist eine PKCS#12-Struktur gemäß Definition in RFC 7292, ein Nest von AuthenticatedSafe-Bags, die um eine verschlüsselte Hülle gewickelt sind, die den privaten Schlüssel enthält. Das Lesen bedeutet das Durchlaufen von ASN.1, das Ableiten eines Schlüssels aus dem Passwort, das Entschlüsseln und das anschließende Übergeben des wiederhergestellten RSA-Schlüssels an den Code, der die Signatur erstellt
In HotPDF sind diese Stufen verschiedenen Units zugeordnet. Die PKCS#12-Containerlogik befindet sich in HPDFPFX. Jedes Tag, jede Länge und jeder Wert, den sie berührt, wird vom ASN.1-Reader in HPDFASN1 decodiert. Die Schlüsselableitung und die PBES2-Entschlüsselung befinden sich in HPDFCrypt zusammen mit PBKDF2HMACSHA256. Wenn der Schlüssel wiederhergestellt ist, wandeln HPDFRSA und der CMS-SignedData-Builder in HPDFCMS ihn in die abgetrennte Signatur um, die im PDF eingebettet ist. Der öffentliche Einstiegspunkt, der die gesamte Kette steuert, ist ein einziger Aufruf
// Drives the full pipeline: load the placeholder PDF, parse the PFX,
// derive the key, build CMS SignedData, write the signed output.
if THotPDF.SignPDFWithPFX('Prepared.pdf', 'Signed.pdf',
'signer.pfx', 'p@ssw0rd') then
// signature embedded
else
// signing did not complete
;
Jedes Byte von signer.pfx fließt durch HPDFASN1 and HPDFPFX, bevor irgendeine Kryptografie stattfindet. Wenn diese beiden Units nicht vorsichtig mit dem umgehen, was die Datei behauptet, bekommt die nachgelagerte Kryptografie nie die Chance, eine Rolle zu spielen
Schwachstelle eins: Eine ASN.1-Länge, die über die Schutzbarriere hinausgewickelt wird
ASN.1 in DER und BER codiert jedes Element als Tag, Länge und entsprechend viele Inhaltsbytes. Die Länge ist das Feld, dem Sie vertrauen, das Sie aber überprüfen müssen, da es dem Parser mitteilt, wie weit er lesen soll, und es von demjenigen geschrieben wurde, der die Datei erstellt hat. X.690 §8.1.3 definiert zwei Codierungen. Die Kurzform packt eine Länge von 0 bis 127 in ein einziges Byte. Die Langform, die für alles Größere verwendet wird, nutzt ein führendes Byte, dessen untere sieben Bits die Anzahl der folgenden Längenbytes angeben, und diese Anzahl von Big-Endian-Bytes trägt dann den eigentlichen Wert. Vier Längenbytes können somit eine Inhaltsgröße von fast vier Gigabyte deklarieren
Nach dem Decodieren eines solchen Wertes muss der Parser prüfen, ob der Inhalt tatsächlich in den Puffer passt, bevor er ihm vertraut. Die naheliegende Prüfung besteht darin, zu bestätigen, dass die aktuelle Position plus der Inhaltslänge nicht über das Ende der Daten hinausläuft. Wenn man dies auf die offensichtliche Weise schreibt, bei der Position, Inhaltslänge und Gesamtlänge alle in vorzeichenbehafteten 32-Bit-Ganzzahlen gehalten werden, ist diese Schutzbarriere wirkungslos
// The trap: signed 32-bit arithmetic. With ContentLen near MaxInt,
// Pos + ContentLen overflows to a NEGATIVE value, so the comparison
// is false and a forged ~2 GB length sails straight through.
if Pos + ContentLen > Total then
raise EHPDFASN1Error.Create('content overruns buffer');
Das Problem ist die Addition, nicht der Vergleich. Wenn ContentLen nahe an MaxInt (2147483647) liegt, Pos + ContentLen über den vorzeichenbehafteten 32-Bit-Bereich hinausläuft und zu einer negativen Zahl wird. Eine negative Summe ist niemals größer als Total, sodass die Schutzbarriere meldet, dass alles in Ordnung ist, und den Parser mit einer Inhaltslänge von etwa zwei Gigabyte fortfahren lässt, die der Puffer gar nicht enthält. Der Schaden entsteht im nächsten Schritt: Der Reader reserviert Speicher für diese angebliche Länge und kopiert Daten hinein, ein SetLength gefolgt von einem Move, das aus der Quelle liest. Die Quelle hat jedoch nur noch wenige hundert Bytes übrig, sodass der Kopiervorgang weit über das Ende der Eingabe hinaus liest - ein Out-of-Bounds-Lesevorgang, der im besten Fall zu einem Absturz führt und im schlimmsten Fall benachbarten Prozessspeicher in den Parse-Vorgang einschleust
Der einzig korrekte Schutz erweitert die Zwischensumme vor dem Vergleich, sodass die Addition nicht den Typ überlaufen kann, in dem sie berechnet wird. Die Behebung stuft beide Operanden auf Int64 hoch
// Correct: both operands widened to Int64 before the add, so the sum
// cannot wrap. A forged 2 GB length now fails the bounds check.
if ContentLen < 0 then
raise EHPDFASN1Error.Create('negative content length after decoding.');
if Int64(Pos) + Int64(ContentLen) > Int64(Total) then
raise EHPDFASN1Error.Create('content overruns buffer');
Ein Int64 hält die Summe zweier 32-Bit-Werte ohne Verlust, sodass der Vergleich die tatsächliche Zahl sieht und die gefälschte Länge zurückweist. Die separate Prüfung auf Nicht-Negativität von ContentLen schließt den Fall aus, in dem ein decodierter Wert von sich aus negativ wird. In HotPDF befindet sich dieser Schutz in HPDFASN1ParseNode, der Funktion, die den Knoten erzeugt, auf dem jeder andere Helper aufbaut. Da HPDFASN1Content seine Zuweisungen von SetLength und Move direkt von der Inhaltslänge des Knotens ableitet, hätte ein Knoten, der eine fehlerhafte Schutzbarriere passiert hat, jeden darauf basierenden Lesevorgang kompromittiert. Die Behebung der Grenze direkt beim Decodieren macht die darüber liegenden Helper sicher
Schwachstelle zwei: Ein PBKDF2-Iterationszähler als Waffe
Der zweite Fehler ist kein Speicherfehler, sondern die Datei, die Ihrer CPU vorschreibt, wie hart sie arbeiten soll. PKCS#12 schützt sein Schlüsselmaterial mit PBES2, dem in RFC 8018 spezifizierten passwortbasierten Schema aus PKCS#5. PBES2 führt eine Schlüsselableitungsfunktion aus, hier PBKDF2 mit HMAC-SHA-256, und anschließend eine Verschlüsselung, hier AES-256-CBC. PBKDF2 benötigt einen Iterationszähler, und dieser Zähler ist ein in der Datei übertragener Parameter. Sein einziger Zweck ist es, langsam zu sein: Mehr Iterationen bedeuten, dass jeder Passwort-Rateversuch mehr kostet, was gut gegen Offline-Angreifer ist. RFC 8018 §4.2 besagt ausdrücklich, dass ein höherer Zähler besser für die Sicherheit ist, und setzt bewusst keine Obergrenze
Diese Offenheit ist in Ordnung, wenn Sie die Datei selbst erstellt haben. Sie ist eine Waffe, wenn der Angreifer sie erstellt hat. Der Iterationszähler ist ein vom Angreifer kontrollierter Arbeitsfaktor, und ein solcher Arbeitsfaktor ist ein Denial-of-Service auf Basis algorithmischer Komplexität. Eine gefälschte .pfx-Datei kann einen Iterationszähler in Milliardenhöhe codieren; der Parser liest ihn pflichtbewusst und ruft PBKDF2 für entsprechend viele Runden HMAC-SHA-256 auf, woraufhin sich der Prozess in einer Schleife verliert, die bei einer einzigen bereitgestellten Datei minuten- oder stundenlang nicht zurückkehrt. Auf einem Signierserver, der ein Zertifikat pro Anfrage verarbeitet, legt ein einziger manipulierter Upload einen Worker lahm
Der Zähler verschlimmert den Überlauf, noch bevor er die CPU zum Glühen bringt. Der Iterationswert liegt in der Datei als ASN.1-INTEGER vor, das keine feste Breite hat, während das Feld, das PBKDF2 letztendlich verbraucht, ein 32-Bit-Integer ist. Decodiert man das INTEGER direkt in dieses Feld, wird ein großer Wert abgeschnitten, und ein Wert, der so gestaltet ist, dass er auf dem Vorzeichenbit landet, kommt negativ oder als eine andere kleine Zahl zurück. So entspricht selbst das Ausmaß der Arbeit nicht mehr dem, was die Datei scheinbar verlangt hat. Die Behebung liest den Wert in voller Breite und begrenzt ihn vor dem Verkleinern
// Read the iteration count as Int64 first, then clamp to a sane band
// BEFORE it is narrowed into the 32-bit Iterations field PBKDF2 uses.
LIter := HPDFASN1ToInteger(Data, Node); // returns Int64
if (LIter < 1) or (LIter > 100000000) then
raise EHPDFPFXError.CreateFmt(
'PBKDF2 iteration count %d is outside the accepted range 1..100000000',
[LIter]);
Iterations := Integer(LIter); // safe: already bounded
Das Einlesen in ein Int64 bedeutet, dass der decodierte Wert der tatsächliche ist und nicht ein abgeschnittener Geist davon. Die untere Grenze weist Null und negative Zähler zurück, die für eine Schlüsselableitung unsinnig sind. Die obere Grenze von einhundert Millionen liegt weit über jeder legitimen PKCS#12-Datei, die heute Zehntausende bis wenige Hunderttausend Iterationen verwendet, während sie den schlimmsten Fall auf eine begrenzte, überlebbare Arbeitsmenge deckelt. Erst nachdem der Wert diesen Bereich bestanden hat, wird er auf das 32-Bit-Feld verkleinert, sodass die Kürzung niemanden mehr überraschen kann. In HotPDF befindet sich diese Begrenzung in ParsePBES2Params, wo die PBKDF2-Parameter auf dem Weg zu PBKDF2HMACSHA256 decodiert werden
Warum beide Behebungen dieselbe Behebung sind
Die beiden Schwachstellen sehen unterschiedlich aus, die eine ist ein Pufferüberlauf und die andere ein hängender Prozess, aber sie sind derselbe Fehler. In jedem Fall wurde eine Zahl aus einer nicht vertrauenswürdigen Datei einen Schritt zu früh in einen Typ mit fester Breite übernommen, bevor sie mit der Realität abgeglichen wurde. Die Länge wurde in 32 Bit vor der Grenzwertprüfung addiert; der Iterationszähler wurde auf 32 Bit vor der Bereichsprüfung verkleinert. Beide weichen derselben Disziplin: Decodieren in voller Breite, Prüfen gegen das reale Limit und erst dann Verkleinern. Das dazwischengeschaltete Int64 ist keine Stilfrage, es ist die einzige Breite, in der die Schutzbarriere den Wert sehen kann, den der Angreifer tatsächlich geschrieben hat. Eine Grenze, die überläuft, ist keine Grenze, und ein Zähler ohne Obergrenze ist kein Parameter, sondern eine Fernsteuerung für Ihre eigene CPU
Praktische Hinweise für eine Signier-Pipeline
Die direkte Lektion lautet, nicht vertrauenswürdige Zertifikatseingaben so zu validieren, wie Sie jeden nicht vertrauenswürdigen Upload validieren würden. Begrenzen Sie die Größe einer akzeptierten .pfx-Datei, da eine legitime Datei Kilobytes und nicht Megabytes groß ist. Behandeln Sie einen Parse-Fehler als routinemäßig abgewiesene Eingabe, nicht als Fehler, der dem Benutzer einen Stacktrace wert ist. Wenn Sie auf einem Server signieren, führen Sie den Import dort aus, wo ein blockierter Worker den Dienst nicht mitreißen kann, und setzen Sie ein Timeout um die Operation, sodass eine unerwartet rechenintensive Datei sowohl durch die Uhrzeit als auch durch die Iterationsbegrenzung gedeckelt wird
Der umfassendere Lektion reicht über Zertifikate hinaus. Parser-Härtung ist kein einmaliges Audit einer Unit, sondern eine Eigenschaft jedes Ortes, an dem Ihre Bibliothek Bytes liest, die sie nicht selbst geschrieben hat. Eine PDF-Bibliothek parst eine menge aus nicht vertrauenswürdigen Quellen: in einem Dokument eingebettete Schriftarten, Bilder in einem halben Dutzend Codecs, Stream-Filter und, auf dem Signierpfad, Zertifikate. Jedes davon ist eine Angriffsfläche, und jedes verdient denselben Verdacht gegenüber jeder Länge und jedem Zähler. HotPDF baut den Import- und Signierpfad auf den gehärteten Units HPDFASN1, HPDFPFX, HPDFCrypt und HPDFCMS auf, die hier beschrieben sind, sodass die von Ihnen übergebenen Anmeldedaten, woher sie auch stammen, defensiv geparst werden, bevor ihnen jemals vertraut wird
Der Signier-Workflow, den diese Prüfungen schützen, wird von Ende zu Ende in our walkthrough of PAdES digital signatures in Delphi behandelt, und dieselbe defensive Haltung, angewendet auf die Dokumentenverschlüsselung (einschließlich des AES-256-Schlüsselpfads, der diese Codebasis teilt), wird in dem Artikel über AES-256-Verschlüsselung und Sicherheit beschrieben. All das wird als Teil der HotPDF Component für Delphi und C++Builder zusammen mit den APIs zum Laden, Bearbeiten, Verschlüsseln und Signieren ausgeliefert, die an anderer Stelle auf diesem Blog behandelt werden