Bài viết kỹ thuật

PDFium Component DocMDP: Widget /P giấu chỉnh sửa trang

Trong các bản dựng PDFium Component trước v3.126.2, TPdf.AnalyzeSignatureRevisions có thể xếp một chỉnh sửa nội dung trang thật sự thành một thay đổi annotation được cho phép dưới DocMDP P=3, vì đồ thị role của revision coi back-reference /P của signature widget trỏ về trang của nó là ownership. Kể từ v3.126.2, PDFium Component tách các cạnh điều hướng khỏi các cạnh payload thuộc sở hữu, nên nội dung trang vẫn là nội dung trang. Bug report đứng sau bản sửa này trông vô hại trên giấy. Một hợp đồng được certify cho phép annotation, bên kia thêm một incremental save, và bộ phân bảo mọi thay đổi sau đó đều được phép. Rồi ai đó diff các trang đã render và số tiền thanh toán ở trang 2 lại khác

Bài này là phần tiếp theo nhìn từ phía kẻ tấn công của tổng quan phân tích thay đổi revision sau chữ ký, nên nó bỏ qua phần căn bản về dựng lại revision và xếp hạng DocMDP để đi thẳng vào đồ thị object: ownership từng được mô hình hóa thế nào, vì sao hướng của một cạnh quyết định một phán quyết bảo mật, v3.126.2 đã đổi gì, và cách audit logic chấp nhận của chính bạn

Vì sao một chỉnh sửa trang qua nổi thành thay đổi annotation dưới DocMDP P=3?

Chỉnh sửa trang qua nổi vì đồ thị role cũ đi theo mọi indirect reference trong một dictionary như thể object được trỏ tới thuộc về bên trỏ tới, còn signature widget lại trỏ ngược về trang của nó. Một annotation dictionary mang /P, một indirect reference tới page object mà nó nằm trên (ISO 32000-1 §12.5.2). Entry đó là một gợi ý điều hướng. Widget không sở hữu trang; trang sở hữu widget qua mảng /Annots của nó

Bộ phân tích gán cho mỗi object một bộ role bit trước khi xếp hạng các thay đổi sau đó: page, annotation, form và vật liệu validation. Object gốc lấy role từ dictionary của chính nó, và role rồi lan tỏa tới mọi thứ nó tham chiếu. Trong phép lan truyền cũ, chuỗi đi thế này:

  1. Signature widget là một /Subtype /Widget với /FT /Sig, nên nó nhận role annotation
  2. /P của widget đẩy role annotation lên page dictionary, thứ đã có sẵn role page
  3. Trang đẩy cả hai role vào /Contents, /Resources và, qua /Parent, ngược lên cây Pages rồi lan sang mọi trang anh em
  4. Một content stream dictionary như << /Length 812 >> không có /Type, nên bộ xếp loại chông chênh về role bit và kiểm tra role annotation trước role page
Sơ đồ PDFium Component về đồ thị role DocMDP trước v3.126.2, nơi back-reference /P của signature widget đẩy role annotation lên page dictionary, trang lan nó qua /Contents xuống một content stream không có entry /Type, bộ xếp loại xuất prckAnnotation và xếp hạng P=3 trả về prdAllowed
Trước v3.126.2, đồ thị role coi mọi indirect reference là ownership, nên entry /P của widget đẩy role annotation lên trang và một chỉnh sửa trang đích thực chui ra khỏi bộ phân tích như một thay đổi annotation được phép

Content stream bị sửa vì thế lọt ra thành prckAnnotation. Theo ISO 32000-1 §12.8.2.2, DocMDP P=3 cho phép thay đổi annotation, nên phán quyết là prdAllowed và report cuộn lên thành prasAllowed. Cùng tệp đó dưới P=2 bị từ chối, nhưng chỉ nhờ trùng hợp: P=2 cấm thay đổi annotation, nên stream bị dán nhầm nhãn bị từ chối vì một lý do sai. Một vòng lan truyền bốn lượt cố định thêm một điểm yếu thứ hai. Payload tới qua một mảng indirect, hay qua một chuỗi dài mà các số object chạy ngược, có thể chẳng bao giờ nhận role nào cả

Vì sao một signature validator phải hỏi ai sở hữu một object?

Một signature validator phải hỏi ai sở hữu một object vì các incremental update của PDF (ISO 32000-1 §7.5.6) cho phép bất kỳ ai nối thêm một revision định nghĩa lại một số object có sẵn, và thân mới định nghĩa không tuyên bố nó là gì. Chữ ký vẫn verify được, vì nó chỉ phủ các byte của revision của chính nó. Mọi tuyến phòng thủ chống thao túng sau ký vì thế phụ thuộc vào việc ánh xạ từng object thay đổi tới cấu trúc đang dùng nó, rồi hỏi người ký có cho phép cấu trúc đó đổi hay không

Vài lớp tấn công đã công bố làm việc đúng ở khe hở đó. Incremental saving attack nối một revision đổi nội dung trang và dựa vào verifier chỉ kiểm tra vùng byte đã ký. Shadow attack gieo nội dung ẩn trước khi ký rồi kích hoạt nó sau bằng một thay đổi nhỏ, nhìn hiền lành. Tấn công trên tài liệu certified lợi dụng việc P=2 và P=3 tường minh cho phép vài chỉnh sửa sau đó, rồi trá hình một chỉnh sửa cấm thành một chỉnh sửa được phép. Một verifier xếp loại object theo nhãn như /Type /Annot, hay theo bất kỳ đường tham chiếu nào tình cờ với tới chúng, là lộ diện với lớp thứ ba: kẻ tấn công chỉ cần một cấu trúc được phép mà với tới được cấu trúc bị cấm

Vì thế câu hỏi không phải object nào đã đổi mà là ai sở hữu chúng. Một content stream với tới từ một trang qua /Contents là nội dung trang bất kể còn gì khác trỏ tới nó. Một annotation trỏ ngược về trang qua /P nói nơi annotation sống, không nói nó sở hữu gì

PDFium Component v3.126.2 mô hình hóa ownership ra sao?

PDFium Component v3.126.2 coi back-reference là điều hướng, giữ chúng ngoài phép lan truyền role, và quyết định những key nào tính là điều hướng từ vai trò cấu trúc của dictionary giữ chúng, không phải từ tên key một mình. Bảng dưới tóm tắt các key điều hướng không còn mang ownership

Dictionary sở hữuKey được coi là điều hướngTham chiếu spec
Page hay node Pages/Parent, /Kids, /AnnotsISO 32000-1 §7.7.3
Annotation hay widget/PISO 32000-1 §12.5.2
Widget hay field dictionary/ParentISO 32000-1 §12.7.3
Đồ thị object của PDFium Component v3.126.2 tách các cạnh payload thuộc sở hữu như /Contents và /Annots, nơi lan role page và annotation, khỏi các cạnh điều hướng như widget /P, thứ không mang role nào, kèm các key điều hướng theo từng dictionary sở hữu và prckPageContent được giữ trên content stream ngay cả dưới một /Type bị giả mạo
v3.126.2 giữ back-reference ngoài phép lan truyền role: role chỉ đi qua ownership thật, nên content stream vẫn là nội dung trang và gợi ý /P chẳng quyết định gì

Lọc theo tên key toàn cục sẽ tạo ra một lỗ hổng mới. Một font hay XObject resource có thể hợp lệ mang tên /P, /Parent hay /Annots, và một dictionary /Resources mà bỏ entry /P của nó khỏi phép lan truyền sẽ để kẻ tấn công giấu một XObject thuộc sở hữu trang sau một cái tên resource hiền lành. Trong v3.126.2, bộ lọc điều hướng chỉ áp dụng khi dictionary sở hữu thật sự là một page, node Pages, annotation, widget hay field. Nếu một trong các dictionary ấy mang một key điều hướng bị nhân đôi, chẳng hạn hai entry /P trong một widget, bộ phân tích không đoán xem viewer sẽ dùng bản sao nào; việc dựng role thất bại và chữ ký thành Indeterminate

Vài luật nữa bịt các đường dán lại nhãn còn sót:

  • Node Pages là root role page trên danh nghĩa riêng của chúng, nên resource kế thừa từ cây Pages (ISO 32000-1 §7.7.3.4) đi vào ngữ cảnh trang qua ownership thật, không phải qua một cuộc dạo /Parent từ trang con
  • Một role annotation hay form chạm tới một catalog, node Pages, page, annotation hay field dictionary thì dừng ở đó, vì những object cấu trúc này thiết lập role riêng của chúng và một role payload đi vào không được ghi đè lên
  • Role page có quyền quyết định trong khi xếp loại: một object thuộc sở hữu trang là prckPageContent ngay cả khi một revision sau viết lại nó với /FT giả mạo, một nhãn /Type /Annot, hay chia sẻ nó với một appearance stream
  • Một Form XObject chỉ được dùng làm appearance cho field hay annotation giữ nguyên loại form hay annotation của nó, nên việc tái tạo appearance bình thường sau khi điền form vẫn được xếp dưới các luật cho phép thường lệ
  • Một widget không có /FT riêng phân giải kiểu field kế thừa qua chuỗi /Parent, và một chuỗi không phân giải được làm hỏng việc dựng role thay vì mặc định thành annotation
  • Role bit từ mọi revision sau được gộp vào role của revision được phủ, nên một cập nhật sau không thể xóa sạch một quan hệ sở hữu trang sớm hơn bằng cách gỡ stream ra trước rồi sửa nó sau

Điểm cố định thay cho một số lượt cố định

Độ với tới của role trong v3.126.2 chạy như một hàng đợi việc lặp cho tới khi không object nào nhận thêm role bit mới, tức là một điểm cố định thật bất kể độ sâu chuỗi hay cách đánh số object. Các mảng indirect như một mảng /Contents được lưu thành object riêng cũng bị đi qua. Mỗi object có thể nhận tối đa bốn role bit phân biệt, nên hàng đợi bị chặn ở bốn entry mỗi số object; vượt ngân sách đó nêu prrResourceLimitExceeded. Một tham chiếu tới object free, một lệch generation hay một header object hỏng nêu prrMalformedRevisionChain, còn payload bên trong một object stream nén nêu prrCompressedObjectUnresolved. Mỗi thất bại trong số ấy đều kết thúc bằng prasIndeterminate, không bao giờ bằng một phán quyết được phép, và khi thất bại xảy ra trong lúc dựng role của revision được phủ, chữ ký không report Changes nào cả

Routine dưới đây liệt kê các chỉnh sửa nội dung trang sống sót qua phép phân tích này. Một thay đổi prckPageContent không bao giờ được xếp prdAllowed: DocMDP P=1, 2 hay 3 biến nó thành prdDisallowed, còn một chữ ký không có DocMDP xếp nó 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, chẳng cần free gì
    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;

Chuyện gì xảy ra khi FieldMDP và một annotation dùng chung một object?

Khi /V của một field và /Contents của một annotation trỏ vào cùng một indirect object, v3.126.2 giữ khóa FieldMDP còn hiệu lực dù thay đổi được xếp loại là một chỉnh sửa annotation. Kịch bản này dựng tay rất dễ: người ký khóa field Total bằng FieldMDP (ISO 32000-1 §12.8.2.4), và kẻ tấn công khiến /Contents của một text annotation tham chiếu đúng object chuỗi đang giữ giá trị field. Dưới P=3, chỉnh sửa annotation được phép, nên trước bản sửa, viết lại chuỗi dùng chung đó đã đổi một giá trị field bị khóa với một phán quyết được phép

Object giờ mang cả role annotation lẫn role form, và phán quyết annotation kiểm tra lại phía form bất cứ khi nào chữ ký có một transform FieldMDP:

  • Dưới P=2, thay đổi annotation bị cấm thẳng, đúng như trước
  • Với FieldMDP All, mọi field đều bị khóa, nên thay đổi dùng chung là prdDisallowed
  • Với FieldMDP Include hay Exclude, bộ phân tích không truy ngược nổi một scalar dùng chung về một tên field, nên phán quyết là prdIndeterminate thay vì một phỏng đoán
  • Không có FieldMDP, luật annotation P=3 áp dụng và thay đổi vẫn được phép
Sơ đồ phán quyết FieldMDP của PDFium Component nơi /V của một field Total bị khóa và /Contents của một annotation tham chiếu cùng một indirect object, rẽ nhánh qua DocMDP P=2, FieldMDP All, FieldMDP Include hay Exclude và không có FieldMDP tới các phán quyết prdDisallowed, prdIndeterminate hay prdAllowed cho cùng một chỉnh sửa dùng chung
Khi một indirect object mang cả role annotation lẫn role form, phán quyết annotation kiểm tra lại khóa FieldMDP, nên cùng một chỉnh sửa trải từ được phép tới bị cấm tới không xác định

Một chi tiết report có ý nghĩa với code cổng. Trường hợp dùng chung được report thành Kind = prckAnnotation với Decision = prdIndeterminate, và prrFieldMdpUnresolved chỉ được thêm vào bộ risk cho các thay đổi được xếp loại là form field. Một cổng đi tìm prrFieldMdpUnresolved mà bỏ qua Status sẽ bỏ sót trường hợp này hoàn toàn

Code Delphi nên fail closed với phân tích revision thế nào?

Code Delphi chỉ nên chấp nhận một tài liệu đã ký khi trạng thái phân tích là prasNoLaterChanges hay prasAllowed và không có structural risk nào, và nó nên coi prasIndeterminate cùng prasSuspicious là không đáng tin, chứ không phải các cảnh báo để log rồi cho qua. Indeterminate nghĩa là bộ phân tích không chứng minh nổi các revision sau được phép; với kẻ tấn công, một input cho ra Indeterminate một cách tin cậy cũng hữu ích như một input cho ra Allowed nếu code của bạn cho nó qua. Hàm toàn cục AnalyzePadesSignatureRevisions nhận bất kỳ TStream nào và đọc từ vị trí 0, hợp với các upload handler chẳng bao giờ cần render tài liệu

uses
  Classes, SysUtils, TypInfo, FPdfPades;

const
  // Định nghĩa trùng lặp được ghi nhận mà không giáng 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 và prasSuspicious là từ chối, không phải cảnh báo
    Reason := 'revision status ' +
      GetEnumName(TypeInfo(TPadesRevisionAnalysisStatus), Ord(Report.Status));
  end;
end;

Hai ranh giới đáng nói thẳng. TPadesRevisionAnalysisReport chẳng nói gì về tính toàn vẹn CMS hay độ tin cậy chứng thư, nên cổng này nằm cạnh validation mật mã và tin cậy, không phải thay thế chúng. Và một đồ thị ownership đúng không làm P=3 trở nên an toàn cho mọi workflow. P=3 thật sự cho phép annotation, và một annotation với appearance mờ đục có thể phủ lên văn bản đã ký mà không đụng tới một content stream nào. Nếu tài liệu certified của bạn là hợp đồng chứ không phải bản duyệt, hoặc certify với P=2, hoặc chuyển các thay đổi annotation được phép cho con người, như trong helper này:

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;

Danh mục kiểm tra audit revision chữ ký

Dùng danh sách này để xem pipeline verify của bạn đã từng lộ diện chưa và giờ đã fail closed chưa:

  • Các bản dựng PDFium Component trước v3.126.2 có thể report prasAllowed cho chỉnh sửa nội dung trang trong tài liệu DocMDP P=3; chạy lại TPdf.AnalyzeSignatureRevisions trên các tệp P=3 certified mà các bản cũ từng chấp nhận
  • Kiểm tra lại các tài liệu P=3 có khóa FieldMDP nơi một giá trị field và một annotation có thể dùng chung một indirect object
  • Chỉ chấp nhận prasNoLaterChanges và prasAllowed; coi prasIndeterminate và prasSuspicious là không đáng tin
  • Thử Report.Risks bên cạnh Report.Status, vì prrDuplicateObjectDefinition tự nó không đổi trạng thái
  • Đừng đọc một mảng Changes rỗng là kết quả sạch khi trạng thái chữ ký là Indeterminate; một lần dựng role thất bại không report thay đổi nào
  • Đừng trông cậy một mình prrFieldMdpUnresolved để bắt các vấn đề FieldMDP, vì trường hợp annotation dùng chung chỉ lộ qua phán quyết và trạng thái
  • Quyết định xem các thay đổi annotation được phép dưới P=3 có cần con người duyệt trong workflow của bạn không
  • Phân tích các byte tệp gốc; một tài liệu viết lại bằng SaveAs không còn chứa chuỗi revision nữa

Phân tích revision là một tầng của một phép kiểm tra chữ ký. Ghép nó với xem xét chữ ký số PDF và các cấp PAdES cho dictionary và baseline level, cùng một audit rủi ro bảo mật PDF rộng hơn cho JavaScript, launch action và tệp nhúng. TPdf.AnalyzeSignatureRevisions, AnalyzePadesSignatureRevisions và đồ thị role biết-ownership nói trong bài này có trong PDFium Component cho Delphi, C++Builder và Lazarus