در buildهای PDFium Component قبل از v3.126.2، تابع TPdf.AnalyzeSignatureRevisions میتوانست یک ویرایش واقعی محتوای صفحه را بهعنوان تغییر annotation مجاز زیر DocMDP با P=3 نمره بدهد، چون گراف نقش نسخههایش ارجاع برگشتی /P مربوط به widget امضا به صفحهاش را مالکیت میگرفت. از v3.126.2 به بعد، PDFium Component یالهای ناوبری را از یالهای payload-متعلق جدا میکند، پس محتوای صفحه محتوای صفحه میماند. باگریپورت پشت این فیکس روی کاغذ بیخطر به نظر میرسد. یک قرارداد مهرشده اجازه annotation میدهد، طرف مقابل یک incremental save اضافه میکند، و تحلیلگر میگوید همه تغییرات بعدی مجازند. بعد یکی صفحات رندرشده را diff میکند و مبلغ پرداخت در صفحه 2 فرق کرده
این مقاله دنباله از دید مهاجمِ مرور کلی تحلیل تغییرات نسخههای بعد از امضا است، پس از مبانی بازسازی نسخهها و نمرهدهی DocMDP میگذرد و مستقیم میرود سراغ گراف شیء: مالکیت چطور مدل شده بود، چرا جهت یک یال حکم امنیتی را تصمیم میگیرد، در v3.126.2 چه چیزی عوض شد، و چطور منطق پذیرش خودتان را ممیزی کنید
چرا یک ویرایش صفحه زیر DocMDP با P=3 بهعنوان تغییر annotation پاس میشد؟
ویرایش صفحه پاس میشد چون گراف نقش قدیمی هر ارجاع غیرمستقیم داخل یک دیکشنری را دنبال میکرد انگار شیء مورد ارجاع متعلق به مرجعدهنده است، و widget امضا به صفحهاش اشاره برگشتی دارد. یک دیکشنری annotation مقدار /P حمل میکند، ارجاعی غیرمستقیم به شیء صفحهای که رویش نشسته (ISO 32000-1 §12.5.2). آن دریه یک راهنمای ناوبری است. widget مالک صفحه نیست؛ صفحه از طریق آرایه /Annots مالک widget است
تحلیلگر قبل از نمرهدهی به تغییرات بعدی، به هر شیء مجموعهای از بیتهای نقش میدهد: صفحه، annotation، فرم و مواد اعتبارسنجی. شیءهای ریشه نقش را از دیکشنری خودشان میگیرند و نقش بعد به همه چیزهایی که ارجاع میدهند پخش میشود. در پخش قدیمی، زنجیره اینطوری میرفت:
- widget امضا یک
/Subtype /Widgetبا/FT /Sigاست، پس نقش annotation میگیرد - مقدار
/Pمربوط به widget نقش annotation را روی دیکشنری صفحه میگذارد، که از قبل نقش صفحه را دارد - صفحه هر دو نقش را به
/Contentsو/Resourcesمیبرد و از طریق/Parentبالا در درخت Pages و بهعرض به هر صفحه خواهر - یک دیکشنری content stream مثل
<< /Length 812 >>هیچ/Typeای ندارد، پس طبقهبند به بیتهای نقش پناه میبرد و نقش annotation را قبل از نقش صفحه چک میکرد
پس content stream دستکاریشده بهشکل prckAnnotation درمیآمد. زیر ISO 32000-1 §12.8.2.2، DocMDP با P=3 تغییرات annotation را اجازه میدهد، پس تصمیم میشد prdAllowed و گزارش جمع میشد به prasAllowed. همان فایل زیر P=2 رد میشد، اما فقط به شانس: P=2 تغییرات annotation را ممنوع میکند، پس stream اشتباه-برچسبخورده به دلیل غلط رد میشد. حلقه پخش ثابت چهار-گذری یک ضعف دوم هم اضافه میکرد. payload ای که از طریق یک آرایه غیرمستقیم میرسید، یا از طریق زنجیره بلندی که شماره شیءهایش عقبرو بود، شاید هرگز هیچ نقشی نمیگرفت
چرا یک validator امضا باید بپرسد مالک یک شیء کیست؟
یک validator امضا باید بپرسد مالک یک شیء کیست چون بهروزرسانیهای افزایشی PDF (ISO 32000-1 §7.5.6) به هر کسی اجازه میدهند نسخهای append کند که شماره یک شیء موجود را دوباره تعریف میکند، و بدنه بازتعریفشده اعلام نمیکند چیست. امضا همچنان verify میشود، چون فقط بایتهای نسخه خودش را پوشش میدهد. پس هر دفاعی در مقابل دستکاری بعد-از-امضا به این وابسته است که هر شیء تغییرکرده را به ساختاری مپ کند که ازش استفاده میکند، و بعد بپرسد آیا امضاکننده اجازه داده آن ساختار عوض شود
چند کلاس حمله منتشرشده دقیقاً از همین شکاف کار میکنند. حملات incremental saving نسخهای append میکنند که محتوای صفحه را سواپ میکند و روی این حساباند که verifier فقط بازه بایت امضاشده را چک میکند. حملات shadow قبل از امضا محتوای پنهان میکارند و بعداً با یک تغییر کوچک بیآزار فعالش میکنند. حملات روی اسناد مهرشده از این سوءاستفاده میکنند که P=2 و P=3 صریحاً بعضی ویرایشهای بعدی را اجازه میدهند، و بعد یک ویرایش ممنوع را لباس یک ویرایش مجاز را میپوشانند. verifier ای که شیءها را با برچسبهایی مثل /Type /Annot طبقهبندی کند، یا با هر مسیر ارجاعی که اتفاقاً به آنها برسد، در برابر کلاس سوم لخت است: مهاجم فقط یک ساختار مجاز لازم دارد که به ساختار ممنوع برسد
برای همین سؤال این نیست که کدام شیءها عوض شدهاند بلکه این است که مالکشان کیست. یک content stream که از یک صفحه از طریق /Contents قابل رسیدن است محتوای صفحه است هرچه غیر از آن هم بهش اشاره کند. یک annotation که از طریق /P به صفحه اشاره برگشتی دارد میگوید annotation کجا زندگی میکند، نه چه چیزی مالکش است
PDFium Component در v3.126.2 مالکیت را چطور مدل میکند؟
PDFium Component در v3.126.2 ارجاعهای برگشتی را ناوبری میگیرد، از پخش نقش بیرون نگهشان میدارد، و تصمیم میگیرد کدام کلیدها ناوبری حساب شوند بر اساس نقش ساختاری دیکشنریای که آنها را دارد، نه فقط از روی نام کلید. جدول خلاصه میکند کلیدهای ناوبریای را که دیگر مالکیت حمل نمیکنند
| دیکشنری مالک | کلیدهایی که ناوبری حساب میشوند | ارجاع مشخصات |
|---|---|---|
| گره صفحه یا 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 اش را از پخش بیندازد به مهاجم اجازه میداد یک XObject متعلق-به-صفحه را پشت یک نام منبع بیآزار پنهان کند. در v3.126.2 فیلتر ناوبری فقط وقتی اعمال میشود که دیکشنری مالک واقعاً صفحه یا گره Pages یا annotation یا widget یا فیلد باشد. اگر یکی از این دیکشنریها یک کلید ناوبری تکراری حمل کند، مثل دو دریه /P در یک widget، تحلیلگر حدس نمیزند viewer از کدام کپی استفاده میکرد؛ ساخت نقش شکست میخورد و امضا Indeterminate میشود
چند قاعده دیگر هم مسیرهای بازبرچسبگذاری باقیمانده را میبندند:
- گرههای Pages خودشان ریشههای نقش-صفحهاند، پس منابع بهارثرسیده از درخت Pages (ISO 32000-1 §7.7.3.4) از طریق مالکیت واقعی وارد بافت صفحه میشوند نه از طریق یک پیادهروی
/Parentاز یک صفحه فرزند - نقش annotation یا فرمی که به یک catalog یا گره Pages یا صفحه یا annotation یا دیکشنری فیلد برسد همانجا میایستد، چون این شیءهای ساختاری نقش خودشان را برقرار میکنند و نباید یک نقش payload ورودی رویشان بنشیند
- نقش صفحه موقع طبقهبندی مرجع است: یک شیء متعلق-به-صفحه
prckPageContentاست حتی اگر یک نسخه بعدی آن را با یک/FTجعلی بازنویسی کند، یا یک برچسب/Type /Annotبگذارد، یا آن را با یک appearance stream شریک کند - یک Form XObject که فقط بهعنوان appearance یک فیلد یا annotation استفاده میشود دسته فرم یا annotation خودش را نگه میدارد، پس بازتولید معمولی appearance بعد از پر کردن فرم همچنان زیر قواعد اجازه عادی نمره میگیرد
- widget ای که
/FTخودش را ندارد نوع فیلد بهارثرسیده را از طریق زنجیره/Parentحل میکند، و زنجیره حلنشدنی بهجای پیشفرض گرفتن annotation ساخت نقش را شکست میدهد - بیتهای نقش از همه نسخههای بعدی داخل نقشهای نسخه پوششدادهشده ادغام میشوند، پس یک بهروزرسانی بعدی نمیتواند یک رابطه مالکیت-صفحه قبلی را با جدا کردن اول یک stream و بعد ویرایشش پاک کند
نقطه ثابت بهجای تعداد گذر ثابت
دسترسپذیری نقش در v3.126.2 بهشکل یک صف کار اجرا میشود که تکرار میکند تا هیچ شیءای بیت نقش جدیدی نگیرد، که یک نقطه ثابت واقعی است مستقل از عمق زنجیره یا شمارهگذاری شیءها. آرایههای غیرمستقیم مثل یک آرایه /Contents که بهعنوان شیء مستقل ذخیره شده هم پیادهروی میشوند. هر شیء حداکثر میتواند چهار بیت نقش متمایز بگیرد، پس صف بهازای هر شماره شیء سقف چهار دریه دارد؛ رد شدن از آن بودجه prrResourceLimitExceeded میدهد. یک ارجاع به شیء آزاد، یک ناهمخوانی generation یا یک هدر شیء شکسته prrMalformedRevisionChain میدهد، و payload داخل یک object stream فشرده prrCompressedObjectUnresolved میدهد. هر یک از این شکستها به prasIndeterminate ختم میشود، هرگز به یک حکم مجاز نه، و وقتی شکست موقع ساخت نقشهای نسخه پوششدادهشده رخ دهد امضا اصلاً هیچ 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 به همان شیء غیرمستقیم اشاره کنند، v3.126.2 قفل FieldMDP را در اعمال نگه میدارد حتی اگر تغییر بهعنوان ویرایش annotation طبقهبندی شود. سناریو راحت با دست ساخته میشود: امضاکننده فیلد Total را با FieldMDP قفل میکند (ISO 32000-1 §12.8.2.4)، و مهاجم کاری میکند /Contents یک annotation متنی به همان شیء رشتهای اشاره کند که مقدار فیلد را دارد. زیر P=3 ویرایش annotation مجاز است، پس قبل از فیکس بازنویسی آن رشته مشترک مقدار یک فیلد قفلشده را با حکم مجاز عوض میکرد
شیء حالا هم نقش annotation دارد هم نقش فرم، و تصمیم annotation هر وقت امضا transform مربوط به FieldMDP داشته باشد سمت فرم را دوباره چک میکند:
- زیر P=2 تغییر annotation کلاً ممنوع است، دقیقاً مثل قبل
- با FieldMDP روی
Allهمه فیلدها قفلاند، پس تغییر مشترکprdDisallowedاست - با FieldMDP روی
IncludeیاExclude، تحلیلگر نمیتواند یک اسکالر مشترک را به یک نام فیلد ردیابی کند، پس تصمیمprdIndeterminateاست نه یک حدس - بدون FieldMDP قاعده annotation یعنی P=3 اعمال میشود و تغییر مجاز میماند
یک جزئیات گزارشدهی برای کد گیت مهم است. حالت مشترک بهعنوان Kind = prckAnnotation با Decision = prdIndeterminate گزارش میشود، و prrFieldMdpUnresolved فقط برای تغییرهایی که فیلد فرم طبقهبندی شدهاند به مجموعه ریسک اضافه میشود. گیتی که دنبال prrFieldMdpUnresolved بگردد و Status را نادیده بگیرد این حالت را کاملاً از دست میدهد
کد دلفی روی تحلیل نسخهها چطور fail-closed رفتار کند؟
کد دلفی باید یک سند امضاشده را فقط وقتی بپذیرد که status تحلیل prasNoLaterChanges یا prasAllowed باشد و هیچ ریسک ساختاری حاضر نباشد، و باید prasIndeterminate و prasSuspicious را نامطمئن بگیرد، نه هشدارهایی که لاگ میشوند و رد میشوند. Indeterminate یعنی تحلیلگر نتوانست ثابت کند نسخههای بعدی مجاز بودهاند؛ برای مهاجم، ورودیای که قابلاعتماد Indeterminate تولید میکند همانقدر بهدردش میخورد که ورودیای که Allowed تولید میکند، اگر کدتان بگذاردش داخل. تابع سراسری AnalyzePadesSignatureRevisions هر TStream ای میگیرد و از موقعیت 0 میخواند، که برای handlerهای upload که هرگز لازم نیست سند را رندر کنند مناسب است
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 یا اعتماد گواهی هیچ چیزی نمیگوید، پس این گیت کنار اعتبارسنجی رمزنگاشتی و اعتماد مینشیند، نه جای آنها. و یک گراف مالکیت درست P=3 را برای هر گردشکاری امن نمیکند. P=3 واقعاً annotation را اجازه میدهد، و annotation ای با appearance مات میتواند روی متن امضاشده بنشیند بدون اینکه یک content stream را هم لمس کند. اگر اسناد مهرشده شما قراردادند نه نسخههای بازبینی، یا با P=2 مهر کنید یا تغییرهای annotation مجاز را به یک انسان حواله دهید، مثل این 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;
چکلیست ممیزی نسخههای امضا
از این لیست برای چک کردن استفاده کنید که pipeline اعتبارسنجیتان در معرض بوده یا نه و اینکه حالا fail-closed میکند یا نه:
- buildهای PDFium Component قبل از v3.126.2 میتوانستند برای ویرایشهای محتوای صفحه در اسناد DocMDP با P=3 مقدار
prasAllowedگزارش کنند؛ روی فایلهای P=3 مهرشدهای که buildهای قدیمی پذیرفتهاند دوبارهTPdf.AnalyzeSignatureRevisionsرا اجرا کنید - اسناد P=3 با قفلهای FieldMDP را دوباره چک کنید، جاهایی که یک مقدار فیلد و یک annotation ممکن است یک شیء غیرمستقیم را شریک باشند
- فقط
prasNoLaterChangesوprasAllowedرا بپذیرید؛ prasIndeterminateوprasSuspiciousرا نامطمئن بگیرید - هم
Report.Risksرا تست کنید همReport.Statusرا، چونprrDuplicateObjectDefinitionبهتنهایی status را عوض نمیکند - وقتی status امضا Indeterminate است یک آرایه
Changesخالی را نتیجه تمیز نگیرید؛ ساخت نقش ناموفق هیچ تغییری گزارش نمیکند - برای گرفتن مشکلات FieldMDP فقط به
prrFieldMdpUnresolvedتکیه نکنید، چون حالت annotation مشترک فقط از طریق تصمیم و status بیرون میزند - تصمیم بگیرید آیا در گردشکاری شما تغییرهای annotation مجاز زیر P=3 نیاز به بازبینی انسانی دارند
- بایتهای فایل اصلی را تحلیل کنید؛ سندی که با
SaveAsبازنویسی شده دیگر زنجیره نسخهها را ندارد
تحلیل نسخهها یک لایه از چک امضاست. آن را با بازرسی امضاهای دیجیتال PDF و سطوح PAdES برای دیکشنری و سطح baseline جفت کنید، و با یک ممیزی ریسک امنیتی PDF جامعتر برای JavaScript و اکشنهای launch و فایلهای جاسازیشده. TPdf.AnalyzeSignatureRevisions و AnalyzePadesSignatureRevisions و گراف نقش آگاه-به-مالکیت که اینجا توضیح داده شد در PDFium Component برای Delphi و C++Builder و Lazarus عرضه میشوند