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:
- Signature widget là một
/Subtype /Widgetvới/FT /Sig, nên nó nhận role annotation /Pcủa widget đẩy role annotation lên page dictionary, thứ đã có sẵn role page- Trang đẩy cả hai role vào
/Contents,/Resourcesvà, qua/Parent, ngược lên cây Pages rồi lan sang mọi trang anh em - 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
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ữu | Key được coi là điều hướng | Tham chiếu spec |
|---|---|---|
| Page hay node Pages | /Parent, /Kids, /Annots | ISO 32000-1 §7.7.3 |
| Annotation hay widget | /P | ISO 32000-1 §12.5.2 |
| Widget hay field dictionary | /Parent | ISO 32000-1 §12.7.3 |
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
/Parenttừ 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à
prckPageContentngay cả khi một revision sau viết lại nó với/FTgiả 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ó
/FTriê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
IncludehayExclude, 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àprdIndeterminatethay 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
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
prasAllowedcho chỉnh sửa nội dung trang trong tài liệu DocMDP P=3; chạy lạiTPdf.AnalyzeSignatureRevisionstrê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
prasNoLaterChangesvàprasAllowed; coiprasIndeterminatevàprasSuspiciouslà không đáng tin - Thử
Report.Risksbên cạnhReport.Status, vìprrDuplicateObjectDefinitiontự nó không đổi trạng thái - Đừng đọc một mảng
Changesrỗ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
SaveAskhô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