مقاله فنی

چرا اکسل کتاب کار رمزگذاری‌شده شما را رد می‌کند: ECB و RC4

شما یک کتاب کار می‌نویسید، آن را با رمز عبور رمزگذاری می‌کنید، فایل را به همکار خود تحویل می‌دهید و همکار شما آن را در اکسل باز می‌کند. اکسل رمز عبور را می‌خواهد. همکارتان آن را تایپ می‌کند و اکسل آن را می‌پذیرد. تا اینجا رمزگذاری درست به نظر می‌رسد. سپس اکسل کادری را نشان می‌دهد که می‌گوید فایل خراب است و باز نمی‌شود، یا آن را با صفحه‌ای از سلول‌های بی‌معنی باز می‌کند. رمز عبور درست بود، اما فایل با این حال خراب است. این گیج‌کننده‌ترین حالت شکست در رمزگذاری آفیس است، زیرا بخشی که به شما می‌گوید رمز عبور صحیح است و بخشی که داده‌های شما را نگه می‌دارد با دو عملیات متفاوت محافظت می‌شوند و درست انجام دادن یکی، هیچ تضمینی برای دیگری ایجاد نمی‌کند

هر دو باگ توضیح داده شده در اینجا دقیقاً همین شکل را داشتند. در هر مورد، تاییدکننده (verifier) موفق می‌شد اما بدنه ناموفق بود، که این موضوع شما را به دنبال یک باگ رمز عبور یا استخراج کلید می‌فرستد که اصلاً وجود ندارد. خطای واقعی در بخش پایین‌دستی بود، در نحوه تبدیل بایت‌های بسته. این دو خطا مستقل از هم هستند، یکی در مسیر AES و دیگری در مسیر RC4، اما در مشکل تشخیص مشترک هستند، بنابراین ارزش دارد ببینیم چرا یک نتیجه نیمه‌صحیح سخت‌ترین نوع برای تحلیل است

چرا یک رمز عبور پذیرفته‌شده هیچ چیز را درباره بدنه ثابت نمی‌کند

فرمتی که فایل‌های مدرن رمزگذاری‌شده XLSX استفاده می‌کنند، رمزگذاری استاندارد ECMA-376 است و دو بخش رمزگذاری‌شده را در کنار هم ذخیره می‌کند. یکی EncryptionVerifier است: یک بلوک کوچک حاوی یک مقدار تصادفی و هش آن مقدار، که با کلید مشتق شده از رمز عبور رمزگذاری شده است. دیگری EncryptedPackage است: کل کانتینر فشرده (zip) کتاب کار، که با همان کلید رمزگذاری شده است. تاییدکننده به این دلیل وجود دارد که خواننده بتواند رمز عبور را قبل از صرف انرژی روی مگابایت‌ها بدنه تایید کند. تاییدکننده رمزگشایی می‌شود، مقدار تصادفی هش می‌گردد، با هش ذخیره‌شده مقایسه می‌شود و اگر مطابقت داشتند، رمز عبور صحیح است

تله این است که تاییدکننده و بسته با فراخوانی‌های جداگانه روی بافرهای جداگانه رمزگذاری می‌شوند. کلیدی که به درستی استخراج شده باشد، تاییدکننده را به درستی رمزگشایی می‌کند بدون توجه به اینکه بعد از آن چه اتفاقی برای بسته می‌افتد. بنابراین اگر استخراج کلید شما درست باشد اما تبدیل بسته شما اشتباه باشد، اکسل رمز عبور را از روی تاییدکننده تایید می‌کند و سپس در بدنه شکست می‌خورد. علامت این خطا به صورت "رمز عبور درست، فایل خراب" خوانده می‌شود که تحقیقات را به سمت مسیر رمز عبور هدایت می‌کند، یعنی تنها بخشی که هرگز خراب نبوده است. همین تفکیک بر مورد قدیمی RC4 نیز حاکم است: ابتدا هش تاییدکننده بررسی می‌شود، و بدنه‌ای که از هماهنگی خارج شده است همچنان آن بررسی را دست‌نخورده باقی می‌گذارد

باگ اول: AES در حالت ECB و نه CBC

استاندارد [MS-OFFCRYPTO] §2.3.4.15 مشخص می‌کند که رمزگذاری استاندارد بسته را با AES در حالت کتاب کد الکترونیکی (Electronic Codebook) رمزگذاری می‌کند. هر بلوک 16 بایتی از بسته پدینگ‌شده به طور مستقل با همان کلید رمزگذاری می‌شود. هیچ زنجیره‌ای بین بلوک‌ها وجود ندارد و هیچ بردار مقداردهی اولیه (IV) وجود ندارد. این یک انتخاب غیرمعمول بر اساس استانداردهای مدرن است که در آن‌ها معمولاً از ECB اجتناب می‌شود، اما سازگاری (interop) جایی برای حدس دوم زدن مشخصات نیست. اکسل بسته را به عنوان ECB رمزگشایی می‌کند، بنابراین یک تولیدکننده باید آن را به عنوان ECB رمزگذاری کند وگرنه این دو با هم توافق نخواهند داشت

باگ این بود که بسته با AES در حالت CBC با استفاده از یک بردار مقداردهی اولیه کاملاً صفر رمزگذاری شده بود. در اینجا دلیل اینکه چرا این حالت تقریباً کار می‌کند، و چرا تقریباً بدترین جای ممکن برای فرود است آمده است. در CBC، اولین بلوک متن ساده قبل از رمزگذاری با IV عملیات XOR می‌شود. وقتی IV کاملاً صفر باشد، آن XOR چیزی را تغییر نمی‌دهد، بنابراین اولین بلوک CBC با IV صفر دقیقاً همان متن رمزگذاری‌شده ECB را تولید می‌کند. از بلوک دوم به بعد، CBC بلوک متن رمزگذاری‌شده قبلی را به بعدی تغذیه می‌کند، بنابراین هر بلوک پس از اولین بلوک با ECB متفاوت می‌شود

حالا این را روی ساختار قرار دهید. چیدمان بسته یک پیشوند طول 8 بایتی لیتل‌اندین را در همان ابتدا قرار می‌دهد، بنابراین بخش‌هایی از فایل که اکسل زودتر از همه بررسی می‌کند در بلوک اول یا دوم قرار دارند. تطابق تصادفی بلوک اول به این معنی است که اولین اعتبارسنجی‌ها با موفقیت انجام می‌شوند در حالی که هر بلوک بعدی به نویز رمزگشایی می‌شود. راه‌حل پس از مشخص شدن حالت سخت نیست: هر بلوک 16 بایتی را با ECB رمزگذاری کنید و زنجیره را متوقف نمایید. در موتور، متد XlsEncryptStdPackage بافرهای پدینگ‌شده را در مراحل 16 بایتی طی می‌کند و روی هر کدام متد AESEncryptECB128Block را فراخوانی می‌نماید، که این همان تابع پایه‌ای است که از قبل برای بلوک‌های تاییدکننده استفاده شده است. کد منبع حاوی کامنتی در حلقه است که قانون را به وضوح بیان می‌کند: CBC با IV صفر فقط برای اولین بلوک با ECB مطابقت دارد، بنابراین بقیه بسته به زباله رمزگشایی می‌شود و اکسل آن را رد خواهد کرد

var
  Book: TXLSXWorkbook;
begin
  Book := TXLSXWorkbook.Create(nil);
  try
    Book.Open('report.xlsx');
    // متد SaveAsEncrypted کتاب کار را سریال‌سازی می‌کند، سپس خط لوله
    // رمزگذاری استاندارد ECMA-376 را اجرا می‌نماید: AES-128 ECB روی
    // بسته مطابق با [MS-OFFCRYPTO] 2.3.4.15. در صورت موفقیت 1 را برمی‌گرداند.
    if Book.SaveAsEncrypted('report_secure.xlsx', 'S3cret!') <> 1 then
      raise Exception.Create('Encryption failed');
  finally
    Book.Free;
  end;
end;

باگ دوم: انحراف کلید مجدد RC4 از ریتم خارج می‌شود

مسیر قدیمی .xls از طرح RC4 CryptoAPI استفاده می‌کند و قانون آن از جنس دیگری است. استاندارد [MS-OFFCRYPTO] §2.3.6 مشخص می‌کند که سایفر در هر مرز بلوک 1024 بایتی دوباره کلیدگذاری می‌شود. جریان به بلوک‌های 1024 بایتی تقسیم می‌شود، یک کلید جدید RC4 برای بلوک شماره 0، 1، 2 و غیره استخراج می‌گردد، و در داخل هر بلوک، جریان کلید (keystream) به طور مداوم بایت به بایت مصرف می‌شود. دو اصل پایدار باید با هم حفظ شوند: کلیدگذاری مجدد در هر مرز، و مصرف جریان کلید بدون شکاف در داخل یک باوک. RC4 یک سایفر جریانی است، بنابراین جریان کلید آن یک دنباله مرتب واحد است؛ n-امین بایتی که می‌کشید بر اساس تعداد بایت‌هایی که قبل از آن کشیده‌اید تعیین می‌شود. رمزگشایی همان XOR در برابر همان دنباله است، که به این معنی است تولیدکننده و مصرف‌کننده باید دقیقاً همان بایت‌ها را در همان موقعیت‌ها بکشند

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

منطق اصلاح‌شده در کلاس TXLSDecrypterRC4 قرار دارد. هر دو متد Skip و Decrypt در یک حلقه مشترک هستند: کلیدگذاری مجدد فقط زمانی انجام می‌شود که موقعیت در حال اجرا وارد بلوک جدیدی شود، جایی که اندیس بلوک برابر است با موقعیت تقسیم بر REKEY_BLOCK_SIZE (1024), سپس تا باقیمانده بلوک فعلی و نه بیشتر مصرف می‌کند. متد MakeKey با اندیس بلوک فراخوانی می‌شود، هرگز با یک اندیس قدیمی یا نگهبان، و موقعیت به تعداد دقیق بایت‌های پردازش شده جلو می‌رود تا Skip و Decrypt با تولیدکننده هم‌فاز بمانند. درس در کوچک‌ترین واحد نهفته است: یک بایت هدر رفته، یک خطای کوچک در یک سایفر جریانی نیست، بلکه از دست دادن کامل همه چیز در ادامه مسیر است

var
  Book: TXLSXWorkbook;
begin
  Book := TXLSXWorkbook.Create(nil);
  try
    // متد CanReadEncrypted امضای فایل مرکب (OLE2) را بررسی می‌کند تا
    // بتوانید قبل از تلاش برای Open عادی شاخه‌بندی کنید. OpenEncrypted
    // فایل‌های ساده را به Open هدایت کرده و کانتینر رمزگذاری‌شده را مدیریت می‌کند.
    if Book.CanReadEncrypted('legacy.xls') then
      Book.OpenEncrypted('legacy.xls', 'S3cret!')
    else
      Book.Open('legacy.xls');
    // read cells here
  finally
    Book.Free;
  end;
end;

سازگاری با مشخصات ثابت، مطابقت با بایت است

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

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

روش پشتیبانی‌شده برای خواندن و نوشتن کتاب‌های کاری محافظت‌شده

سطح عمومی کوچک است. برای نوشتن یک کتاب کار مدرن محافظت‌شده با رمز عبور، یک TXLSXWorkbook را پر یا باز کنید و متد SaveAsEncrypted را با نام فایل و رمز عبور فراخوانی نمایید;این متد کتاب کار را سریال‌سازی کرده و خط لوله رمزگذاری استاندارد را که اولین اصلاح برطرف کرد اجرا می‌کند و در صورت موفقیت 1 را برمی‌گرداند. برای خواندن، متد CanReadEncrypted را فراخوانی کنید تا آزمایش کنید آیا یک فایل کانتینر فایل مرکب رمزگذاری‌شده است یا خیر، سپس شاخه‌بندی کنید: OpenEncrypted مسیر رمزگذاری‌شده را مدیریت می‌کند و برای فایل‌های ساده به Open بازمی‌گردد، و Open با رمز عبور مستقیماً در دسترس است. مدیریت حالت و حلقه کلیدگذاری مجدد که در بالا توضیح داده شد در زیر این فراخوانی‌ها قرار دارند؛ شما رمز عبور و نام فایل را ارائه می‌دهید و موتور مشخصات را به جای شما مطابقت می‌دهد

var
  Book: TXLSXWorkbook;
begin
  Book := TXLSXWorkbook.Create(nil);
  try
    Book.Open('quarterly.xlsx');
    Book.SaveAsEncrypted('quarterly_locked.xlsx', 'P@ssphrase');
    // باز کردن مجدد در سمت مصرف‌کننده
    Book.OpenEncrypted('quarterly_locked.xlsx', 'P@ssphrase');
  finally
    Book.Free;
  end;
end;

شکل خروجی محافظت‌شده، جریان EncryptionInfo، بلوک‌های تاییدکننده و چیدمان بسته در بررسی خروجی XLSX محافظت‌شده با AES ما پوشش داده شده‌اند. برای سوال مجزا درباره قفل کردن در سطح برگه و نحوه تعامل محافظت با تنظیمات صفحه و چاپ، به مقاله محافظت، تنظیمات صفحه و چاپ مراجعه کنید. هر دو بر اساس مسیر رمزگذاری توضیح داده شده در اینجا ساخته شده‌اند که به عنوان بخشی از کامپوننت صفحه‌گسترده HotXLS برای دلفی و C++Builder در کنار APIهای خواندن، نوشتن و رندر ارائه می‌شود که در بخش‌های دیگر این وبلاگ پوشش داده شده‌اند