Tehnički članak

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

HotXLS zapisuje Excel 5.0/95 (BIFF5) radne knjige zaštićene XOR obfuskacijom koje Excel 16 otvara samo kad se tri detalja točno poklapaju s [MS-OFFCRYPTO]: FILEPASS ključ mora biti CreateXorKey_Method1(password), 16-bajtno XOR polje mora se graditi s XorRor (rotacija udesno za jedan bit), a svaki bajt mora koristiti XorArrayIndex = (pomak toka + duljina zapisa) mod 16. HotXLS je indeks pogodio u v2.384.47, a ključ i rotaciju u v2.384.54. Prije toga, svaka datoteka BIFF5 zaštićena lozinkom koju je proizveo otvarala se lijepo u HotXLS-u i padala u Excelu

Ta posljednja rečenica jest cijela priča u malom. Čitač i pisač koji dijele istu krivu ideju savršeno se slažu jedan s drugim, pa testovi povratnog ciklusa ostaju zeleni dok jedini potrošač koji je važan govori ne. Excel 16 je rekao ne dvaput, s dvije različite poruke, a svaka je poruka pokazivala na drugi sloj sheme. Ovaj članak prolazi kroz te slojeve redoslijedom kojim ih Excel provjerava, s detaljima na razini bajta koji vam koriste bez obzira zovete li HotXLS ili pišete vlastiti BIFF čitač

Što BIFF XOR obfuskacija uopće pohranjuje?

BIFF XOR obfuskacija u datoteci pohranjuje samo dvije 16-bitne riječi, a sve ostalo preračunava se iz lozinke. Zapis FILEPASS ($002F) sjedi odmah iza globals BOF-a radne knjige, a u BIFF5 datoteci njegovo je tijelo točno 4 bajta: XOR ključ iza kojeg slijedi verifier lozinke. Nema soli, nema identifikatora algoritma nema ni šifriranog verifier bloba kakav nose RC4 i AES sheme

Iz te dvije riječi čitač ponovno gradi tri stvari:

  • Verifier, 16-bitni hash bajtova lozinke XOR-ovanih s $CE4B. Usporedba s pohranjenom riječi jest provjera lozinke, i jedina
  • XOR ključ, 16-bitna vrijednost iz CreateXorKey_Method1 u [MS-OFFCRYPTO] §2.3.7.2, kojeg pokreću dvije konstantne tablice (InitialCode, 15 riječi, i XorMatrix, 105 riječi)
  • XOR polje, 16 bajtova od bajtova lozinke dopunjenih fiksnim 16-bajtnim punjenjem, svaki XOR-ovan s nižim bajtom ključa (parne pozicije) ili višim bajtom ključa (neparne pozicije), pa rotiran udesno za jedan bit

Zaglavlja zapisa ostaju u običnom tekstu, a tako i gomilica cijelih zapisa koje shema izuzima, među njima BOF, FILEPASS i INTERFACEHDR. Svako drugo tijelo zapisa transformira se bajt po bajt: rotacija ulijevo za 5 bitova, pa XOR s jednim unosom 16-bajtnog polja. Dešifriranje, koje [MS-OFFCRYPTO] §2.3.7.3 izlaže kao DecryptData_Method1, jest zrcalna slika: prvo XOR, pa rotacija udesno za 5

U HotXLS-u ničega od ovoga ne dirate izravno. Postavite lozinku, odaberite format, a SaveAs emitira FILEPASS i transformira 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; i xletAuto bi odabrao nju
  Wb.EncryptionType := xletXor;
  // Držite se ASCII-a i najviše 15 znakova (vidi niže)
  Wb.EncryptionPassword := 'secret';

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

Zašto Excel govori da je lozinka kriva kad se verifier poklapa?

Excel odbija lozinku jer ne vjeruje pohranjenom ključu: Excel izvodi ključ iz upisane lozinke s CreateXorKey_Method1 i uspoređuje ga s FILEPASS ključnom riječi, pa datoteka čiji je ključ bilo što drugo pada na provjeri lozinke i kad je verifier ispravan. Specifikacija opisuje ključ kao izlaz lozinke, a ne kao slobodan parametar, i Excel 16 provodi to čitanje

HotXLS pisač prije v2.384.54 punio je ključnu riječ s dva slučajna bajta. Na papiru to izgleda bezopasno, jer je verifier dokumentirana provjera lozinke, a polje se gradi iz kojeg god ključa datoteka deklarira. HotXLS je sam čitao te datoteke bez problema, jer je njegov čitač ključ iz FILEPASS-a uzimao zadanom činjenicom. Excel 16, primivši istu datoteku i ispravnu lozinku, odgovorio je da lozinka nije ispravna. Od v2.384.54 ključ se izvodi, pa FILEPASS za lozinku secret uvijek drži ključ $014D i verifier $DAA7, vrijednosti unakrižno provjerene neovisnom implementacijom specifikacije

HotXLS dijagram BIFF5 XOR FILEPASS zapisa koji Excel 16 provjerava pri otvaranju: obično četverobajtno tijelo drži XOR ključ i verifier lozinke, Excel izvodi ključ iz upisane lozinke s CreateXorKey_Method1 i odbija slučajni ključ pogreškom lozinke čak i kad se verifier poklapa; HotXLS sprema izvedeni ključ 014D za secret
FILEPASS tijelo su samo dvije riječi, ali Excel ključ izvodi iz vaše lozinke i uspoređuje; slučajno ispunjen ključ pada na provjeri čak i uz ispravan verifier, pa ga HotXLS od v2.384.54 izvodi

Samo izvođenje kratko je čim su dvije tablice na mjestu. Prođite lozinkom unatrag, gledajte bit 6 svakog bajta sedam puta uz pomak ulijevo i XOR-ujte jedan XorMatrix unos svaki put kad je bit postavljen. Slijedi načelna skica koja reproducira algoritam iz specifikacije i poklapa se s HotXLS implementacijom; nije to 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;                    // posljednji XorMatrix unos
  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));   // rotacija udesno 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 polje 1F 32 17 B9 14 BA 7B 7F 59 DD 59 7F 7A C0 A6 DF, zgodan fixture ako testirate vlastiti čitač

HotXLS dijagram gradnje XOR polja za BIFF5 obfuskaciju: šesnaest bajtova posijanih iz lozinke i fiksnog punjenja XOR-uje se s nižim bajtom ključa na parnim pozicijama i višim bajtom ključa na neparnim pozicijama, pa rotira udesno za jedan bit s XorRor, dajući fixture 1F 32 17 B9 za lozinku secret
Bajtovi polja dolaze iz lozinke, punjenja i dva bajta ključa, jedna rotacija na kraju; rotacija ulijevo 2 radila je samo kad je transformacija bajta išla obrnutim redoslijedom, a Excel slijedi redoslijed iz specifikacije

Zašto pravi ključ i dalje proizvodi oštećenu datoteku?

Ispravan ključ i dalje proizvodi oštećenu datoteku kad se XOR polje rotira krivim smjerom: [MS-OFFCRYPTO] definira korak polja kao XorRor, rotaciju udesno za jedan bit, a polje rotirano ulijevo za dva dešifrira svako tijelo zapisa u šum. Popravak ključa gurnuo je Excel 16 preko upita za lozinku ravno u drugu grešku, izvještaj da datoteka ima problem i ne može se otvoriti

Stari HotXLS kod rotirao je svaki bajt polja ulijevo za 2 bita, forma koja kruži u nekoliko BIFF implementacija. Budući da je HotXLS koristio istu rotaciju na obje strane, vlastiti mu čitač nikad nije primijetio. Excel 16 više ne može spremati Excel 5.0/95 datoteke, a XOR ne nudi pri spremanju BIFF8, pa nije bilo nativnog Excel uzorka za usporedbu. Dokazi su morali doći iz drugog smjera: zapisati jedan običan BIFF5 tok, ponovno ga kodirati na osam načina i pustiti Excel 16 da otvori svaku varijantu. Osam je varijanti ukrstilo tri neovisne odluke:

OdlukaOpcija AOpcija B
Rotacija poljaXorRor (rotacija udesno 1)Rotacija ulijevo 2
Indeks polja(offset + record length) mod 16offset mod 16
Redoslijed transformacije bajtaRotacija ulijevo 5, pa XORXOR, pa rotacija ulijevo 5

Excel 16 otvorio je točno dvije od osam: XorRor s rotiraj-pa-XOR i indeksom po duljini zapisa, te jednu varijantu koja se samo čini drugačijom. Rotacija-ulijevo-2 s XOR-pa-rotiraj i istim indeksom ista je funkcija samo prerušena. Rotacija se distribuirá preko XOR-a, pa je rol5(p xor rol2(b)) jednako rol5(p) xor rol7(b), a na 8-bitnoj vrijednosti rotacija ulijevo za 7 jest rotacija udesno za 1. Ukratko, rol5 ∘ rol2 = ror1, pa izolirano gledano polje s rotacijom-ulijevo-2 izgleda vjerodostojno: točno je samo uz obrnuti redoslijed transformacije. U paru s redoslijedom iz specifikacije pokvari svaki transformirani bajt

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

Kako se XorArrayIndex računa za svaki bajt?

XorArrayIndex za bajt jest njegov pomak u toku radne knjige plus duljina cijelih podataka zapisa kojem pripada, mod 16. Indeks se zato za svaki zapis ponovno pokreće od vrijednosti koja ovisi o zapisu i povećava za jedan po bajtu unutar njega. Pseudokod iz specifikacije imenuje ulaze FileOffset i Data.Length, što se lako pogrešno pročita kao sam pomak početka zapisa, a to pogrešno čitanje točno je ono što je HotXLS isporučivao do v2.384.47

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

  • 4-bajtno zaglavlje zapisa nikad se ne transformira, ali i dalje zauzima pozicije toka, pa prvi bajt tijela zapisa sjedi na pomaku zaglavlja + 4
  • Član duljine jest puna duljina podataka zapisa, ne broj bajtova koji su stvarno transformirani
  • BOUNDSHEET je djelomično običan: njegova prva 4 bajta, lbPlyPos, pomak toka BOF-a lista, ostaju čitljivi da parser može locirati listove. Tih 4 bajta transformacija preskače ali se i dalje računaju i u pomak i u duljinu zapisa
HotXLS dijagram pravila XorArrayIndex za BIFF5 XOR obfuskaciju: svaki bajt tijela koristi pomak toka plus punu duljinu podataka zapisa modulo 16, obično četverobajtno zaglavlje i BOUNDSHEET lbPlyPos prefiks i dalje se računaju u pomak, a ignoriranje člana duljine zapisa bila je greška koju je HotXLS popravio u v2.384.47
Indeks polja ponovno se pokreće jednom po zapisu, ne jednom po toku: zaglavlje i svaki obični prefiks zauzimaju pozicije, duljina podataka zapisa hrani modulo, a obje polovice starog HotXLS-a slagale su se oko krive formule

Složeno, transformacija po zapisu jest nekoliko redaka. Opet, ovo je skica pravila, ne nešto što trebate zvati:

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

// Obfuskiraj tijelo jednog zapisa na mjestu. BodyPos jest pomak toka od
// Body[0], tj. pomak zaglavlja zapisa + 4. PlainPrefix je 4 za
// BOUNDSHEET, puna duljina 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 zrcalna slika: B := Body[I] xor Arr[...];
// pa rotacija udesno za 5, tj. Rol8(B, 3)

Prije v2.384.47 HotXLS čitač računao je indeks iz same pozicije toka, a pisač koristio je obični bajtni pomak. Oboje je ignoriralo duljinu zapisa, pa su opet dvije polovice pristajale jedna na drugu i ni s kim drugim. Neovisno napisan dekoder točno je čitao v2.384.47 izlaz, a stariji izlaz kao smeće, pa je test s osam varijanti u Excelu 16 kasnije potvrdio pravilo protiv stvarnog cilja

Što je s XOR datotekama koje su zapisale starije HotXLS verzije?

HotXLS i dalje čita vlastite XOR datoteke prije v2.384.54 provjerom FILEPASS ključa: kad je pohranjeni ključ jednak ključu izvedenom iz lozinke, čitač gradi XorRor polje iz specifikacije, a kad se razlikuje, čitač datoteku tretira kao stariju HotXLS datoteku i ponovno gradi polje s rotacijom-ulijevo-2. Excelom zapisane datoteke uvijek nose izvedeni ključ, pa uvijek idu putem iz specifikacije

Test je heuristika s točnom stopom pogreške. Stara bi se datoteka čiji se slučajni ključ slučajno poklopio s izvedenim ključem čitala krivim poljem, a šansa za to jest 1 u 65,536. Fallback pokriva samo rotaciju polja; pravilo indeksa ne prebacuje se, pa datoteke koje spašava one su zapisane između v2.384.47 i v2.384.53. Ako još držite BIFF5 XOR datoteke iz tog prozora, otvorite ih s trenutnim HotXLS-om i spremite ih ponovno da dobijete datoteku koju Excel prihvaća

Dva detalja o lozinkama vrijede za svaku datoteku, staru ili novu:

  • Duljina. CreateXorKey_Method1 čita samo prvih 15 bajtova lozinke, što je granica iz specifikacije. HotXLS primjenjuje taj strop na ključ i drži verifier i polje na njihovim uobičajenim pravilima pune duljine i 16 bajtova, dosljedno na obje strane. Excel sam odbija lozinke duže od 15 znakova za ovaj format, pa smatrajte 15 stvarnim maksimumom
  • Skup znakova. HotXLS pretvara lozinku u bajtove kroz sistemsku ANSI kodnu stranicu. Specifikacija opisuje uzimanje nižeg bajta svakog UTF-16 znaka, što se slaže za ASCII. Bez Excel uzoraka zaštićenih ne-ASCII lozinkama nema utemeljene istine o ostalom, pa se držite ASCII lozinki za XOR datoteke

Na strani čitanja TXLSWorkbook.OnPassword vam dopuštra da zatražite lozinku kad Open naiđe na FILEPASS zapis. Događaj je TXLSPasswordEvent s var PassWord: WideString i var Retry: Boolean; postavite Retry na True za novi pokušaj, 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: lozinka je potrebna ali nije dana; -1005: kriva 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) potpuno preskače događaj

Je li XOR obfuskacija dovoljno sigurna za išta?

BIFF XOR obfuskacija nije šifriranje i ne štiti ništa od motiviranog čitača. Provjera lozinke jest 16-bitni verifier, ključ ima 16 bitova, a 16-bajtno se polje ponavlja kroz cijeli tok, pa predvidljivi sadržaji BIFF zapisa izlažu bajtove polja bez ikakve lozinke. HotXLS piše XOR samo jer Excel 5.0/95 datoteke nemaju drugu opciju, a razlog za proizvodnju takvih datoteka danas jest naslijeđeni potrošač koji ne čita ništa novije

Klasični motor bira shemu kroz TXLSWorkbook.EncryptionType, a kombinacija sa formatom spremanja provjerava se strogo:

  • xletAuto (zadano) piše RC4 CryptoAPI za xlExcel97 i XOR za xlExcel5, u skladu s onim što je Excel sam zapisivao za svaki format
  • xletXor vrijedi samo za BIFF5; uz xlExcel97 spremanje podiže iznimku umjesto da tiho padne natrag
  • xletRC4 i xletRC4CryptoAPI samo su BIFF8, a traženje njih pri spremanju BIFF5 također podiže iznimku

RC4 je isto zastario, a detalji njegove interoperabilnosti pokriveni su u zašto Excel odbija šifriranu radnu knjigu s ispravnom lozinkom. Ako primatelj može čitati XLSX, koristite umjesto toga XLSX motor: TXLSXWorkbook.SaveAsEncryptedAgile piše Agile Encryption (SHA-512 hashiranje lozinke sa spin countom od 100.000 iteracija i AES-256-CBC), format koji Excel 2010 i noviji zapisuju po zadanom, dok SaveAsEncrypted piše starije AES-128 Standard Encryption. Komromise između njih dvoje pokriva šifriranje XLSX datoteka s AES-om u Delphiju, a stranu čitanja pokriva čitanje Agile šifriranih Excel datoteka s HotXLS-om

Brza referenca: BIFF XOR obfuskacija koju Excel 16 prihvaća

  • FILEPASS ($002F) slijedi globals BOF; u BIFF5 njegovo je tijelo 4 bajta: ključ, pa verifier
  • Ključ = CreateXorKey_Method1(password) po [MS-OFFCRYPTO] §2.3.7.2, nikad slučajan; za secret to je $014D
  • Polje = bajtovi lozinke + punjenje, XOR s nižim bajtom ključa na parnim pozicijama i višim na neparnim pozicijama, pa XorRor (rotacija udesno 1)
  • Šifriraj bajt: rotacija ulijevo 5, pa XOR; dešifriraj: XOR, pa rotacija udesno 5 (§2.3.7.3)
  • XorArrayIndex = (pomak bajta u toku + duljina podataka zapisa) mod 16; zaglavlja i obični prefiksi računaju se u pomak
  • BOUNDSHEET drži svoja prva 4 bajta običnima; BOF, FILEPASS i INTERFACEHDR ostaju potpuno obični
  • Lozinke: ASCII, najviše 15 znakova
  • HotXLS: indeks popravljen u v2.384.47, ključ i XorRor u v2.384.54, starije HotXLS XOR datoteke otkrivaju se nepoklapanjem ključa
  • Za stvarnu zaštitu koristite barem BIFF8 RC4 CryptoAPI, ili XLSX Agile Encryption

HotXLS obrađuje zaštitu lozinkom za BIFF5 i BIFF8, XLSX Standard i Agile Encryption te callback lozinke na strani čitanja iz jedne Delphi i C++Builder knjižnice, s gore navedenim detaljima interoperabilnosti riješenima umjesto vas. Pogledajte HotXLS Delphi proračunsku komponentu za izdanja, platforme i probno preuzimanje