Tehnični članak

HotXLS XLS XOR obfuskacija: izpelja ključa in XorRor

HotXLS zapiše delovne zvezke Excel 5.0/95 (BIFF5), obfuskirane z XOR, ki jih Excel 16 odpre samo, kadar se tri podrobnosti točno ujemajo s [MS-OFFCRYPTO]: ključ FILEPASS mora biti CreateXorKey_Method1(password), 16-bajtna matrika XOR mora biti zgrajena z XorRor (zasuk v desno za en bit), vsak bajt pa mora uporabiti XorArrayIndex = (odmik toka + dolžina zapisa) mod 16. HotXLS je indeks uredil v v2.384.47, ključ in zasuk pa v v2.384.54. Pred tem se je vsaka geslom zaščitena datoteka BIFF5, ki jo je izdelal, v HotXLS odprla brez težav, v Excelu pa spodletela

Zadnji stavek je cela zgodba v malem. Bralnik in zapisovalnik, ki si delita isto napačno predstavo, se popolnoma strinjata med sabo, tako da testi povratnega pretvarjanja ostanejo zeleni, medtem ko edini potrošnik, ki šteje, pravi ne. Excel 16 je dvakrat rekel ne, z dvema različnima sporočiloma, in vsako sporočilo je kazalo na drugo plast sheme. Ta članek gre skozi te plasti v vrstnem redu, v katerem jih preverja Excel, s podrobnostmi na ravni bajtov, uporabnimi tako, kadar kličete HotXLS, kot kadar pišete svoj lasten bralnik BIFF

Kaj sploh shrani BIFF XOR obfuskacija?

BIFF XOR obfuskacija shrani v datoteko samo dve 16-bitni besedi, vse ostalo pa se izračuna znova iz gesla. Zapis FILEPASS ($002F) stoji takoj za globals BOF delovnega zvezka, v datoteki BIFF5 pa je njegovo telo točno 4 bajti: ključ XOR, mu sledi preverjevalnik gesla. Ni soli, ni identifikatorja algoritma in ni šifrirane oblokinke preverjevalnika, kakršno nosita shemi RC4 in AES

Iz teh dveh besed bralnik znova zgradi tri stvari:

  • Preverjevalnik, 16-bitni hash bajtov gesla, združen z XOR in $CE4B. Primerjava s shranjeno besedo je preverjanje gesla, in edino
  • Ključ XOR, 16-bitna vrednost iz CreateXorKey_Method1 v [MS-OFFCRYPTO] §2.3.7.2, gnana z dvema tabelama konstant (InitialCode, 15 besed, in XorMatrix, 105 besed)
  • Matrika XOR, 16 bajtov: bajti gesla, dopolnjeni s fiksnim 16-bajtnim polnilom, vsak združen z XOR z nižjim bajtom ključa (soda mesta) ali višjim bajtom ključa (liha mesta), nato zasukanih v desno za en bit

Glave zapisov ostanejo v čistem besedilu, prav tako peščica celih zapisov, ki jih shema izjema, med njimi BOF, FILEPASS in INTERFACEHDR. Vsako drugo telo zapisa se preoblikuje bajt za bajtom: zasuk v levo za 5 bitov, nato XOR z enim vnosom 16-bajtne matrike. Dešifriranje, ki ga [MS-OFFCRYPTO] §2.3.7.3 zapiše kot DecryptData_Method1, je zrcalna slika: najprej XOR, nato zasuk v desno za 5

V HotXLS tega vsega nikoli ne dotaknete neposredno. Nastavite geslo, izberete format, SaveAs pa izda FILEPASS in preoblikuje 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 podpira samo XOR obfuskacijo; xletAuto bi jo izbral tudi sam
  Wb.EncryptionType := xletXor;
  // Naj bo ASCII in največ 15 znakov (glej spodaj)
  Wb.EncryptionPassword := 'secret';

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

Zakaj Excel pravi, da je geslo napačno, čeprav se preverjevalnik ujema?

Excel zavrne geslo, ker ne zaupa shranjenemu ključu: Excel izpelje ključ iz vtipičanega gesla s CreateXorKey_Method1 in ga primerja s ključno besedo FILEPASS, zato datoteka, katere ključ je karkoli drugega, padne pri preverjanju gesla, tudi če je preverjevalnik pravilen. Specifikacija opisuje ključ kot izhod gesla, ne kot prosti parameter, Excel 16 pa to branje vsiljuje

Zapisovalnik HotXLS pred v2.384.54 je ključno besedo zapolnil z dvema naključnima bajtoma. Na papirju izgleda to neškodljivo, saj je preverjevalnik dokumentirano preverjanje gesla, matrika pa je zgrajena iz kakršnega koli ključa, ki ga datoteka naznanja. HotXLS sam je te datoteke bral brez težav, ker je njegov bralnik ključ iz FILEPASS vzel kot dano. Excel 16, ki je dobil isto datoteko in pravilno geslo, je odgovoril, da geslo ni pravilno. Od v2.384.54 je ključ izpeljan, zato FILEPASS za geslo secret vedno nosi ključ $014D in preverjevalnik $DAA7, vrednosti, preverjene še z neodvisno implementacijo specifikacije

HotXLS diagram zapisa BIFF5 XOR FILEPASS, ki ga Excel 16 preveri ob odpiranju: čisto štiribajtno telo nosi ključ XOR in preverjevalnik gesla, Excel izpelje ključ iz vtipičanega gesla s CreateXorKey_Method1 in zavrne naključni ključ z napako gesla, tudi kadar se preverjevalnik ujema; HotXLS shrani izpeljani ključ 014D za secret
Telo FILEPASS sta le dve besedi, Excel pa ključ iz vašega gesla izpelje znova in primerja; naključno zapolnjen ključ pade pri preverjanju tudi s pravilnim preverjevalnikom, zato ga HotXLS od v2.384.54 izpeljuje

Sama izpelja je kratka, ko sta tabeli na mestu. Sprehodite se po geslu od zadaj, sedemkrat poglejte bit 6 vsakega bajta, medtem ko ga pomikate v levo, vsakič, ko je bit postavljen, pa dodajte z XOR enega vnosa XorMatrix. Naslednje je načelna skica, ki ponovi algoritem specifikacije in se ujema z implementacijo HotXLS; ni pa API HotXLS:

// Načelna skica [MS-OFFCRYPTO] 2.3.7.2 CreateXorKey_Method1
// in CreateXorArray_Method1 (samo ilustracija, ni 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;                       // ključ vidi samo 15 bajtov
  if Len = 0 then
    Exit;
  Result := InitialCode[Len - 1];
  Element := $68;                    // zadnji vnos 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));   // zasuk v desno za en 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 to da matriko 1F 32 17 B9 14 BA 7B 7F 59 DD 59 7F 7A C0 A6 DF, priročen testni primer, če preizkušate svoj bralnik

HotXLS diagram izgradnje matrike XOR za BIFF5 obfuskacijo: šestnajst bajtov, sejan iz gesla in fiksne oblokinje, je z XOR združen z nižjim bajtom ključa na sodih mestih in višjim na lihih, nato zasukanih v desno za en bit z XorRor, kar da fiksni primer 1F 32 17 B9 za geslo secret
Bajti matrike pridejo iz gesla, polnila in dveh bajtov ključa, en zasuk na koncu; zasuk v levo za 2 je deloval samo, ko se je preoblikovanje bajtov vleklo v obratnem vrstnem redu, Excel pa sledi vrstnemu redu specifikacije

Zakaj pravi ključ še vedno da poškodovano datoteko?

Pravilen ključ še vedno da poškodovano datoteko, kadar je matrika XOR zasukana v napačno smer: [MS-OFFCRYPTO] definira korak matrike kot XorRor, zasuk v desno za en bit, matrika zasukana v levo za dva pa dešifrira vsako telo zapisa v šum. Urejen ključ je pognal Excel 16 čez poziv za geslo in naravnost v drugo napako, poročilo, da ima datoteka težavo in je ni mogoče odpreti

Stara koda HotXLS je vsak bajt matrike zasukala v levo za 2 bita, oblika, ki kroži v nekaj implementacijah BIFF. Ker je HotXLS uporabil isti zasuk na obeh straneh, njegov lasten bralnik nikoli ni opazil. Excel 5.0/95 datotek Excel 16 ne zna več shranjevati, pri shranjevanju BIFF8 pa ne ponudi XOR, zato ni bilo izvornega vzorca Excela za primerjavo. Dokazi so morali priti iz druge smeri: zapišite en čist tok BIFF5, znova ga kodirajte na osem načinov in pustite Excelu 16, da odpre vsako različico. Osem različic je prečkalo tri neodvisne izbire:

IzbiraMožnost AMožnost B
Zasuk matrikeXorRor (zasuk v desno 1)Zasuk v levo 2
Indeks matrike(odmik + dolžina zapisa) mod 16odmik mod 16
Vrstni red preoblikovanja bajtovZasuk v levo 5, nato XORXOR, nato zasuk v levo 5

Excel 16 je od osmih odprl točno dve: XorRor z zasukom pred XOR in indeksom dolžine zapisa ter eno različico, ki je videti samo drugačna. Zasuk-v-levo-2 z XOR-pred-zasukom in istim indeksom je ista funkcija v preobleki. Zasuk se razporedi čez XOR, zato je rol5(p xor rol2(b)) enako rol5(p) xor rol7(b), na 8-bitni vrednosti pa je zasuk v levo za 7 zasuk v desno za 1. Na kratko, rol5 ∘ rol2 = ror1, zato matrika z zasukom v levo za 2 v izolaciji izgleda verjetna: pravilna je samo skupaj z obratnim vrstnim redom preoblikovanja. V paru z vrstnim redom specifikacije pokvari vsak preoblikovani bajt

Isti poskus je poravnil še drugo vprašanje. Različice, ki so iz indeksa izpustile dolžino zapisa, so vse spodletele, kar je potrdilo pravilo indeksa, ki ga je HotXLS sprejel eno izdajo prej samo na podlagi besedila specifikacije

Kako se izračuna XorArrayIndex za vsak bajt?

XorArrayIndex za bajt je njegov odmik v toku delovnega zvezka plus dolžina celotnih podatkov zapisa, ki mu bajt pripada, mod 16. Indeks se zato pri vsakem zapisu ponastavi na vrednost, odvisno od zapisa, znotraj njega pa naraste za ena na bajt. Psevdokoda specifikacije vhoda poimenuje FileOffset in Data.Length, kar zlahka preberete napačno kot samo odmik začetka zapisa, ta napačna interpretacija pa je točno to, kar je HotXLS izdal do v2.384.47

Tri podrobnosti odločajo, ali se vaši indeksi izrovnajo z Excelom:

  • 4-bajtna glava zapisa se nikoli ne preoblikuje, a vseeno zaseda položaje v toku, zato prvi bajt telesa zapisa stoji pri odmiku glave + 4
  • Člen dolžine je polna dolžina podatkov zapisa, ne število dejansko preoblikovanih bajtov
  • BOUNDSHEET je deloma čist: njegovih prvih 4 bajtov, lbPlyPos, odmik toka lista BOF, ostane berljiv, da razčlenjevalnik najde liste. Tistih 4 bajtov preoblikovanje preskoči, štejejo pa še vedno tako v odmik kot v dolžino zapisa
HotXLS diagram pravila XorArrayIndex za BIFF5 XOR obfuskacijo: vsak bajt telesa uporablja odmik toka plus polno dolžino podatkov zapisa modulo 16, čista štiribajtna glava in predpona BOUNDSHEET lbPlyPos še vedno štejeta v odmik, ignoriranje člena dolžine zapisa pa je bil defekt, ki ga je HotXLS uredil v v2.384.47
Indeks matrike se ponastavi enkrat na zapis, ne enkrat na tok: glava in morebitna čista predpona zasedajo položaje, dolžina podatkov zapisa napaja modulo, obe polovici starega HotXLS pa sta se strinjali z napačno formulo

Skupaj vzeto, je preoblikovanje na zapis nekaj vrstic. Še enkrat, to je skica pravila, ničesar, kar bi morali klicati:

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

// Obfuskira eno telo zapisa na mestu. BodyPos je odmik toka
// od Body[0], torej odmik glave zapisa + 4. PlainPrefix je 4 za
// BOUNDSHEET, polna dolžina za BOF / FILEPASS, 0 za večino zapisov
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;

// Branje je zrcalna slika: B := Body[I] xor Arr[...];
// nato zasuk v desno za 5, torej Rol8(B, 3)

Pred v2.384.47 je bralnik HotXLS izračunal indeks samo iz položaja v toku, zapisovalnik pa uporabil čisti bajtni odmik. Oba sta ignorirala dolžino zapisa, zato sta se spet strinjali polovici med sabo in nihče drug. Neodvisno napisan dekodirnik je izhod v2.384.47 prebral pravilno, starejši izhod pa kot smeti, osemrazličični test v Excelu 16 pa je pravilo pozneje potrdil proti pravemu cilju

Kaj se zgodi z datotekami XOR, zapisanimi s starejšimi različicami HotXLS?

HotXLS še naprej bere svoje lastne datoteke XOR iz časov pred v2.384.54 tako, da preveri ključ FILEPASS: kadar se shranjeni ključ ujema s ključem, izpeljanim iz gesla, bralnik zgradi matriko XorRor po specifikaciji, kadar pa se razlikuje, datoteko obravnava kot starejšo datoteko HotXLS in znova zgradi matriko z zasukom v levo za 2. Datoteke, zapisane z Excelom, vedno nosijo izpeljani ključ, zato vedno gredo po poti specifikacije

Test je hevristika z natančno stopnjo spodletelosti. Stara datoteka, katere naključni ključ je po naključju ravno enak izpeljanemu ključu, bi se prebrala z napačno matriko, možnost za to pa je 1 od 65.536. Rezervna pot pokrije samo zasuk matrike; pravilo indeksa se ne preklopi, zato so datoteke, ki jih reši, tiste, zapisane med v2.384.47 in v2.384.53. Če še hranite datoteke BIFF5 XOR iz tega okna, jih odprite s trenutnim HotXLS in shranite znova, da dobite datoteko, ki jo Excel sprejme

Dve podrobnosti o geslih veljata za vsako datoteko, staro ali novo:

  • Dolžina. CreateXorKey_Method1 prebere samo prvih 15 bajtov gesla, kar je omejitev specifikacije. HotXLS ta strop uporabi na ključ, preverjevalnik in matriko pa pusti na njunih običajnih pravilih polne dolžine in 16 bajtov, dosledno na obeh straneh. Excel sam zavrača gesla, daljša od 15 znakov, pri tem formatu, zato 15 obravnavajte kot pravo najvišjo mejo
  • Znakovni nabor. HotXLS pretvori geslo v bajte prek sistemske kodne strani ANSI. Specifikacija opisuje jemanje nizkega bajta vsakega znaka UTF-16, kar se pri ASCII ujema. Brez vzorcev Excela, zaščitenih z ne-ASCII gesli, za ostalo ni prave resnice, zato pri datotekah XOR ostanite pri geslih ASCII

Na strani branja vam TXLSWorkbook.OnPassword omogoča poziv za geslo, kadar Open naleti na zapis FILEPASS. Dogodek je TXLSPasswordEvent s var PassWord: WideString in var Retry: Boolean; nastavite Retry na True, da poskusite znova, do tri ponovitve:

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: geslo je zahtevano, a ni bilo podano; -1005: napačno geslo
  if Rc <> 1 then
    raise Exception.CreateFmt('Cannot open %s (code %d)', [FileName, Rc]);
  ShowMessage(VarToStr(Wb.Sheets[1].Range['A1', 'A1'].Value));
end;

Če geslo že poznate, Open(FileName, APassWord) dogodka sploh ne sproži

Ali je XOR obfuskacija za kaj sploh dovolj varna?

BIFF XOR obfuskacija ni šifriranje in ne ščiti nič pred motiviranim bralcem. Preverjanje gesla je 16-bitni preverjevalnik, ključ ima 16 bitov, 16-bajtna matrika pa se ponavlja po celem toku, tako da vnaprej uganjiva vsebina zapisov BIFF izpostavi bajte matrike brez vsakršnega gesla. HotXLS zapiše XOR samo zato, ker datoteke Excel 5.0/95 nimajo druge možnosti, razlog za izdelavo takih datotek danes pa je zastareli potrošnik, ki ne zna brati ničesar novejšega

Klasični pogon izbere shemo prek TXLSWorkbook.EncryptionType, kombinacija z granilnim formatom pa je strogo preverjena:

  • xletAuto (privzeto) zapiše RC4 CryptoAPI za xlExcel97 in XOR za xlExcel5, v skladu s tem, kar je Excel sam zapisal za vsak format
  • xletXor velja samo za BIFF5; pri xlExcel97 shranjevanje sproži izjemo in ne tihe rezerve
  • xletRC4 in xletRC4CryptoAPI sta samo za BIFF8, zahteva po njiju pri shranjevanju BIFF5 pa prav tako sproži izjemo

RC4 je tudi sam zastarel, podrobnosti o medsebojni združljivosti pa so pokrite v zakaj Excel zavrne šifriran delovni zvezek s pravilnim geslom. Če prejemnik zna brati XLSX, uporabite raje pogon XLSX: TXLSXWorkbook.SaveAsEncryptedAgile zapiše Agile Encryption (hashiranje gesla SHA-512 s števcem vrtenja 100.000 ponovitev in AES-256-CBC), format, ki ga Excel 2010 in novejši zapisuje privzeto, SaveAsEncrypted pa starejše AES-128 Standard Encryption. Izmenjava kompromisov med obema je v šifriranju datotek XLSX z AES v Delphiju, stran branja pa je pokrita v brisanju z Agile šifriranih datotek Excel s HotXLS

Hiter pregled: BIFF XOR obfuskacija, ki jo Excel 16 sprejme

  • FILEPASS ($002F) sledi globals BOF; v BIFF5 je njegovo telo 4 bajti: ključ, nato preverjevalnik
  • Ključ = CreateXorKey_Method1(password) po [MS-OFFCRYPTO] §2.3.7.2, nikoli naključen; za secret je $014D
  • Matrika = bajti gesla + polnilo, XOR z nižjim bajtom ključa na sodih mestih in višjim na lihih, nato XorRor (zasuk v desno 1)
  • Šifriranje bajta: zasuk v levo 5, nato XOR; dešifriranje: XOR, nato zasuk v desno 5 (§2.3.7.3)
  • XorArrayIndex = (bajtni odmik toka + dolžina podatkov zapisa) mod 16; glave in čiste predpone štejejo v odmik
  • BOUNDSHEET obdrži svojih prvih 4 bajtov čistih; BOF, FILEPASS in INTERFACEHDR ostanejo povsem čisti
  • Gesla: ASCII, največ 15 znakov
  • HotXLS: indeks popravljen v v2.384.47, ključ in XorRor v v2.384.54, starejše datoteke XOR HotXLS pa so prepoznane po neujemanju ključa
  • Za pravo zaščito uporabite vsaj BIFF8 RC4 CryptoAPI ali XLSX Agile Encryption

HotXLS iz ene knjižnice za Delphi in C++Builder opravi zaščito z geslom BIFF5 in BIFF8, XLSX Standard in Agile Encryption ter povratni klic gesla na strani branja, zgoraj navedene podrobnosti združljivosti pa so opravljene za vas. Za izdaje, platforme in preizkusni prenos glej komponento HotXLS Delphi preglednic