HotXLS کارپوشههای مبهمشده با XOR یعنی Excel 5.0/95 (BIFF5) را طوری مینویسد که Excel 16 فقط وقتی آنها را باز میکند که سه جزئیات دقیقاً با [MS-OFFCRYPTO] جور دربیاید: کلید FILEPASS باید CreateXorKey_Method1(password) باشد، آرایه 16 بایتی XOR باید با XorRor ساخته شود (چرخش یکبیتی به راست)، و هر بایت باید از XorArrayIndex = (stream offset + record length) mod 16 استفاده کند. HotXLS اندیس را در v2.384.47 درست کرد و کلید و چرخش را در v2.384.54. قبل از آن، هر فایل BIFF5 محافظتشده با رمز عبوری که تولید میکرد در HotXLS درست باز میشد و در Excel خطا میداد
آن جمله آخر کل ماجرا در مقیاس کوچک است. یک reader و یک writer که ایده غلط مشترکی دارند با هم کاملاً کنار میآیند، پس تستهای round-trip سبز میمانند در حالی که تنها مصرفکننده مهم میگوید نه. Excel 16 دو بار نه گفت، با دو پیام متفاوت، و هر پیام به لایه متفاوتی از طرح اشاره میکرد. این مقاله آن لایهها را به همان ترتیبی که Excel چکشان میکند مرور میکند، با جزئیات در سطح بایت که چه HotXLS را صدا بزنید چه BIFF reader خودتان را بنویسید به کارتان میآید
مبهمسازی XOR در BIFF واقعاً چه چیزی در فایل ذخیره میکند؟
مبهمسازی XOR در BIFF فقط دو word شانزدهبیتی در فایل ذخیره میکند و بقیه همه از رمز عبور دوباره محاسبه میشود. رکورد FILEPASS یعنی $002F بلافاصله بعد از workbook globals BOF مینشیند، و در یک فایل BIFF5 بدنهاش دقیقاً 4 بایت است: کلید XOR و بعدش password verifier. نه salt در کار است، نه شناسه الگوریتم، و نه blob verifier رمزنگاریشده از جنس طرحهای RC4 و AES
از همین دو word، یک reader سه چیز را بازسازی میکند:
- verifier، یک هش 16 بیتی از بایتهای رمز عبور که با
$CE4BXOR شده. مقایسهاش با word ذخیرهشده همان چک رمز عبور است، و تنها چک - کلید XOR، یک مقدار 16 بیتی از
CreateXorKey_Method1در [MS-OFFCRYPTO] §2.3.7.2، که دو جدول ثابت یعنیInitialCodeبا 15 word وXorMatrixبا 105 word آن را میرانند - آرایه XOR، 16 بایت ساختهشده از بایتهای رمز عبور با padding یک pad ثابت 16 بایتی، هر کدام XOR شده با بایت کم کلید (موقعیتهای زوج) یا بایت بالا کلید (موقعیتهای فرد)، و بعد چرخش یکبیتی به راست
هدر رکوردها بهصورت متن ساده میمانند، و تعداد کمی رکورد کامل هم که طرح از آنها معاف است همینطور، از جمله BOF و FILEPASS و INTERFACEHDR. بدنه هر رکورد دیگر بایت به بایت تبدیل میشود: چرخش 5 بیتی به چپ، بعد XOR با یکی از درایههای آرایه 16 بایتی. رمزگشایی که [MS-OFFCRYPTO] §2.3.7.3 با نام DecryptData_Method1 شرحش میدهد تصویر آینهای همان است: اول XOR، بعد چرخش 5 بیتی به راست
در HotXLS هیچوقت مستقیم سر و کار با هیچکدام از اینها ندارید. یک رمز عبور ست کنید، فرمت را انتخاب کنید، و SaveAs هم FILEPASS صادر میکند هم stream را تبدیل:
uses
SysUtils, lxHandle;
procedure SaveLegacyProtectedBook(const FileName: string);
var
Wb: IXLSWorkbook;
begin
Wb := TXLSWorkbook.Create;
Wb.Sheets.Add.Name := 'Ledger';
Wb.Sheets[1].Range['A1', 'A1'].Value := 'Account';
Wb.Sheets[1].Range['B1', 'B1'].Value := 1250.75;
// BIFF5 فقط مبهمسازی XOR را پشتیبانی میکند؛ xletAuto هم همین را انتخاب میکرد
Wb.EncryptionType := xletXor;
// اسکی نگهش دارید و حداکثر 15 کاراکتر (پایین را ببینید)
Wb.EncryptionPassword := 'secret';
if Wb.SaveAs(FileName, xlExcel5) <> 1 then
raise Exception.Create('BIFF5 save failed');
end;
چرا وقتی verifier درست است Excel میگوید رمز عبور غلط است؟
Excel رمز عبور را رد میکند چون به کلید ذخیرهشده اعتماد نمیکند: Excel کلید را از رمز عبور تایپشده با CreateXorKey_Method1 میسازد و با word کلید در FILEPASS مقایسه میکند، پس فایلی که کلیدش چیز دیگری باشد حتی با verifier درست هم چک رمز عبور را رد میشود. مشخصات کلید را خروجی رمز عبور توصیف میکند نه پارامتری آزاد، و Excel 16 همین خوانش را الزامی میکند
نویسنده HotXLS قبل از v2.384.54 word کلید را با دو بایت تصادفی پر میکرد. روی کاغذ بیضرر به نظر میرسد، چون verifier همان چک مستند رمز عبور است و آرایه هم از هر کلیدی که فایل declare کرده ساخته میشود. خود HotXLS این فایلها را بدون دردسر میخواند، چون reader آن کلید را همانطور که هست از FILEPASS میگرفت. Excel 16 با همان فایل و رمز عبور درست جواب میداد که رمز عبور درست نیست. از v2.384.54 کلید مشتق میشود، پس FILEPASS برای رمز عبور secret همیشه کلید $014D و verifier $DAA7 را دارد، مقادیری که با یک پیادهسازی مستقل از مشخصات cross-check شدهاند
خود derivation وقتی دو جدول سر جایشان باشند کوتاه است. رمز عبور را از آخر به اول بروید، هفت بار بیت 6 هر بایت را نگاه کنید در حالی که آن را به چپ شیفت میکنید، و هر بار که بیت یک بود یکی از درایههای XorMatrix را XOR کنید. چیزی که میبینید یک طرح مفهومی است که الگوریتم مشخصات را بازتولید میکند و با پیادهسازی HotXLS جور است؛ API مربوط به HotXLS نیست:
// طرح مفهومی [MS-OFFCRYPTO] 2.3.7.2 یعنی CreateXorKey_Method1
// و CreateXorArray_Method1 (فقط برای توضیح، API مربوط به HotXLS نیست)
type
TXorArray = array [0..15] of Byte;
function DemoCreateXorKey(const Password: AnsiString): Word;
const
InitialCode: array [0..14] of Word = ($E1F0, $1D0F, $CC9C, $84C0, $110C,
$0E10, $F1CE, $313E, $1872, $E139, $D40F, $84F9, $280C, $A96A, $4EC3);
XorMatrix: array [0..104] of Word = (
$AEFC, $4DD9, $9BB2, $2745, $4E8A, $9D14, $2A09,
$7B61, $F6C2, $FDA5, $EB6B, $C6F7, $9DCF, $2BBF,
$4563, $8AC6, $05AD, $0B5A, $16B4, $2D68, $5AD0,
$0375, $06EA, $0DD4, $1BA8, $3750, $6EA0, $DD40,
$D849, $A0B3, $5147, $A28E, $553D, $AA7A, $44D5,
$6F45, $DE8A, $AD35, $4A4B, $9496, $390D, $721A,
$EB23, $C667, $9CEF, $29FF, $53FE, $A7FC, $5FD9,
$47D3, $8FA6, $0F6D, $1EDA, $3DB4, $7B68, $F6D0,
$B861, $60E3, $C1C6, $93AD, $377B, $6EF6, $DDEC,
$45A0, $8B40, $06A1, $0D42, $1A84, $3508, $6A10,
$AA51, $4483, $8906, $022D, $045A, $08B4, $1168,
$76B4, $ED68, $CAF1, $85C3, $1BA7, $374E, $6E9C,
$3730, $6E60, $DCC0, $A9A1, $4363, $86C6, $1DAD,
$3331, $6662, $CCC4, $89A9, $0373, $06E6, $0DCC,
$1021, $2042, $4084, $8108, $1231, $2462, $48C4);
var
Len, I, Bit, Element: Integer;
Ch: Byte;
begin
Result := 0;
Len := Length(Password);
if Len > 15 then
Len := 15; // کلید فقط 15 بایت را میبیند
if Len = 0 then
Exit;
Result := InitialCode[Len - 1];
Element := $68; // آخرین درایه XorMatrix
for I := Len downto 1 do
begin
Ch := Ord(Password[I]);
for Bit := 1 to 7 do
begin
if (Ch and $40) <> 0 then
Result := Result xor XorMatrix[Element];
Ch := Byte(Ch shl 1);
Dec(Element);
end;
end;
end;
function XorRor(B, KeyByte: Byte): Byte;
begin
B := B xor KeyByte;
Result := Byte((B shr 1) or (B shl 7)); // چرخش یکبیتی به راست
end;
procedure DemoCreateXorArray(const Password: AnsiString; out Arr: TXorArray);
const
PadArray: TXorArray = ($BB, $FF, $FF, $BA, $FF, $FF, $B9, $80,
$00, $BE, $0F, $00, $BF, $0F, $00, $00);
var
Key: Word;
Len, I: Integer;
begin
Key := DemoCreateXorKey(Password);
Len := Length(Password);
if Len > 16 then
Len := 16;
for I := 0 to Len - 1 do
Arr[I] := Ord(Password[I + 1]);
for I := Len to 15 do
Arr[I] := PadArray[I - Len];
for I := 0 to 15 do
if Odd(I) then
Arr[I] := XorRor(Arr[I], Byte(Key shr 8))
else
Arr[I] := XorRor(Arr[I], Byte(Key and $FF));
end;
برای secret این تابع آرایه 1F 32 17 B9 14 BA 7B 7F 59 DD 59 7F 7A C0 A6 DF را تولید میکند، که اگر reader خودتان را تست میکنید یک fixture دستدومن دردگیر است
چرا کلید درست هم هنوز فایل خراب تولید میکند؟
یک کلید درست وقتی آرایه XOR به سمت اشتباه بچرخد همچنان فایل خراب تولید میکند: [MS-OFFCRYPTO] مرحله آرایه را XorRor تعریف میکند، یعنی چرخش یکبیتی به راست، و آرایهای که به چپ دو بیت بچرخد بدنه هر رکورد را به آشغال رمزگشایی میکند. فیکس کلید Excel 16 را از پرامپت رمز عبور رد کرد و مستقیم به یک خطای متفاوت رساند، گزارشی که فایل مشکل دارد و قابل باز کردن نیست
کد قدیمی HotXLS هر بایت آرایه را 2 بیت به چپ میچرخاند، فرمی که در چند پیادهسازی BIFF دست به دست میچرخد. چون HotXLS همان چرخش را در دو طرف استفاده میکرد، reader خودش هرگز متوجه نشد. Excel 16 دیگر نمیتواند فایلهای Excel 5.0/95 ذخیره کند و هنگام ذخیره BIFF8 هم XOR پیشنهاد نمیدهد، پس نمونه بومی Excel برای diff کردن وجود نداشت. شواهد باید از جهت دیگر میآمد: یک stream بگیرید که کاملاً متن ساده است، آن را هشت جور دوباره کد کنید، و بگذارید Excel 16 هر واریانت را باز کند. آن هشت واریانت سه انتخاب مستقل را پوشش میدادند:
| انتخاب | گزینه A | گزینه B |
|---|---|---|
| چرخش آرایه | XorRor (چرخش به راست 1) | چرخش به چپ 2 |
| اندیس آرایه | (offset + record length) mod 16 | offset mod 16 |
| ترتیب تبدیل بایت | چرخش به چپ 5، بعد XOR | XOR، بعد چرخش به چپ 5 |
Excel 16 از هشت واریانت دقیقاً دو تا را باز کرد: XorRor با چرخش-بعد-XOR و اندیس مبتنی بر طول رکورد، و یک واریانت که فقط متفاوت به نظر میرسید. چرخش-به-چپ-2 با XOR-بعد-چرخش و همان اندیس همان تابع است با مبدل لباس. چرخش روی XOR توزیع میشود، پس rol5(p xor rol2(b)) برابر rol5(p) xor rol7(b) است، و روی مقدار 8 بیتی چرخش به چپ 7 همان چرخش به راست 1 است. خلاصه اینکه rol5 ∘ rol2 = ror1، و برای همین آرایه چرخش-به-چپ-2 در تنهایی باورپذیر میشود: فقط همراه ترتیب معکوس تبدیل درست است. جفت با ترتیب مشخصات، هر بایت تبدیلشده را خراب میکند
همان آزمایش یک سؤال دوم را هم حل کرد. واریانتهایی که طول رکورد را از اندیس حذف میکردند همه شکست خوردند، و این قاعده اندیسی را تأیید کرد که HotXLS یک نسخه قبلتر فقط به اتکای متن مشخصات پذیرفته بود
XorArrayIndex برای هر بایت چطور محاسبه میشود؟
XorArrayIndex برای یک بایت، offset آن در workbook stream بهعلاوه طول کل داده رکوردی که به آن تعلق دارد، mod 16 است. پس اندیس برای هر رکورد از مقداری وابسته به همان رکورد شروع میشود و داخل آن بایت به بایت یکییکی جلو میرود. شبهکد مشخصات ورودیها را FileOffset و Data.Length مینامد، که بهراحتی میشود آن را فقط offset شروع رکورد خواند، و همان بدفهمی دقیقاً چیزی است که HotXLS تا v2.384.47 عرضه میکرد
سه جزئیات تصمیم میگیرند که اندیسهایتان با Excel جور شود:
- هدر 4 بایتی رکورد هرگز تبدیل نمیشود اما همچنان جای stream را اشغال میکند، پس اولین بایت بدنه رکورد در header offset + 4 مینشیند
- جمله طول، طول کامل داده رکورد است نه تعداد بایتهایی که واقعاً تبدیل میشوند
- BOUNDSHEET تا حدی ساده است: 4 بایت اولش یعنی
lbPlyPos، همان offset stream از BOF شیت، خوانا میماند تا parser بتواند شیتها را پیدا کند. این 4 بایت از تبدیل رد میشوند اما هم در offset و هم در طول رکورد حساب میشوند
یکجا جمع شود، تبدیل بهازای هر رکورد چند خط است. باز هم این طرحی از قاعده است، نه چیزی که لازم باشد صدا بزنید:
function Rol8(B: Byte; N: Integer): Byte;
begin
Result := Byte((B shl N) or (B shr (8 - N)));
end;
// بدنه یک رکورد را درجا مبهم میکند. BodyPos همان offset stream از
// Body[0] است، یعنی offset هدر رکورد + 4. مقدار PlainPrefix برای
// BOUNDSHEET برابر 4، برای BOF / FILEPASS طول کامل، و برای بیشتر رکوردها 0
procedure DemoObfuscateRecord(var Body: array of Byte; RecordLength: Word;
PlainPrefix: Integer; BodyPos: LongWord; const Arr: TXorArray);
var
I: Integer;
begin
for I := PlainPrefix to RecordLength - 1 do
Body[I] := Rol8(Body[I], 5) xor
Arr[(BodyPos + LongWord(I) + RecordLength) mod 16];
end;
// خواندن تصویر آینهای همان است: B := Body[I] xor Arr[...];
// بعد چرخش 5 بیتی به راست، یعنی Rol8(B, 3)
قبل از v2.384.47 خواننده HotXLS اندیس را فقط از موقعیت stream میگرفت و نویسنده از offset بایت ساده استفاده میکرد. هر دو طول رکورد را نادیده میگرفتند، پس باز هم دو نیمه با هم توافق داشتند و با هیچکس دیگر. یک decoder که مستقل نوشته شده بود خروجی v2.384.47 را درست میخواند و خروجی قدیمیتر را بهعنوان garbage، و تست هشت-واریانتی Excel 16 هم بعداً قاعده را مقابل هدف واقعی تأیید کرد
با فایلهای XOR نوشتهشده توسط نسخههای قدیمیتر HotXLS چه میشود؟
HotXLS فایلهای XOR خودش قبل از v2.384.54 را با چک کردن کلید FILEPASS همچنان میخواند: وقتی کلید ذخیرهشده با کلید مشتقشده از رمز عبور برابر باشد، reader آرایه مطابق مشخصات یعنی XorRor را میسازد، و وقتی متفاوت باشد فایل را فایل قدیمی HotXLS تلقی میکند و آرایه چرخش-به-چپ-2 را بازسازی میکند. فایلهای نوشتهشده توسط Excel همیشه کلید مشتقشده دارند، پس همیشه مسیر مشخصات را میروند
این تست یک heuristic با نرخ خطای دقیق است. یک فایل قدیمی که کلید تصادفیاش اتفاقی با کلید مشتقشده برابر میبود با آرایه غلط خوانده میشد، و شانس آن 1 در 65,536 است. fallback فقط چرخش آرایه را پوشش میدهد؛ قاعده اندیس سوییچ نمیشود، پس فایلهایی که نجات پیدا میکنند همانهاییاند که بین v2.384.47 و v2.384.53 نوشته شدهاند. اگر هنوز فایلهای XOR با BIFF5 از آن بازه دارید، با HotXLS فعلی بازشان کنید و دوباره ذخیرهشان کنید تا فایلی بگیرید که Excel میپذیرد
دو جزئیات رمز عبور روی هر فایلی اعمال میشود، قدیمی یا جدید:
- طول.
CreateXorKey_Method1فقط 15 بایت اول رمز عبور را میخواند، که همان سقف مشخصات است. HotXLS این سقف را روی کلید اعمال میکند و verifier و آرایه را روی قواعد همیشگی خودشان یعنی طول کامل و 16 بایت نگه میدارد، بهطور سازگار در هر دو طرف. خود Excel برای این فرمت رمزهای عبور بلندتر از 15 کاراکتر را رد میکند، پس 15 را سقف واقعی بدانید - مجموعه کاراکتر. HotXLS رمز عبور را از طریق code page ANSI سیستم به بایت تبدیل میکند. مشخصات میگوید بایت پایین هر کاراکتر UTF-16 برداشته شود، که برای ASCII یکی است. بدون نمونه Excel محافظتشده با رمزهای غیر-اسکی برای بقیه هیچ ground truth وجود ندارد، پس برای فایلهای XOR به رمزهای اسکی بچسبید
در سمت خواندن، TXLSWorkbook.OnPassword اجازه میدهد وقتی Open به رکورد FILEPASS میخورد رمز عبور بپرسید. event از نوع TXLSPasswordEvent است با یک var PassWord: WideString و یک var Retry: Boolean؛ Retry را True کنید تا دوباره تلاش شود، تا سه بار تلاش:
procedure TImportForm.WorkbookPassword(Sender: TObject;
var PassWord: WideString; var Retry: Boolean);
var
S: string;
begin
S := '';
Retry := InputQuery('Protected workbook', 'Password:', S);
PassWord := S;
end;
procedure TImportForm.ImportLegacyFile(const FileName: string);
var
Wb: IXLSWorkbook;
Rc: Integer;
begin
Wb := TXLSWorkbook.Create;
Wb.OnPassword := WorkbookPassword;
Rc := Wb.Open(FileName);
// -1003 یعنی رمز عبور لازم است اما داده نشده؛ -1005 یعنی رمز عبور غلط
if Rc <> 1 then
raise Exception.CreateFmt('Cannot open %s (code %d)', [FileName, Rc]);
ShowMessage(VarToStr(Wb.Sheets[1].Range['A1', 'A1'].Value));
end;
اگر رمز عبور را از قبل میدانید، Open(FileName, APassWord) کلاً از کنار event رد میشود
مبهمسازی XOR برای هیچچیز امنیت کافی دارد؟
مبهمسازی XOR در BIFF رمزنگاری نیست و مقابل یک خواننده مصمم هیچ چیزی را حفاظت نمیکند. چک رمز عبور یک verifier شانزدهبیتی است، کلید 16 بیت است، و آرایه 16 بایتی در کل stream تکرار میشود، پس محتوای قابل پیشبینی رکوردهای BIFF بایتهای آرایه را بدون هیچ رمز عبوری لو میدهد. HotXLS فقط برای این XOR مینویسد که فایلهای Excel 5.0/95 گزینه دیگری ندارند، و دلیل تولید چنین فایلهایی امروز یک مصرفکننده legacy است که چیزی جدیدتر نمیخواند
موتور کلاسیک طرح را از طریق TXLSWorkbook.EncryptionType انتخاب میکند، و ترکیبش با فرمت ذخیره سختگیرانه چک میشود:
xletAuto(پیشفرض) برایxlExcel97یعنی RC4 CryptoAPI مینویسد و برایxlExcel5یعنی XOR، مطابق همان چیزی که خود Excel برای هر فرمت مینوشتxletXorفقط برای BIFF5 معتبر است؛ باxlExcel97عملیات ذخیره بهجای fallback بیسروصدا exception میدهدxletRC4وxletRC4CryptoAPIفقط برای BIFF8 هستند، و درخواستشان روی ذخیره BIFF5 هم exception میدهد
RC4 هم قدیمی است، و جزئیات interop کردنش در چرا اکسل کتاب کار رمزگذاریشده شما را رد میکند: ECB و RC4 پوشش داده شده. اگر گیرنده میتواند XLSX بخواند، بهجایش موتور XLSX را بردارید: TXLSXWorkbook.SaveAsEncryptedAgile مینویسد Agile Encryption (هش رمز عبور SHA-512 با spin count صد-هزار-تکراری و AES-256-CBC)، همان فرمتی که Excel 2010 و بعدتر بهطور پیشفرض مینویسند، و SaveAsEncrypted مینویسد Standard Encryption قدیمیتر با AES-128. مقایسه این دو در رمزگذاری فایلهای XLSX با AES در دلفی با HotXLS است و سمت خواندن در خواندن فایلهای اکسل رمزگذاریشده Agile در دلفی با HotXLS پوشش داده شده
مرجع سریع: مبهمسازی XOR در BIFF که Excel 16 میپذیرد
- FILEPASS یعنی
$002Fبعد از globals BOF میآید؛ در BIFF5 بدنهاش 4 بایت است: کلید، بعد verifier - کلید =
CreateXorKey_Method1(password)طبق [MS-OFFCRYPTO] §2.3.7.2، هرگز تصادفی نیست؛ برایsecretبرابر$014Dاست - آرایه = بایتهای رمز عبور + pad، موقعیت زوج با بایت کم کلید XOR میشود و موقعیت فرد با بایت بالا کلید، بعد XorRor (چرخش به راست 1)
- رمز کردن یک بایت: چرخش به چپ 5، بعد XOR؛ رمزگشایی: XOR، بعد چرخش به راست 5 (§2.3.7.3)
- XorArrayIndex = (offset بایت در stream + طول داده رکورد) mod 16؛ هدرها و پیشوندهای ساده در offset حساب میشوند
- BOUNDSHEET چهار بایت اولش را ساده نگه میدارد؛ BOF و FILEPASS و INTERFACEHDR کاملاً ساده میمانند
- رمزهای عبور: اسکی، حداکثر 15 کاراکتر
- HotXLS: اندیس در v2.384.47 فیکس شد، کلید و XorRor در v2.384.54، فایلهای XOR قدیمی HotXLS با ناهمخوانی کلید شناسایی میشوند
- برای حفاظت واقعی حداقل BIFF8 RC4 CryptoAPI یا Agile Encryption در XLSX را بردارید
HotXLS حفاظت با رمز عبور BIFF5 و BIFF8 و Standard Encryption و Agile Encryption در XLSX و callback رمز عبور سمت خواندن را در یک کتابخانه Delphi و C++Builder جا داده، با همان جزئیات interop بالا که برای شما مدیریت میشوند. برای نسخهها، پلتفرمها و دانلود آزمایشی کامپوننت صفحهگسترده HotXLS در Delphi را ببینید