مقاله فنی

ایمن‌سازی امضاکننده PDF در دلفی در برابر PKCS#12 مخرب

هنگامی که یک PDF را امضا می‌کنید، معمولاً کلید امضا را چیزی می‌دانید که تحت کنترل شماست. این کلید در یک فایل .pfx که خودتان تولید کرده‌اید قرار دارد و توسط رمزی که انتخاب کرده‌اید محافظت می‌شود. کدی که این فایل را می‌خواند بیشتر شبیه لوله‌کشی به نظر می‌رسد تا یک مرز امنیتی. این تصور از لحظه‌ای که گواهی دیگر متعلق به شما نیست، اشتباه از آب در می‌آید. یک ابزار دسکتاپ که به کاربر اجازه می‌دهد هر فایل .pfx را انتخاب کند، سروری که یک گواهی‌نامه آپلود شده را می‌پذیرد، یا یک امضاکننده دسته‌ای که گواهی‌ها را از طریق شبکه دریافت می‌کند، همگی پیش از تولید حتی یک بایت از امضا، بایت‌های تحت نفوذ مهاجم را به یک تجزیه‌کننده می‌سپارند. یک خواننده PKCS#12 به همان معنایی که یک رمزگشای تصویر یا بارگذار فونت سطح حمله است، یک سطح حمله محسوب می‌شود

این مقاله به بررسی دو نقص واقعی می‌پردازد که در چنین خواننده‌ای وجود داشته‌اند؛ هر دو در مسیری که یک گواهی‌نامه امضا را وارد می‌کند. هیچ‌کدام عجیب و غریب نیستند. هر دو ناشی از یک علت ریشه‌ای مشترک هستند که تقریباً بر هر تجزیه‌کننده باینری نوشته شده در زبانی با اعداد صحیح با عرض ثابت تأثیر می‌گذارد: اعتماد بیش از حد به طول یا شمارشی از فایل. یکی منجر به خواندن خارج از محدوده حافظه می‌شود و دیگری پردازشی را تا زمانی که آن را متوقف کنید، به حالت تعلیق درمی‌آورد

مسیر حرکت بایت‌ها

وارد کردن یک .pfx برای امضای یک سند تنها یک عملیات نیست، بلکه یک خط لوله کوتاه است و هر مرحله چیزی را تجزیه می‌کند که ممکن است توسط یک مهاجم نوشته شده باشد. این ظرف یک ساختار PKCS#12 همان‌طور که در RFC 7292 تعریف شده است می‌باشد؛ مجموعه‌ای تودرتو از بسته‌های AuthenticatedSafe که دور یک پوشش رمزنگاری شده حاوی کلید خصوصی پیچیده شده‌اند. خواندن آن به معنای پیمایش ASN.1، استخراج کلید از رمز عبور، رمزگشایی و در نهایت سپردن کلید RSA بازیابی شده به کدی است که امضا را می‌سازد

در HotPDF، این مراحل به واحدهای مجزایی نگاشت می‌شوند. منطق ظرف PKCS#12 در HPDFPFX قرار دارد. هر برچسب، طول و مقداری که با آن سروکار دارد توسط خواننده ASN.1 در HPDFASN1 رمزگشایی می‌شود. استخراج کلید و رمزگشایی PBES2 در HPDFCrypt در کنار PBKDF2HMACSHA256 قرار دارند. پس از بازیابی کلید، HPDFRSA و سازنده SignedData CMS در HPDFCMS آن را به یک امضای جداگانه تعبیه شده در 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 که از مرز محافظ عبور می‌کند

ASN.1 در DER و BER هر عنصر را به عنوان یک برچسب، یک طول و به همان اندازه بایت محتوا رمزگذاری می‌کند. طول فیلدی است که باید به آن اعتماد کنید اما آن را تأیید کنید، زیرا به تجزیه‌کننده می‌گوید تا کجا بخواند و توسط هر کسی که فایل را تولید کرده، نوشته شده است. X.690 §8.1.3 دو نوع رمزگذاری را تعریف می‌کند. فرم کوتاه، طولی از 0 تا 127 را در یک بایت منفرد قرار می‌دهد. فرم طولانی که برای مقادیر بزرگتر استفاده می‌شود، یک بایت پیشرو صرف می‌کند که هفت بیت پایین آن تعداد بایت‌های طول بعدی را مشخص می‌کند؛ سپس آن تعداد بایت از نوع big-endian مقدار واقعی را در بر می‌گیرند. بنابراین، چهار بایت طول می‌توانند اندازه محتوایی نزدیک به چهار گیگابایت را اعلام کنند

پس از رمزگشایی چنین مقداری، تجزیه‌کننده باید پیش از اعتماد به آن، بررسی کند که محتوا واقعاً در بافر جای می‌گیرد. بررسی طبیعی این است که تأیید شود موقعیت فعلی به اضافه طول محتوا از انتهای داده‌ها فراتر نمی‌رود. اگر این بررسی به روش بدیهی نوشته شود، به طوری که موقعیت، طول محتوا و مجموع کل همگی در اعداد صحیح علامت‌دار 32 بیتی نگهداری شوند، این محافظ شکسته می‌شود:

// 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 از محدوده 32 بیتی علامت‌دار سرریز کرده و به یک عدد منفی تبدیل می‌شود. یک مجموع منفی هرگز از Total بزرگتر نیست، بنابراین محافظ گزارش می‌دهد که همه‌چیز مرتب است و به تجزیه‌کننده اجازه می‌دهد با طول محتوایی در حدود دو گیگابایت که در بافر وجود ندارد، ادامه دهد. آنچه پس از آن اتفاق می‌افتد همان آسیب است: خواننده بافری برای آن طول ادعا شده تخصیص می‌دهد و درون آن کپی می‌کند؛ یک SetLength که به دنبال آن یک Move از منبع خوانده می‌شود. منبع تنها چند صد بایت باقی‌مانده دارد، بنابراین عملیات کپی بسیار فراتر از انتهای ورودی می‌خواند؛ یک خواندن خارج از محدوده که در بهترین حالت باعث کرش کردن برنامه می‌شود و در بدترین حالت، حافظه پردازش مجاور را به درون تجزیه‌کننده نشت می‌دهد

تنها محافظ صحیح این است که مجموع میانی را پیش از مقایسه گسترش دهیم تا عملیات جمع نتواند از نوع داده‌ای که در آن محاسبه می‌شود، سرریز کند. این اصلاح هر دو عملوند را به 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 قرار دارد، تابعی که گره‌ای را تولید می‌کند که هر تابع کمکی دیگری بر اساس آن ساخته می‌شود. از آنجا که HPDFASN1Content اندازه‌های SetLength و Move خود را مستقیماً از طول محتوای گره می‌گیرد، گره‌ای که از یک محافظ معیوب عبور کرده باشد، تمام خواندن‌های انجام شده از آن را مسموم می‌کند. اصلاح مرز در نقطه رمزگشایی همان چیزی است که توابع کمکی بالاتر از آن را ایمن می‌سازد

نقص دوم: استفاده از تعداد تکرار PBKDF2 به عنوان یک سلاح

نقص دوم یک خطای حافظه نیست، بلکه فایلی است که به پردازنده شما می‌گوید چقدر باید سخت کار کند. PKCS#12 از مواد کلید خود با PBES2 محافظت می‌کند، یک طرح مبتنی بر رمز عبور از PKCS#5 که در RFC 8018 مشخص شده است. PBES2 یک تابع استخراج کلید، در اینجا PBKDF2 با HMAC-SHA-256، و سپس یک رمزنگار، در اینجا AES-256-CBC را اجرا می‌کند. PBKDF2 یک تعداد تکرار دریافت می‌کند و این تعداد، پارامتری است که در فایل حمل می‌شود. تمام هدف آن کُند بودن است: تکرارهای بیشتر به این معناست که هر حدس رمز عبور هزینه بیشتری خواهد داشت، که این در برابر یک مهاجم آفلاین مفید است. RFC 8018 §4.2 صریحاً بیان می‌کند که تعداد بیشتر برای امنیت بهتر است و عمداً هیچ سقفی تعیین نمی‌کند

این عدم محدودیت زمانی که شما فایل را تولید کرده‌اید مشکلی ندارد، اما زمانی که مهاجم آن را ساخته باشد، تبدیل به یک سلاح می‌شود. تعداد تکرار، فاکتور کاری تحت کنترل مهاجم است و فاکتور کاری تحت کنترل مهاجم به معنای حمله محروم‌سازی از سرویس (DoS) از نوع پیچیدگی الگوریتمی است. یک فایل .pfx جعلی می‌تواند تعداد تکراری در مقیاس میلیاردها را رمزگذاری کند؛ تجزیه‌کننده به وظیفه خود عمل کرده و آن را می‌خواند و PBKDF2 را برای آن تعداد دور از HMAC-SHA-256 فراخوانی می‌کند، در نتیجه پردازش در حلقه‌ای گرفتار می‌شود که برای دقایقی یا ساعت‌ها به ازای هر فایل ورودی بازنخواهد گشت. در یک سرور امضا که با هر درخواست یک گواهی‌نامه را پردازش می‌کند، یک فایل آپلودی دستکاری شده باعث متوقف شدن یک worker می‌شود

پیش از آنکه این تعداد باعث درگیر شدن پردازنده شود، مشکل سرریز را وخیم‌تر می‌کند. مقدار تکرار در فایل به عنوان یک ASN.1 INTEGER ذخیره می‌شود که عرض ثابتی ندارد، در حالی که فیلدی که در نهایت PBKDF2 مصرف می‌کند یک Integer 32 بیتی است. اگر INTEGER را مستقیماً به آن فیلد رمزگشایی کنید، یک مقدار بزرگ قطع (truncate) می‌شود و مقداری که به گونه‌ای ساخته شده تا روی بیت علامت قرار گیرد، به صورت یک عدد منفی یا یک عدد کوچک نامرتبط بازمی‌گردد، بنابراین حتی اندازه کار نیز دیگر آن چیزی نیست که فایل درخواست کرده بود. راه حل این است که مقدار را با عرض کامل بخوانیم و پیش از باریک کردن (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 بیتی باریک می‌شود، بنابراین قطع شدن مقدار دیگر نمی‌تواند کسی را غافلگیر کند. در HotPDF این محدودسازی در ParsePBES2Params قرار دارد، جایی که پارامترهای PBKDF2 در مسیر ارسال به PBKDF2HMACSHA256 رمزگشایی می‌شوند

چرا هر دو اصلاح در واقع یکسان هستند

این دو نقص متفاوت به نظر می‌رسند، یکی سرریز بافر و دیگری توقف پردازش، اما در حقیقت هر دو اشتباهی یکسان هستند. در هر مورد، عددی از یک فایل نامطمئن یک گام زودتر به یک نوع داده با عرض ثابت منتقل شده است، پیش از آنکه با واقعیت سنجیده شود. طول، پیش از بررسی محدوده به صورت 32 بیتی جمع زده شد؛ تعداد تکرار، پیش از بررسی بازه به 32 بیت باریک گردید. هر دو به یک راهکار یکسان نیاز دارند: با عرض کامل رمزگشایی کنید، در برابر محدودیت واقعی بررسی کنید، و تنها پس از آن نوع را باریک کنید. استفاده واسطه‌ای از Int64 یک انتخاب سبک برنامه‌نویسی نیست، بلکه تنها عرضی است که در آن محافظ می‌تواند مقداری را که مهاجم واقعاً نوشته است مشاهده کند. مرزی که سرریز می‌کند دیگر مرز نیست و شماری که سقفی ندارد یک پارامتر نیست، بلکه دریچه کنترلی از راه دور روی پردازنده شماست

راهنمای عملی برای خط لوله امضا

درس مشخص در اینجا این است که ورودی گواهی نامطمئن را همان‌طور که هر آپلود نامطمئنی را ارزیابی می‌کنید، اعتبارسنجی کنید. برای اندازه فایل .pfx که می‌پذیرید سقف تعیین کنید، زیرا یک فایل معتبر در حد کیلوبایت است، نه مگابایت. با یک خطای تجزیه به عنوان ورودی رد شده روتین برخورد کنید، نه خطایی که ارزش نشان دادن stack trace به کاربر را داشته باشد. اگر عملیات امضا را روی سرور انجام می‌دهید، فرایند وارد کردن را در جایی اجرا کنید که یک worker متوقف شده نتواند کل سرویس را مختل کند، و برای عملیات محدودیت زمانی (timeout) در نظر بگیرید تا یک فایل غیرمنتظره و پرهزینه، علاوه بر سقف تکرار، توسط زمان نیز محدود شود

درس گسترده‌تر فراتر از گواهی‌ها می‌رود. ایمن‌سازی تجزیه‌کننده یک ممیزی یک‌باره از یک واحد نیست، بلکه ویژگی هر مکانی است که کتابخانه شما بایت‌هایی را می‌خواند که خودش ننوشته است. یک کتابخانه PDF حجم زیادی از داده‌ها را از منابع نامطمئن تجزیه می‌کند: فونت‌های تعبیه‌شده در سند، تصاویر در انواع کدک‌ها، فیلترهای جریان و در مسیر امضا، گواهی‌ها. هر یک از این موارد یک سطح حمله محسوب می‌شود و سزاوار همان شک و تردید در مورد هر طول و هر شمارش است. HotPDF مسیر وارد کردن و امضا را بر روی واحدهای ایمن‌شده HPDFASN1، HPDFPFX، HPDFCrypt و HPDFCMS که در اینجا توضیح داده شد، بنا می‌کند تا گواهی‌نامه‌ای که به آن می‌سپارید، از هر کجا که آمده باشد، پیش از آنکه هرگز به آن اعتمادی شود، به شکل تدافعی تجزیه گردد

گردش کار امضا که این بررسی‌ها از آن محافظت می‌کنند، به صورت کامل در راهنمای ما برای امضاهای دیجیتال PAdES در دلفی پوشش داده شده است و همین موضع تدافعی اعمال شده در رمزگذاری اسناد، از جمله مسیر کلید AES-256 که از همین پایگاه کد استفاده می‌کند، در مقاله مربوط به رمزگذاری و امنیت AES-256 توضیح داده شده است. تمامی این قابلیت‌ها به عنوان بخشی از کامپوننت HotPDF برای دلفی و C++Builder، در کنار رابط‌های برنامه‌نویسی برای بارگذاری، ویرایش، رمزگذاری و امضا که در بخش‌های دیگر این وبلاگ به آن‌ها پرداخته شده، عرضه می‌شوند