HotXLS, Excel 16'ın yalnızca üç ayrıntı [MS-OFFCRYPTO] ile bire bir örtüştüğünde açtığı Excel 5.0/95 (BIFF5) XOR gizlemeli çalışma kitapları yazar: FILEPASS anahtarı CreateXorKey_Method1(password) olmalı, 16 baytlık XOR dizisi XorRor ile (bir bit sağa döndürme) kurulmalı ve her bayt XorArrayIndex = (stream offset + record length) mod 16 kullanmalı. HotXLS indeksi v2.384.47'de, anahtar ile döndürmeyi v2.384.54'te doğru yaptı. O tarihe kadar ürettiği parola korumalı her BIFF5 dosyası HotXLS'te sorunsuz açılır, Excel'de açılmazdı
Son cümle, hikâyenin bütününün minyatürüdür. Aynı yanlış fikri paylaşan bir okuyucu ile bir yazıcı birbirleriyle kusursuz anlaşır; dolayısıyla round-trip testleri yeşil kalırken önemli tek tüketici hayır der. Excel 16 iki kez hayır dedi, iki farklı iletiyle ve her ileti şemanın farklı bir katmanını işaret etti. Bu yazı o katmanları Excel'in denetleme sırasıyla gezinir; HotXLS çağırsanız da kendi BIFF okuyucunuzu yazsanız da kullanabileceğiniz bayt düzeyi ayrıntıyla
BIFF XOR gizlemesi aslında neyi saklar?
BIFF XOR gizlemesi dosyada yalnızca iki 16 bitlik word saklar; gerisi paroladan yeniden hesaplanır. FILEPASS record'u ($002F), workbook globals BOF'un hemen ardından durur ve bir BIFF5 dosyasında gövdesi tam 4 bayttır: XOR anahtarı, ardından parola verifier'ı. Ne salt vardır ne algoritma tanımlayıcısı ne de RC4 ve AES şemalarının taşıdığı türden şifreli bir verifier blob'u
Okuyucu o iki word'den üç şeyi yeniden kurar:
- Verifier, parola baytlarının
$CE4Bile XOR'lanmış 16 bitlik hash'i. Saklanan word ile karşılaştırmak parola kontrolüdür; üstelik tek olanı - XOR anahtarı, [MS-OFFCRYPTO] §2.3.7.2'deki
CreateXorKey_Method1'den gelen 16 bitlik değer; iki sabit tablo sürer (InitialCode, 15 word veXorMatrix, 105 word) - XOR dizisi, sabit bir 16 baytlık pad ile doldurulmuş parola baytlarından oluşan 16 bayt; her biri alçak anahtar baytıyla (çift konumlar) ya da yüksek anahtar baytıyla (tek konumlar) XOR'lanır, sonra bir bit sağa döndürülür
Record header'ları düz metin kalır; şemanın muaf tuttuğu bir avuç bütün record da öyle, aralarında BOF, FILEPASS ve INTERFACEHDR var. Öteki her record gövdesi bayt bayt dönüştürülür: 5 bit sola döndür, sonra 16 baytlık dizinin bir girdisiyle XOR'la. [MS-OFFCRYPTO] §2.3.7.3'ün DecryptData_Method1 olarak yazdığı decryption, aynadaki görüntüsüdür: önce XOR, sonra 5 sağa döndür
HotXLS'te bunların hiçbirine doğrudan dokunmazsınız. Parolayı set edin, biçimi seçin; SaveAs FILEPASS'i boşaltır ve stream'i dönüştürür:
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 yalnızca XOR gizlemesini destekler; xletAuto da onu seçerdi
Wb.EncryptionType := xletXor;
// ASCII tutun ve en çok 15 karakter (aşağıya bakın)
Wb.EncryptionPassword := 'secret';
if Wb.SaveAs(FileName, xlExcel5) <> 1 then
raise Exception.Create('BIFF5 save failed');
end;
Verifier eşleşirken Excel neden parolanın yanlış olduğunu söylüyor?
Excel parolayı reddeder, çünkü saklanan anahtara güvenmez: Excel anahtarı yazılan paroladan CreateXorKey_Method1 ile türetir ve FILEPASS key word'üyle karşılaştırır; dolayısıyla anahtarı başka bir şey olan dosya, verifier doğruyken bile parola kontrolünden geçemez. Şartname anahtarı parolanın bir çıktısı olarak tanımlar, serbest bir parametre olarak değil ve Excel 16 bu okumayı dayatır
v2.384.54 öncesindeki HotXLS yazıcısı, key word'ü iki rastgele baytla dolduruyordu. Kâğıt üzerinde zararsız görünür; çünkü verifier belgelenmiş parola kontrolüdür ve dizi, dosyanın bildirdiği anahtar neyse ondan kurulur. HotXLS'in kendisi o dosyaları sorunsuz okudu; çünkü okuyucusu anahtarı FILEPASS'ten hazır kabul aldı. Aynı dosyayı ve doğru parolayı alan Excel 16, parolanın doğru olmadığını yanıtladı. v2.384.54'ten beri anahtar türetilir; dolayısıyla secret parolası için FILEPASS daima $014D anahtarını ve $DAA7 verifier'ını taşır — değerler şartnamenin bağımsız bir gerçeklemesiyle çapraz kontrol edildi
Türetmenin kendisi, iki tablo yerindeyken kısadır. Parolayı tersten yürüyün, her baytın 6. bitine sola kaydırırken yedi kez bakın ve bit setli olduğu her seferde bir XorMatrix girdisini XOR'layın. Aşağıdakisi şartname algoritmasını yeniden üreten ve HotXLS gerçeklemesiyle örtüşen bir ilke taslağıdır; HotXLS API'si değildir:
// [MS-OFFCRYPTO] 2.3.7.2 CreateXorKey_Method1 ilke taslağı
// ve CreateXorArray_Method1 (yalnızca gösterim, HotXLS API'si değildir)
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; // anahtar yalnızca 15 bayt görür
if Len = 0 then
Exit;
Result := InitialCode[Len - 1];
Element := $68; // son XorMatrix girdisi
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)); // bir bit sağa döndürme
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 için bu, 1F 32 17 B9 14 BA 7B 7F 59 DD 59 7F 7A C0 A6 DF dizisini üretir; kendi okuyucunuzu test ediyorsanız el altında tutulacak pratik bir sabittir
Doğru anahtar neden hâlâ bozuk bir dosya üretir?
XOR dizisi yanlış yönde döndürüldüğünde doğru bir anahtar hâlâ bozuk bir dosya üretir: [MS-OFFCRYPTO] dizi adımını XorRor olarak, yani bir bit sağa döndürme olarak tanımlar ve iki sola döndürülmüş bir dizi her record gövdesini gürültüye deşifre eder. Anahtarı düzeltmek, Excel 16'yı parola isteminden geçirdi ve doğrudan başka bir hataya, dosyanın sorunu olduğu ve açılamayacağı raporuna soktu
Eski HotXLS kodu her dizi baytını 2 bit sola döndürüyordu; birkaç BIFF gerçeklemesinde dolaşan bir biçim. HotXLS aynı döndürmeyi iki tarafta da kullandığı için kendi okuyucusu hiç fark etmedi. Excel 16 artık Excel 5.0/95 dosyalarını kaydedemiyor ve BIFF8 kaydederken XOR sunmuyor; dolayısıyla diff alınacak yerel bir Excel örneği yoktu. Kanıt öbür yönden gelmek zorundaydı: tek bir düz metin BIFF5 stream yazın, sekiz biçimde yeniden kodlayın ve Excel 16'ya her varyantı açtırın. Sekiz varyant üç bağımsız seçimi kesitiyordu:
| Seçim | A seçeneği | B seçeneği |
|---|---|---|
| Dizi döndürmesi | XorRor (1 sağa döndürme) | 2 sola döndürme |
| Dizi indeksi | (offset + record length) mod 16 | offset mod 16 |
| Bayt dönüşüm sırası | 5 sola döndür, sonra XOR | XOR, sonra 5 sola döndür |
Excel 16 sekizin tam ikisini açtı: rotate-then-XOR ve record-length indeksiyle XorRor ile yalnızca farklı görünen bir varyant. XOR-sonra-döndür ve aynı indeksle 2-sola-döndür, kılık değiştirmiş aynı fonksiyondur. Döndürme XOR üzerine dağılır; dolayısıyla rol5(p xor rol2(b)), rol5(p) xor rol7(b)'ye eşittir ve 8 bitlik bir değer üzerinde 7 sola döndürme, 1 sağa döndürmedir. Kısacası rol5 ∘ rol2 = ror1; 2-sola-döndür dizisinin tek başına inandırıcı görünmesinin nedeni budur: yalnızca ters dönüşüm sırasıyla birlikte doğrudur. Şartname sırasıyla eşleşince dönüştürülmüş her baytı bozar
Aynı deney ikinci bir soruyu da kapattı. İndeksten record uzunluğunu atan varyantların hepsi başarısız oldu; bu, HotXLS'in bir sürüm önce yalnızca şartname metnine dayanarak benimsediği indeks kuralını doğruladı
XorArrayIndex her bayt için nasıl hesaplanır?
Bir bayt için XorArrayIndex, workbook stream'indeki offset'inin artı ait olduğu bütün record verisinin uzunluğunun, mod 16'dır. İndeks bu yüzden her record için record'a bağlı bir değerden yeniden başlar ve içindeki her baytta bir artar. Şartname pseudocode'u girdileri FileOffset ve Data.Length adlandırır; bu, kolayca yalnızca record başlangıç offset'i diye yanlış okunur ve o yanlış okuma, HotXLS'in v2.384.47'ye dek dağıttığı şeyin ta kendisiydi
Üç ayrıntı, indekslerinizin Excel'le hizalanıp hizalanmadığına karar verir:
- 4 baytlık record header'ı asla dönüştürülmez ama yine de stream konumları işgal eder; dolayısıyla bir record'un ilk gövde baytı header offset + 4'te durur
- Uzunluk terimi, gerçekten dönüştürülen bayt sayısı değil, bütün record veri uzunluğudur
- BOUNDSHEET kısmen düzdür: ilk 4 baytı, yani sheet BOF'un stream offset'i olan
lbPlyPos, okunabilir kalır; böylece bir parser sheet'leri konumlayabilir. O 4 bayt dönüşümün dışında kalır ama hem offset'e hem record uzunluğuna sayılır
Bir araya getirince record başına dönüşüm birkaç satırdır. Yine hatırlatalım, bu kuralın bir taslağı; çağırmanız gereken bir şey değil:
function Rol8(B: Byte; N: Integer): Byte;
begin
Result := Byte((B shl N) or (B shr (8 - N)));
end;
// Bir record gövdesini yerinde gizler. BodyPos, stream içinde
// Body[0]'ın offset'idir, yani record header offset + 4. PlainPrefix,
// BOUNDSHEET için 4, BOF / FILEPASS için tam uzunluk, çoğu record için 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;
// Okuma aynadaki görüntüdür: B := Body[I] xor Arr[...];
// sonra 5 sağa döndürme, yani Rol8(B, 3)
v2.384.47 öncesinde HotXLS okuyucusu indeksi yalnızca stream konumundan hesaplıyor, yazıcı ise düz bayt offset'i kullanıyordu. İkisi de record uzunluğunu yok sayıyordu; dolayısıyla yine iki yarı birbirine uydu, başkasına değil. Bağımsız yazılmış bir decoder v2.384.47 çıktısını doğru okudu, eski çıktıyı çöp olarak; sekiz varyantlı Excel 16 testi ise kuralı daha sonra gerçek hedefe karşı doğruladı
Eski HotXLS sürümlerince yazılmış XOR dosyalarına ne olur?
HotXLS, kendi v2.384.54 öncesi XOR dosyalarını FILEPASS anahtarını denetleyerek okumaya devam eder: saklanan anahtar, paroladan türetilen anahtara eşitse okuyucu şartnamedeki XorRor dizisini kurar; farklıysa dosyayı eski bir HotXLS dosyası sayar ve 2-sola-döndür dizisini yeniden kurar. Excel'in yazdığı dosyalar daima türetilmiş anahtarı taşır; dolayısıyla daima şartname yolunu izler
Test, kesin bir hata oranlı bir sezgiseldir. Rastgele anahtarı tesadüfen türetilmiş anahtara eşit çıkan eski bir dosya yanlış diziyle okunurdu ve bunun olasılığı 65.536'da 1'dir. Fallback yalnızca dizi döndürmesini kapsar; indeks kuralı değişmez, dolayısıyla kurtardığı dosyalar v2.384.47 ile v2.384.53 arasında yazılanlardır. O aralıktan BIFF5 XOR dosyalarınız hâlâ duruyorsa onları güncel HotXLS ile açıp yeniden kaydedin ki Excel'in kabul edeceği bir dosyanız olsun
İki parola ayrıntısı her dosyaya uygulanır, eski ya da yeni:
- Uzunluk.
CreateXorKey_Method1yalnızca ilk 15 parola baytını okur; şartname sınırı budur. HotXLS o tavanı anahtara uygular ve verifier ile diziyi olağan tam uzunluk ve 16 bayt kurallarında tutar, iki tarafta da tutarlı biçimde. Excel'in kendisi bu biçim için 15 karakterden uzun parolaları reddeder; dolayısıyla 15'i gerçek tavan sayın - Karakter seti. HotXLS parolayı sistem ANSI kod sayfası üzerinden baytlara çevirir. Şartname, her UTF-16 karakterinin alçak baytını almayı tanımlar; ASCII için ikisi de aynıdır. ASCII dışı parolalarla korunan Excel örnekleri olmadan gerisi için bir hakiki referans yoktur; dolayısıyla XOR dosyalarında ASCII parolalara sadık kalın
Okuma tarafında TXLSWorkbook.OnPassword, Open bir FILEPASS record'a rastladığında parola sormanızı sağlar. Olay, var PassWord: WideString ve var Retry: Boolean taşıyan bir TXLSPasswordEvent'tir; yeniden denemek için Retry'yi True yapın, üç denemeye kadar:
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: parola gerekli ama verilmedi; -1005: yanlış parola
if Rc <> 1 then
raise Exception.CreateFmt('Cannot open %s (code %d)', [FileName, Rc]);
ShowMessage(VarToStr(Wb.Sheets[1].Range['A1', 'A1'].Value));
end;
Parolayı çoktan biliyorsanız Open(FileName, APassWord) olayı tamamen atlar
XOR gizlemesi hiçbir şey için yeterince güvenli mi?
BIFF XOR gizlemesi şifreleme değildir ve kararlı bir okuyucuya karşı hiçbir şey korumaz. Parola kontrolü 16 bitlik bir verifier'dır, anahtar 16 bittir ve 16 baytlık dizi bütün stream boyunca tekrar eder; dolayısıyla öngörülebilir BIFF record içerikleri dizi baytlarını hiç parola olmaksızın ifşa eder. HotXLS XOR'u yalnızca Excel 5.0/95 dosyalarının başka seçeneği olmadığı için yazar; bugün böyle dosyalar üretmenin nedeni, daha yenisini okuyamayan bir legacy tüketicidir
Klasik motor şemayı TXLSWorkbook.EncryptionType üzerinden seçer ve kayıt biçimiyle kombinasyonu titizlikle denetlenir:
xletAuto(varsayılan),xlExcel97için RC4 CryptoAPI vexlExcel5için XOR yazar; Excel'in kendisinin her biçim için yazdığıyla eşleşirxletXoryalnızca BIFF5 için geçerlidir;xlExcel97ile kayıt, sessizce geri düşmek yerine exception fırlatırxletRC4vexletRC4CryptoAPIBIFF8'e özgüdür ve BIFF5 kaydında onları istemek de exception fırlatır
RC4 de eskimiştir ve onu birlikte çalışır kılmanın ayrıntıları Excel'in doğru parolayla şifreli çalışma kitabını neden reddettiği yazısında. Alıcı XLSX okuyabiliyorsa onun yerine XLSX motorunu kullanın: TXLSXWorkbook.SaveAsEncryptedAgile Agile Encryption yazar (100.000 iterasyonlu spin count ile SHA-512 parola hash'leme ve AES-256-CBC) — Excel 2010 ve sonrasının varsayılan olarak yazdığı biçim; SaveAsEncrypted ise daha eski AES-128 Standard Encryption yazar. İkisi arasındaki tavizler Delphi'de AES ile XLSX dosyalarını şifreleme yazısında, okuma tarafı ise HotXLS ile Agile şifreli Excel dosyalarını okuma yazısında
Hızlı başvuru: Excel 16'nın kabul ettiği BIFF XOR gizlemesi
- FILEPASS (
$002F), globals BOF'un ardından gelir; BIFF5'te gövdesi 4 bayttır: anahtar, sonra verifier - Anahtar = [MS-OFFCRYPTO] §2.3.7.2 gereği
CreateXorKey_Method1(password), asla rastgele değil;secretiçin$014D'dir - Dizi = parola baytları + pad; çift konumlarda alçak anahtar baytıyla, tek konumlarda yüksek anahtar baytıyla XOR, sonra XorRor (1 sağa döndürme)
- Bir baytı şifreleme: 5 sola döndür, sonra XOR; deşifre: XOR, sonra 5 sağa döndür (§2.3.7.3)
- XorArrayIndex = (bayt stream offset + record veri uzunluğu) mod 16; header'lar ve düz önekler offset'e sayılır
- BOUNDSHEET ilk 4 baytını düz tutar; BOF, FILEPASS ve INTERFACEHDR tamamen düz kalır
- Parolalar: ASCII, en çok 15 karakter
- HotXLS: indeks v2.384.47'de, anahtar ve XorRor v2.384.54'te düzeltildi; eski HotXLS XOR dosyaları anahtar uyuşmazlığından saptanır
- Gerçek koruma için en azından BIFF8 RC4 CryptoAPI, yoksa XLSX Agile Encryption kullanın
HotXLS, BIFF5 ve BIFF8 parola korumasını, XLSX Standard ve Agile Encryption'ı ve okuma tarafı parola callback'ini tek bir Delphi ve C++Builder kütüphanesinden yürütür; yukarıdaki interop ayrıntıları sizin için halledilidir. Sürümler, platformlar ve deneme indirmesi için HotXLS Delphi elektronik tablo bileşeni sayfasına bakın