Teknik Makale

HotXLS XLS XOR gizleme: Anahtar türetme ve XorRor

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 $CE4B ile 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 ve XorMatrix, 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

Excel 16'nın açılışta denetlediği BIFF5 XOR FILEPASS record'unun HotXLS şeması: düz dört baytlık gövde XOR anahtarını ve parola verifier'ını taşır; Excel anahtarı yazılan paroladan CreateXorKey_Method1 ile türetir ve verifier eşleşse bile rastgele bir anahtarı parola hatasıyla reddeder; HotXLS, secret için türetilmiş 014D anahtarını saklar
FILEPASS gövdesi yalnızca iki word'dür ama Excel anahtarı parolanızdan yeniden türetir ve karşılaştırır; rastgele doldurulmuş bir anahtar, verifier doğruyken bile kontrolü geçemez; HotXLS'in onu v2.384.54'ten beri türetmesinin nedeni budur

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

BIFF5 gizlemesi için XOR dizi kurulumunun HotXLS şeması: paroladan ve sabit bir pad'den tohumlanan on altı bayt, çift konumlarda alçak anahtar baytıyla tek konumlarda yüksek anahtar baytıyla XOR'lanır ve XorRor ile bir bit sağa döndürülür; secret parolası için 1F 32 17 B9 sabiti üretilir
Dizi baytları paroladan, pad'den ve iki anahtar baytından gelir, sonunda tek bir döndürme vardır; 2 sola döndürme yalnızca bayt dönüşümü ters sırada koştuğunda işe yaradı ve Excel şartname sırasını izler

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çimA seçeneğiB seçeneği
Dizi döndürmesiXorRor (1 sağa döndürme)2 sola döndürme
Dizi indeksi(offset + record length) mod 16offset mod 16
Bayt dönüşüm sırası5 sola döndür, sonra XORXOR, 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
BIFF5 XOR gizlemesi için XorArrayIndex kuralının HotXLS şeması: her gövde baytı stream offset artı bütün record veri uzunluğu mod 16 kullanır; düz dört baytlık header ve BOUNDSHEET lbPlyPos öneki yine de offset'e sayılır ve record uzunluk terimini yok saymak, HotXLS'in v2.384.47'de düzelttiği kusurdu
Dizi indeksi stream başına bir kez değil, record başına bir kez yeniden başlar: header ve herhangi bir düz önek konumları işgal eder, record veri uzunluğu modulo'yu besler ve eski HotXLS'in iki yarısı da yanlış formülde anlaşıyordu

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_Method1 yalnı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), xlExcel97 için RC4 CryptoAPI ve xlExcel5 için XOR yazar; Excel'in kendisinin her biçim için yazdığıyla eşleşir
  • xletXor yalnızca BIFF5 için geçerlidir; xlExcel97 ile kayıt, sessizce geri düşmek yerine exception fırlatır
  • xletRC4 ve xletRC4CryptoAPI BIFF8'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; secret iç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