يقبل مسار استيعاب المستندات الملفات التي كتبها غرباء. الفواتير، وعمليات المسح الضوئي، والمرفقات من نموذج ويب: يدعي كل منها أنه ملف PDF ويحمل مئات الأرقام التي يُتوقع من المحلل اللغوي الخاص بك العمل عليها. أطوال البث، وأبعاد الصورة، وإزاحات البايت، ومراجع الكائنات - تم اختيار كل واحدة منها بواسطة من أنتج الملف، وسيضع التحميل المقطوع أو المستند المشوه عمدًا في النهاية أحد هذه الأرقام في المكان الذي يتسبب فيه في حدوث ضرر. الفرق بين المحلل اللغوي الذي ينجو من هذا الملف والمحلل اللغوي الذي يتعطل، أو يستمر في العمل مع ذاكرة تالفة، هو مجموعة صغيرة من العادات التي لا تعتمد على أي مكتبة PDF معينة
تشترك العادات في فرضية واحدة: القيمة المقروءة من الملف هي ادعاء وليست قياسًا. تصبح قابلة للاستخدام فقط بعد فحصها مقابل شيء قام المحلل اللغوي بقياسه بنفسه - الحجم الحقيقي للملف، والعدد الحقيقي للبايتات التي أنتجها جهاز فك التشفير، والعمق الحقيقي للعودية. ما يلي هو تلك الفرضية المطبقة على الأماكن التي تنكسر فيها محللات المستندات فعليًا
الطول المصرح به هو ادعاء، وليس قياسًا
أبسط عدم تطابق هو طول البث. يعلن كائن بث PDF عن عدد البايت الخاص به في مفتاح /Length، وتستقر البيانات الفعلية بين الكلمتين الأساسيتين stream و endstream. لا شيء يجبر الاثنين على الاتفاق. يحتفظ الملف المقطوع ببايتات حقيقية أقل من العدد المصرح به؛ يمكن للملف الوارد من منشئ معطل الإعلان عن طول يمتد بعد نهاية الملف أو إلى كائن مجاور. التخصيص من القيمة المصرح بها والنسخ حتى endstream وتجاوز المخزن المؤقت؛ اقرأ العدد المصرح به بالضبط دون التحقق من التوفر وستخرج من نهاية الملف. دع القيمة المصرح بها تدفع التخصيص فقط بعد تقييدها مقابل المسافة المقاسة إلى نهاية البيانات، وتعامل مع الخلاف كنقطة اتخاذ قرار - الإصلاح عن طريق المسح الضوئي لـ endstream، أو رفض البث - ولا تعتبره أبدًا شيئًا تصدقه بصمت
معلمات الصورة التي تصف نقطية أكبر مما قمت بتخصيصه
ترفع تدفقات الصور المخاطر لأن مجموعتين مستقلتين من الأرقام تصفان وحدات البكسل نفسها. يحمل قاموس الصور /Width و /Height، وعادة ما يتم تحديد حجم المخازن المؤقتة النقطية من هذه. يحمل مرشح فك التشفير هندسته الخاصة: يأخذ CCITTFaxDecode /Columns و /Rows و /K من DecodeParms الخاصة به، حيث يحدد /K المخطط Group 3 أو Group 4 ويصدر جهاز فك التشفير (Columns + 7) div 8 بايت لكل خط مسح ضوئي. الملف الذي يعلن عن /Width 100 ولكنه يسلم المرشح /Columns 1728 - الافتراضي - يجعل جهاز فك التشفير ينتج أكثر من ستة عشر ضعف البايتات لكل صف يتوقعه المخزن المؤقت، ويهبط الفائض سطر مسح ضوئي واحدًا في كل مرة في كل ما يجلس بعد التخصيص. في حالة غياب /Rows، يعمل جهاز فك التشفير حتى تقول البيانات توقف، لذلك يجب تقييد عدد الصفوف أيضًا. يمتلك DCTDecode نفس التماس: تحمل بيانات JPEG العرض والارتفاع الخاصين بها في علامة SOF الخاصة بها، ولا يوجد شيء يلزمها بمطابقة القاموس
القاعدة الدفاعية ميكانيكية: احسب الحجم النقطي المتوقع من معلمات فك التشفير التي تم التحقق من صحتها - /Columns و /Rows الخاصة بالمرشح بالنسبة لـ CCITT، وأبعاد SOF لـ DCT - وتحقق منها مقابل حدودك، وقم بالتخصيص منها، وتحقق أثناء فك التشفير من أن الإخراج لا يتجاوز التخصيص أبدًا. عندما يختلف القاموس والمرشح حول الهندسة، فقم بالتوفيق بينهما أو رفض الصورة. ما يجب ألا يفعله المحلل اللغوي أبدًا هو تحديد حجم المخزن المؤقت من مجموعة واحدة من الأرقام وترك جهاز فك التشفير يعمل على المجموعة الأخرى
أخطاء حساب وتخصيص Delphi
هناك ثلاثة سلوكيات في Delphi تقوض حتى المحلل اللغوي الذي يعتزم التحقق من صحته. السلوك الأول هو ضرب 32 بت: يقوم Delphi بتقييم منتج معاملين Integer عند 32 بت بغض النظر عن عرض الوجهة، لذلك يمكن أن يلتف Width * Height * BytesPerPixel حتى عندما يمر كل عامل بفحص السلامة الخاص به. يمثل المسح الضوئي 30000 في 30000 بثلاثة بايت لكل بكسل 2.7 مليار بايت، والتي تلتف سالبة في حساب 32 بت الموقّع؛ تلتف العوامل المختلفة قليلاً إلى طول إيجابي صغير يخصص المخزن المؤقت ويقلل من حجمه. فرض التعبير بأكمله عريضًا عن طريق إرسال المعامل الأول - Size := Int64(Width) * Height * BytesPerPixel - ثم مقارنته بحد أقصى صريح قبل أن يصل أي شيء إلى SetLength
الثاني هو التحقق من النطاق. يتم شحن تكوين الإصدار الافتراضي في Delphi مع إيقاف تشغيله، لذلك لا يثير الفهرس خارج النطاق المحسوب من بيانات الملف - فهو يقرأ أو يكتب الذاكرة المجاورة للمصفوفة. أعد تشغيله باستخدام {$R+} (و {$Q+} لتجاوز سعة الحساب) في الجزء العلوي من كل وحدة تقوم بالفهرسة باستخدام قيم مشتقة من الملفات. التكلفة غير قابلة للقياس بجانب الإدخال/الإخراج الذي يقوم به المحلل اللغوي على أي حال، ويحول الفساد الصامت إلى ERangeError يمكن التقاطه
الثالث هو TMemoryStream.SetSize باستخدام Int64 المقدم من الملف. في RTL الحالي، يتم تخصيص كل ما طلبه الملف، لذلك يصبح التدفق الفردي الذي يدعي أربعة غيغابايت فشلًا في نفاد الذاكرة في منتصف الاستيعاب. في أجهزة RTL الأقدم، حيث يأخذ SetSize Longint، يتم تضييق القيمة بصمت أولاً: تصبح القيمة $100000010 المعلنة 16، وينجح التخصيص، وتعمل كتابة البيانات الحقيقية بعيدًا عنها. تحقق من صحة كل حجم مقابل الحجم المقاس للمصدر والحد الأقصى الثابت قبل أن تراه أي استدعاء تخصيص
الإزاحات التي تشير إلى خارج الملف
يقوم جدول الإسناد الترافقي بتعيين أرقام الكائنات إلى إزاحات البايت المطلقة، ويسعى المحلل اللغوي أينما يشير. في الملف التالف أو المعادي، تهبط هذه الإزاحات بعد نهاية الملف أو داخل هياكل غير ذات صلة. يجعل TStream الفشل هادئًا: ضبط Position بما يتجاوز Size ليس خطأ، وRead البسيط الذي يتجاوز النهاية يعيد ببساطة بايتات أقل من المطلوب، لذلك تستمر التعليمات البرمجية التي تتخطى فحص العدد في تحليل البايتات القديمة من الكائن السابق. الدفاع عبارة عن نقطة اختناق - مساعد واحد يمر من خلاله كل بحث وقراءة تعتمد على الملف، ويتحقق من صحة الإزاحة والعد مقابل حجم الملف المقاس قبل تحرك البث
uses
System.SysUtils, System.Classes;
const
MAX_OBJECT_BYTES = 64 * 1024 * 1024; // no single object may exceed 64 MB
type
EPdfBoundsError = class(Exception);
// Every file-driven seek and read goes through here. Offset and Count are
// file-supplied claims; Source.Size is the measurement they must fit.
procedure ReadBounded(Source: TStream; Offset, Count: Int64;
var Buffer: TBytes);
begin
if (Offset < 0) or (Count < 0) or (Count > MAX_OBJECT_BYTES) or
(Offset > Source.Size) or (Count > Source.Size - Offset) then
raise EPdfBoundsError.CreateFmt(
'object extent %d+%d exceeds file size %d',
[Offset, Count, Source.Size]);
SetLength(Buffer, Count);
if Count = 0 then
Exit;
Source.Position := Offset;
Source.ReadBuffer(Buffer[0], Count);
end;
قم بتوجيه إزاحات الإسناد الترافقي وامتدادات البث وقراءات الملفات المضمنة من خلاله، وتصبح الإزاحة السيئة رفضًا نظيفًا يذكر الأرقام بدلاً من انتهاك الوصول ثلاث مكالمات لاحقة
الدورات والعمق في رسم بياني الكائن
PDF هو رسم بياني، وليس شجرة. يمكن أن تكون أي قيمة مرجعًا غير مباشر، وقد يُترجم المرجع إلى مرجع آخر - /Length 12 0 R، حيث يحتفظ الكائن 12 بـ 13 0 R - ولا شيء يمنع السلسلة من الإغلاق مرة أخرى على نفسها. يعود المحلل الذي يتبع المراجع بسذاجة حتى يتم استنفاد المكدس الأصلي، واستنفاد المكدس ليس شيئًا تلتقطه؛ ينهي العملية. تصل المصفوفات والقواميس المتداخلة بعمق إلى نفس النهاية دون أي دورة على الإطلاق
استخدم حارسين معًا: يحد عداد العمق الصريح الحالة الصادقة ولكن العميقة عند حد لا يقترب منه أي ملف مشروع، وتكتشف مجموعة تمت زيارتها دورة حقيقية في زيارتها الثانية، وتحولها إلى خطأ دقيق يمكن الإبلاغ عنه بدلاً من رحلة حد
uses
System.SysUtils, System.Generics.Collections;
const
MAX_RESOLVE_DEPTH = 32; // far deeper than any legitimate reference chain
type
EPdfStructureError = class(Exception);
TPdfValueKind = (pvNull, pvNumber, pvName, pvString, pvArray,
pvDictionary, pvStream, pvReference);
TPdfValue = record
Kind: TPdfValueKind;
RefNumber: Integer; // meaningful when Kind = pvReference
// ... payload fields for the remaining kinds
end;
// LoadObject is your own routine: it looks up the xref offset for
// ObjNumber, reads the object with ReadBounded, and parses it.
function ResolveObject(ObjNumber, Depth: Integer;
Visited: TDictionary<Integer, Boolean>): TPdfValue;
begin
if Depth > MAX_RESOLVE_DEPTH then
raise EPdfStructureError.Create('reference chain exceeds depth limit');
if Visited.ContainsKey(ObjNumber) then
raise EPdfStructureError.CreateFmt(
'circular reference through object %d', [ObjNumber]);
Visited.Add(ObjNumber, True);
try
Result := LoadObject(ObjNumber);
if Result.Kind = pvReference then // e.g. /Length 12 0 R
Result := ResolveObject(Result.RefNumber, Depth + 1, Visited);
finally
Visited.Remove(ObjNumber); // siblings may legally share this object
end;
end;
فك الضغط هو مضخم
يمكن أن تتضخم بضعة كيلوبايتات من إدخال FlateDecode إلى غيغابايت؛ يكافئ الضغط للأغراض العامة النص العادي المتكرر، ويمكن للمهاجم جعله متكررًا إلى أقصى حد. حدد سقفًا للحجم المتضخم لكل بث عند ما يمكن أن يحتاجه المستهلك بشكل معقول، واحتفظ بميزانية ثانية لكل مستند: خمسمائة بث كل منها أقل بقليل من سقف كل بث تستنفد الذاكرة تمامًا مثل بث واحد عملاق. ينتمي الفحص إلى داخل حلقة التضخم، ويحسب بايتات الإخراج عند إنتاجها وإحباطها عند الخرق، وليس بعد الحلقة عندما يتم إنفاق الذاكرة بالفعل. تعمل ميزانية المستند المعبر عنها كمضاعف لحجم الملف المضغوط بشكل جيد، حيث تتجمع المستندات المشروعة أقل بكثير من النسب التي يصل إليها بث مصمم
الدفاع المتعمق خارج وحداتك الخاصة
تعيش نفس فئات الخلل داخل المكتبات. تتناول دراستا حالة في هذه المدونة أمثلة حقيقية: التفاف الأعداد الصحيحة، والتكرار غير المحدود، والمخازن المؤقتة غير المهيأة المغلقة في محرك Pascal الأصلي في تقوية محلل PDF في Pascal ضد الملفات الخبيثة، ومخاطر اصطلاح الاستدعاء، وعرض العدد الصحيح، والملكية الخاصة بربط محرك C في تقوية ربط مكون PDFium. من أجل الاستيعاب غير الموثوق به حقًا - نموذج تحميل عام، وصندوق بريد غير مصدق - قم أيضًا بتشغيل التحليل وفك التشفير في عملية امتياز منخفض منفصلة، بحيث يكلف الملف الذي يهزم كل حارس في العملية مهمة فاشلة بدلاً من خدمة متوقفة
قائمة فحص قبل الرحلة
قبل شحن البنية التالية، قم بمشي المحلل اللغوي مقابل هذه القائمة: كل مخزن بث مؤقت بحجم الطول المقيد بدلاً من الطول المعلن؛ يتم تحديد حجم كل نقطية من معلمات وحدة فك التشفير التي تم التحقق من صحتها والتحقق منها مقابل إخراج وحدة فك التشفير؛ يتم تقييم كل منتج أبعاد في Int64 ومقارنته بسقف صريح؛ {$R+} نشط في كل وحدة تقوم بالفهرسة بقيم مشتقة من الملفات؛ كل بحث مقيد تم التحقق منه مقابل حجم الملف المقاس؛ يتم تحديد عمق حل كل مرجع وفحص دورته؛ تحسب كل حلقة تضخم الإخراج مقابل الميزانيات لكل تيار ولكل وثيقة. لا يكلف أي من هذه الفحوصات وقتًا قابلاً للقياس في مستند شرعي، ويقوم كل منها بتحويل تلف الذاكرة إلى رفض نظيف وقابل للتسجيل
ملاحظة: تطبق مكون HotPDF من losLab، و مكتبة PDFlibPas Delphi PDF، و مكون PDFium عمليات فحص الحدود هذه، وحدود العمق، وقبعات التوسيع داخليًا، لذلك يبدأ مسار الاستيعاب المبني عليها من خط أساس صلب