Odborný článok

XOR obfuskácia XLS v HotXLS: derivácia kľúča a XorRor

HotXLS zapisuje XOR obfuskované zošity Excel 5.0/95 (BIFF5), ktoré Excel 16 otvorí len vtedy, keď tri detaily sedia s [MS-OFFCRYPTO] presne na bajt: kľúč FILEPASS musí byť CreateXorKey_Method1(password), 16-bajtové XOR pole sa musí stavať cez XorRor (rotácia doprava o jeden bit) a každý bajt musí používať XorArrayIndex = (stream offset + record length) mod 16. HotXLS mal index správne od v2.384.47 a kľúč aj rotáciu od v2.384.54. Predtým sa každý heslom chránený BIFF5 súbor, ktorý vyprodukoval, v HotXLS otváral normálne a v Exceli zlyhal

Tá posledná veta je celý príbeh v miniatúre. Čítačka a zapisovač, ktoré zdieľajú tú istú zlú predstavu, sa navzájom potvrdia dokonale, takže round-trip testy zostávajú zelené, kým jediný konzument, ktorý sa počíta, hovorí nie. Excel 16 povedal nie dvakrát, s dvomi rôznymi hláškami, a každá hláška ukazovala na inú vrstvu schémy. Tento článok prechádza tie vrstvy v poradí, v akom ich Excel kontroluje, s detailmi na úrovni bajtov, ktoré využijete, či už voláte HotXLS, alebo píšete vlastnú BIFF čítačku

Čo BIFF XOR obfuskácia v súbore vlastne uchováva?

BIFF XOR obfuskácia uchováva v súbore len dve 16-bitové slová a všetko ostatné sa prepočíta z hesla. Záznam FILEPASS ($002F) sedí hneď za workbook globals BOF a v súbore BIFF5 má jeho telo presne 4 bajty: XOR kľúč nasledovaný verifierom hesla. Žiadny salt, žiadny identifikátor algoritmu a žiadna zašifrovaná verifier blob, aké nesú schémy RC4 a AES

Z týchto dvoch slov si čítačka znovu postaví tri veci:

  • Verifier, 16-bitový hash bajtov hesla XORovaný s $CE4B. Porovnanie s uloženým slovom je kontrola hesla a jediná, ktorá tu je
  • XOR kľúč, 16-bitová hodnota z CreateXorKey_Method1 v [MS-OFFCRYPTO] §2.3.7.2, poháňaná dvomi konštantnými tabuľkami (InitialCode, 15 slov, a XorMatrix, 105 slov)
  • XOR pole, 16 bajtov zložených z bajtov hesla doplnených fixným 16-bajtovým padom, každý XORovaný s nižším bajtom kľúča (párne pozície) alebo vyšším bajtom kľúča (nepárne pozície) a potom rotovaný doprava o jeden bit

Hlavičky záznamov ostávajú v čistom texte a rovnako aj hrsť celých záznamov, ktoré schéma vynecháva, medzi nimi BOF, FILEPASS a INTERFACEHDR. Telo každého ďalšieho záznamu sa transformuje bajt po bajte: rotácia doľava o 5 bitov, potom XOR s jednou položkou 16-bajtového poľa. Dešifrovanie, ktoré [MS-OFFCRYPTO] §2.3.7.3 popisuje ako DecryptData_Method1, je zrkadlový obraz: najprv XOR, potom rotácia doprava o 5

V HotXLS sa ničoho z toho priamo nedotknete. Nastavte heslo, zvoľte formát a SaveAs emituje FILEPASS a transformuje stream:

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 podporuje len XOR obfuskáciu; xletAuto by ju tiež zvolil
  Wb.EncryptionType := xletXor;
  // Držte ho v ASCII a maximálne 15 znakov (viac nižšie)
  Wb.EncryptionPassword := 'secret';

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

Prečo Excel hovorí, že heslo je zlé, keď verifier sedí?

Excel odmietne heslo, pretože neverí uloženému kľúču: kľúč si odvodí z napísaného hesla cez CreateXorKey_Method1 a porovná ho s kľúčovým slovom v FILEPASS, takže súbor s akýmkoľvek iným kľúčom neprejde kontrolou hesla, aj keď verifier je správny. Špecifikácia popisuje kľúč ako výstup hesla, nie ako voľný parameter, a Excel 16 toto čítanie vynucuje

Zapisovač HotXLS pred v2.384.54 naplnil kľúčové slovo dvomi náhodnými bajtmi. Na papieri to vyzerá neškodne, keďže verifier je zdokumentovaná kontrola hesla a pole sa stavia z kľúča, ktorý súbor deklaruje. HotXLS sám také súbory čítal bez problémov, lebo jeho čítačka brala kľúč z FILEPASS ako daný. Excel 16, ktorému ste podali ten istý súbor a správne heslo, odpovedal, že heslo nie je správne. Od v2.384.54 sa kľúč odvodzuje, takže FILEPASS pre heslo secret vždy drží kľúč $014D a verifier $DAA7, hodnoty skontrolované proti nezávislej implementácii špecifikácie

Diagram HotXLS záznamu BIFF5 XOR FILEPASS, ktorý Excel 16 kontroluje pri otvorení: čisté štyribajtové telo drží XOR kľúč a verifier hesla, Excel si odvodí kľúč z napísaného hesla cez CreateXorKey_Method1 a náhodný kľúč odmietne chybou hesla, aj keď verifier sedí; HotXLS ukladá odvodený kľúč 014D pre secret
Telo FILEPASS má len dve slová, ale Excel si kľúč odvodí z vášho hesla nanovo a porovná; kľúč vyplnený náhodne kontrolou neprejde aj so správnym verifierom, preto ho HotXLS odvádza od v2.384.54

Samotná derivácia je krátka, akonáhle máte obe tabuľky na mieste. Prechádzajte heslo odzadu, sedemkrát sa pozrite na bit 6 každého bajtu, zatiaľ čo ho posúvate doľava, a XORujte do neho jednu položku XorMatrix vždy, keď je bit nastavený. Nasleduje náčrt princípu, ktorý reprodukuje algoritmus zo špecifikácie a sedí s implementáciou HotXLS; nie je to API HotXLS:

// Náčrt princípu [MS-OFFCRYPTO] 2.3.7.2 CreateXorKey_Method1
// a CreateXorArray_Method1 (len ilustrácia, nie 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;                       // kľúč vidí len 15 bajtov
  if Len = 0 then
    Exit;
  Result := InitialCode[Len - 1];
  Element := $68;                    // posledná položka 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));   // rotácia doprava o jeden 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;

Pre secret to vyprodukuje pole 1F 32 17 B9 14 BA 7B 7F 59 DD 59 7F 7A C0 A6 DF, čo je šikovná fixtúra, ak testujete vlastnú čítačku

Diagram HotXLS stavby XOR poľa pre obfuskáciu BIFF5: šestnásť bajtov vysiatych z hesla a fixného padu sa XORuje s nižším bajtom kľúča na párnych pozíciách a vyšším na nepárnych, potom sa rotuje doprava o jeden bit cez XorRor, čo dáva fixtúru 1F 32 17 B9 pre heslo secret
Bajty poľa prichádzajú z hesla, padu a dvoch bajtov kľúča, jedna rotácia na konci; rotácia doľava o 2 fungovala len vtedy, keď bajtová transformácia bežala v opačnom poradí, a Excel nasleduje poradie zo špecifikácie

Prečo aj správny kľúč dáva poškodený súbor?

Správny kľúč stále dáva poškodený súbor, keď je XOR pole rotované nesprávnym smerom: [MS-OFFCRYPTO] definuje krok poľa ako XorRor, rotáciu doprava o jeden bit, a pole rotované doľava o dva dešifruje telo každého záznamu na šum. Oprava kľúča posunula Excel 16 cez výzvu na heslo priamo k inej chybe, hláseniu, že súbor má problém a nedá sa otvoriť

Starý kód HotXLS rotuje každý bajt poľa doľava o 2 bity, forma, ktorá koluje v niekoľkých BIFF implementáciách. Keďže HotXLS používal tú istú rotáciu na oboch stranách, jeho vlastná čítačka si toho nikdy nevšimla. Excel 16 už nevie ukladať súbory Excel 5.0/95 a pri ukladaní BIFF8 XOR nenabídne, takže nebol žiadny natívny Excel vzor na diff. Dôkaz musel prísť z opačnej strany: napísať jeden čistý BIFF5 stream, prekódovať ho ôsmimi spôsobmi a nechať Excel 16 otvoriť každú variantu. Osem variánt prekrížilo tri nezávislé voľby:

VoľbaMožnosť AMožnosť B
Rotácia poľaXorRor (rotácia doprava o 1)Rotácia doľava o 2
Index poľa(offset + record length) mod 16offset mod 16
Poradie bajtovej transformácieRotácia doľava o 5, potom XORXOR, potom rotácia doľava o 5

Excel 16 otvoril presne dve z ôsmich: XorRor s poradím najprv rotovať, potom XOR a s indexom nesúcim dĺžku záznamu, a jednu variantu, ktorá vyzerá len inak. Rotácia-doľava-2 s XOR-potom-rotuj a tým istým indexom je tá istá funkcia v prestrojení. Rotácia sa distribuuje cez XOR, takže rol5(p xor rol2(b)) sa rovná rol5(p) xor rol7(b) a na 8-bitovej hodnote je rotácia doľava o 7 rotáciou doprava o 1. Stručne, rol5 ∘ rol2 = ror1, a preto pôsobí pole s rotáciou doľava o 2 v izolácii vierohodne: je správne len spolu s opačným poradím transformácie. Spárované s poradím zo špecifikácie pokazí každý transformovaný bajt

Ten istý experiment vyriešil aj druhú otázku. Varianty, ktoré vypustili z indexu dĺžku záznamu, všetky zlyhali, čo potvrdilo pravidlo indexu, ktoré si HotXLS prijal vydanie skôr len na základe textu špecifikácie

Ako sa pre každý bajt počíta XorArrayIndex?

XorArrayIndex bajtu je jeho offset v streami zošita plus dĺžka celých dát záznamu, do ktorého bajt patrí, všetko mod 16. Index sa preto pre každý záznam reštartuje na hodnotu závislú od záznamu a vo vnútri zvyšuje o jeden na bajt. Pseudokód špecifikácie menuje vstupy FileOffset a Data.Length, čo sa ľahko prečíta ako samotný počiatočný offset záznamu, a práve toto zlé čítanie posielal HotXLS až do v2.384.47

O tom, či vaše indexy sedia s Excelom, rozhodujú tri detaily:

  • 4-bajtová hlavička záznamu sa nikdy netransformuje, ale aj tak zaberá pozície v streame, takže prvý telový bajt záznamu sedí na offset hlavičky + 4
  • Člen dĺžky je plná dĺžka dát záznamu, nie počet bajtov skutočne transformovaných
  • BOUNDSHEET je čiastočne čistý: jeho prvé 4 bajty, lbPlyPos, stream offset BOF hárka, ostávajú čitateľné, aby parser vedel hárky nájsť. Tých 4 bajtov transformácia preskočí, ale aj tak sa počítajú do offsetu aj do dĺžky záznamu
Diagram HotXLS pravidla XorArrayIndex pre XOR obfuskáciu BIFF5: každý telový bajt používa stream offset plus plnú dĺžku dát záznamu modulo 16, čistá štyribajtová hlavička aj prefix BOUNDSHEET lbPlyPos sa počítajú do offsetu a ignorovanie člena dĺžky záznamu bola chyba, ktorú HotXLS opravil vo v2.384.47
Index poľa sa reštartuje raz na záznam, nie raz na stream: hlavička a každý čistý prefix zaberajú pozície, dĺžka dát záznamu živí modulo a obe polovice starého HotXLS sa zhodli na zlej funkcii

Zložená dohromady, transformácia na záznam je pár riadkov. Aj toto je náčrt pravidla, nie niečo, čo musíte volať:

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

// Obfuskuje telo jedného záznamu na mieste. BodyPos je stream offset
// Body[0], teda offset hlavičky záznamu + 4. PlainPrefix je 4 pre
// BOUNDSHEET, plná dĺžka pre BOF / FILEPASS, 0 pre väčšinu záznamov
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;

// Čítanie je zrkadlový obraz: B := Body[I] xor Arr[...];
// potom rotácia doprava o 5, teda Rol8(B, 3)

Pred v2.384.47 počítala čítačka HotXLS index len zo stream pozície a zapisovač použil čistý bajtový offset. Obe polovice ignorovali dĺžku záznamu, takže sa opäť zhodli navzájom a s nikým iným. Nezávisle napísaný dekodér čítal výstup v2.384.47 správne a starší výstup ako odpad a test ôsmich variánt v Exceli 16 neskôr potvrdil pravidlo proti skutočnému cieľu

Čo sa deje so súbormi XOR napísanými staršími verziami HotXLS?

HotXLS naďalej číta vlastné XOR súbory z čias pred v2.384.54 kontrolou kľúča FILEPASS: keď sa uložený kľúč rovná kľúču odvodenému z hesla, čítačka stavia XorRor pole zo špecifikácie, a keď sa líši, považuje súbor za starší súbor HotXLS a znovu postaví pole s rotáciou doľava o 2. Súbory písané Excelom vždy nesú odvodený kľúč, takže vždy idú cestou špecifikácie

Ten test je heuristika s presnou mierou zlyhania. Starý súbor, ktorého náhodný kľúč sa práve rovnal odvodenému kľúču, by sa čítal so zlým poľom a šanca na to je 1 zo 65 536. Fallback pokrýva len rotáciu poľa; pravidlo indexu sa neprepína, takže zachránené súbory sú tie, ktoré vznikli medzi v2.384.47 a v2.384.53. Ak ešte držíte BIFF5 XOR súbory z tohto okna, otvorte ich v aktuálnom HotXLS a uložte znova, aby ste dostali súbor, ktorý Excel prijme

Dva detaily hesla platia pre každý súbor, starý aj nový:

  • Dĺžka. CreateXorKey_Method1 číta len prvých 15 bajtov hesla, čo je limit zo špecifikácie. HotXLS aplikuje ten strop na kľúč a verifier aj pole drží na svojich obvyklých pravidlách plnej dĺžky a 16 bajtov, dôsledne na oboch stranách. Excel sám odmieta heslá dlhšie ako 15 znakov pre tento formát, takže 15 berte ako skutočné maximum
  • Znaková sada. HotXLS prevádza heslo na bajty cez systémovú ANSI kódovú stránku. Špecifikácia popisuje branie nižšieho bajtu každého znaku UTF-16, čo sa pre ASCII zhoduje. Bez Excelových vzorov chránených ne-ASCII heslami neexistuje pre ostatné znaky pravda vyššieho rázu, takže pri XOR súboroch zostaňte pri ASCII heslách

Na čítacej strane vám TXLSWorkbook.OnPassword umožní vyzvať na heslo, keď Open narazí na záznam FILEPASS. Udalosť je TXLSPasswordEvent s var PassWord: WideString a var Retry: Boolean; nastavte Retry na True, aby ste to skúsili znova, najviac tri pokusy:

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: heslo je potrebné, ale žiadne nebolo dodané; -1005: zlé heslo
  if Rc <> 1 then
    raise Exception.CreateFmt('Cannot open %s (code %d)', [FileName, Rc]);
  ShowMessage(VarToStr(Wb.Sheets[1].Range['A1', 'A1'].Value));
end;

Ak heslo už poznáte, Open(FileName, APassWord) udalosť celkom preskočí

Je XOR obfuskácia aspoň na niečo dostatočne bezpečná?

BIFF XOR obfuskácia nie je šifrovanie a proti motivovanému čitateľovi nechráni nič. Kontrola hesla je 16-bitový verifier, kľúč má 16 bitov a 16-bajtové pole sa opakuje po celom streame, takže predvídateľný obsah BIFF záznamov vystaví bajty poľa bez akéhokoľvek hesla. HotXLS zapisuje XOR len preto, lebo súbory Excel 5.0/95 nemajú inú možnosť, a dôvodom produkovať také súbory dnes je legacy konzument, ktorý nečíta nič novšie

Klasický engine volí schému cez TXLSWorkbook.EncryptionType a kombinácia s ukladacím formátom sa kontroluje prísne:

  • xletAuto (predvolené) zapisuje RC4 CryptoAPI pre xlExcel97 a XOR pre xlExcel5, presne to, čo Excel sám zapisoval pre každý formát
  • xletXor je platný len pre BIFF5; pri xlExcel97 uloženie vyhodí výnimku namiesto tichého fallbacku
  • xletRC4 a xletRC4CryptoAPI sú len BIFF8 a ich vyžiadanie pri ukladaní BIFF5 tiež vyhodí výnimku

RC4 má svoje roky a detaily interoperability popisuje článok o tom, prečo Excel odmietne zašifrovaný zošit so správnym heslom. Ak príjemca vie čítať XLSX, použite radšej engine XLSX: TXLSXWorkbook.SaveAsEncryptedAgile zapisuje Agile Encryption (hashovanie hesla SHA-512 so spin countom 100 000 iterácií a AES-256-CBC), formát, ktorý Excel 2010 a novšie zapisujú predvolene, zatiaľ čo SaveAsEncrypted zapisuje staršie AES-128 Standard Encryption. Rozvahy medzi oboma sú v článku o šifrovaní XLSX súborov s AES v Delphi a čítacia strana je pokrytá v článku o čítaní Agile zašifrovaných Excel súborov s HotXLS

Rýchly prehľad: XOR obfuskácia BIFF, ktorú Excel 16 prijme

  • FILEPASS ($002F) nasleduje za globals BOF; v BIFF5 má telo 4 bajty: kľúč, potom verifier
  • Kľúč = CreateXorKey_Method1(password) podľa [MS-OFFCRYPTO] §2.3.7.2, nikdy náhodný; pre secret je to $014D
  • Pole = bajty hesla + pad, XOR s nižším bajtom kľúča na párnych pozíciách a vyšším na nepárnych, potom XorRor (rotácia doprava o 1)
  • Zašifrovanie bajtu: rotácia doľava o 5, potom XOR; dešifrovanie: XOR, potom rotácia doprava o 5 (§2.3.7.3)
  • XorArrayIndex = (stream offset bajtu + dĺžka dát záznamu) mod 16; hlavičky a čisté prefixy sa počítajú do offsetu
  • BOUNDSHEET necháva svoje prvé 4 bajty čisté; BOF, FILEPASS a INTERFACEHDR ostávajú úplne čisté
  • Heslá: ASCII, maximálne 15 znakov
  • HotXLS: index opravený vo v2.384.47, kľúč a XorRor opravené vo v2.384.54, staršie XOR súbory HotXLS rozpoznané podľa nezhody kľúča
  • Pre skutočnú ochranu použite minimálne BIFF8 RC4 CryptoAPI alebo XLSX Agile Encryption

HotXLS obsluhuje ochranu heslom pre BIFF5 a BIFF8, XLSX Standard aj Agile Encryption a čítací heslový callback z jednej knižnice pre Delphi a C++Builder, s detailmi interoperability vyššie vyriešenými za vás. Edície, platformy a skúšobnú verziu na stiahnutie nájdete na stránke tabuľkového komponentu HotXLS pre Delphi