مقاله فنی

DocMDP در PDFium Component: پنهان‌کاری /P یک widget

در 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، فرم و مواد اعتبارسنجی. شیءهای ریشه نقش را از دیکشنری خودشان می‌گیرند و نقش بعد به همه چیزهایی که ارجاع می‌دهند پخش می‌شود. در پخش قدیمی، زنجیره این‌طوری می‌رفت:

  1. widget امضا یک /Subtype /Widget با /FT /Sig است، پس نقش annotation می‌گیرد
  2. مقدار /P مربوط به widget نقش annotation را روی دیکشنری صفحه می‌گذارد، که از قبل نقش صفحه را دارد
  3. صفحه هر دو نقش را به /Contents و /Resources می‌برد و از طریق /Parent بالا در درخت Pages و به‌عرض به هر صفحه خواهر
  4. یک دیکشنری content stream مثل << /Length 812 >> هیچ /Type ای ندارد، پس طبقه‌بند به بیت‌های نقش پناه می‌برد و نقش annotation را قبل از نقش صفحه چک می‌کرد
نمودار PDFium Component از گراف نقش DocMDP قبل از v3.126.2 که در آن ارجاع برگشتی /P مربوط به widget امضا نقش annotation را روی دیکشنری صفحه می‌گذارد، صفحه آن را از طریق /Contents به یک content stream بدون دریه /Type می‌برد، طبقه‌بند خروجی می‌دهد prckAnnotation و نمره‌دهی P=3 برمی‌گرداند prdAllowed
قبل از v3.126.2 گراف نقش هر ارجاع غیرمستقیم را مالکیت می‌گرفت، پس دریه /P مربوط به widget نقش annotation را روی صفحه می‌گذاشت و یک ویرایش واقعی صفحه از تحلیلگر به‌عنوان تغییر 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، /AnnotsISO 32000-1 §7.7.3
annotation یا widget/PISO 32000-1 §12.5.2
دیکشنری widget یا فیلد/ParentISO 32000-1 §12.7.3
نمودار گراف شیء در PDFium Component نسخه v3.126.2 که یال‌های payload-متعلق مثل /Contents و /Annots که نقش‌های صفحه و annotation را پخش می‌کنند از یال‌های ناوبری مثل /P مربوط به widget که هیچ نقشی حمل نمی‌کنند جدا می‌کند، با کلیدهای ناوبری به‌ازای هر دیکشنری مالک و حفظ prckPageContent روی content stream حتی زیر یک /Type جعلی
‏v3.126.2 ارجاع‌های برگشتی را از پخش نقش بیرون نگه می‌دارد: نقش‌ها فقط از مالکیت واقعی سفر می‌کنند، پس content stream محتوای صفحه می‌ماند و راهنمای /P هیچ چیزی را تصمیم نمی‌گیرد

فیلتر کردن سراسری با نام کلید یک حفره جدید درست می‌کرد. یک فونت یا منبع 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 اعمال می‌شود و تغییر مجاز می‌ماند
نمودار تصمیم FieldMDP در PDFium Component که در آن مقدار /V فیلد قفل‌شده Total و مقدار /Contents یک annotation به یک شیء غیرمستقیم مشترک اشاره می‌کنند، و روی DocMDP با P=2 و FieldMDP روی All و FieldMDP روی Include یا Exclude و بدون FieldMDP منشعب می‌شود به حکم‌های prdDisallowed و prdIndeterminate یا prdAllowed برای همان ویرایش مشترک
وقتی یک شیء غیرمستقیم هم نقش annotation دارد هم نقش فرم، تصمیم annotation قفل FieldMDP را دوباره چک می‌کند، پس همان ویرایش از مجاز تا ممنوع تا نامشخص در نوسان است

یک جزئیات گزارش‌دهی برای کد گیت مهم است. حالت مشترک به‌عنوان 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 عرضه می‌شوند