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

DocMDP PDFium: πώς ένα widget /P έκρυψε επεξεργασίες σελίδας

Σε 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, και ο ρόλος μετά απλώνεται σε ό,τι αναφέρουν. Στην παλιά διάδοση η αλυσίδα πήγαινε έτσι:

  1. Το signature widget είναι /Subtype /Widget με /FT /Sig, οπότε παίρνει τον annotation ρόλο
  2. Το /P του widget σπρώχνει τον annotation ρόλο πάνω στο page dictionary, που έχει ήδη τον page ρόλο
  3. Η σελίδα σπρώχνει και τους δύο ρόλους μέσα στο /Contents, το /Resources και, μέσω /Parent, πάνω στο Pages tree και πέρα σε κάθε sibling σελίδα
  4. Ένα content stream dictionary όπως << /Length 812 >> δεν έχει /Type, οπότε ο classifier έπεφτε πίσω στα role bits και έλεγχε τον annotation ρόλο πριν τον page ρόλο
Διάγραμμα PDFium Component του DocMDP role graph πριν το v3.126.2 όπου back-reference /P signature widget σπρώχνει τον annotation ρόλο πάνω στο page dictionary, η σελίδα τον απλώνει μέσω /Contents πάνω σε content stream χωρίς εγγραφή /Type, ο classifier δίνει prckAnnotation και η βαθμολόγηση P=3 επιστρέφει prdAllowed
Πριν το v3.126.2 το role graph μεταχειριζόταν κάθε indirect reference ως ιδιοκτησία, οπότε η εγγραφή /P του widget έσπρωχνε τον annotation ρόλο πάνω στη σελίδα και μια γνήσια επεξεργασία σελίδας έφευγε από τον analyzer ως επιτρεπτή αλλαγή annotation

Το τροποποιημένο 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, /AnnotsISO 32000-1 §7.7.3
Annotation ή widget/PISO 32000-1 §12.5.2
Dictionary widget ή field/ParentISO 32000-1 §12.7.3
Object graph PDFium Component v3.126.2 που χωρίζει ακμές owned-payload όπως /Contents και /Annots, που απλώνουν page και annotation ρόλους, από navigation ακμές όπως widget /P, που δεν κουβαλάνε ρόλους, με τα navigation keys ανά owner dictionary και prckPageContent κρατημένο στο content stream ακόμα και υπό πλαστογραφημένο /Type
Το v3.126.2 κρατά back-references έξω από τη διάδοση ρόλων· οι ρόλοι ταξιδεύουν μόνο μέσω πραγματικής ιδιοκτησίας, οπότε το content stream μένει περιεχόμενο σελίδας και η υπόδειξη /P δεν αποφασίζει τίποτα

Φιλτράρισμα κατά όνομα 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 ισχύει και η αλλαγή μένει επιτρεπτή
Διάγραμμα απόφασης FieldMDP PDFium Component όπου κλειδωμένο field Total /V και annotation /Contents αναφέρουν το ίδιο indirect object, με διακλάδωση πάνω σε DocMDP P=2, FieldMDP All, FieldMDP Include ή Exclude και όχι FieldMDP προς ετυμηγορίες prdDisallowed, prdIndeterminate ή prdAllowed για την ίδια μοιρασμένη επεξεργασία
Όταν ένα indirect object κουβαλά και τους annotation και form ρόλους, η annotation απόφαση ξαναελέγχει το FieldMDP lock, οπότε η ίδια επεξεργασία κυμαίνεται από επιτρεπτή σε απαγορευμένη σε ακαθόριστη

Μία λεπτομέρεια 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