Technical Article

مقاوم‌سازی یک امضاکننده PDF در Delphi در برابر PKCS#12 مخرب

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

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

بایت‌ها به کجا سفر می‌کنند

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

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

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

// 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 (۲۱۴۷۴۸۳۶۴۷) نزدیک است، عبارت Pos + ContentLen از محدوده ۳۲ بیتی علامت‌دار سرریز می‌کند و به یک عدد منفی تبدیل می‌شود. یک مجموع منفی هرگز بزرگ‌تر از 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 مجموع دو مقدار ۳۲ بیتی را بدون از دست دادن اطلاعات نگه می‌دارد، بنابراین مقایسه عدد واقعی را می‌بیند و طول جعلی را رد می‌کند. بررسی غیرمنفی مجزا روی ContentLen، مورد مشابهی را که در آن مقدار رمزگشایی‌شده به خودی خود منفی می‌شود، می‌بندد. در HotPDF این محافظ در HPDFASN1ParseNode قرار دارد، تابعی که گرهی را تولید می‌کند که هر هلپر دیگری روی آن ساخته می‌شود. از آنجا که HPDFASN1Content مقدار SetLength و Move خود را مستقیماً از روی طول محتوای گره تعیین می‌کند، گرهی که از یک محافظ خراب عبور می‌کرد، تمام خواندن‌های گرفته‌شده از آن را مسموم می‌ساخت. اصلاح مرز در نقطه رمزگشایی چیزی است که هلپرهای بالای آن را ایمن می‌کند

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

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

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

این تعداد، سرریز را قبل از اینکه CPU شروع به چرخش کند، بدتر می‌کند. مقدار تکرار در فایل به عنوان یک INTEGER در ASN.1 قرار دارد که عرض ثابتی ندارد، در حالی که فیلدی که PBKDF2 در نهایت مصرف می‌کند یک Integer ۳۲ بیتی است. اگر INTEGER را مستقیماً در آن فیلد رمزگشایی کنید، یک مقدار بزرگ کوتاه می‌شود و مقداری که طوری طراحی شده که روی بیت علامت بیفتد، منفی یا به عنوان یک عدد کوچک نامربوط بازمی‌گرداند، به طوری که حتی اندازه کار دیگر آن چیزی نیست که به نظر می‌رسید فایل درخواست کرده است. راه حل، خواندن مقدار در عرض کامل و محدود کردن آن قبل از باریک کردن است

// 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 معتبر است که امروزه از ده‌ها تا چند صد هزار تکرار استفاده می‌کند، در حالی که بدترین حالت را به یک کار محدود و قابل تحمل محدود می‌کند. تنها پس از اینکه مقدار از آن محدوده عبور کرد، به فیلد ۳۲ بیتی باریک می‌شود، بنابراین کوتاه‌سازی دیگر نمی‌تواند کسی را شگفت‌زده کند. در HotPDF این محدودساز در ParsePBES2Params قرار دارد، جایی که پارامترهای PBKDF2 در مسیر PBKDF2HMACSHA256 رمزگشایی می‌شوند

چرا هر دو راه حل، یک راه حل هستند

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

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

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

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

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