مقال تقني

‏DocMDP في PDFium Component: كيف أخفى /P لودجت تعديل صفحة

في بناءات PDFium Component قبل v3.126.2 كان TPdf.AnalyzeSignatureRevisions يستطيع تدرّج تعديل حقيقي لمحتوى صفحةٍ بوصفه تغيير تعليقات مسموحاً تحت DocMDP ‏P=3، لأن مخطط أدوار المراجعات عنده عامل المرجعَ العكسي /P من ودجت التوقيع إلى صفحته ملكيةً. ومنذ v3.126.2 يفصل PDFium Component حافات الملاحة عن حافات الحمولة المملوكة، فيبقى محتوى الصفحة محتوى صفحة. وبلاغ العيب خلف هذا الإصلاح يبدو غير مؤذٍ على الورق: عقد موثق يسمح بالتعليقات، يضيف الطرف الآخر حفظاً تزايدياً واحداً، فيقول المحلل إن كل تغيير لاحق مسموح. ثم يفرّق أحدهم بين الصفحات المصيَّرة فيختلف مبلغ الدفع في الصفحة 2

هذه المقالة هي التتمة بعين المهاجم لـ نظرة عامة على تحليل تغييرات المراجعات بعد التوقيع، فتتخطى أساسيات إعادة بناء المراجعات وتدرّج DocMDP وتذهب مباشرة إلى مخطط الكائنات: كيف صيغت الملكية، ولماذا يقرر اتجاه الحافة حكماً أمنياً، وما تغير في v3.126.2، وكيف تدقق منطق قبولك الخاص

لماذا اجتاز تعديل صفحة بوصفه تغيير تعليقات تحت DocMDP ‏P=3؟

اجتاز تعديل الصفحة لأن مخطط الأدوار القديم تبع كل مرجع غير مباشر في قاموس كأن الكائن المشار إليه يخص المُشير إليه، وودجت التوقيع يشير عائداً إلى صفحته. يحمل قاموس التعليق /P، مرجعاً غير مباشر إلى كائن الصفحة الذي يجلس عليه (ISO 32000-1 §12.5.2). وذلك المدخل تلميح ملاحة. الودجت لا يملك الصفحة؛ الصفحة هي من تملك الودجت عبر مصفوفة /Annots

يسند المحلل لكل كائن مجموعة بتات أدوار قبل أن يدرّج التغييرات اللاحقة: صفحة، وتعليق، ونموذج، ومواد تحقق. وتأخذ الكائنات الجذورة دورها من قاموسها الخاص، ثم ينتشر الدور إلى كل ما تشير إليه. وفي الانتشار القديم جرت السلسلة هكذا:

  1. ودجت التوقيع هو /Subtype /Widget مع /FT /Sig، فيحصل على دور التعليق
  2. يدفع /P الخاص بالودجت دور التعليق إلى قاموس الصفحة، الذي يملك دور الصفحة أصلاً
  3. يدفع الصفحة الدورين معاً إلى /Contents و /Resources، وعبر /Parent إلى أعلى شجرة Pages وعبرها إلى كل صفحة شقيقة
  4. قاموس تدفق المحتوى مثل << /Length 812 >> لا يملك /Type، فتراجع المصنِّف إلى بتات الأدوار وفحص دور التعليق قبل دور الصفحة
مخطط PDFium Component لمخطط أدوار DocMDP قبل v3.126.2 حيث يدفع المرجع العكسي /P لودجت التوقيع دور التعليق إلى قاموس الصفحة، وينشر الصفحة الدور عبر /Contents إلى تدفق محتوى بلا مدخل /Type، فيخرج المصنف prckAnnotation ويعيد تدرج P=3 الحكم prdAllowed
قبل v3.126.2 عامل مخطط الأدوار كل مرجع غير مباشر ملكيةً، فدفع مدخل الودجت /P دورَ التعليق إلى الصفحة، وخرج تعديل صفحة حقيقي من المحلل تغيير تعليقات مسموحاً

خرج تدفق المحتوى المعدل إذن prckAnnotation. وتحت ISO 32000-1 §12.8.2.2 يسمح DocMDP ‏P=3 بتغييرات التعليقات، فكان القرار prdAllowed وتدحرج التقرير إلى prasAllowed. والملف نفسه تحت P=2 رُفض، لكن بالمصادفة فقط: يمنع P=2 تغييرات التعليقات، فرُفض التدفق المخلوَم لسبب خاطئ. وأضافت حلقة الانتشار ذات الأربع مرات الثابتة ضعفاً ثانياً. الحمولة التي تصل عبر مصفوفة غير مباشرة، أو عبر سلسلة طويلة تجري أعداد كائناتها بالعكس، قد لا تستلم أي دور على الإطلاق

لماذا يجب على مدقق التوقيعات أن يسأل من يملك الكائن؟

على مدقق التوقيعات أن يسأل من يملك الكائن لأن التحديثات التزايدية في PDF ‏(ISO 32000-1 §7.5.6) تتيح لأي أحد إلحاق مراجعة تعيد تعريف رقم كائن قائم، وجسمها المعاد تعريفه لا يعلن ما هو. يظل التوقيع متحققاً، لأنه يغطي بايتات مراجعته وحدها. فكل دفاع عن التلاعب بعد التوقيع يعتمد على إسقاط كل كائن متغير إلى البنية التي تستخدمه، ثم السؤال هل سمح الموقّع بتغير تلك البنية

عدة أصناف هجوم منشورة تعمل في تلك الفجوة بالضبط. هجمات الحفظ التزايدي تلحق مراجعة تستبدل محتوى الصفحة وتعتمد على أن المدقق يفحص المدى البايتي الموقع وحده. هجمات الظل تزرع محتوى مخفياً قبل التوقيع وتفعّله بعدها بتغيير صغير بريء المظهر. وهجمات المستندات الموثقة تستغل أن P=2 و P=3 يسمحان صراحةً ببعض التعديلات اللاحقة، ثم يلبسون تعديلاً ممنوعاً زيَّ تعديل مسموح. والمدقق الذي يصنف الكائنات ببطاقات مثل /Type /Annot، أو بأي مسار مرجع يصادف وصوله إليها، معرَّض للصنف الثالث: لا يحتاج المهاجم إلا بنية واحدة مسموحة تستطيع الوصول إلى الممنوعة

لهذا ليس السؤال أي الكائنات تغيرت بل من يملكها. تدفق المحتوى الذي يُبلغ من صفحة عبر /Contents هو محتوى صفحة أياً كان ما يشير إليه غيره. والتعليق الذي يشير عائداً إلى الصفحة عبر /P يقول أين يسكن التعليق، لا ما يملكه

كيف يصوغ PDFium Component ‏v3.126.2 الملكية؟

يعامل PDFium Component ‏v3.126.2 المراجعَ العكسية ملاحةً، ويبقيها خارج انتشار الأدوار، ويحسم أي المفاتيح تعد ملاحةً من الدور البنيوي للقاموس الذي يحويها، لا من اسم المفتاح وحده. يلخص الجدول مفاتيح الملاحة التي لم تعد تحمل ملكية

قاموس المالكمفاتيح تُعامل ملاحةمرجع المواصفة
صفحة أو عقدة Pages/Parent، /Kids، /AnnotsISO 32000-1 §7.7.3
تعليق أو ودجت/PISO 32000-1 §12.5.2
ودجت أو قاموس حقل/ParentISO 32000-1 §12.7.3
مخطط كائنات PDFium Component ‏v3.126.2 يفصل حافات الحمولة المملوكة مثل /Contents و /Annots التي تنشر أدوار الصفحة والتعليق عن حافات الملاحة مثل /P للودجت التي لا تحمل أدواراً، مع مفاتيح الملاحة لكل قاموس مالك وبقاء prckPageContent على تدفق المحتوى حتى تحت /Type مزوَّر
يبقي v3.126.2 المراجع العكسية خارج انتشار الأدوار: تسافر الأدوار عبر الملكية الحقيقية وحدها، فيبقى تدفق المحتوى محتوى صفحة ولا يحسم تلميح /P شيئاً

كانت الترشيح باسم المفتاح عالمياً ستفتح ثغرة جديدة. يمكن لمورد خط أو XObject أن يسمى /P أو /Parent أو /Annots مشروعاً، وقاموس /Resources يُسقط مدخل /P من الانتشار سيسمح للمهاجم بإخفاء XObject مملوك لصفحةٍ خلف اسم مورد بريء. في v3.126.2 لا ينطبق مرشح الملاحة إلا حين يكون القاموس المالك فعلاً صفحةً أو عقدة Pages أو تعليقاً أو ودجت أو حقل. وإن حمل أحد تلك القواميس مفتاح ملاحة مكرراً، مثل مدخلي /P في ودجت، لا يخمّن المحلل أي النسختين سيستخدمها العارض؛ يفشل بناء الأدوار ويصير التوقيع Indeterminate

عدة قواعد إضافية تغلق طرق إعادة التسمية الباقية:

  • عقد Pages جذور دور صفحة بذاتها، فالموارد الموروثة من شجرة Pages ‏(ISO 32000-1 §7.7.3.4) تدخل سياق الصفحة عبر ملكية حقيقية، لا عبر تجول /Parent من صفحة ابن
  • دور تعليق أو نموذج يصل إلى كتالوج أو عقدة Pages أو صفحة أو تعليق أو قاموس حقل يتوقف هناك، لأن تلك الكائنات البنيوية تثبت أدوارها الخاصة ولا يجوز لدور حمولة واصل أن يسودها
  • دور الصفحة هو السيد أثناء التصنيف: الكائن المملوك لصفحة هو prckPageContent حتى لو أعادت مراجعة لاحقة كتابته بـ /FT مزوَّر أو بطاقة /Type /Annot أو شاركه مع تدفق مظهر
  • Form XObject المستخدم مظهر حقل أو تعليق وحدها يبقي صنف نموذجه أو تعليقه، فيظل تجديد المظهر العادي بعد ملء نموذجٍ متدرجاً تحت قواعد الإذن العادية
  • الودجت بلا /FT خاصة يحل نوع الحقل الموروث عبر سلسلة /Parent، والسلسلة العاجزة عن الحل تفشل بناء الأدوار بدل الافتراض إلى تعليق
  • تُدمج بتات الأدوار من كل مراجعة لاحقة في أدوار المراجعة المغطاة، فلا يستطيع تحديث لاحق محو علاقة ملكية صفحة سابقة بفصل تدفق أولاً وتحريره بعدها

نقطة ثابتة بدل عدد مرات ثابت

يجري بلوغ الأدوار في v3.126.2 طابور عمل يتكرر حتى لا يكسب أي كائن بت دورٍ جديداً، وهي نقطة ثابتة حقيقية أياً كان عمق السلسلة أو ترقيم الكائنات. وتُجوال المصفوفات غير المباشرة أيضاً، مثل مصفوفة /Contents المخزنة كائناً خاصاً بها. ويستطيع كل كائن أن يكسب أربع بتات أدوار مميزة على الأكثر، فيحصر الطابور عند أربعة مدخلات لكل رقم كائن؛ وتجاوز ذلك الميزانية يرفع prrResourceLimitExceeded. ومرجع إلى كائن حر أو تباين جيل أو ترويسة كائن مكسورة يرفع prrMalformedRevisionChain، والحمولة داخل تدفق كائنات مضغوط يرفع 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 وتعليق كائناً واحداً؟

حين تشير /V لحقلٍ و /Contents لتعليقٍ إلى الكائن غير المباشر نفسه، يبقي v3.126.2 قفل FieldMDP نافذاً رغم أن التغيير يُصنف تحرير تعليق. ويسهل بناء السيناريو يدوياً: يُقفل الموقّع حقل Total بـ FieldMDP ‏(ISO 32000-1 §12.8.2.4)، ويجعل المهاجم /Contents لتعليق نصي تشير إلى كائن السلسلة نفسه الذي يحمل قيمة الحقل. تحت P=3 تعديل التعليق مسموح، فقبل الإصلاح كانت إعادة كتابة تلك السلسلة المشتركة تغيّر قيمة حقل مقفول بحكمٍ مسموح

يحمل الكائن الآن دورَي التعليق والنموذج معاً، ويعيد قرار التعليق فحص جانب النموذج كلما امتلك التوقيع تحويل FieldMDP:

  • تحت P=2 يُمنع تغيير التعليق منعاً تاماً، تماماً كما كان
  • مع FieldMDP ‏All كل الحقول مقفولة، فالتغيير المشترك prdDisallowed
  • مع FieldMDP ‏Include أو Exclude لا يستطيع المحلل تتبع عددي مشترك إلى اسم حقل واحد، فالقرار prdIndeterminate لا تخمين
  • من دون FieldMDP تسري قاعدة تعليقات P=3 ويبقى التغيير مسموحاً
مخطط قرار FieldMDP في PDFium Component حيث يشير /V لحقل Total مقفول و /Contents لتعليق إلى الكائن غير المباشر نفسه، يتفرع على DocMDP ‏P=2 و FieldMDP ‏All و FieldMDP ‏Include أو Exclude ولا FieldMDP إلى أحكام prdDisallowed و prdIndeterminate و prdAllowed للتغيير المشترك نفسه
حين يحمل كائن غير مباشر دورَي التعليق والنموذج معاً يعيد قرار التعليق فحص قفل FieldMDP، فيتراوح التغيير نفسه من مسموح إلى ممنوع إلى غير محسوم

تفصيل إبلاغي مهم لكود البوابات. الحالة المشتركة تُبلَّغ Kind = prckAnnotation مع Decision = prdIndeterminate، ولا يضاف prrFieldMdpUnresolved إلى مجموعة المخاطر إلا للتغييرات المصنفة حقول نماذج. بوابة تبحث عن prrFieldMdpUnresolved وتتجاهل Status تفوت هذه الحالة كلياً

كيف يفشل كود Delphi بإحكام على تحليل المراجعات؟

ينبغي لكود Delphi أن يقبل مستنداً موقعاً فقط حين تكون حالة التحليل prasNoLaterChanges أو prasAllowed ومن دون أي خطر بنيوي، ويعامل prasIndeterminate و prasSuspicious غيرَ موثوقين، لا تحذيرين يُسجلان ويُعبران. غير المحسوم يعني أن المحلل لم يستطع إثبات أن المراجعات اللاحقة مسموحة؛ وعند مهاجمٍ فإن مدخلاً ينتج Indeterminate بموثوقية نفيسٌ كمدخل ينتج Allowed إن كان كودك يمرره. تأخذ الدالة العامة AnalyzePadesSignatureRevisions أي TStream وتقرأه من الموضع 0، وهو ما يناسب معالجات الرفع التي لا تحتاج إلى تصيير المستند

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 يسمح فعلاً بالتعليقات، وتستطيع تعليقة بمظهر مبهم أن تغطي نصاً موقعاً دون لمس أي تدفق محتوى. فإن كانت مستنداتك الموثقة عقوداً لا نسخ مراجعة، فإما أن توثّق بـ P=2 وإما توجه تعديلات التعليقات المسموحة إلى إنسان، كما في هذا المساعد:

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;

قائمة تدقيق مراجعات التوقيعات

استخدم هذه القائمة لتفحص أنبوب التحقق لديك: هل كان معرضاً وهل يفشل الآن بإحكام:

  • بناءات PDFium Component قبل v3.126.2 استطاعت الإبلاغ prasAllowed لتعديلات محتوى صفحات في مستندات DocMDP ‏P=3؛ أعد تشغيل TPdf.AnalyzeSignatureRevisions على ملفات P=3 الموثقة التي قبلتها البناءات الأقدم
  • أعد فحص مستندات P=3 ذات أقفال FieldMDP حيث قد تتشارك قيمة حقل وتعليق كائناً غير مباشر
  • اقبل prasNoLaterChanges و prasAllowed وحدهما؛ واعتبر prasIndeterminate و prasSuspicious غير موثوقين
  • افحص Report.Risks إلى جانب Report.Status، لأن prrDuplicateObjectDefinition لا يغيّر الحالة بنفسه
  • لا تقرأ مصفوفة Changes الفارغة نتيجةً نظيفة حين تكون حالة التوقيع Indeterminate؛ بناء الأدوار الفاشل لا يبلّغ عن تغييرات
  • لا تعتمد prrFieldMdpUnresolved وحدها لالتقاط مشاكل FieldMDP، فحالة التعليق المشترك تظهر عبر القرار والحالة فقط
  • احسم هل تحتاج تعديلات التعليقات المسموحة تحت P=3 إلى مراجعة بشرية في سير عملك
  • حلل بايتات الملف الأصلي؛ مستند أُعيدت كتابته بـ SaveAs لم يعد يحوي سلسلة المراجعات

تحليل المراجعات طبقة واحدة من فحص التوقيع. اقرنها بـ تفتيش التوقيعات الرقمية في PDF ومستويات PAdES للقاموس والمستوى الأساسي، وبتدقيق أوسع لمخاطر أمان PDF من أجل JavaScript وأفعال الإطلاق والملفات المدمجة. يأتي TPdf.AnalyzeSignatureRevisions و AnalyzePadesSignatureRevisions ومخطط الأدوار الواعي بالملكية الموصوف هنا مع PDFium Component لـ Delphi و C++Builder و Lazarus