عندما توقع ملف PDF، فإنك عادة ما تفكر في مفتاح التوقيع كشيء تتحكم فيه. إنه يعيش في ملف .pfx قمت بإنشائه، ومحمي بكلمة مرور اخترتها. يبدو الكود الذي يقرأ هذا الملف وكأنه توصيلات (plumbing)، وليس حداً أمنياً. هذا الحدس خاطئ في اللحظة التي تتوقف فيها الشهادة عن كونها ملكاً لك. أداة سطح المكتب التي تتيح للمستخدم اختيار أي .pfx، وخادم يقبل بيانات اعتماد محملة، ومُوقّع دفعات (batch signer) يُغذى بشهادات عبر الشبكة، كلها تسلم بايتات متأثرة بالمهاجم إلى محلل (parser) قبل إنتاج بايت توقيع واحد. قارئ PKCS#12 هو سطح هجوم، بنفس المعنى الذي يكون عليه وحدة فك ترميز الصور (image decoder) أو محمل الخطوط (font loader)
يستعرض هذا المقال عيبين حقيقيين عاشا في هذا القارئ، كلاهما في المسار الذي يستورد بيانات اعتماد التوقيع. لا يعتبر أي منهما غريباً. كلاهما يأتي من نفس السبب الجذري الذي يصيب تقريباً كل محلل ثنائي (binary parser) مكتوب بلغة ذات أعداد صحيحة بعرض ثابت (fixed-width integers): يتم الوثوق بطول أو عدد من الملف خطوة واحدة أبعد مما ينبغي. يؤدي أحدهما إلى قراءة خارج الحدود (out-of-bounds read)، ويؤدي الآخر إلى عملية تتوقف (hangs) حتى تقوم بإنهائها
أين تسافر البايتات
لا يعد استيراد .pfx لتوقيع مستند عملية واحدة، بل هو خط أنابيب (pipeline) قصير، وتقوم كل مرحلة بتحليل شيء ربما كتبه مهاجم. الحاوية عبارة عن بنية PKCS#12 كما هو محدد في RFC 7292، وهي عبارة عن عش من حقائب AuthenticatedSafe ملفوفة حول غطاء مشفر يحمل المفتاح الخاص. قراءتها تعني المشي عبر ASN.1، واستنتاج مفتاح من كلمة المرور، وفك التشفير، ثم تسليم مفتاح RSA المسترد إلى الكود الذي يبني التوقيع
في HotPDF يتم تعيين هذه المراحل إلى وحدات (units) مميزة. يعيش منطق حاوية PKCS#12 في HPDFPFX. يتم فك تشفير كل وسم وطول وقيمة يلمسها بواسطة قارئ ASN.1 في HPDFASN1. استنتاج المفتاح وفك تشفير PBES2 يقعان في HPDFCrypt جنباً إلى جنب مع PBKDF2HMACSHA256. عند استرداد المفتاح، يقوم HPDFRSA ومنشئ SignedData الخاص بـ CMS في HPDFCMS بتحويله إلى التوقيع المنفصل (detached signature) المضمن في ملف PDF. نقطة الدخول العامة التي تدفع السلسلة بأكملها هي استدعاء واحد
// Drives the full pipeline: load the placeholder PDF, parse the PFX,
// derive the key, build CMS SignedData, write the signed output.
if THotPDF.SignPDFWithPFX('Prepared.pdf', 'Signed.pdf',
'signer.pfx', 'p@ssw0rd') then
// signature embedded
else
// signing did not complete
;
يتدفق كل بايت من signer.pfx من خلال HPDFASN1 و HPDFPFX قبل حدوث أي تشفير. إذا لم تكن هاتان الوحدتان حذرتين بشأن ما يدعيه الملف، فإن التشفير في المراحل اللاحقة لا يحصل أبداً على فرصة ليكون له أهمية
العيب الأول: طول ASN.1 الذي يلتف متجاوزاً الحارس (guard)
يقوم ASN.1 في DER و BER بترميز كل عنصر كـ وسم، وطول، وهذا العدد من بايتات المحتوى. الطول هو الحقل الذي يجب أن تثق به ولكن تتحقق منه، لأنه يخبر المحلل (parser) إلى أي مدى يجب أن يقرأ، وقد تمت كتابته بواسطة من أنتج الملف. يحدد X.690 §8.1.3 ترميزين. يجمع الشكل القصير طولاً من 0 إلى 127 في بايت واحد. الشكل الطويل، المستخدم لأي شيء أكبر، يستهلك بايت بادئ واحد تعطي بتاته السبعة السفلية عدد بايتات الطول التي تليه، ثم يحمل هذا العدد من بايتات big-endian القيمة الفعلية. لذلك يمكن لأربعة بايتات طول أن تعلن عن حجم محتوى يقترب من أربعة جيجابايت
بعد فك تشفير مثل هذه القيمة، يجب على المحلل التحقق من أن المحتوى يتناسب فعلياً داخل المخزن المؤقت (buffer) قبل الوثوق به. الفحص الطبيعي هو تأكيد أن الموضع الحالي زائد طول المحتوى لا يتجاوز نهاية البيانات. مكتوباً بالطريقة الواضحة، مع الاحتفاظ بالموضع، وطول المحتوى، والإجمالي جميعاً في أعداد صحيحة بإشارة (signed integers) من 32 بت، فإن هذا الحارس (guard) يكون مكسوراً:
// The trap: signed 32-bit arithmetic. With ContentLen near MaxInt,
// Pos + ContentLen overflows to a NEGATIVE value, so the comparison
// is false and a forged ~2 GB length sails straight through.
if Pos + ContentLen > Total then
raise EHPDFASN1Error.Create('content overruns buffer');
المشكلة هي الجمع، وليس المقارنة. عندما يكون ContentLen قريباً من MaxInt (2147483647)، فإن Pos + ContentLen يفيض (overflows) عن نطاق 32 بت ذي الإشارة ويلتف (wraps around) إلى رقم سالب. المجموع السالب لا يكون أبداً أكبر من Total، لذا يفيد الحارس بأن كل شيء على ما يرام ويسمح للمحلل بالمضي قدماً مع طول محتوى يبلغ حوالي اثنين جيجابايت لا يحتوي عليه المخزن المؤقت. ما يحدث بعد ذلك هو الضرر: يخصص القارئ مخزناً مؤقتاً لهذا الطول المزعوم وينسخ إليه، SetLength يليه Move يقرأ من المصدر. لم يتبق في المصدر سوى بضع مئات من البايتات، لذلك يقرأ النسخ إلى ما بعد نهاية الإدخال بكثير، وهي قراءة خارج الحدود (out-of-bounds read) تؤدي في أحسن الأحوال إلى حدوث عطل وفي أسوأ الأحوال إلى تسريب ذاكرة العملية المجاورة إلى التحليل
الحارس الصحيح الوحيد يوسع المجموع الوسيط قبل المقارنة، لذلك لا يمكن للجمع أن يفيض عن النوع الذي يتم حسابه فيه. يرقّي الإصلاح كلا المعاملين إلى Int64:
// Correct: both operands widened to Int64 before the add, so the sum
// cannot wrap. A forged 2 GB length now fails the bounds check.
if ContentLen < 0 then
raise EHPDFASN1Error.Create('negative content length after decoding.');
if Int64(Pos) + Int64(ContentLen) > Int64(Total) then
raise EHPDFASN1Error.Create('content overruns buffer');
يحتفظ Int64 بمجموع قيمتين من 32 بت دون فقدان، لذا ترى المقارنة الرقم الحقيقي وترفض الطول المزور. يغلق الفحص المنفصل غير السالب على ContentLen الحالة المطابقة حيث تهبط القيمة المفككة التشفير سالبة بمفردها. في HotPDF، يعيش هذا الحارس في HPDFASN1ParseNode، الدالة التي تنتج العقدة (node) التي يعتمد عليها كل مُساعِد آخر. نظراً لأن HPDFASN1Content يحدد حجم SetLength و Move الخاصين به مباشرة من طول محتوى العقدة، فإن العقدة التي اجتازت حارساً سيئاً كانت ستسمم كل قراءة مأخوذة منها. إصلاح الحد (bound) عند نقطة فك التشفير هو ما يجعل المُساعِدين فوقه آمنين
العيب الثاني: استخدام عدد تكرارات PBKDF2 كسلاح
الخلل الثاني ليس خطأ في الذاكرة، إنه الملف الذي يخبر وحدة المعالجة المركزية (CPU) الخاصة بك بمدى صعوبة العمل. يحمي PKCS#12 مادة المفتاح الخاصة به باستخدام PBES2، وهو المخطط القائم على كلمة المرور من PKCS#5، والمحدد في RFC 8018. يشغّل PBES2 دالة استنتاج مفتاح، وهنا PBKDF2 مع HMAC-SHA-256، ثم شيفرة (cipher)، وهنا AES-256-CBC. يأخذ PBKDF2 عدد التكرارات (iteration count)، وهذا العدد هو معلمة (parameter) محمولة في الملف. غرضه بالكامل هو أن يكون بطيئاً: فالمزيد من التكرارات يعني أن كل تخمين لكلمة المرور يكلف أكثر، وهو أمر جيد ضد مهاجم غير متصل بالإنترنت. يوضح RFC 8018 §4.2 صراحة أن العدد الأكبر أفضل للأمان، ويتعمد عدم وضع سقف
هذا الانفتاح يكون جيداً عندما تكون أنت من أنشأ الملف. ولكنه سلاح عندما يقوم المهاجم بذلك. عدد التكرارات هو عامل عمل يتحكم فيه المهاجم، وعامل العمل الذي يتحكم فيه المهاجم هو رفض خدمة (denial of service) ناتج عن التعقيد الخوارزمي. يمكن لـ .pfx مزور أن يرمز عدد تكرارات بالمليارات؛ فيقرأه المحلل بوفاء ويستدعي PBKDF2 لذلك العدد من الجولات من HMAC-SHA-256، وتختفي العملية في حلقة (loop) لن تعود لدقائق أو ساعات في ملف واحد مزود. على خادم توقيع يتعامل مع بيانات اعتماد واحدة لكل طلب، يؤدي تحميل ملف واحد مصمم خصيصاً إلى تعطيل عامل (worker)
يزيد العدد من سوء الالتفاف (wraparound) قبل أن يجعل وحدة المعالجة المركزية تدور. تعيش قيمة التكرار في الملف كـ INTEGER من نوع ASN.1، والذي ليس له عرض ثابت، بينما الحقل الذي يستهلكه PBKDF2 في النهاية هو Integer من 32 بت. قم بفك تشفير INTEGER مباشرة في هذا الحقل وسيتم اقتطاع قيمة كبيرة، والقيمة المصممة لتهبط على بت الإشارة تعود سالبة أو كرقم صغير غير ذي صلة، لذلك حتى حجم العمل لم يعد ما بدا أن الملف يطلبه. يقرأ الإصلاح القيمة بالعرض الكامل ويحدها قبل تضييقها (narrowing):
// Read the iteration count as Int64 first, then clamp to a sane band
// BEFORE it is narrowed into the 32-bit Iterations field PBKDF2 uses.
LIter := HPDFASN1ToInteger(Data, Node); // returns Int64
if (LIter < 1) or (LIter > 100000000) then
raise EHPDFPFXError.CreateFmt(
'PBKDF2 iteration count %d is outside the accepted range 1..100000000',
[LIter]);
Iterations := Integer(LIter); // safe: already bounded
القراءة في Int64 تعني أن القيمة المفككة التشفير هي القيمة الحقيقية، وليست شبحاً مقتطعاً منها. يرفض الحد الأدنى الأعداد الصفرية والسالبة، والتي لا معنى لها لاستنتاج المفتاح. الحد الأقصى، مائة مليون، يقع أعلى بكثير من أي ملف PKCS#12 شرعي، والذي يستخدم اليوم عشرات إلى مئات الآلاف المنخفضة من التكرارات، مع وضع سقف للحالة الأسوأ عند قدر محدود ويمكن النجاة منه من العمل. فقط بعد اجتياز القيمة لهذا النطاق، يتم تضييقها إلى حقل 32 بت، لذلك لا يمكن للاقتطاع (truncation) أن يفاجئ أي شخص بعد الآن. في HotPDF، يعيش هذا التقييد (clamp) في ParsePBES2Params، حيث يتم فك تشفير معلمات PBKDF2 في الطريق إلى PBKDF2HMACSHA256
لماذا يعتبر كلا الإصلاحين نفس الإصلاح
يبدو العيبان مختلفين، أحدهما تجاوز في المخزن المؤقت (buffer overrun) والآخر عملية معلقة (hung process)، لكنهما نفس الخطأ. في كل حالة تم حمل رقم من ملف غير موثوق به إلى نوع ذي عرض ثابت مبكراً بخطوة واحدة، قبل أن يتم التحقق منه مقابل الواقع. تمت إضافة الطول في 32 بت قبل اختبار الحدود؛ تم تضييق عدد التكرارات إلى 32 بت قبل اختبار النطاق. كلاهما يخضع لنفس الانضباط: فك التشفير بعرض كامل، وتحقق مقابل الحد الحقيقي، ثم ضيق فقط بعد ذلك. Int64 الوسيط ليس خياراً للأسلوب، بل هو العرض الوحيد الذي يستطيع الحارس من خلاله رؤية القيمة التي كتبها المهاجم فعلياً. الحد الذي يفيض ليس حداً، والعدد الذي ليس له سقف ليس معلمة، بل هو خانق (throttle) عن بعد لوحدة المعالجة المركزية الخاصة بك
إرشادات عملية لخط أنابيب التوقيع
الدرس الضيق هو التحقق من صحة مدخلات الشهادة غير الموثوقة بالطريقة التي تتحقق بها من أي تحميل غير موثوق. ضع حداً أقصى لحجم .pfx الذي تقبله، لأن الملف الشرعي يكون بالكيلوبايت، وليس بالميجابايت. تعامل مع فشل التحليل كإدخال مرفوض روتيني، وليس كخطأ يستحق تتبع التكدس (stack trace) للمستخدم. إذا قمت بالتوقيع على خادم، فقم بتشغيل الاستيراد حيث لا يمكن لعامل متعطل أن يسقط الخدمة معه، وضع مهلة زمنية (timeout) للعملية بحيث يكون الملف المكلف بشكل غير متوقع مقيداً بساعة الحائط بالإضافة إلى سقف التكرار
يصل الدرس الأوسع إلى ما هو أبعد من الشهادات. تحصين المحلل ليس تدقيقاً لمرة واحدة لوحدة واحدة، بل هو خاصية لكل مكان تقرأ فيه مكتبتك بايتات لم تكتبها. تحلل مكتبة PDF قدراً كبيراً من المصادر غير الموثوقة: الخطوط المضمنة في مستند، والصور في نصف دزينة من برامج الترميز، ومرشحات التدفق (stream filters)، وعلى مسار التوقيع، الشهادات. كل واحد من هؤلاء هو سطح هجوم، وكل واحد يستحق نفس الشك في كل طول وكل عدد. يبني HotPDF مسار الاستيراد والتوقيع على الوحدات المحصنة HPDFASN1، و HPDFPFX، و HPDFCrypt، و HPDFCMS الموصوفة هنا، بحيث يتم تحليل بيانات الاعتماد التي تسلمها له، أينما جاءت، بشكل دفاعي قبل أن يتم الوثوق بها على الإطلاق
يتم تغطية سير عمل التوقيع الذي تحميه هذه الفحوصات من البداية إلى النهاية في استعراضنا للتوقيعات الرقمية PAdES في Delphi، ويتم وصف نفس الموقف الدفاعي المطبق على تشفير المستندات، بما في ذلك مسار مفتاح AES-256 الذي يشارك قاعدة الكود هذه، في المقال حول تشفير AES-256 والأمان. كل ذلك يتم شحنه كجزء من مكون HotPDF لكل من Delphi و C++Builder، إلى جانب واجهات برمجة تطبيقات التحميل، والتحرير، والتشفير، والتوقيع التي يتم تناولها في أماكن أخرى من هذه المدونة