يكتب HotXLS مصنفات BIFF5 (Excel 5.0/95) معتمّة بـ XOR لا يفتحها Excel 16 إلا إذا تطابقت ثلاث تفاصيل مع [MS-OFFCRYPTO] تماماً: يجب أن يكون مفتاح FILEPASS هو CreateXorKey_Method1(password)، وبناء مصفوفة XOR ذات الـ 16 بايتة بـ XorRor (تدوير يمين بمقدار بتة واحدة)، واستخدام كل بايتةٍ XorArrayIndex = (إزاحة التدفق + طول السجل) mod 16. أصاب HotXLS الفهرس في v2.384.47 والمفتاح والتدوير في v2.384.54. وقبل ذلك كان كل ملف BIFF5 محمي بكلمة سر ينتجه يفتح في HotXLS بلا مشكلة ويفشل في Excel
تلك الجملة الأخيرة هي الحكاية كلها في صورة مصغرة. القارئ والكاتب اللذان يتقاسمان الفكرة الخاطئة نفسها يوافق كل منهما الآخر توافقاً تاماً، فتبقى اختبارات الدورة خضراء بينما المستهلك الوحيد المهم يقول لا. وقال Excel 16 لا مرتين، برسالتين مختلفتين، وأشارت كل رسالة إلى طبقة مختلفة من المخطط. تمشي هذه المقالة في تلك الطبقات بترتيب فحص Excel لها، بتفصيل على مستوى البايتة ينفعك سواء استدعيت HotXLS أو كتبت قارئ BIFF خاص بك
ماذا يخزن تعتيم BIFF XOR فعلاً؟
لا يخزن تعتيم BIFF XOR في الملف إلا كلمتين 16-بت، وكل ما عداهما يعاد حسابه من كلمة السر. يجلس سجل FILEPASS ($002F) بعد BOF لمتغيرات المصنف العامة مباشرةً، وجسمه في ملف BIFF5 4 بايتات بالضبط: مفتاح XOR يليه المُتحقِّق من كلمة السر. ولا ملح، ولا معرّف خوارزمية، ولا كتلة تحقق مشفرة من النوع الذي تحمله مخططات RC4 و AES
من تلك الكلمتين يعيد القارئ بناء ثلاثة أشياء:
- المُتحقِّق، تجزئة 16-بت لبايتات كلمة السر مؤثَّرة بـ XOR مع
$CE4B. ومقارنته بالكلمة المخزنة هي فحص كلمة السر، والفحص الوحيد - مفتاح XOR، قيمة 16-بت من
CreateXorKey_Method1في [MS-OFFCRYPTO] §2.3.7.2، يقودها جدولان ثابتان (InitialCodeبـ 15 كلمة وXorMatrixبـ 105 كلمات) - مصفوفة XOR، 16 بايتة من بايتات كلمة السر مدعومة بوسادة ثابتة من 16 بايتة، تؤثر كل بايتة بـ XOR مع بايتة المفتاح الدنيا (المواضع الزوجية) أو العليا (المواضع الفردية)، ثم تُدوَّر يميناً بمقدار بتة واحدة
تبقى ترويسات السجلات نصاً صريحاً، وكذلك حفنة سجلات كاملة يعفيها المخطط، من بينها BOF و FILEPASS و INTERFACEHDR. وكل جسم سجل آخر يتحول بايتةً بايتة: تدوير يسار بمقدار 5 بتات، ثم XOR مع مدخل واحد من مصفوفة الـ 16 بايتة. وفك التشفير، الذي يهجئه [MS-OFFCRYPTO] §2.3.7.3 باسم DecryptData_Method1، هو الصورة المعكوسة: XOR أولاً، ثم تدوير يمين بمقدار 5
في HotXLS لا تلمس شيئاً من هذا مباشرةً قط. تضبط كلمة سر وتختار الصيغة، فيبث SaveAs FILEPASS ويحول التدفق:
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;
// أبقه ASCII وبخمسة عشر محرفاً على الأكثر (انظر أدناه)
Wb.EncryptionPassword := 'secret';
if Wb.SaveAs(FileName, xlExcel5) <> 1 then
raise Exception.Create('BIFF5 save failed');
end;
لماذا يقول Excel إن كلمة السر خاطئة والمُتحقِّق مطابق؟
يرفض Excel كلمة السر لأنه لا يثق بالمفتاح المخزن: يشتق Excel المفتاح من كلمة السر المكتوبة بـ CreateXorKey_Method1 ويقارنه بكلمة مفتاح FILEPASS، فالملف الذي يكون مفتاحه غير ذلك يرسب فحص كلمة السر حتى لو كان المُتحقِّق صائباً. وتصف المواصفة المفتاح بوصفه ناتجاً عن كلمة السر، لا وسيطاً حراً، ويُلزم Excel 16 بذلك القراءة
كان كاتب HotXLS قبل v2.384.54 يملأ كلمة المفتاح بايتتين عشوائيتين. يبدو ذلك غير مؤذٍ على الورق، فالمُتحقِّق هو فحص كلمة السر الموثق والمصفوفة تُبنى من المفتاح الذي يعلنه الملف أياً كان. وكان HotXLS نفسه يقرأ تلك الملفات بلا عناء، لأن قارئه يأخذ المفتاح من FILEPASS على أنه معطى. وسُلّم Excel 16 الملف نفسه وكلمة السر الصحيحة فأجاب أن كلمة السر غير صحيحة. ومنذ v2.384.54 يُشتق المفتاح، فيحمل FILEPASS دائماً لكلمة السر secret المفتاح $014D والمُتحقِّق $DAA7، قيمتين قورنتا ضربياً بتنفيذ مستقل للمواصفة
الاشتقاق نفسه قصير متى وضعت الجدولين. تجول على كلمة السر بالعكس، وانظر بتة 6 من كل بايتة سبع مرات مع إزاحتها يساراً، وأثر بـ XOR مدخل XorMatrix واحداً كلما ارتفعت البتة. والتالي رسم مبدئي يعيد خوارزمية المواصفة ويطابق تنفيذ HotXLS؛ وليس واجهة HotXLS:
// رسم مبدئي لـ [MS-OFFCRYPTO] 2.3.7.2 CreateXorKey_Method1
// و CreateXorArray_Method1 (توضيح فقط، وليس واجهة 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، وهي عينة ثابتة مفيدة إن كنت تختبر قارئك الخاص
لماذا ينتج المفتاح الصحيح ملفاً تالفاً مع ذلك؟
ينتج المفتاح الصحيح ملفاً تالفاً حين تُدوَّر مصفوفة XOR بالاتجاه الخطأ: تعرّف [MS-OFFCRYPTO] خطوة المصفوفة بوصفها XorRor، تدويراً يميناً بمقدار بتة واحدة، ومصفوفة مدورة يساراً بمقدار اثنين تفك كل جسم سجل إلى ضجيج. وأصلح المفتاح نقل Excel 16 من سؤال كلمة السر إلى خطأ مختلف تماماً، بلاغ بأن للملف مشكلة ولا يمكن فتحه
كان كود HotXLS القديم يدور كل بايتة مصفوفة يساراً بمقدار بتتين، وهي صيغة تتناقلها عدة تنفيذات لـ BIFF. ولأن HotXLS استخدم الدوران نفسه على الجانبين، لم ينتبه قارئه الخاص قط. لم يعد Excel 16 قادراً على حفظ ملفات Excel 5.0/95، ولا يعرض XOR عند حفظ BIFF8، فلم يكن ثمة نموذج Excel أصلي يُقارن به. اضطر الدليل أن يأتي من الاتجاه الآخر: اكتب تدفق BIFF5 نصاً صريحاً واحداً، وأعد ترميزه بثماني صور، ودع 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 لبايتةٍ ما هو إزاحتها في تدفق المصنف زائد طول بيانات السجل الكامل التي تنتمي إليها، محسوبة mod 16. فيبدأ الفهرس من جديد عند قيمة تعتمد على السجل لكل سجل، ويزيد واحداً لكل بايتة داخله. وتسمي الشيفرة الزائفة للمواصفة المدخلات FileOffset و Data.Length، ويسهل سوء قراءتها على أنها إزاحة بداية السجل وحدها، وذلك السوء في القراءة هو بالضبط ما شحنه HotXLS حتى v2.384.47
ثلاثة تفاصيل تحسم مواءمة فهارسك مع Excel:
- ترويسة السجل ذات البايتات الأربع لا تُحوَّل قط، لكنها تشغل مواضع من التدفق مع ذلك، فأول بايتة جسم في السجل تجلس عند إزاحة الترويسة + 4
- حد الطول هو طول بيانات السجل الكامل، لا عدد البايتات المحولة فعلاً
- BOUNDSHEET صريح جزئياً: أول 4 بايتات فيه،
lbPlyPos، إزاحة تدفق BOF الورقة، تبقى مقروءة ليستطيع المحلل تحديد مواضع الأوراق. تلك البايتات الأربع تتخطاها عملية التحويل لكنها تحتسب في الإزاحة وطول السجل معاً
مجتمعةً، التحويل لكل سجل بضعة أسطر. وأكرر، هذا رسم للقاعدة، لا شيء تحتاج لاستدعائه:
function Rol8(B: Byte; N: Integer): Byte;
begin
Result := Byte((B shl N) or (B shr (8 - N)));
end;
// تعتيم جسم سجل واحد في موضعه. BodyPos هو إزاحة التدفق لـ
// Body[0]، أي إزاحة ترويسة السجل + 4. و PlainPrefix يساوي 4 لـ
// BOUNDSHEET والطول الكامل لـ 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 يحسب الفهرس من موضع التدفق وحده وكان الكاتب يستخدم إزاحة البايتة الصريحة. كلاهما أغفل طول السجل، فاتفق نصفا المكتبة مجدداً مع بعضهما ولا أحد سواهما. وقرأ فاحص شيفرة مستقل ناتج v2.384.47 قراءة صحيحة والناتج الأقدم قراءة خردة، وأكد اختبار الصور الثماني في Excel 16 القاعدة لاحقاً أمام الهدف الحقيقي
ماذا يحدث لملفات XOR التي كتبتها إصدارات HotXLS الأقدم؟
يواصل HotXLS قراءة ملفاته المعتمة بـ XOR السابقة لـ v2.384.54 بفحص مفتاح FILEPASS: حين يساوي المفتاح المخزن المفتاح المشتق من كلمة السر يبني القارئ مصفوفة XorRor المواصفية، وحين يخالفه يعامل الملف ملف HotXLS أقدم ويعيد بناء مصفوفة التدوير-يسار-2. أما الملفات المكتوبة من Excel فتحمل المفتاح المشتق دوماً، فتسلك المسار المواصفي دوماً
الفحص استدلالي بمعدل إخفاق محدد. ملف قديم صدف أن مفتاحه العشوائي ساوى المفتاح المشتق سيقرأ بالمصفوفة الخطأ، واحتمال ذلك 1 من 65,536. والبديل يغطي دوران المصفوفة فقط؛ قاعدة الفهرس لا تُبدَّل، فالملفات التي ينجدها هي المكتوبة بين v2.384.47 و v2.384.53. فإن كنت لا تزال تملك ملفات BIFF5 معتمة بـ XOR من تلك النافذة، افتحها بـ HotXLS الحالي واحفظها من جديد لتحصل على ملف يقبله Excel
تفصيلان لكلمة السر يسريان على كل ملف، قديماً كان أو جديداً:
- الطول. لا يقرأ
CreateXorKey_Method1إلا أول 15 بايتة من كلمة السر، وهو حد المواصفة. يطبق HotXLS ذلك السقف على المفتاح ويبقي المُتحقِّق والمصفوفة على قاعدتيهما المعتادتين للطول الكامل والـ 16 بايتة، باتساق على الجانبين. ويرفض Excel نفسه كلمات سر أطول من 15 محرفاً لهذه الصيغة، فاعتبار 15 هو الحد الأقصى الحقيقي - مجموعة المحارف. يحول HotXLS كلمة السر إلى بايتات عبر صفحة الشيفرة ANSI للنظام. وتصف المواصفة أخذ البايتة الدنيا من كل محرف UTF-16، وهو ما يتوافق مع ASCII. ومن دون نماذج Excel محمية بكلمات سر غير ASCII لا يوجد مرجع حقيقي لما سواها، فالتزم كلمات سر ASCII لملفات XOR
على جانب القراءة تتيح لك TXLSWorkbook.OnPassword سؤال المستخدم عن كلمة سر حين يصطدم Open بسجل FILEPASS. الحدث هو 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) الحدث كله
هل تعتيم XOR آمن بما يكفي لأي شيء؟
تعتيم BIFF XOR ليس تشفيراً ولا يحمي شيئاً من قارئ مصمم. فحص كلمة السر مُتحقِّق 16-بت، والمفتاح 16 بتة، ومصفوفة الـ 16 بايتة تتكرر عبر التدفق كله، فمحتويات سجلات BIFF المتوقعة تكشف بايتات المصفوفة دون أي كلمة سر أصلاً. لا يكتب HotXLS XOR إلا لأن ملفات Excel 5.0/95 لا خيار آخر لها، وسبب إنتاج ملفات كهذه اليوم هو مستهلك قديم لا يقرأ شيئاً أحدث
يختار المحرك الكلاسيكي المخطط عبر TXLSWorkbook.EncryptionType، ويفحص التوافق مع صيغة الحفظ بصرامة:
-
xletAuto(الافتراضي) يكتب RC4 CryptoAPI لـxlExcel97و XOR لـxlExcel5، موافقاً ما كتبه Excel نفسه لكل صيغة -
xletXorصالح لـ BIFF5 وحده؛ ومعxlExcel97يرفع الحفظ استثناءً بدل التراجع بصمت -
xletRC4وxletRC4CryptoAPIلـ BIFF8 وحده، وطلبهما على حفظ BIFF5 يرفع استثناءً كذلك
RC4 عتيق هو أيضاً، وتفاصيل مواءمته مغطاة في لماذا يرفض Excel مصنفاً مشفراً بكلمة سر صحيحة. وإن كان المستلم يقرأ XLSX فاستخدم محرك XLSX بدلاً منه: يكتب TXLSXWorkbook.SaveAsEncryptedAgile Agile Encryption (تجزئة كلمة سر SHA-512 بعدد دورات 100,000 و AES-256-CBC)، وهي الصيغة التي يكتبها Excel 2010 وما بعده افتراضياً، بينما يكتب SaveAsEncrypted Standard Encryption الأقدم بـ AES-128. والموازنة بينهما في تشفير ملفات XLSX بـ AES في Delphi، وجانب القراءة مغطى في قراءة ملفات Excel المشفرة بـ Agile مع HotXLS
مرجع سريع: تعتيم BIFF XOR الذي يقبله Excel 16
- FILEPASS (
$002F) يلي BOF المتغيرات العامة؛ وجسمه في BIFF5 4 بايتات: المفتاح ثم المُتحقِّق - المفتاح =
CreateXorKey_Method1(password)وفق [MS-OFFCRYPTO] §2.3.7.2، لا عشوائي قط؛ ولـsecretهو$014D - المصفوفة = بايتات كلمة السر + الوسادة، XOR مع بايتة المفتاح الدنيا في المواضع الزوجية والعليا في المواضع الفردية، ثم XorRor (تدوير يمين 1)
- تشفير بايتة: تدوير يسار 5 ثم XOR؛ وفك التشفير: XOR ثم تدوير يمين 5 (§2.3.7.3)
- XorArrayIndex = (إزاحة بايتة التدفق + طول بيانات السجل) mod 16؛ والترويسات والسوابق الصريحة تحتسب في الإزاحة
- يبقي BOUNDSHEET أول 4 بايتات صريحة؛ وتبقى BOF و FILEPASS و INTERFACEHDR صريحة كاملة
- كلمات السر: ASCII وبخمسة عشر محرفاً على الأكثر
- HotXLS: أصلح الفهرس في v2.384.47 والمفتاح و XorRor في v2.384.54، وملفات HotXLS القديمة المعتمة بـ XOR تُكشف باختلاف المفتاح
- للحماية الحقيقية استخدم BIFF8 RC4 CryptoAPI على الأقل، أو XLSX Agile Encryption
يتولى HotXLS حماية كلمات السر في BIFF5 و BIFF8، وتشفير XLSX Standard و Agile، واستدعاء كلمة السر عند القراءة من مكتبة واحدة لـ Delphi و C++Builder، مع تولي تفاصيل التوافق أعلاه عنك. راجع مكوّن HotXLS Delphi للجداول الحسابية للإصدارات والمنصات وتنزيل النسخة التجريبية