Technical Article

Θωράκιση ενός Delphi PDF Signer έναντι κακόβουλων PKCS#12

Όταν υπογράφετε ένα PDF, συνήθως σκέφτεστε το κλειδί υπογραφής ως κάτι που ελέγχετε. Βρίσκεται σε ένα αρχείο .pfx που δημιουργήσατε, προστατευμένο από έναν κωδικό πρόσβασης που επιλέξατε. Ο κώδικας που διαβάζει αυτό το αρχείο μοιάζει με απλή σύνδεση, όχι με όριο ασφαλείας. Αυτή η διαίσθηση είναι λανθασμένη τη στιγμή που το πιστοποιητικό παύει να είναι δικό σας. Ένα εργαλείο επιφάνειας εργασίας που επιτρέπει σε έναν χρήστη να επιλέξει οποιοδήποτε .pfx, ένας διακομιστής που δέχεται ένα μεταφορτωμένο διαπιστευτήριο, ένας μηχανισμός μαζικής υπογραφής που τροφοδοτείται με πιστοποιητικά μέσω του δικτύου, όλα παραδίδουν byte που επηρεάζονται από τον επιτιθέμενο σε έναν αναλυτή (parser) πριν παραχθεί έστω και ένα byte υπογραφής. Ένας αναγνώστης PKCS#12 είναι επιφάνεια επίθεσης (attack surface), με την ίδια έννοια που είναι ένας αποκωδικοποιητής εικόνας ή ένας φορτωτής γραμματοσειρών.

Αυτό το άρθρο παρουσιάζει δύο πραγματικά ελαττώματα που υπήρχαν σε αυτόν τον αναγνώστη, και τα δύο στη διαδρομή εισαγωγής ενός διαπιστευτηρίου υπογραφής. Κανένα δεν είναι εξωτικό. Και τα δύο προέρχονται από την ίδια βασική αιτία που πλήττει σχεδόν κάθε δυαδικό αναλυτή γραμμένο σε γλώσσα με ακέραιους σταθερού πλάτους: ένα μήκος ή ένας αριθμός από το αρχείο εμπιστεύεται ένα βήμα περισσότερο από ό,τι θα έπρεπε. Το ένα οδηγεί σε ανάγνωση εκτός ορίων (out-of-bounds read), το άλλο σε μια διεργασία που κολλάει μέχρι να την τερματίσετε.

Από πού περνούν τα byte

Η εισαγωγή ενός .pfx για την υπογραφή ενός εγγράφου δεν είναι μία μόνο λειτουργία, είναι μια σύντομη διοχέτευση (pipeline), και κάθε στάδιο αναλύει κάτι που μπορεί να έχει γράψει ένας επιτιθέμενος. Το κοντέινερ είναι μια δομή PKCS#12 όπως ορίζεται στο RFC 7292, μια φωλιά από πακέτα AuthenticatedSafe τυλιγμένα γύρω από ένα κρυπτογραφημένο κάλυμμα που κρατά το ιδιωτικό κλειδί. Η ανάγνωσή του σημαίνει περιήγηση στο ASN.1, παραγωγή κλειδιού από τον κωδικό πρόσβασης, αποκρυπτογράφηση και, στη συνέχεια, παράδοση του ανακτηθέντος κλειδιού RSA στον κώδικα που δημιουργεί την υπογραφή.

Στο HotPDF αυτά τα στάδια αντιστοιχίζονται σε διακριτές μονάδες. Η λογική κοντέινερ του PKCS#12 βρίσκεται στο HPDFPFX. Κάθε ετικέτα (tag), μήκος και τιμή που αγγίζει αποκωδικοποιείται από τον αναγνώστη ASN.1 στο HPDFASN1. Η παραγωγή κλειδιού και η αποκρυπτογράφηση PBES2 βρίσκονται στο HPDFCrypt μαζί με το PBKDF2HMACSHA256. Όταν ανακτάται το κλειδί, το HPDFRSA και ο δημιουργός CMS SignedData στο HPDFCMS το μετατρέπουν σε ξεχωριστή υπογραφή (detached signature) ενσωματωμένη στο PDF. Το δημόσιο σημείο εισόδου που οδηγεί ολόκληρη την αλυσίδα είναι μία κλήση.

// 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
;

Και byte του signer.pfx ρέει μέσω των HPDFASN1 και HPDFPFX πριν συμβεί οποιαδήποτε κρυπτογραφική λειτουργία. Εάν αυτές οι δύο μονάδες δεν είναι προσεκτικές με όσα ισχυρίζεται το αρχείο, η κρυπτογράφηση στη συνέχεια δεν θα έχει ποτέ την ευκαιρία να παίξει ρόλο.

Ελάττωμα ένα: ένα μήκος ASN.1 που υπερχειλίζει πέρα από τον φύλακα

Το ASN.1 σε DER και BER κωδικοποιεί κάθε στοιχείο ως ετικέτα (tag), μήκος και αντίστοιχα byte περιεχομένου. Το μήκος είναι το πεδίο που πρέπει να εμπιστεύεστε αλλά να επαληθεύετε, επειδή λέει στον αναλυτή πόσο μακριά να διαβάσει, και γράφτηκε από όποιον δημιούργησε το αρχείο. Το πρότυπο X.690 §8.1.3 ορίζει δύο κωδικοποιήσεις. Η σύντομη μορφή συσκευάζει ένα μήκος από 0 έως 127 σε ένα μόνο byte. Η μακρά μορφή, που χρησιμοποιείται για οτιδήποτε μεγαλύτερο, καταναλώνει ένα αρχικό byte του οποίου τα επτά χαμηλότερα bit δίνουν το πλήθος των byte μήκους που ακολουθούν, και στη συνέχεια τόσα byte big-endian μεταφέρουν την πραγματική τιμή. Τέσσερα byte μήκους μπορούν επομένως να δηλώσουν μέγεθος περιεχομένου που πλησιάζει τα τέσσερα gigabyte.

Μετά την αποκωδικοποίηση μιας τέτοιας τιμής, ο αναλυτής πρέπει να ελέγξει ότι το περιεχόμενο χωράει πράγματι μέσα στο buffer πριν το εμπιστευτεί. Ο φυσιολογικός έλεγχος είναι να επιβεβαιωθεί ότι η τρέχουσα θέση συν το μήκος του περιεχομένου δεν ξεπερνά το τέλος των δεδομένων. Γραμμένος με τον προφανή τρόπο, με τη θέση, το μήκος περιεχομένου και το σύνολο να διατηρούνται σε προσημασμένους ακέραιους 32 bit, αυτός ο φύλακας (guard) αποτυγχάνει:

// 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');

Το πρόβλημα είναι η πρόσθεση, όχι η σύγκριση. Όταν το ContentLen είναι κοντά στο MaxInt (2147483647), η πράξη Pos + ContentLen προκαλεί υπερχείλιση στο προσημασμένο εύρος 32 bit και αναδιπλώνεται σε αρνητικό αριθμό. Ένα αρνητικό άθροισμα δεν είναι ποτέ μεγαλύτερο από το Total, επομένως ο φύλακας αναφέρει ότι όλα είναι καλά και επιτρέπει στον αναλυτή να προχωρήσει με ένα μήκος περιεχομένου περίπου δύο gigabyte που το buffer δεν περιέχει. Αυτό που συμβαίνει στη συνέχεια είναι η ζημιά: ο αναγνώστης εκχωρεί ένα buffer για αυτό το υποτιθέμενο μήκος και αντιγράφει σε αυτό, μια SetLength ακολουθούμενη από μια Move που διαβάζει από την πηγή. Η πηγή έχει απομείνει με λίγες εκατοντάδες byte μόνο, οπότε η αντιγραφή διαβάζει πολύ πέρα από το τέλος της εισόδου, μια ανάγνωση εκτός ορίων (out-of-bounds read) που στην καλύτερη περίπτωση προκαλεί κατάρρευση και στη χειρότερη διαρρέει γειτονική μνήμη διεργασίας στην ανάλυση.

Ο μόνος σωστός φύλακας διευρύνει το ενδιάμεσο άθροισμα πριν από τη σύγκριση, ώστε η πρόσθεση να μην μπορεί να υπερχειλίσει τον τύπο στον οποίο υπολογίζεται. Η διόρθωση προάγει και τους δύο τελεστέους σε Int64:

// 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');

Ένας τύπος Int64 διατηρεί το άθροισμα δύο τιμών 32 bit χωρίς απώλεια, οπότε η σύγκριση βλέπει τον πραγματικό αριθμό και απορρίπτει το πλαστό μήκος. Ο ξεχωριστός έλεγχος μη αρνητικής τιμής στο ContentLen κλείνει την αντίστοιχη περίπτωση όπου μια αποκωδικοποιημένη τιμή καταλήγει αρνητική από μόνη της. Στο HotPDF αυτός ο φύλακας βρίσκεται στο HPDFASN1ParseNode, τη συνάρτηση που παράγει τον κόμβο πάνω στον οποίο βασίζεται κάθε άλλος βοηθός. Επειδή το HPDFASN1Content ορίζει το μέγεθος των SetLength και Move απευθείας από το μήκος περιεχομένου του κόμβου, ένας κόμβος που θα περνούσε από έναν κακό φύλακα θα δηλητηρίαζε κάθε ανάγνωση που λαμβανόταν από αυτόν. Η διόρθωση του ορίου στο σημείο της αποκωδικοποίησης είναι αυτό που καθιστά ασφαλείς τους βοηθούς που βρίσκονται από πάνω του.

Ελάττωμα δύο: ένας αριθμός επαναλήψεων PBKDF2 που χρησιμοποιείται ως όπλο

Το δεύτερο ελάττωμα δεν είναι σφάλμα μνήμης, είναι το αρχείο που λέει στην CPU σας πόσο σκληρά να εργαστεί. Το PKCS#12 προστατεύει το υλικό κλειδιού του με το PBES2, το σχήμα που βασίζεται σε κωδικό πρόσβασης από το PKCS#5, το οποίο ορίζεται στο RFC 8018. Το PBES2 εκτελεί μια συνάρτηση παραγωγής κλειδιού, εδώ την PBKDF2 με HMAC-SHA-256, και στη συνέχεια έναν κρυπτογράφο, εδώ τον AES-256-CBC. Η PBKDF2 δέχεται έναν αριθμό επαναλήψεων, και αυτός ο αριθμός είναι μια παράμετρος που μεταφέρεται στο αρχείο. Ο σκοπός του είναι να είναι αργός: περισσότερες επαναλήψεις σημαίνουν ότι κάθε προσπάθεια εύρεσης κωδικού κοστίζει περισσότερο, κάτι που είναι καλό έναντι ενός offline επιτιθέμενου. Το RFC 8018 §4.2 αναφέρει ρητά ότι ένας μεγαλύτερος αριθμός είναι καλύτερος για την ασφάλεια, και εσκεμμένα δεν θέτει κανένα ανώτατο όριο.

Αυτή η ελευθερία είναι εντάξει όταν δημιουργήσατε εσείς το αρχείο. Είναι όμως όπλο όταν το δημιούργησε ο επιτιθέμενος. Ο αριθμός επαναλήψεων είναι ένας παράγοντας εργασίας ελεγχόμενος από τον επιτιθέμενο, και ένας τέτοιος παράγοντας αποτελεί άρνηση υπηρεσίας αλγοριθμικής πολυπλοκότητας (algorithmic-complexity denial of service). Ένα πλαστό .pfx μπορεί να κωδικοποιήσει έναν αριθμό επαναλήψεων σε δισεκατομμύρια. Ο αναλυτής τον διαβάζει πιστά και καλεί την PBKDF2 για τόσους γύρους HMAC-SHA-256, και η διεργασία χάνεται σε έναν βρόχο που δεν θα επιστρέψει για λεπτά ή ώρες με ένα μόνο παρεχόμενο αρχείο. Σε έναν διακομιστή υπογραφής που χειρίζεται ένα διαπιστευτήριο ανά αίτημα, μια μεμονωμένη τροποποιημένη μεταφόρτωση παγώνει έναν εργάτη (worker).

Ο αριθμός κάνει την αναδίπλωση (wraparound) χειρότερη προτού κάνει την CPU να περιστρέφεται άσκοπα. Η τιμή επανάληψης υπάρχει στο αρχείο ως ASN.1 INTEGER, το οποίο δεν έχει σταθερό πλάτος, ενώ το πεδίο που καταναλώνει τελικά η PBKDF2 είναι ένα Integer 32 bit. Αποκωδικοποιήστε το INTEGER απευθείας σε αυτό το πεδίο και μια μεγάλη τιμή αποκόπτεται, ενώ μια τιμή σχεδιασμένη να καταλήγει στο bit προσήμου επιστρέφει αρνητική ή ως κάποιος άσχετος μικρός αριθμός, οπότε ακόμη και το μέγεθος της εργασίας δεν είναι πλέον αυτό που φαινόταν να ζητά το αρχείο. Η διόρθωση διαβάζει την τιμή στο πλήρες πλάτος της και θέτει όρια πριν από τη μείωση του πλάτους:

// 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

Πρακτικό πλαίσιο

Τα δύο ελαττώματα φαίνονται διαφορετικά, το ένα είναι υπερχείλιση buffer και το άλλο μια παγωμένη διεργασία, αλλά αποτελούν το ίδιο λάθος. Σε κάθε περίπτωση, ένας αριθμός από ένα μη έμπιστο αρχείο μεταφέρθηκε σε έναν τύπο σταθερού πλάτους ένα βήμα πολύ νωρίς, προτού ελεγχθεί έναντι της πραγματικότητας. Το μήκος προστέθηκε στα 32 bit πριν από τη δοκιμή ορίων. Ο αριθμός επαναλήψεων μειώθηκε στα 32 bit πριν από τη δοκιμή εύρους. Και τα δύο υπακούουν στην ίδια πειθαρχία: αποκωδικοποίηση στο πλήρες πλάτος, έλεγχος έναντι του πραγματικού ορίου και μόνο τότε μείωση του πλάτους. Το ενδιάμεσο Int64 δεν είναι επιλογή στυλ, είναι το μόνο πλάτος στο οποίο ο φύλακας μπορεί να δει την τιμή που πραγματικά έγραψε ο επιτιθέμενος. Ένα όριο που υπερχειλίζει δεν είναι όριο, και ένας αριθμός χωρίς οροφή δεν είναι παράμετρος, είναι ένας απομακρυσμένος ρυθμιστής (throttle) της δικής σας CPU.

Πρακτική καθοδήγηση για μια διοχέτευση υπογραφής

Το ειδικό μάθημα είναι να επικυρώνετε την είσοδο μη έμπιστων πιστοποιητικών με τον ίδιο τρόπο που θα επικυρώνατε οποιαδήποτε μη έμπιστη μεταφόρτωση. Περιορίστε το μέγεθος του .pfx που αποδέχεστε, καθώς ένα νόμιμο είναι της τάξης των kilobyte, όχι των megabyte. Αντιμετωπίστε μια αποτυχία ανάλυσης ως συνηθισμένη απορριφθείσα είσοδο, όχι ως σφάλμα που αξίζει μια αναδρομή στοίβας (stack trace) προς τον χρήστη. Εάν υπογράφετε σε διακομιστή, εκτελέστε την εισαγωγή εκεί όπου ένας παγωμένος εργάτης δεν μπορεί να παρασύρει την υπηρεσία μαζί του, και θέστε ένα χρονικό όριο (timeout) γύρω από τη λειτουργία, ώστε ένα απροσδόκητα βαρύ αρχείο να περιορίζεται τόσο από τον πραγματικό χρόνο όσο και από το όριο επαναλήψεων.

Το ευρύτερο μάθημα εκτείνεται πέρα από τα πιστοποιητικά. Η θωράκιση του αναλυτή δεν είναι ένας έλεγχος μιας φοράς για μια μονάδα, είναι μια ιδιότητα κάθε σημείου όπου η βιβλιοθήκη σας διαβάζει byte που δεν έγραψε η ίδια. Μια βιβλιοθήκη PDF αναλύει πολλά στοιχεία από μη έμπιστες πηγές: γραμματοσειρές ενσωματωμένες σε ένα έγγραφο, εικόνες σε μισή δωδεκάδα κωδικοποιητές, φίλτρα ροής και, στη διαδρομή υπογραφής, πιστοποιητικά. Κάθε ένα από αυτά είναι επιφάνεια επίθεσης, και κάθε ένα αξίζει την ίδια καχυποψία για κάθε μήκος και κάθε μέτρηση. Το HotPDF χτίζει τη διαδρομή εισαγωγής και υπογραφής στις θωρακισμένες μονάδες HPDFASN1, HPDFPFX, HPDFCrypt και HPDFCMS που περιγράφονται εδώ, έτσι ώστε το διαπιστευτήριο που του παραδίδετε, από όπου κι αν προήλθε, να αναλύεται αμυντικά πριν γίνει ποτέ έμπιστο.

Η ροή εργασίας υπογραφής που προστατεύουν αυτοί οι έλεγχοι καλύπτεται από άκρη σε άκρη στον οδηγό μας για τις ψηφιακές υπογραφές PAdES σε Delphi, και η ίδια αμυντική στάση που εφαρμόζεται στην κρυπτογράφηση εγγράφων, συμπεριλαμβανομένης της διαδρομής κλειδιού AES-256 που μοιράζεται αυτή τη βάση κώδικα, περιγράφεται στο άρθρο για την κρυπτογράφηση και την ασφάλεια AES-256. Όλα αυτά παρέχονται ως μέρος του HotPDF Component για Delphi και C++Builder, μαζί με τα API φόρτωσης, επεξεργασίας, κρυπτογράφησης και υπογραφής που καλύπτονται σε άλλα σημεία αυτού του ιστολογίου.