Τεχνικό Άρθρο

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

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

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

Διάγραμμα HotPDF του ελαττώματος και της διόρθωσης του μετρητή επαναλήψεων PBKDF2: ένα πλαστό πλήθος τεσσάρων δισεκατομμυρίων γύρων ακινητοποίησε την CPU μέχρι ένα κάτω και άνω όριο στο HPDFCrypt να το απορρίψει
εκατό εκατομμύρια κάθεται πάνω από κάθε νόμιμο αρχείο ενώ περιορίζει τη χειρότερη περίπτωση — αποκωδικοποίησε το πλάτος, μετά βάλε όριο, μετά χρήση
HotPDF: Λωρίδες πριν και μετά του ελαττώματος αναδίπλωσης μήκους ASN.1: ένα πλαστό μήκος προστιθέμενο σε 32 bits ξεπέρασε τον έλεγχο ορίων μέχρι το HPDFASN1ParseNode να διαβάσει σε Int64 και να απορρίψει
ο φρουρός έτρεξε απέναντι σε περικομμένο φάντασμα της πραγματικής τιμής· η ανάγνωση του πλάτους πρώτα κάνει το πλαστό μήκος ορατό πριν οποιαδήποτε SetLength

Από πού περνούν τα 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. Το δημόσιο σημείο εισόδου που οδηγεί ολόκληρη την αλυσίδα είναι μία κλήση

Αγωγός εισαγωγής .pfx στο HotPDF: bytes επηρεασμένα από επιτιθέμενο περνούν από HPDFPFX και HPDFASN1 πριν την παράγωγή κλειδιού HPDFCrypt και την υπογραφή HPDFRSA/HPDFCMS
ένας reader PKCS#12 είναι επιφάνεια επίθεσης — η κρυπτογραφία κατάντη δεν μετρά ποτέ αν οι δύο parser είναι ανέξυπνοι
// Οδηγεί ολόκληρη τη διοχέτευση: φορτώστε το PDF-κενό, αναλύστε το PFX,
// εξάγετε το κλειδί, δημιουργήστε το CMS SignedData και γράψτε την υπογεγραμμένη έξοδο.
if THotPDF.SignPDFWithPFX('Prepared.pdf', 'Signed.pdf',
     'signer.pfx', 'p@ssw0rd') then
  // η υπογραφή ενσωματωμένη
else
  // η υπογραφή δεν ολοκληρώθηκε
;

Και 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) αποτυγχάνει:

// Η παγίδα: προσημασμένη αριθμητική 32 bit. Με το ContentLen κοντά στο MaxInt,
// το Pos + ContentLen υπερχειλίζει σε ΑΡΝΗΤΙΚΗ τιμή, οπότε η σύγκριση
// είναι ψευδής και ένα πλαστό μήκος ~2 GB περνάει ελεύθερα.
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:

// Σωστό: και οι δύο τελεστέοι επεκτάθηκαν σε Int64 πριν από την πρόσθεση, οπότε το άθροισμα
// δεν μπορεί να αναδιπλωθεί. Ένα πλαστό μήκος 2 GB πλέον αποτυγχάνει στον έλεγχο ορίων.
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 προσήμου επιστρέφει αρνητική ή ως κάποιος άσχετος μικρός αριθμός, οπότε ακόμη και το μέγεθος της εργασίας δεν είναι πλέον αυτό που φαινόταν να ζητά το αρχείο. Η διόρθωση διαβάζει την τιμή στο πλήρες πλάτος της και θέτει όρια πριν από τη μείωση του πλάτους:

// Διαβάστε πρώτα τον αριθμό επαναλήψεων ως Int64 και μετά περιορίστε τον σε ένα λογικό εύρος
// ΠΡΙΝ μειωθεί σε πεδίο 32 bit Iterations που χρησιμοποιεί η PBKDF2.
LIter := HPDFASN1ToInteger(Data, Node);          // επιστρέφει 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);                    // ασφαλές: ήδη εντός ορίων

Η ανάγνωση σε Int64 σημαίνει ότι η αποκωδικοποιημένη τιμή είναι η πραγματική, όχι ένα περικομμένο φάντασμά της. Το κάτω όριο απορρίπτει μηδενικούς και αρνητικούς αριθμούς, που δεν βγάζουν κανένα νόημα για μια παραγωγή κλειδιού. Το άνω όριο, τα εκατό εκατομμύρια, βρίσκεται πολύ πάνω από οποιοδήποτε νόμιμο αρχείο PKCS#12, που σήμερα χρησιμοποιεί δεκάδες έως λίγες εκατοντάδες χιλιάδες επαναλήψεις, ενώ ταυτόχρονα κρατά τη χειρότερη περίπτωση σε έναν φραγμένο, υποφερτό όγκο εργασίας. Μόνο αφού η τιμή περάσει αυτή τη ζώνη μειώνεται το πλάτος της στο πεδίο των 32 bit, οπότε η αποκοπή δεν μπορεί πια να εκπλήξει κανέναν. Στο HotPDF αυτός ο περιορισμός βρίσκεται στην ParsePBES2Params, όπου αποκωδικοποιούνται οι παράμετροι PBKDF2 στον δρόμο προς την PBKDF2HMACSHA256

Γιατί και οι δύο διορθώσεις είναι η ίδια διόρθωση

Τα δύο ελαττώματα φαίνονται διαφορετικά, το ένα είναι υπερχείλιση 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 Delphi Component για Delphi και C++Builder, μαζί με τα API φόρτωσης, επεξεργασίας, κρυπτογράφησης και υπογραφής που καλύπτονται σε άλλα σημεία αυτού του ιστολογίου