Tehnički članak

HotXLS XLS XOR obfuskacija: izvođenje ključa i XorRor

HotXLS piše Excel 5.0/95 (BIFF5) radne sveske obfuskovane XOR-om koje Excel 16 otvara samo kada se tri detalja tačno poklope s [MS-OFFCRYPTO]: FILEPASS ključ mora biti CreateXorKey_Method1(password), 16-bajtni XOR niz mora se graditi s XorRor (rotacija u desno za jedan bit), a svaki bajt mora koristiti XorArrayIndex = (ofset u toku + dužina zapisa) mod 16. HotXLS je indeks pogodio u v2.384.47, a ključ i rotaciju u v2.384.54. Pre toga, svaki BIFF5 fajl zaštićen lozinkom koji je proizveo otvarao se lepo u HotXLS-u i padao u Excelu

Ta poslednja rečenica je cela priča u malom. Čitač i writer koji dele istu pogrešnu ideju savršeno se slažu međusobno, pa round-trip testovi ostaju zeleni dok jedini potrošač koji je važan govori ne. Excel 16 je rekao ne dva puta, s dve različite poruke, i svaka poruka je ukazivala na drugi sloj šeme. Ovaj tekst prolazi kroz te slojeve redom kojim ih Excel proverava, s detaljima na nivou bajta koji vam koriste bilo da pozivate HotXLS bilo da pišete svoj BIFF čitač

Šta BIFF XOR obfuskacija zapravo čuva?

BIFF XOR obfuskacija čuva u fajlu samo dve 16-bitne reči, a sve ostalo se preračunava iz lozinke. FILEPASS zapis ($002F) sedi odmah iza workbook globals BOF-a, i u BIFF5 fajlu njegovo telo je tačno 4 bajta: XOR ključ pa verifier lozinke. Nema salta, nema identifikatora algoritma, nema šifrovanog verifier bloba kakav nose RC4 i AES šeme

Iz te dve reči čitač ponovo gradi tri stvari:

  • verifier, 16-bitni heš bajtova lozinke XOR-ovan s $CE4B. Poređenje sa sačuvanom rečju jeste provera lozinke, i jedina
  • XOR ključ, 16-bitna vrednost iz CreateXorKey_Method1 u [MS-OFFCRYPTO] §2.3.7.2, vođen dvema konstantnim tabelama (InitialCode, 15 reči, i XorMatrix, 105 reči)
  • XOR niz, 16 bajtova napravljenih od bajtova lozinke dopunjenih fiksnim 16-bajtnim padom, svaki XOR-ovan s nižim bajtom ključa (parne pozicije) ili višim bajtom ključa (neparne pozicije), pa rotiran u desno za jedan bit

Zaglavlja zapisa ostaju u čistom tekstu, kao i nekolicina celih zapisa koje šema izuzima, među njima BOF, FILEPASS i INTERFACEHDR. Svako drugo telo zapisa transformiše se bajt po bajt: rotacija u levo za 5 bitova, pa XOR s jednim elementom 16-bajtnog niza. Dešifrovanje, koje [MS-OFFCRYPTO] §2.3.7.3 izlaže kao DecryptData_Method1, je ogledalo: prvo XOR, pa rotacija u desno za 5

U HotXLS-u ničeg od ovoga ne dirate direktno. Postavite lozinku, izaberite format, i SaveAs emituje FILEPASS i transformiše tok:

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 podržava samo XOR obfuskaciju; xletAuto bi je i sam izabrao
  Wb.EncryptionType := xletXor;
  // Držite se ASCII-a i najviše 15 znakova (vidi dole)
  Wb.EncryptionPassword := 'secret';

  if Wb.SaveAs(FileName, xlExcel5) <> 1 then
    raise Exception.Create('BIFF5 save failed');
end;

Zašto Excel kaže da je lozinka pogrešna kad se verifier poklapa?

Excel odbija lozinku jer ne veruje sačuvanom ključu: Excel izvodi ključ iz ukucene lozinke s CreateXorKey_Method1 i poredi ga s ključnom reči iz FILEPASS-a, pa fajl čiji je ključ bilo šta drugo pada na proveri lozinke čak i kad je verifier ispravan. Specifikacija opisuje ključ kao izlaz lozinke, a ne kao slobodan parametar, i Excel 16 to tumačenje nameće

HotXLS writer pre v2.384.54 popunjavao je ključnu reč s dva slučajna bajta. Na papiru to deluje bezopasno, jer je verifier dokumentovana provera lozinke, a niz se gradi od ključa koji fajl sam deklariše. HotXLS je te fajlove čitao bez problema, jer je njegov čitač uzimao ključ iz FILEPASS-a kao dato. Excel 16, kad mu se pruži isti fajl i ispravna lozinka, odgovarao je da lozinka nije ispravna. Od v2.384.54 ključ se izvodi, pa FILEPASS za lozinku secret uvek nosi ključ $014D i verifier $DAA7, vrednosti proverene upoređivanjem s nezavisnom implementacijom specifikacije

HotXLS dijagram BIFF5 XOR FILEPASS zapisa koji Excel 16 proverava pri otvaranju: čisto četvorobajtno telo nosi XOR ključ i verifier lozinke, Excel izvodi ključ iz ukucene lozinke s CreateXorKey_Method1 i odbacuje slučajan ključ s greškom o lozinci čak i kad se verifier poklapa; HotXLS čuva izvedeni ključ 014D za secret
Telo FILEPASS-a su samo dve reči, ali Excel ključ ponovo izvodi iz vaše lozinke i poredi ga; slučajno popunjen ključ pada na proveri čak i s ispravnim verifierom, pa ga HotXLS od v2.384.54 izvodi

Sama izvedba je kratka kad su dve tabele na mestu. Prođite lozinku od kraja, gledajte bit 6 svakog bajta sedam puta dok ga pomerate u levo, i XOR-ujte po jedan XorMatrix element svaki put kad je bit postavljen. Sledeće je načelna skica koja reprodukuje algoritam iz specifikacije i poklapa se s HotXLS implementacijom; nije HotXLS API:

// Načelna skica [MS-OFFCRYPTO] 2.3.7.2 CreateXorKey_Method1
// i CreateXorArray_Method1 (samo ilustracija, nije HotXLS API)
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;                       // ključ vidi samo 15 bajtova
  if Len = 0 then
    Exit;
  Result := InitialCode[Len - 1];
  Element := $68;                    // poslednji XorMatrix element
  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));   // rotiraj u desno za jedan 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;

Za secret ovo daje niz 1F 32 17 B9 14 BA 7B 7F 59 DD 59 7F 7A C0 A6 DF, zgodan fixture ako testirate svoj čitač

HotXLS dijagram gradnje XOR niza za BIFF5 obfuskaciju: šesnaest bajtova posejanih iz lozinke i fiksnog pada XOR-uje se s nižim bajtom ključa na parnim pozicijama i višim bajtom ključa na neparnim pozicijama, pa se rotira u desno za jedan bit s XorRor, dajući fixture 1F 32 17 B9 za lozinku secret
Bajtovi niza dolaze iz lozinke, pada i dva bajta ključa, jedna rotacija na kraju; rotacija u levo za 2 radila je samo kad se transformacija bajta odvijala obrnutim redom, a Excel prati red iz specifikacije

Zašto ispravan ključ i dalje daje oštećen fajl?

Ispravan ključ i dalje daje oštećen fajl kad se XOR niz rotira na pogrešnu stranu: [MS-OFFCRYPTO] definiše korak niza kao XorRor, rotaciju u desno za jedan bit, a niz rotiran u levo za dva dešifruje svako telo zapisa u šum. Ispravka ključa je gurnula Excel 16 preko upita za lozinku pravo u drugu grešku, izveštaj da fajl ima problem i ne može se otvoriti

Stari HotXLS kod rotirao je svaki bajt niza u levo za 2 bita, oblik koji kruži u nekoliko BIFF implementacija. Pošto je HotXLS koristio istu rotaciju s obe strane, njegov sopstveni čitač nikada nije primetio. Excel 16 više ne može da čuva fajlove Excel 5.0/95, i ne nudi XOR pri čuvanju BIFF8, pa nije bilo nativnog Excel uzorka za poređenje. Dokaz je morao doći iz drugog smera: napisati jedan čist tekstualni BIFF5 tok, ponovo ga enkodovati na osam načina i pustiti Excel 16 da otvori svaku varijantu. Osam varijanti ukrstilo je tri nezavisne odluke:

OdlukaOpcija AOpcija B
Rotacija nizaXorRor (rotacija u desno 1)Rotacija u levo 2
Indeks niza(ofset + dužina zapisa) mod 16ofset mod 16
Red transformacije bajtaRotacija u levo 5, pa XORXOR, pa rotacija u levo 5

Excel 16 je otvorio tačno dve od osam: XorRor s rotiraj-pa-XOR i indeksom dužine zapisa, i jednu varijantu koja se samo čini drugačijom. Rotacija u levo 2 s XOR-pa-rotiraj i istim indeksom je ista funkcija u prerušavanju. Rotacija se distribuje preko XOR-a, pa je rol5(p xor rol2(b)) jednako rol5(p) xor rol7(b), a na 8-bitnoj vrednosti rotacija u levo za 7 je rotacija u desno za 1. Ukratko, rol5 ∘ rol2 = ror1, pa niz s rotacijom u levo 2 deluje uverljivo izolovan: ispravan je samo uz obrnuti red transformacije. U paru s redom iz specifikacije pokvari svaki transformisani bajt

Isti eksperiment rešio je i drugo pitanje. Varijante koje su izbacile dužinu zapisa iz indeksa sve su pale, čime je potvrđeno pravilo indeksa koje je HotXLS usvojio izdanje ranije, na snagu samog teksta specifikacije

Kako se XorArrayIndex računa za svaki bajt?

XorArrayIndex za bajt je njegov ofset u toku radne sveske plus dužina celokupnih podataka zapisa kojem pripada, mod 16. Indeks se dakle restartuje na vrednost zavisnu od zapisa za svaki zapis i uvećava se po jedan za svaki bajt unutar njega. Pseudokod specifikacije imenuje ulaze FileOffset i Data.Length, što se lako pročita kao sam ofset početka zapisa, i to pogrešno čitanje je upravo ono što je HotXLS isporučivao do v2.384.47

Tri detalja odlučuju da li se vaši indeksi poklapaju s Excelom:

  • Četvorobajtno zaglavlje zapisa se nikada ne transformiše, ali i dalje zauzima pozicije u toku, pa prvi bajt tela zapisa sedi na ofsetu zaglavlja + 4
  • Član dužine je puna dužina podataka zapisa, ne broj bajtova zaista transformisanih
  • BOUNDSHEET je delimično čist: njegova prva 4 bajta, lbPlyPos, ofset sheet BOF-a u toku, ostaju čitljivi da parser može da locira listove. Tih 4 bajta transformacija preskače, ali i dalje računaju i u ofset i u dužinu zapisa
HotXLS dijagram pravila XorArrayIndex za BIFF5 XOR obfuskaciju: svaki bajt tela koristi ofset u toku plus punu dužinu podataka zapisa modulo 16, čisto četvorobajtno zaglavlje i BOUNDSHEET lbPlyPos prefiks i dalje računaju u ofset, a ignorisanje člana dužine zapisa bila je greška koju je HotXLS ispravio u v2.384.47
Indeks niza restartuje se jednom po zapisu, ne jednom po toku: zaglavlje i svaki čist prefiks zauzimaju pozicije, dužina podataka zapisa hrani modulo, i obe polovine starog HotXLS-a slažu se u pogrešnoj formuli

Sve skupa, transformacija po zapisu je nekoliko redova. Opet, ovo je skica pravila, ne nešto što treba da zovete:

function Rol8(B: Byte; N: Integer): Byte;
begin
  Result := Byte((B shl N) or (B shr (8 - N)));
end;

// Šifruje telo jednog zapisa na mestu. BodyPos je ofset u toku za
// Body[0], tj. ofset zaglavlja zapisa + 4. PlainPrefix je 4 za
// BOUNDSHEET, puna dužina za BOF / FILEPASS, 0 za većinu zapisa
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;

// Čitanje je ogledalna slika: B := Body[I] xor Arr[...];
// pa rotacija u desno za 5, tj. Rol8(B, 3)

Pre v2.384.47 HotXLS čitač računao je indeks samo iz pozicije u toku, a writer je koristio čisti bajt-ofset. Obojica su ignorisali dužinu zapisa, pa su se opet dve polovine slagale jedna s drugom i ni s kim drugim. Nezavisno napisan dekoder čitao je izlaz v2.384.47 ispravno a stariji izlaz kao đubre, i test s osam varijanti u Excelu 16 kasnije je potvrdio pravilo protiv pravog cilja

Šta je sa XOR fajlovima koje su napisale starije HotXLS verzije?

HotXLS i dalje čita svoje XOR fajlove starije od v2.384.54 proverom FILEPASS ključa: kad je sačuvani ključ jednak ključu izvedenom iz lozinke, čitač gradi XorRor niz iz specifikacije, a kad se razlikuje, čitač tretira fajl kao stariji HotXLS fajl i ponovo gradi niz s rotacijom u levo 2. Fajlovi napisani iz Excela uvek nose izvedeni ključ, pa uvek idu putem specifikacije

Ta provera je heuristika s tačnom stopom greške. Stari fajl čiji se slučajni ključ desio jednakim izvedenom ključu čitao bi se s pogrešnim nizom, a šansa za to je 1 u 65.536. Fallback pokriva samo rotaciju niza; pravilo indeksa se ne prebacuje, pa fajlovi koje spašava su oni napisani između v2.384.47 i v2.384.53. Ako još držite BIFF5 XOR fajlove iz tog prozora, otvorite ih trenutnim HotXLS-om i sačuvajte ih ponovo da dobijete fajl koji Excel prihvata

Dva detalja o lozinci važe za svaki fajl, stari ili novi:

  • Dužina. CreateXorKey_Method1 čita samo prvih 15 bajtova lozinke, što je granica iz specifikacije. HotXLS tu granicu primenjuje na ključ i drži verifier i niz na njihovim uobičajenim pravilima pune dužine i 16 bajtova, dosledno s obe strane. Excel sam odbija lozinke duže od 15 znakova za ovaj format, pa tretirajte 15 kao pravi maksimum
  • Skup znakova. HotXLS prevodi lozinku u bajtove kroz sistemsku ANSI code page. Specifikacija opisuje uzimanje nižeg bajta svakog UTF-16 znaka, što se poklapa za ASCII. Bez Excel uzoraka zaštićenih ne-ASCII lozinkama nema istine o ostatku, pa se držite ASCII lozinki za XOR fajlove

Na strani čitanja, TXLSWorkbook.OnPassword vam omogućava da upitite lozinku kad Open naiđe na FILEPASS zapis. Događaj je TXLSPasswordEvent s var PassWord: WideString i var Retry: Boolean; postavite Retry na True da pokušate ponovo, do tri ponovna pokušaja:

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: traži se lozinka ali nije data; -1005: pogrešna lozinka
  if Rc <> 1 then
    raise Exception.CreateFmt('Cannot open %s (code %d)', [FileName, Rc]);
  ShowMessage(VarToStr(Wb.Sheets[1].Range['A1', 'A1'].Value));
end;

Ako lozinku već znate, Open(FileName, APassWord) preskače događaj sasvim

Da li je XOR obfuskacija dovoljno sigurna za išta?

BIFF XOR obfuskacija nije šifrovanje i ne štiti ništa od čitača s voljom. Provera lozinke je 16-bitni verifier, ključ je 16 bitova, a 16-bajtni niz se ponavlja kroz ceo tok, pa predvidljivi sadržaji BIFF zapisa izlažu bajtove niza bez ikakve lozinke. HotXLS piše XOR samo zato što fajlovi Excel 5.0/95 nemaju drugu opciju, a razlog za proizvodnju takvih fajlova danas je legacy potrošač koji ne čita ništa novije

Klasični engine bira šemu kroz TXLSWorkbook.EncryptionType, i kombinacija sa formatom čuvanja proverava se strogo:

  • xletAuto (podrazumevano) piše RC4 CryptoAPI za xlExcel97 i XOR za xlExcel5, u skladu s tim što je Excel sam pisao za svaki format
  • xletXor važi samo za BIFF5; uz xlExcel97 čuvanje baca izuzetak umesto da tiho padne na stariju opciju
  • xletRC4 i xletRC4CryptoAPI su samo za BIFF8, i traženje njih pri BIFF5 čuvanju takođe baca izuzetak

RC4 je isto zastareo, a detalji oko njegove interoperabilnosti pokriveni su u tekstu o tome zašto Excel odbija šifrovanu radnu svesku s ispravnom lozinkom. Ako primač ume da čita XLSX, koristite XLSX engine: TXLSXWorkbook.SaveAsEncryptedAgile piše Agile Encryption (SHA-512 heširanje lozinke sa spin count od 100.000 iteracija i AES-256-CBC), format koji Excel 2010 i kasnije pišu podrazumevano, dok SaveAsEncrypted piše stariji AES-128 Standard Encryption. Kompromisi između ta dva su u tekstu o šifrovanju XLSX fajlova s AES-om u Delphiju, a strana čitanja pokrivena je u tekstu o čitanju Agile-šifrovanih Excel fajlova s HotXLS-om

Brzi podsetnik: BIFF XOR obfuskacija koju Excel 16 prihvata

  • FILEPASS ($002F) ide iza globals BOF-a; u BIFF5 njegovo telo je 4 bajta: ključ, pa verifier
  • Ključ = CreateXorKey_Method1(password) po [MS-OFFCRYPTO] §2.3.7.2, nikad slučajan; za secret je $014D
  • Niz = bajtovi lozinke + pad, XOR s nižim bajtom ključa na parnim pozicijama i višim na neparnim, pa XorRor (rotacija u desno 1)
  • Šifruj bajt: rotacija u levo 5, pa XOR; dešifruj: XOR, pa rotacija u desno 5 (§2.3.7.3)
  • XorArrayIndex = (ofset bajta u toku + dužina podataka zapisa) mod 16; zaglavlja i čisti prefiksi računaju se u ofset
  • BOUNDSHEET drži prvih 4 bajta čistim; BOF, FILEPASS i INTERFACEHDR ostaju sasvim čisti
  • Lozinke: ASCII, najviše 15 znakova
  • HotXLS: indeks ispravljen u v2.384.47, ključ i XorRor u v2.384.54, stariji HotXLS XOR fajlovi se prepoznaju po nepoklapanju ključa
  • Za pravu zaštitu koristite bar BIFF8 RC4 CryptoAPI, ili XLSX Agile Encryption

HotXLS pokriva BIFF5 i BIFF8 zaštitu lozinkom, XLSX Standard i Agile Encryption i callback za lozinku na strani čitanja iz jedne Delphi i C++Builder biblioteke, s interoperabilnim detaljima iz gore rešenim umesto vas. Pogledajte HotXLS Delphi spreadsheet component za izdanja, platforme i probnu verziju