HotXLS menulis workbook Excel 5.0/95 (BIFF5) ter-obfuscate XOR yang hanya dibuka Excel 16 ketika tiga detail cocok persis dengan [MS-OFFCRYPTO]: key FILEPASS harus CreateXorKey_Method1(password), array XOR 16 byte harus dibangun dengan XorRor (rotate right satu bit), dan tiap byte harus memakai XorArrayIndex = (stream offset + record length) mod 16. HotXLS mengurus index dengan benar di v2.384.47, lalu key dan rotasinya di v2.384.54. Sebelum itu, setiap file BIFF5 berpassword buatannya terbuka mulus di HotXLS dan gagal di Excel
Kalimat terakhir itu adalah keseluruhan cerita dalam ukuran mini. Reader dan writer yang berbagi ide salah yang sama akan sepakat satu sama lain dengan sempurna, jadi test round-trip tetap hijau sementara satu-satunya consumer yang penting bilang tidak. Excel 16 bilang tidak dua kali, dengan dua pesan berbeda, dan tiap pesan menunjuk lapisan skema yang berbeda. Artikel ini menelusuri lapisan-lapisan itu sesuai urutan pemeriksaan Excel, dengan detail level byte yang berguna baik Anda memanggil HotXLS maupun menulis reader BIFF sendiri
Apa yang sebenarnya disimpan oleh obfuscation XOR BIFF?
Obfuscation XOR BIFF hanya menyimpan dua word 16-bit di file, dan sisanya dihitung ulang dari password. Record FILEPASS ($002F) duduk tepat setelah workbook globals BOF, dan di file BIFF5 body-nya tepat 4 byte: key XOR diikuti password verifier. Tidak ada salt, tidak ada identifier algoritma, dan tidak ada blob verifier terenkripsi seperti yang dibawa skema RC4 dan AES
Dari dua word itu, reader membangun ulang tiga hal:
- Verifier, hash 16-bit dari byte password yang di-XOR dengan
$CE4B. Membandingkannya dengan word tersimpan adalah pemeriksaan password, dan satu-satunya - Key XOR, nilai 16-bit dari
CreateXorKey_Method1di [MS-OFFCRYPTO] §2.3.7.2, digerakkan oleh dua tabel konstanta (InitialCode15 word danXorMatrix105 word) - Array XOR, 16 byte yang terdiri dari byte password yang di-pad dengan pad 16 byte tetap, masing-masing di-XOR dengan byte key rendah (posisi genap) atau byte key tinggi (posisi ganjil), lalu di-rotate right satu bit
Header record tetap plain text, begitu juga segelintir record utuh yang dikecualikan skema ini, di antaranya BOF, FILEPASS, dan INTERFACEHDR. Setiap body record lainnya ditransformasi byte demi byte: rotate left 5 bit, lalu XOR dengan satu entri array 16 byte. Dekripsi, yang dirinci [MS-OFFCRYPTO] §2.3.7.3 sebagai DecryptData_Method1, adalah bayangan cerminnya: XOR dulu, lalu rotate right 5
Di HotXLS Anda tak pernah menyentuh semua ini secara langsung. Set password, pilih format, dan SaveAs memancarkan FILEPASS serta mentransformasi stream-nya:
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 hanya mendukung obfuscation XOR; xletAuto juga akan memilihnya
Wb.EncryptionType := xletXor;
// Jaga tetap ASCII dan maksimal 15 karakter (lihat di bawah)
Wb.EncryptionPassword := 'secret';
if Wb.SaveAs(FileName, xlExcel5) <> 1 then
raise Exception.Create('BIFF5 save failed');
end;
Mengapa Excel bilang password salah padahal verifier-nya cocok?
Excel menolak passwordnya karena ia tidak memercayai key tersimpan: Excel menurunkan key dari password yang diketik dengan CreateXorKey_Method1 dan membandingkannya dengan key word FILEPASS, jadi file yang key-nya bukan itu akan gagal pemeriksaan password meskipun verifier-nya benar. Spesifikasi menggambarkan key sebagai output dari password, bukan parameter bebas, dan Excel 16 menegakkan pembacaan itu
Writer HotXLS sebelum v2.384.54 mengisi key word dengan dua byte acak. Tampak tak berbahaya di atas kertas, karena verifier adalah pemeriksaan password yang terdokumentasi dan array dibangun dari key apa pun yang dideklarasikan file. HotXLS sendiri membaca file-file itu tanpa masalah, karena readernya menerima key dari FILEPASS apa adanya. Excel 16, yang diberi file yang sama dan password yang benar, menjawab bahwa password tidak benar. Sejak v2.384.54 key diturunkan, jadi FILEPASS untuk password secret selalu memuat key $014D dan verifier $DAA7, nilai yang sudah dicek silang terhadap implementasi independen dari spesifikasi
Derivasinya sendiri pendek begitu dua tabel itu tersedia. Telusuri password dari belakang, perhatikan bit 6 tiap byte tujuh kali sambil menggesernya ke kiri, dan XOR-kan satu entri XorMatrix setiap kali bit itu terisi. Berikut sketsa prinsip yang mereproduksi algoritma spesifikasi dan cocok dengan implementasi HotXLS; ini bukan API HotXLS:
// Sketsa prinsip [MS-OFFCRYPTO] 2.3.7.2 CreateXorKey_Method1
// dan CreateXorArray_Method1 (ilustrasi saja, bukan 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; // key hanya melihat 15 byte
if Len = 0 then
Exit;
Result := InitialCode[Len - 1];
Element := $68; // entri XorMatrix terakhir
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)); // rotate right satu bit
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;
Untuk secret, ini menghasilkan array 1F 32 17 B9 14 BA 7B 7F 59 DD 59 7F 7A C0 A6 DF, fixture yang praktis kalau Anda sedang mengetes reader sendiri
Mengapa key yang benar tetap menghasilkan file rusak?
Key yang benar tetap menghasilkan file rusak ketika array XOR dirotasi ke arah yang salah: [MS-OFFCRYPTO] mendefinisikan langkah array sebagai XorRor, rotate right satu bit, dan array yang dirotasi left dua akan mendekripsi setiap body record menjadi derau. Memperbaiki key membawa Excel 16 melewati prompt password dan langsung ke error berbeda, laporan bahwa file bermasalah dan tak bisa dibuka
Kode HotXLS lama mengrotasi tiap byte array ke kiri 2 bit, bentuk yang beredar di beberapa implementasi BIFF. Karena HotXLS memakai rotasi yang sama di kedua sisi, readernya sendiri tak pernah menyadarinya. Excel 16 tak lagi bisa menyimpan file Excel 5.0/95, dan ia tak menawarkan XOR saat menyimpan BIFF8, jadi tak ada sampel Excel native untuk di-diff. Bukti harus datang dari arah sebaliknya: tulis satu stream BIFF5 plain-text, enkode ulang dengan delapan cara, dan biarkan Excel 16 membuka tiap varian. Delapan varian itu menyilangkan tiga pilihan independen:
| Pilihan | Opsi A | Opsi B |
|---|---|---|
| Rotasi array | XorRor (rotate right 1) | Rotate left 2 |
| Index array | (offset + record length) mod 16 | offset mod 16 |
| Urutan transformasi byte | Rotate left 5, lalu XOR | XOR, lalu rotate left 5 |
Excel 16 membuka tepat dua dari delapan: XorRor dengan rotate-lalu-XOR dan index record-length, serta satu varian yang hanya terlihat berbeda. Rotate-left-2 dengan XOR-lalu-rotate dan index yang sama adalah fungsi yang sama dalam penyamaran. Rotasi terdistribusi atas XOR, jadi rol5(p xor rol2(b)) sama dengan rol5(p) xor rol7(b), dan pada nilai 8-bit rotate left 7 adalah rotate right 1. Singkatnya, rol5 ∘ rol2 = ror1, itulah sebabnya array rotate-left-2 terlihat masuk akal bila berdiri sendiri: ia benar hanya bersama urutan transformasi yang terbalik. Dipasangkan dengan urutan spesifikasi, ia merusak setiap byte yang ditransformasi
Eksperimen yang sama menuntaskan pertanyaan kedua. Varian yang membuang record length dari index semuanya gagal, yang mengonfirmasi aturan index yang diadopsi HotXLS satu rilis sebelumnya hanya berdasarkan teks spesifikasi
Bagaimana XorArrayIndex dihitung untuk tiap byte?
XorArrayIndex untuk sebuah byte adalah offsetnya di workbook stream plus panjang seluruh record data tempatnya, mod 16. Index karena itu dimulai ulang dari nilai tergantung record untuk setiap record dan bertambah satu per byte di dalamnya. Pseudocode spesifikasi menamai inputnya FileOffset dan Data.Length, yang mudah disalahartikan sebagai offset awal record saja, dan kesalahpahaman itulah persis yang dikirim HotXLS sampai v2.384.47
Tiga detail menentukan apakah index Anda segaris dengan Excel:
- Header record 4 byte tak pernah ditransformasi, tapi ia tetap menduduki posisi stream, jadi byte body pertama sebuah record berada di header offset + 4
- Term panjang adalah panjang record data penuh, bukan jumlah byte yang benar-benar ditransformasi
- BOUNDSHEET sebagian plain: 4 byte pertamanya,
lbPlyPos, stream offset dari sheet BOF, tetap terbaca agar parser bisa menemukan sheet. Keempat byte itu dilewati transform tapi tetap dihitung baik ke offset maupun ke record length
Dikumpulkan, transformasi per-record hanya beberapa baris. Sekali lagi, ini sketsa aturannya, bukan sesuatu yang perlu Anda panggil:
function Rol8(B: Byte; N: Integer): Byte;
begin
Result := Byte((B shl N) or (B shr (8 - N)));
end;
// Obfuscate satu body record secara in place. BodyPos adalah stream offset dari
// Body[0], yaitu offset header record + 4. PlainPrefix bernilai 4 untuk
// BOUNDSHEET, panjang penuh untuk BOF / FILEPASS, 0 untuk kebanyakan record
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;
// Membaca adalah bayangan cerminnya: B := Body[I] xor Arr[...];
// lalu rotate right 5, yaitu Rol8(B, 3)
Sebelum v2.384.47, reader HotXLS menghitung index dari posisi stream saja dan writer memakai offset byte plain. Keduanya mengabaikan record length, jadi lagi-lagi kedua belah pihak sepakat satu sama lain dan dengan tidak ada pihak lain. Decoder yang ditulis independen membaca output v2.384.47 dengan benar dan output lama sebagai sampah, dan test delapan varian Excel 16 belakangan mengonfirmasi aturan itu terhadap target sesungguhnya
Apa yang terjadi pada file XOR yang ditulis versi HotXLS lama?
HotXLS tetap membaca file XOR pra-v2.384.54 miliknya sendiri dengan mengecek key FILEPASS: ketika key tersimpan sama dengan key hasil turunan dari password, reader membangun array XorRor spesifikasi, dan ketika berbeda, reader memperlakukan file itu sebagai file HotXLS lama dan membangun ulang array rotate-left-2. File yang ditulis Excel selalu membawa key hasil turunan, jadi selalu mengambil jalur spesifikasi
Tesnya adalah heuristik dengan tingkat gagal yang presisi. File lama yang key acaknya kebetulan sama dengan key hasil turunan akan terbaca dengan array yang salah, dan peluangnya 1 banding 65.536. Fallback hanya mencakup rotasi array; aturan index tidak ditukar, jadi file yang diselamatkannya adalah yang ditulis antara v2.384.47 dan v2.384.53. Kalau Anda masih menyimpan file XOR BIFF5 dari jendela itu, buka dengan HotXLS terkini dan simpan lagi untuk mendapat file yang diterima Excel
Dua detail password berlaku untuk setiap file, lama maupun baru:
- Panjang.
CreateXorKey_Method1hanya membaca 15 byte password pertama, itu batas spesifikasi. HotXLS menerapkan batas itu pada key dan menjaga verifier serta array pada aturan panjang penuh dan 16 byte biasanya, konsisten di kedua sisi. Excel sendiri menolak password lebih dari 15 karakter untuk format ini, jadi perlakukan 15 sebagai maksimum sesungguhnya - Set karakter. HotXLS mengonversi password ke byte lewat system ANSI code page. Spesifikasi mendeskripsikan pengambilan byte rendah tiap karakter UTF-16, yang cocok untuk ASCII. Tanpa sampel Excel yang dilindungi password non-ASCII, tak ada ground truth untuk sisanya, jadi patuhi password ASCII untuk file XOR
Di sisi pembacaan, TXLSWorkbook.OnPassword memungkinkan Anda meminta password ketika Open menemui record FILEPASS. Event-nya adalah TXLSPasswordEvent dengan var PassWord: WideString dan var Retry: Boolean; set Retry ke True untuk mencoba lagi, hingga tiga kali retry:
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: password dibutuhkan tapi tak diberikan; -1005: password salah
if Rc <> 1 then
raise Exception.CreateFmt('Cannot open %s (code %d)', [FileName, Rc]);
ShowMessage(VarToStr(Wb.Sheets[1].Range['A1', 'A1'].Value));
end;
Kalau Anda sudah tahu passwordnya, Open(FileName, APassWord) melewati event sepenuhnya
Apakah obfuscation XOR cukup aman untuk apa pun?
Obfuscation XOR BIFF bukan enkripsi dan tak melindungi apa pun dari reader yang berniat. Pemeriksaan passwordnya adalah verifier 16-bit, key-nya 16 bit, dan array 16 byte itu berulang di sepanjang stream, jadi isi record BIFF yang mudah ditebak membongkar byte array tanpa password sama sekali. HotXLS menulis XOR hanya karena file Excel 5.0/95 tak punya pilihan lain, dan alasan memproduksi file semacam itu hari ini adalah consumer legacy yang tak bisa membaca apa pun yang lebih baru
Engine classic memilih skema lewat TXLSWorkbook.EncryptionType, dan kombinasinya dengan format simpan diperiksa ketat:
xletAuto(default) menulis RC4 CryptoAPI untukxlExcel97dan XOR untukxlExcel5, sesuai dengan yang ditulis Excel sendiri untuk tiap formatxletXorhanya valid untuk BIFF5; denganxlExcel97, save melempar exception alih-alih diam-diam fallbackxletRC4danxletRC4CryptoAPIhanya untuk BIFF8, dan memintanya pada save BIFF5 juga melempar exception
RC4 juga sudah tua, dan detail membuatnya interop dibahas di mengapa Excel menolak workbook terenkripsi dengan password yang benar. Kalau penerimanya bisa membaca XLSX, pakai engine XLSX saja: TXLSXWorkbook.SaveAsEncryptedAgile menulis Agile Encryption (hashing password SHA-512 dengan spin count 100.000 iterasi dan AES-256-CBC), format yang ditulis Excel 2010 ke atas secara default, sedangkan SaveAsEncrypted menulis Standard Encryption AES-128 yang lebih lama. Trade-off keduanya ada di enkripsi file XLSX dengan AES di Delphi, dan sisi pembacaan dibahas di membaca file Excel terenkripsi Agile dengan HotXLS
Referensi cepat: obfuscation XOR BIFF yang diterima Excel 16
- FILEPASS (
$002F) mengikuti globals BOF; di BIFF5 body-nya 4 byte: key, lalu verifier - Key =
CreateXorKey_Method1(password)menurut [MS-OFFCRYPTO] §2.3.7.2, tak pernah acak; untuksecretnilainya$014D - Array = byte password + pad, XOR byte key rendah di posisi genap dan byte key tinggi di posisi ganjil, lalu XorRor (rotate right 1)
- Enkripsi satu byte: rotate left 5, lalu XOR; dekripsi: XOR, lalu rotate right 5 (§2.3.7.3)
- XorArrayIndex = (stream offset byte + record data length) mod 16; header dan prefix plain dihitung ke offset
- BOUNDSHEET menjaga 4 byte pertamanya plain; BOF, FILEPASS, dan INTERFACEHDR sepenuhnya plain
- Password: ASCII, maksimal 15 karakter
- HotXLS: index diperbaiki di v2.384.47, key dan XorRor di v2.384.54, file XOR HotXLS lama terdeteksi lewat ketidakcocokan key
- Untuk perlindungan nyata, pakai RC4 CryptoAPI BIFF8 minimal, atau Agile Encryption XLSX
HotXLS menangani proteksi password BIFF5 dan BIFF8, Standard dan Agile Encryption XLSX, serta callback password sisi baca dari satu library Delphi dan C++Builder, dengan detail interop di atas sudah ditangani untuk Anda. Lihat komponen spreadsheet Delphi HotXLS untuk edisi, platform, dan unduhan trial