Σε builds του PDFium Component πριν το v3.126.2, το TPdf.AnalyzeSignatureRevisions μπορούσε να βαθμολογήσει πραγματική επεξεργασία περιεχομένου σελίδας ως επιτρεπτή αλλαγή annotation υπό DocMDP P=3, επειδή το revision role graph του μεταχειριζόταν το back-reference /P του signature widget προς τη σελίδα του ως ιδιοκτησία. Από το v3.126.2, το PDFium Component χωρίζει navigation ακμές από ακμές owned-payload, οπότε το περιεχόμενο σελίδας μένει περιεχόμενο σελίδας. Το bug report πίσω από αυτή τη διόρθωση φαίνεται αβλαβές στο χαρτί. Ένα πιστοποιημένο συμβόλαιο επιτρέπει annotations, ο αντισυμβαλλόμενος κάνει ένα incremental save, και ο analyzer λέει ότι κάθε μεταγενέστερη αλλαγή επιτρέπεται. Μετά κάποιος κάνει diff τις αποδιδόμενες σελίδες και το ποσό πληρωμής στη σελίδα 2 είναι διαφορετικό
Αυτό το άρθρο είναι το σίκβελ με τα μάτια του attacker του overview ανάλυσης αλλαγών revision μετά την υπογραφή, οπότε προσπερνά τα βασικά του revision rebuilding και της βαθμολόγησης DocMDP και πηγαίνει κατευθείαν στο object graph: πώς μοντελοποιούνταν η ιδιοκτησία, γιατί η κατεύθυνση μιας ακμής αποφασίζει ετυμηγορία ασφαλείας, τι άλλαξε στο v3.126.2, και πώς να ελέγξετε τη δική σας λογική αποδοχής
Γιατί μια επεξεργασία σελίδας πέρασε ως αλλαγή annotation υπό DocMDP P=3;
Η επεξεργασία σελίδας πέρασε επειδή το παλιό role graph ακολουθούσε κάθε indirect reference σε dictionary σαν το αναφερμένο αντικείμενο να ανήκει στο αναφέρον, και το signature widget δείχνει πίσω στη σελίδα του. Ένα annotation dictionary κουβαλά /P, indirect reference στο object της σελίδας πάνω στο οποίο κάθεται (ISO 32000-1 §12.5.2). Εκείνη η εγγραφή είναι navigation υπόδειξη. Το widget δεν κατέχει τη σελίδα· η σελίδα κατέχει το widget μέσω του array /Annots της
Ο analyzer αναθέτει σε κάθε object σύνολο role bits πριν βαθμολογήσει μεταγενέστερες αλλαγές: σελίδα, annotation, form και validation υλικό. Τα root objects παίρνουν τον ρόλο τους από το δικό τους dictionary, και ο ρόλος μετά απλώνεται σε ό,τι αναφέρουν. Στην παλιά διάδοση η αλυσίδα πήγαινε έτσι:
- Το signature widget είναι
/Subtype /Widgetμε/FT /Sig, οπότε παίρνει τον annotation ρόλο - Το
/Pτου widget σπρώχνει τον annotation ρόλο πάνω στο page dictionary, που έχει ήδη τον page ρόλο - Η σελίδα σπρώχνει και τους δύο ρόλους μέσα στο
/Contents, το/Resourcesκαι, μέσω/Parent, πάνω στο Pages tree και πέρα σε κάθε sibling σελίδα - Ένα content stream dictionary όπως
<< /Length 812 >>δεν έχει/Type, οπότε ο classifier έπεφτε πίσω στα role bits και έλεγχε τον annotation ρόλο πριν τον page ρόλο
Το τροποποιημένο content stream άρα βγαίνει ως prckAnnotation. Υπό ISO 32000-1 §12.8.2.2, το DocMDP P=3 επιτρέπει αλλαγές annotations, οπότε η απόφαση ήταν prdAllowed και η report τυλίχθηκε σε prasAllowed. Το ίδιο αρχείο υπό P=2 απορρίφθηκε, αλλά μόνο κατά σύμπτωση: το P=2 απαγορεύει αλλαγές annotations, οπότε ο μισανοιγμένος stream απορρίφθηκε για λάθος λόγο. Ένας σταθερός βρόχος διάδοσης τεσσάρων περασμάτων πρόσθεσε δεύτερη αδυναμία. Payload που φτάνει μέσω indirect array, ή μέσω μακριάς αλυσίδας της οποίας οι αριθμοί objects τρέχουν ανάποδα, ίσως δεν λάμβανε καθόλου ρόλο
Γιατί πρέπει ένας signature validator να ρωτά ποιος κατέχει ένα object;
Ένας signature validator πρέπει να ρωτά ποιος κατέχει ένα object επειδή τα PDF incremental updates (ISO 32000-1 §7.5.6) αφήνουν τον καθένα να προσαρτήσει revision που ξαναορίζει υπάρχοντα αριθμό object, και το ξαναορισμένο body δεν ανακοινώνει τι είναι. Η υπογραφή εξακολουθεί να επαληθεύεται, αφού καλύπτει μόνο τα bytes της δικής της revision. Κάθε άμυνα ενάντια σε χειρισμό μετά την υπογραφή εξαρτάται άρα από το να αντιστοιχίσει κάθε αλλαγμένο object στη δομή που το χρησιμοποιεί, και μετά να ρωτήσει αν ο υπογράφων επέτρεπε σε εκείνη τη δομή να αλλάξει
Πολλές δημοσιευμένες κλάσεις επιθέσεων δουλεύουν ακριβώς σε εκείνο το κενό. Οι incremental saving attacks προσαρτούν revision που ανταλλάσσει περιεχόμενο σελίδας και βασίζονται στον verifier να ελέγχει μόνο το signed byte range. Οι shadow attacks φυτεύουν κρυμμένο περιεχόμενο πριν την υπογραφή και το ενεργοποιούν μετά με μικρή, αθωοφανή αλλαγή. Οι επιθέσεις σε certified documents εκμεταλλεύονται ότι τα P=2 και P=3 επιτρέπουν ρητά κάποιες μεταγενέστερες επεξεργασίες, και μετά ντύνουν μια απαγορευμένη επεξεργασία ως επιτρεπτή. Verifier που ταξινομεί objects με ετικέτες όπως /Type /Annot, ή με οποιαδήποτε reference διαδρομή που τυχαίνει να τα φτάνει, εκτίθεται στην τρίτη κλάση: ο attacker χρειάζεται μόνο μία επιτρεπτή δομή που μπορεί να φτάσει την απαγορευμένη
Γι’ αυτό η ερώτηση δεν είναι ποια objects άλλαξαν αλλά ποιος τα κατέχει. Ένα content stream που φτάνει από σελίδα μέσω /Contents είναι περιεχόμενο σελίδας ό,τι άλλο κι αν δείχνει σε αυτό. Ένα annotation που δείχνει πίσω στη σελίδα μέσω /P λέει πού ζει το annotation, όχι τι κατέχει
Πώς μοντελοποιεί το PDFium Component v3.126.2 την ιδιοκτησία;
Το PDFium Component v3.126.2 μεταχειρίζεται back-references ως navigation, τα κρατά έξω από τη διάδοση ρόλων, και αποφασίζει ποια keys μετράνε ως navigation από τον δομικό ρόλο του dictionary που τα κρατά, όχι από το όνομα key μόνο. Ο πίνακας συνοψίζει τα navigation keys που δεν κουβαλάνε πλέον ιδιοκτησία
| Dictionary ιδιοκτήτης | Keys μεταχειριζόμενα ως navigation | Αναφορά spec |
|---|---|---|
| Σελίδα ή κόμβος Pages | /Parent, /Kids, /Annots | ISO 32000-1 §7.7.3 |
| Annotation ή widget | /P | ISO 32000-1 §12.5.2 |
| Dictionary widget ή field | /Parent | ISO 32000-1 §12.7.3 |
Φιλτράρισμα κατά όνομα key καθολικά θα δημιουργούσε νέα τρύπα. Ένας πόρος font ή XObject μπορεί νόμιμα να ονομάζεται /P, /Parent ή /Annots, και dictionary /Resources που έριχνε την εγγραφή /P του έξω από τη διάδοση θα άφηνε attacker να κρύψει page-owned XObject πίσω από αθώο όνομα πόρου. Στο v3.126.2 το navigation φίλτρο ισχύει μόνο όταν το dictionary ιδιοκτήτης είναι όντως σελίδα, κόμβος Pages, annotation, widget ή field. Αν ένα από εκείνα τα dictionaries κουβαλά διπλασιασμένο navigation key, όπως δύο εγγραφές /P σε widget, ο analyzer δεν μαντεύει ποιο αντίγραφο θα χρησιμοποιούσε ένας viewer· το role build αποτυγχάνει και η υπογραφή γίνεται Indeterminate
Περισσότεροι κανόνες κλείνουν τις υπόλοιπες διαδρομές μετονομασίας ρόλων:
- Οι κόμβοι Pages είναι root page ρόλων από μόνοι τους, οπότε πόροι κληρονομημένοι από το Pages tree (ISO 32000-1 §7.7.3.4) μπαίνουν στο page context μέσω πραγματικής ιδιοκτησίας, όχι μέσω περιπατου
/Parentαπό child σελίδα - Annotation ή form ρόλος που φτάνει σε catalog, κόμβο Pages, σελίδα, annotation ή field dictionary σταματά εκεί, επειδή εκείνα τα δομικά objects εγκαθιστούν τους δικούς τους ρόλους και incoming payload ρόλος δεν πρέπει να τους επιβληθεί
- Ο page ρόλος είναι αυθεντικός κατά την ταξινόμηση: object σε κατοχή σελίδας είναι
prckPageContentακόμα κι αν μεταγενέστερη revision το ξαναγράψει με πλαστογραφημένο/FT, ετικέτα/Type /Annot, ή το μοιραστεί με appearance stream - Form XObject που χρησιμοποιείται μόνο ως appearance field ή annotation κρατά την κατηγορία form ή annotation του, οπότε συνηθισμένη αναγέννηση appearance μετά από συμπλήρωση form εξακολουθεί να βαθμολογείται υπό τους κανονικούς κανόνες αδειών
- Widget χωρίς δικό του
/FTαναλύει τον κληρονομημένο field type μέσω της αλυσίδας/Parent, και μη επιλύσιμη αλυσίδα αποτυγχάνει το role build αντί να πέσει σε προεπιλογή annotation - Role bits από κάθε μεταγενέστερη revision συγχωνεύονται στους ρόλους της καλυμμένης revision, οπότε μεταγενέστερο update δεν μπορεί να σβήσει προγενέστερη σχέση κατοχής σελίδας αποσπώντας πρώτα ένα stream και επεξεργαζόμενο μετά
Σταθερό σημείο αντί για σταθερό πλήθος περασμάτων
Η role reachability στο v3.126.2 τρέχει ως work queue που επαναλαμβάνεται μέχρι κανένα object να μην κερδίσει νέο role bit, που είναι αληθές fixed point ανεξάρτητα από βάθος αλυσίδας ή αρίθμηση objects. Indirect arrays όπως array /Contents αποθηκευμένο ως δικό του object διασχίζονται κι αυτά. Κάθε object μπορεί να κερδίσει το πολύ τέσσερα διακριτά role bits, οπότε η ουρά δεσμεύεται σε τέσσερις εγγραφές ανά αριθμό object· η υπέρβαση εκείνου του budget σηκώνει prrResourceLimitExceeded. Reference σε free object, ασυμφωνία generation ή χαλασμένο object header σηκώνει prrMalformedRevisionChain, και payload μέσα σε compressed object stream σηκώνει prrCompressedObjectUnresolved. Κάθε μία από εκείνες τις αστοχίες τελειώνει με prasIndeterminate, ποτέ με ετυμηγορία επιτρεπτό, και όταν η αστοχία συμβεί ενώ χτίζονται οι ρόλοι της καλυμμένης revision η υπογραφή δεν αναφέρει καθόλου Changes
Η παρακάτω ρουτίνα απαριθμεί τις επεξεργασίες page content που επιβιώνουν από αυτή την ανάλυση. Αλλαγή prckPageContent δεν βαθμολογείται ποτέ prdAllowed: DocMDP P=1, 2 ή 3 την κάνει prdDisallowed, και υπογραφή χωρίς DocMDP τη βαθμολογεί prdSuspicious
uses
SysUtils, TypInfo, PDFium, FPdfPades;
procedure ListPageContentEdits(const FileName: string);
const
ShadowTag: array[Boolean] of string = (' (unreferenced shadow)', '');
var
Pdf: TPdf;
Report: TPadesRevisionAnalysisReport;
Sig: TPadesSignatureRevisionAnalysis;
Change: TPadesRevisionObjectChange;
i, j: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := FileName;
Pdf.Active := True;
Report := Pdf.AnalyzeSignatureRevisions; // record, τίποτα για free
for i := 0 to High(Report.Signatures) do
begin
Sig := Report.Signatures[i];
if not (prrPageContentChanged in Sig.Risks) then
Continue;
Writeln(Format('Signature %d (DocMDP P=%d): page content changed',
[Sig.SignatureIndex, Sig.DocMdpPermission]));
for j := 0 to High(Sig.Changes) do
begin
Change := Sig.Changes[j];
if Change.Kind <> prckPageContent then
Continue;
Writeln(Format(' revision %d object %d %d R %s%s',
[Change.RevisionIndex, Change.ObjectNumber, Change.Generation,
GetEnumName(TypeInfo(TPadesRevisionDecision), Ord(Change.Decision)),
ShadowTag[Change.IsAuthoritative]]));
end;
end;
finally
Pdf.Free;
end;
end;
Τι συμβαίνει όταν FieldMDP και annotation μοιράζονται ένα object;
Όταν το /V ενός field και το /Contents ενός annotation δείχνουν στο ίδιο indirect object, το v3.126.2 κρατά το FieldMDP lock σε ισχύ παρόλο που η αλλαγή ταξινομείται ως επεξεργασία annotation. Το σενάριο χτίζεται εύκολα στο χέρι: υπογράφων κλειδώνει το field Total με FieldMDP (ISO 32000-1 §12.8.2.4), και ο attacker κάνει το /Contents ενός text annotation να αναφέρει το ίδιο string object που κρατά την τιμή του field. Υπό P=3 η επεξεργασία annotation επιτρέπεται, οπότε πριν τη διόρθωση η ξαναγραφή εκείνου του μοιρασμένου string άλλαζε κλειδωμένη τιμή field με ετυμηγορία επιτρεπτό
Το object τώρα κουβαλά και τους annotation και form ρόλους, και η annotation απόφαση ξαναελέγχει την πλευρά form όποτε η υπογραφή έχει FieldMDP transform:
- Υπό P=2 η αλλαγή annotation απαγορεύεται ευθέως, ακριβώς όπως πριν
- Με FieldMDP
All, κάθε field είναι κλειδωμένο, οπότε η μοιρασμένη αλλαγή είναιprdDisallowed - Με FieldMDP
IncludeήExclude, ο analyzer δεν μπορεί να ανιχνεύσει μοιρασμένο scalar πίσω σε ένα όνομα field, οπότε η απόφαση είναιprdIndeterminateκαι όχι μαντεψιά - Χωρίς FieldMDP, ο κανόνας annotation P=3 ισχύει και η αλλαγή μένει επιτρεπτή
Μία λεπτομέρεια reporting μετράει για gate κώδικα. Η μοιρασμένη περίπτωση αναφέρεται ως Kind = prckAnnotation με Decision = prdIndeterminate, και το prrFieldMdpUnresolved προστίθεται στο risk set μόνο για αλλαγές ταξινομημένες ως form fields. Gate που ψάχνει prrFieldMdpUnresolved και αγνοεί το Status χάνει εκείνη την περίπτωση τελείως
Πώς πρέπει κώδικας Delphi να αποτυγχάνει κλειστά στην ανάλυση revision;
Ο κώδικας Delphi πρέπει να δέχεται υπογεγραμμένο έγγραφο μόνο όταν το analysis status είναι prasNoLaterChanges ή prasAllowed και δεν υπάρχει δομικός κίνδυνος, και πρέπει να μεταχειρίζεται prasIndeterminate και prasSuspicious ως αναξιόπιστα, όχι ως προειδοποιήσεις προς καταγραφή και πέρασμα. Indeterminate σημαίνει ότι ο analyzer δεν μπόρεσε να αποδείξει ότι οι μεταγενέστερες revisions επιτρέπονταν· για attacker, είσοδος που παράγει αξιόπιστα Indeterminate είναι τόσο χρήσιμη όσο μία που παράγει Allowed αν ο κώδικάς σας την αφήσει να περάσει. Το καθολικό AnalyzePadesSignatureRevisions παίρνει οποιοδήποτε TStream και το διαβάζει από τη θέση 0, που ταιριάζει σε upload handlers που δεν χρειάζεται ποτέ να αποδώσουν το έγγραφο
uses
Classes, SysUtils, TypInfo, FPdfPades;
const
// Διπλασιασμένοι ορισμοί καταγράφονται χωρίς υποβάθμιση Status
BlockingRisks: TPadesRevisionRisks = [prrMalformedRevisionChain,
prrDuplicateObjectDefinition, prrUnreferencedObjectDefinition,
prrSignatureObjectRedefined, prrCompressedObjectUnresolved,
prrResourceLimitExceeded];
function SignedRevisionsAcceptable(const FileName: string;
out Reason: string): Boolean;
var
Source: TFileStream;
Report: TPadesRevisionAnalysisReport;
begin
Result := False;
Reason := '';
Source := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
try
Report := AnalyzePadesSignatureRevisions(Source);
finally
Source.Free;
end;
if Report.SignatureCount = 0 then
begin
Reason := 'no signature anchors the analysis';
Exit;
end;
if Report.Risks * BlockingRisks <> [] then
begin
Reason := 'structural risk in the revision chain';
Exit;
end;
case Report.Status of
prasNoLaterChanges, prasAllowed:
Result := True;
else
// prasIndeterminate και prasSuspicious είναι απορρίψεις, όχι προειδοποιήσεις
Reason := 'revision status ' +
GetEnumName(TypeInfo(TPadesRevisionAnalysisStatus), Ord(Report.Status));
end;
end;
Δύο όρια αξίζουν να ειπωθούν καθαρά. Το TPadesRevisionAnalysisReport δεν λέει τίποτα για CMS ακεραιότητα ή εμπιστοσύνη πιστοποιητικών, οπότε αυτή η πύλη κάθεται δίπλα σε κρυπτογραφική και trust επαλήθευση, όχι στη θέση τους. Και σωστό ownership graph δεν κάνει το P=3 ασφαλές για κάθε workflow. Το P=3 επιτρέπει γνήσια annotations, και annotation με αδιαφανές appearance μπορεί να καλύψει υπογεγραμμένο κείμενο χωρίς να αγγίξει ούτε ένα content stream. Αν τα certified documents σας είναι συμβόλαια και όχι αντίγραφα αναθεώρησης, είτε πιστοποιήστε με P=2 είτε δρομολογήστε επιτρεπτές αλλαγές annotations σε άνθρωπο, όπως σε αυτό το helper:
function AllowedAnnotationEditsUnderP3(
const Report: TPadesRevisionAnalysisReport): Integer;
var
i, j: Integer;
begin
Result := 0;
for i := 0 to High(Report.Signatures) do
if Report.Signatures[i].DocMdpPermission = 3 then
for j := 0 to High(Report.Signatures[i].Changes) do
if (Report.Signatures[i].Changes[j].Kind = prckAnnotation) and
(Report.Signatures[i].Changes[j].Decision = prdAllowed) then
Inc(Result);
end;
Λίστα ελέγχου: audit signature revision
Χρησιμοποιήστε αυτή τη λίστα να τσεκάρετε αν η δική σας pipeline επαλήθευσης ήταν εκτεθειμένη και αν τώρα αποτυγχάνει κλειστά:
- Builds του PDFium Component πριν το v3.126.2 μπορούσαν να αναφέρουν
prasAllowedγια επεξεργασίες page content σε DocMDP P=3 έγγραφα· ξανατρέξτεTPdf.AnalyzeSignatureRevisionsπάνω σε certified P=3 αρχεία που δέχτηκαν παλαιότερα builds - Ξανατσεκάρετε P=3 έγγραφα με FieldMDP locks όπου τιμή field και annotation ίσως μοιράζονται indirect object
- Δέχεστε μόνο
prasNoLaterChangesκαιprasAllowed· μεταχειριστείτεprasIndeterminateκαιprasSuspiciousως αναξιόπιστα - Τεστάρετε
Report.Risksόπως καιReport.Status, επειδή τοprrDuplicateObjectDefinitionδεν αλλάζει το status από μόνο του - Μην διαβάζετε κενό array
Changesως καθαρό αποτέλεσμα όταν το status της υπογραφής είναι Indeterminate· αποτυχημένο role build δεν αναφέρει αλλαγές - Μην βασίζεστε μόνο στο
prrFieldMdpUnresolvedνα πιάσει FieldMDP προβλήματα, αφού η μοιρασμένη περίπτωση annotation εμφανίζεται μόνο μέσω της απόφασης και του status - Αποφασίστε αν οι επιτρεπτές αλλαγές annotation υπό P=3 χρειάζονται ανθρώπινη αναθεώρηση στο workflow σας
- Αναλύστε τα πρωτότυπα bytes του αρχείου· έγγραφο ξαναγραμμένο με
SaveAsδεν περιέχει πια την αλυσίδα revisions
Η ανάλυση revisions είναι ένα στρώμα ελέγχου υπογραφής. Ζευγρώστε την με επιθεώρηση ψηφιακών υπογραφών PDF και επιπέδων PAdES για το dictionary και το baseline επίπεδο, και με ευρύτερο audit κινδύνων ασφαλείας PDF για JavaScript, launch actions και ενσωματωμένα αρχεία. Το TPdf.AnalyzeSignatureRevisions, το AnalyzePadesSignatureRevisions και το ownership-aware role graph που περιγράφεται εδώ έρχονται στο PDFium Component για Delphi, C++Builder και Lazarus