В версии на PDFium Component преди v3.126.2 TPdf.AnalyzeSignatureRevisions можеше да оцени истинска промяна по съдържанието на страница като разрешена annotation промяна под DocMDP P=3, защото графът на revision ролите третираше /P обратната референция на signature widget-а към страницата му като ownership. От v3.126.2 насам PDFium Component разделя navigation ребрата от ребрата на притежаван payload, така че съдържанието на страница си остава съдържание на страница. Бъг репортът зад тази поправка изглежда безобиден на хартия. Сертифициран договор позволява annotations, контрагентът добавя един incremental save, а анализаторът казва, че всяка по-късна промяна е позволена. После някой diff-ва визуализираните страници и сумата за плащане на страница 2 е друга
Тази статия е продължението от гледна точка на атакуващия на прегледа на анализа на промените в revision-и след подписа, затова прескача основите на възстановяването на revision-и и на DocMDP оценяването и отива направо в обектния граф: как е бил моделиран ownership, защо посоката на едно ребро решава security извода, какво се е променило в v3.126.2 и как да одитирате собствена си логика за приемане
Защо промяна по страница мина като annotation промяна под DocMDP P=3?
Промяната мина, защото старият ролеви граф следваше всяка indirect референция в речник така, сякаш соченият обект принадлежи на сочещия, а signature widget-ът сочи обратно към страницата си. Annotation речникът носи /P — indirect референция към обекта страница, върху който стои (ISO 32000-1 §12.5.2). Този запис е navigation намек. Widget-ът не притежава страницата; страницата притежава widget-а чрез масива си /Annots
Анализаторът раздава на всеки обект набор от ролеви битове, преди да оцени по-късните промени: страница, annotation, форма и validation материал. Root обектите получават ролята си от собствения си речник, а ролята след това се разпространява към всичко, което сочат. В старата пропагация веригата минаваше така:
- Signature widget-ът е
/Subtype /Widgetс/FT /Sig, така че получава annotation ролята /Pна widget-а тласка annotation ролята върху речника страница, който вече има page ролята- Страницата тласка и двете роли в
/Contents,/Resourcesи през/Parentнагоре в Pages дървото и по диагонал към всяка сестра страница - Речник на content stream като
<< /Length 812 >>няма/Type, така че класификаторът се връщаше на ролевите битове и проверяваше annotation ролята преди page ролята
Промененият content stream затова излизаше като prckAnnotation. Под ISO 32000-1 §12.8.2.2 DocMDP P=3 позволява annotation промени, така че решението беше prdAllowed, а докладът се повиши до prasAllowed. Същият файл при P=2 беше отхвърлян, но само по случайност: P=2 забранява annotation промени, така че грешно етикетираният stream беше отрязан по погрешна причина. Фиксираният цикъл от четири прохода на пропагация добави и втора слабост. Payload, стигнат през indirect масив или през дълга верига, чиито обектни номера текат назад, може изобщо да не получи роля
Защо валидатор на подписи трябва да пита кой притежава даден обект?
Валидатор на подписи трябва да пита кой притежава обекта, защото incremental обновяванията в PDF (ISO 32000-1 §7.5.6) позволяват на всеки да долепи revision, който предефинира съществуващ обектен номер, а предефинираното тяло не обявява какво е. Подписът пак верифицира, защото покрива само байтовете на собствения си revision. Всяка защита от манипулация след подписването затова зависи от нанасянето на всеки променен обект върху структурата, която го ползва, и после от въпроса дали подписалият е позволил тази структура да се променя
Няколко публикувани класа атаки работят точно в тази празнина. Incremental saving attack-ите долепят revision, който сменя съдържанието на страница, и разчитат верификаторът да проверява само подписания байтов диапазон. Shadow attack-ите засаждат скрито съдържание преди подписването и го активират после с малка, невинна на вид промяна. Атаките върху сертифицирани документи злоупотребяват с факта, че P=2 и P=3 изрично позволяват някои по-късни промени, а после обличат забранена промяна като позволена. Верификатор, който класифицира обектите по етикети като /Type /Annot или по произволен референтен път, който случайно ги достига, е изложен на третия клас: на атакуващия му трябва само една позволена структура, която да достигне забранената
Затова въпросът не е кои обекти са се променили, а кой ги притежава. Content stream, достигнат от страница през /Contents, е съдържание на страница, каквото и друго да сочи към него. Annotation, сочеща обратно към страницата през /P, казва къде живее annotation-ът, а не какво притежава
Как PDFium Component v3.126.2 моделира ownership?
PDFium Component v3.126.2 третира обратните референции като navigation, държи ги извън ролевата пропагация и решава кои ключове се броят за navigation от структурната роля на речника, който ги носи, а не от самото име на ключа. Таблицата обобщава navigation ключовете, които вече не носят ownership
| Притежаващ речник | Ключове, третирани като navigation | Препратка към стандарта |
|---|---|---|
| Страница или Pages възел | /Parent, /Kids, /Annots | ISO 32000-1 §7.7.3 |
| Annotation или widget | /P | ISO 32000-1 §12.5.2 |
| Widget или полеви речник | /Parent | ISO 32000-1 §12.7.3 |
Глобално филтриране по име на ключ би създало нова дупка. Ресурс шрифт или XObject може легитимно да се казва /P, /Parent или /Annots, а /Resources речник, който махне записа си /P от пропагацията, би позволил на атакуващия да скрие page-притежаван XObject зад невинно име на ресурс. В v3.126.2 navigation филтърът се прилага само когато притежаващият речник реално е страница, Pages възел, annotation, widget или поле. Ако някой от тези речници носи дублиран navigation ключ, например два записа /P в widget, анализаторът не отгатва кое копие би ползвал viewer-ът; ролевата конструкция проваля и подписът става Indeterminate
Още няколко правила затварят останалите маршрути за преетикетиране:
- Pages възлите са page-ролеви корени сами по себе си, така че ресурсите, наследени от Pages дървото (ISO 32000-1 §7.7.3.4), влизат в page контекст през истински ownership, а не през разходка по
/Parentот дъщерна страница - Annotation или form роля, стигнала до catalog, Pages възел, страница, annotation или полеви речник, спира там, защото тези структурни обекти установяват собствените си роли и входяща payload роля не бива да ги презаписва
- Page ролята е авторитетна при класификацията: обект, притежаван от страница, е
prckPageContent, дори по-късен revision да го презапише с фалшив/FT, с етикет/Type /Annotили да го сподели с appearance stream - Form XObject, ползван само като appearance на поле или annotation, пази категорията си form или annotation, така че обичайната регенерация на appearances след попълване на форма пак се оценява по нормалните правила за разрешения
- Widget без собствен
/FTразрешава наследения field тип през веригата/Parent, а неразрешима верига проваля ролевата конструкция вместо да default-не към annotation - Ролевите битове от всеки по-късен revision се сливат в ролите на покрития revision, така че по-късна актуализация не може да изтрие по-ранна page-ownership връзка, като първо откачи stream и го редактира след това
Фиксирана точка вместо фиксиран брой проходи
Ролевата достижимост в v3.126.2 върви като work queue, която итерира, докато нито един обект не добие нов ролеви бит — истинска фиксирана точка, независимо от дълбочината на веригата и номерацията на обектите. Indirect масиви като /Contents масив, записан като самостоятелен обект, също се обхождат. Всеки обект може да добие най-много четири различни ролеви бита, така че queue-ът е ограничен до четири записа на обектен номер; превишаването вдига prrResourceLimitExceeded. Референция към свободен обект, несъответствие в generation или счупен обектен header вдига prrMalformedRevisionChain, а payload вътре в компресиран обектен stream вдига prrCompressedObjectUnresolved. Всяко от тези провали завършва с prasIndeterminate, никога с позволен извод, и когато провалът стане при строенето на ролите на покрития revision, подписът изобщо не докладва Changes
Рутината по-долу изброява промените по съдържание на страница, които оцеляват след този анализ. Промяна 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; // запис, нищо за освобождаване
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 споделят един обект?
Когато /V на поле и /Contents на annotation сочат към един и същ indirect обект, v3.126.2 пази FieldMDP заключването в сила, дори промяната да е класифицирана като annotation редакция. Сценарият се строи лесно на ръка: подписалият заключва полето Total с FieldMDP (ISO 32000-1 §12.8.2.4), а атакуващият кара /Contents на текстова annotation да сочи същия string обект, който държи стойността на полето. При P=3 annotation редакцията е позволена, така че преди поправката презаписването на този споделен string променяше заключена полева стойност с позволен извод
Обектът вече носи и annotation, и form роля, а annotation решението преотчита form страната, когато подписът има FieldMDP transform:
- При P=2 annotation промяната е отхвърляна направо, точно както преди
- С FieldMDP
Allвсяко поле е заключено, така че споделената промяна еprdDisallowed - С FieldMDP
IncludeилиExcludeанализаторът не може да проследи споделен скалар назад до едно име на поле, така че решението еprdIndeterminate, а не догадка - Без FieldMDP важи правилото за annotation при P=3 и промяната си остава позволена
Един детайл в доклада е важен за gate кода. Споделеният случай се докладва като Kind = prckAnnotation с Decision = prdIndeterminate, а prrFieldMdpUnresolved се добавя към набора рискове само за промени, класифицирани като form полета. Gate, който търси prrFieldMdpUnresolved и игнорира Status, пропусва този случай напълно
Как Delphi код да fail closed при анализ на revision-и?
Delphi код трябва да приема подписан документ само когато статусът на анализа е prasNoLaterChanges или prasAllowed и няма структурен риск, а prasIndeterminate и prasSuspicious да третира като недоверени, а не като предупреждения за логване и пропускане. Indeterminate значи, че анализаторът не е успял да докаже, че по-късните revision-и са били позволени; за атакуващ вход, който надеждно дава Indeterminate, е също толкова полезен, колкото вход, даващ Allowed, ако кодът ви го пропуска. Глобалната AnalyzePadesSignatureRevisions приема произволен TStream и го чете от позиция 0, което пасува на upload handler-и, които никога не трябва да визуализират документа
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 цялостност или доверие в сертификати, така че този gate стои до криптографската и trust валидацията, а не на тяхно място. И правилен ownership граф не прави P=3 безопасен за всеки работен поток. P=3 реално позволява annotations, а annotation с непрозрачен appearance може да покрие подписан текст, без да пипне нито един content stream. Ако сертифицираните ви документи са договори, а не копия за преглед, или сертифицирайте с P=2, или насочвайте позволените annotation промени към човек — като в този помощник:
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;
Чеклист за одит на revision анализ на подписи
Ползвайте този списък, за да проверите дали верификационната ви верига е била изложена и дали вече fail-ва затворено:
- Версии на PDFium Component преди v3.126.2 можеха да докладват
prasAllowedза промени по съдържание на страница в DocMDP P=3 документи; преизпълнетеTPdf.AnalyzeSignatureRevisionsвърху сертифицирани P=3 файлове, приети от по-стари версии - Прегледайте пак P=3 документи с FieldMDP заключвания, при които полева стойност и annotation може да споделят indirect обект
- Приемайте само
prasNoLaterChangesиprasAllowed; третирайтеprasIndeterminateиprasSuspiciousкато недоверени - Тествайте
Report.Risksсъщо кактоReport.Status, защотоprrDuplicateObjectDefinitionсам по себе си не променя статуса - Не четете празен
Changesмасив като чист резултат, когато статусът на подписа е Indeterminate; провалена ролева конструкция не докладва промени - Не разчитайте само на
prrFieldMdpUnresolved, за да хващате FieldMDP проблеми, защото споделеният annotation случай излиза наяве само чрез решението и статуса - Решете дали позволените annotation промени при P=3 се нуждаят от човешки преглед във вашия работен поток
- Анализирайте оригиналните байтове на файла; документ, презаписан с
SaveAs, вече не съдържа revision веригата
Revision анализът е един слой от проверката на подпис. Комбинирайте го с прегледа на PDF цифрови подписи и PAdES нива за речниците и baseline нивото, и с по-широк одит на PDF security рискове за JavaScript, launch действия и вградени файлове. TPdf.AnalyzeSignatureRevisions, AnalyzePadesSignatureRevisions и ownership-съзнателният ролеви граф, описан тук, се доставят в PDFium Component за Delphi, C++Builder и Lazarus