Teknisk artikkel

HotXLS XLS XOR-obfuskering: Nøkkelderivasjon og XorRor

HotXLS skriver Excel 5.0/95-arbeidsbøker (BIFF5) med XOR-obfuskering som Excel 16 bare åpner når tre detaljer stemmer nøyaktig med [MS-OFFCRYPTO]: FILEPASS-nøkkelen må være CreateXorKey_Method1(password), 16-byte XOR-matrisen må bygges med XorRor (rotasjon mot høyre med én bit), og hver byte må bruke XorArrayIndex = (strøm-offset + postlengde) mod 16. HotXLS fikk indeksen riktig i v2.384.47 og nøkkelen og rotasjonen riktig i v2.384.54. Før det åpnet hver passordbeskyttede BIFF5-fil komponenten lagde fint i HotXLS og feilet i Excel

Den siste setningen er hele historien i miniatyr. En leser og en skriver som deler samme feilide, er perfekt enige med hverandre, så rundtur-testene forblir grønne mens den eneste konsumenten som betyr noe sier nei. Excel 16 sa nei to ganger, med to ulike meldinger, og hver melding pekte på et ulikt lag av ordningen. Denne artikkelen går gjennom disse lagene i den rekkefølgen Excel sjekker dem, med bytenivådetaljer du kan bruke enten du kaller HotXLS eller skriver din egen BIFF-leser

Hva lagrer BIFF XOR-obfuskering egentlig?

BIFF XOR-obfuskering lagrer bare to 16-bit ord i filen, og alt annet beregnes ut fra passordet. FILEPASS-posten ($002F) ligger rett etter arbeidsbokens globale BOF, og i en BIFF5-fil er kroppen nøyaktig 4 byte: XOR-nøkkelen etterfulgt av passord-verifieren. Det finnes ingen salt, ingen algoritmeidentifikator og ingen kryptert verifier-blob av den typen RC4- og AES-ordningene bærer

Ut fra de to ordene bygger en leser tre ting:

  • Verifieren, en 16-bits hash av passord-bytene XOR-et med $CE4B. Å sammenligne den med det lagrede ordet er passordsjekken, og den eneste
  • XOR-nøkkelen, en 16-bits verdi fra CreateXorKey_Method1 i [MS-OFFCRYPTO] §2.3.7.2, drevet av to konstante tabeller (InitialCode, 15 ord, og XorMatrix, 105 ord)
  • XOR-matrisen, 16 byte satt sammen av passord-bytene polstret med en fast 16-byte polstring, hver XOR-et med den lave nøkkelbyten (partallsposisjoner) eller den høye nøkkelbyten (oddetallsposisjoner), og deretter rotert mot høyre med én bit

Posthodene forblir i klartekst, og det gjør også en håndfull hele poster som ordningen unntar, blant dem BOF, FILEPASS og INTERFACEHDR. Alle andre postkropper transformeres byte for byte: rotasjon mot venstre med 5 biter, deretter XOR med én oppføring i 16-byte-matrisen. Dekrypteringen, som [MS-OFFCRYPTO] §2.3.7.3 beskriver som DecryptData_Method1, er speilbildet: XOR først, deretter rotasjon mot høyre med 5

I HotXLS rører du ikke noe av dette direkte. Sett et passord, velg formatet, og SaveAs sender ut FILEPASS og transformerer strømmen:

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 støtter bare XOR-obfuskering; xletAuto ville valgt den også
  Wb.EncryptionType := xletXor;
  // Hold den ASCII og på maks 15 tegn (se nedenfor)
  Wb.EncryptionPassword := 'secret';

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

Hvorfor sier Excel at passordet er feil når verifieren stemmer?

Excel avviser passordet fordi det ikke stoler på den lagrede nøkkelen: Excel avleder nøkkelen fra det inn-tastede passordet med CreateXorKey_Method1 og sammenligner den med FILEPASS-nøkkelordet, så en fil hvis nøkkel er noe annet, stryker på passordsjekken selv når verifieren er riktig. Spesifikasjonen beskriver nøkkelen som en utdata av passordet, ikke som en fri parameter, og Excel 16 håndhever den lesingen

HotXLS-skriveren før v2.384.54 fylte nøkkelordet med to tilfeldige byte. Det ser harmløst ut på papiret, siden verifieren er den dokumenterte passordsjekken og matrisen bygges ut fra den nøkkelen filen deklarerer. HotXLS selv leste disse filene uten problemer, fordi leseren tok nøkkelen fra FILEPASS som gitt. Excel 16, som fikk samme fil og det riktige passordet, svarte at passordet ikke var riktig. Siden v2.384.54 avledes nøkkelen, så FILEPASS for passordet secret inneholder alltid nøkkelen $014D og verifieren $DAA7, verdier krysssjekket mot en uavhengig implementasjon av spesifikasjonen

HotXLS-diagram av BIFF5 XOR FILEPASS-posten som Excel 16 sjekker ved åpning: den klare fire-byte-kroppen inneholder XOR-nøkkelen og passord-verifieren, Excel avleder nøkkelen fra det inn-tastede passordet med CreateXorKey_Method1 og avviser en tilfeldig nøkkel med en passordfeil selv når verifieren stemmer; HotXLS lagrer den avledede nøkkelen 014D for secret
FILEPASS-kroppen er bare to ord, men Excel avleder nøkkelen på nytt fra passordet ditt og sammenligner; en tilfeldig fylt nøkkel stryker på sjekken selv med en riktig verifier, og det er derfor HotXLS har avledet den siden v2.384.54

Derivasjonen selv er kort når de to tabellene er på plass. Gå gjennom passordet baklengs, se på bit 6 i hver byte sju ganger mens du skyver den mot venstre, og XOR inn én XorMatrix-oppføring hver gang biten er satt. Det følgende er en prinsippskisse som gjenskaper spesifikasjonsalgoritmen og stemmer med HotXLS-implementasjonen; det er ikke et HotXLS-API:

// Prinsippskisse av [MS-OFFCRYPTO] 2.3.7.2 CreateXorKey_Method1
// og CreateXorArray_Method1 (kun illustrasjon, ikke et 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;                       // nøkkelen ser bare 15 byte
  if Len = 0 then
    Exit;
  Result := InitialCode[Len - 1];
  Element := $68;                    // siste XorMatrix-oppføring
  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));   // rotasjon mot høyre med én 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;

For secret gir dette matrisen 1F 32 17 B9 14 BA 7B 7F 59 DD 59 7F 7A C0 A6 DF, som er en hendig fast verdi hvis du tester din egen leser

HotXLS-diagram av XOR-matrisekonstruksjonen for BIFF5-obfuskering: seksten byte sådd fra passordet og en fast polstring XOR-es med den lave nøkkelbyten på partallsposisjoner og den høye på oddetallsposisjoner, og roteres deretter mot høyre med én bit med XorRor, noe som gir fastverdien 1F 32 17 B9 for passordet secret
Matrisebytene kommer fra passordet, polstringen og de to nøkkelbytene, én rotasjon til slutt; rotasjon mot venstre med 2 fungerte bare når byte-transformasjonen gikk i motsatt rekkefølge, og Excel følger spesifikasjonens rekkefølge

Hvorfor gir riktig nøkkel likevel en ødelagt fil?

En riktig nøkkel gir likevel en ødelagt fil når XOR-matrisen roteres feil vei: [MS-OFFCRYPTO] definerer matrisesteget som XorRor, en rotasjon mot høyre med én bit, og en matrise rotert mot venstre med to dekrypterer hver postkropl til støy. Å fikse nøkkelen førte Excel 16 forbi passordprompten og rett inn i en annen feil, en melding om at filen har et problem og ikke kan åpnes

Den gamle HotXLS-koden roterte hver matrisebyte mot venstre med 2 biter, en form som sirkulerer i flere BIFF-implementasjoner. Etter som HotXLS brukte samme rotasjon på begge sider, la aldri dens egen leser merke til noe. Excel 16 kan ikke lenger lagre Excel 5.0/95-filer, og tilbyr ikke XOR ved lagring av BIFF8, så det fantes ingen naturlig Excel-prøve å sammenligne mot. Bevisene måtte komme fra den andre retningen: skriv én BIFF5-strøm i klartekst, re-enkod den åtte måter, og la Excel 16 åpne hver variant. De åtte variantene krysset tre uavhengige valg:

ValgAlternativ AAlternativ B
MatriserotasjonXorRor (rotasjon høyre 1)Rotasjon venstre 2
Matriseindeks(offset + postlengde) mod 16offset mod 16
Byte-transformasjonsrekkefølgeRotasjon venstre 5, deretter XORXOR, deretter rotasjon venstre 5

Excel 16 åpnet nøyaktig to av de åtte: XorRor med rotasjon-deretter-XOR og postlengde-indeksen, og én variant som bare ser annerledes ut. Rotasjon-venstre-2 med XOR-deretter-rotasjon og samme indeks er samme funksjon i forkledning. Rotasjon distribuerer over XOR, så rol5(p xor rol2(b)) er lik rol5(p) xor rol7(b), og på en 8-bit verdi er en rotasjon mot venstre med 7 en rotasjon mot høyre med 1. Kort sagt er rol5 ∘ rol2 = ror1, og det er derfor matrisen med rotasjon-venstre-2 ser plausibel ut i isolasjon: den er riktig bare sammen med den motsatte transformasjonsrekkefølgen. Paret med spesifikasjonens rekkefølge korruperer den hver transformerte byte

Samme eksperiment avgjorde et annet spørsmål. Variantene som droppet postlengden fra indeksen, strøk alle, noe som bekreftet indeksregelen HotXLS hadde vedtatt en utgivelse tidligere på spesifikasjonsteksten alene

Hvordan beregnes XorArrayIndex for hver byte?

XorArrayIndex for en byte er dens offset i arbeidsbokstrømmen pluss lengden på hele postdataden den tilhører, mod 16. Indeksen starter derfor på en postavhengig verdi for hver post og øker med én per byte inni den. Spesifikasjonens pseudokode navngir inngangene FileOffset og Data.Length, noe som er lett å mislese som postens start-offset alene, og den mislesningen er nøyaktig det HotXLS frakt frem til v2.384.47

Tre detaljer avgjør om indeksene dine stemmer med Excel:

  • Posthodet på 4 byte transformeres aldri, men opptar likevel strømpozisjoner, så en posts første kroppbyte ligger på header-offset + 4
  • Lengdeleddet er hele postdatadens lengde, ikke antall byte som faktisk transformeres
  • BOUNDSHEET er delvis klartekst: de første 4 bytene, lbPlyPos, strøm-offseten til arkenes BOF, forblir lesbare så en parser kan finne arkene. Disse 4 bytene hoppes over av transformasjonen, men teller likevel med i både offseten og postlengden
HotXLS-diagram av XorArrayIndex-regelen for BIFF5 XOR-obfuskering: hver kroppbyte bruker strøm-offset pluss hele postdatalengden modulo 16, det klare fire-byte hodet og BOUNDSHEET lbPlyPos-prefikset teller likevel med i offseten, og å ignorere postlengdeleddet var defekten HotXLS rettet i v2.384.47
Matriseindeksen starter på nytt én gang per post, ikke én gang per strøm: hodet og eventuelle klare prefikser opptar posisjoner, postdatalengden går inn i modulo-en, og begge halvdelene av gamle HotXLS var enige om feil formel

Satt sammen er per-post-transformasjonen noen få linjer. Igjen: dette er en skisse av regelen, ikke noe du trenger å kalle:

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

// Obfusker én postkropl på stedet. BodyPos er strøm-offseten til
// Body[0], altså posthode-offset + 4. PlainPrefix er 4 for
// BOUNDSHEET, full lengde for BOF / FILEPASS, 0 for de fleste poster
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;

// Lesing er speilbildet: B := Body[I] xor Arr[...];
// deretter rotasjon mot høyre med 5, altså Rol8(B, 3)

Før v2.384.47 beregnet HotXLS-leseren indeksen ut fra strømposisjonen alene, og skriveren brukte den klare byte-offseten. Begge ignorerte postlengden, så nok en gang var de to halvdelene enige med hverandre og med ingen andre. En uavhengig skrevet dekoder leste v2.384.47-utdataen riktig og den eldre utdataen som søppel, og åtte-variant-testen mot Excel 16 bekreftet senere regelen mot det virkelige målet

Hva skjer med XOR-filer skrevet av eldre HotXLS-versjoner?

HotXLS fortsetter å lese sine egne XOR-filer fra før v2.384.54 ved å sjekke FILEPASS-nøkkelen: når den lagrede nøkkelen er lik nøkkelen avledet fra passordet, bygger leseren spesifikasjonens XorRor-matrise, og når den avviker, behandler leseren filen som en eldre HotXLS-fil og bygger matrisen med rotasjon-venstre-2 på nytt. Excel-skrivne filer bærer alltid den avledede nøkkelen, så de tar alltid spesifikasjonsstien

Testen er en heuristikk med presis feilrate. En gammel fil hvis tilfeldige nøkkel tilfeldigvis var lik den avledede nøkkelen, ville blitt lest med feil matrise, og sjansen for det er 1 av 65 536. Fallbacken dekker bare matriserotasjonen; indeksregelen byttes ikke, så filene den redder, er de skrevet mellom v2.384.47 og v2.384.53. Har du fortsatt BIFF5 XOR-filer fra det vinduet, åpner du dem med dagens HotXLS og lagrer dem på nytt for å få en fil Excel godtar

To passorddetaljer gjelder hver fil, gammel som ny:

  • Lengde. CreateXorKey_Method1 leser bare de første 15 passord-bytene, som er spesifikasjonsgrensen. HotXLS bruker den taket på nøkkelen og beholder verifieren og matrisen på sine vanlige fullengde- og 16-byte-regler, konsekvent på begge sider. Excel selv nekter passord lengre enn 15 tegn for dette formatet, så behandle 15 som det virkelige maksimumet
  • Tegnsett. HotXLS konverterer passordet til byte gjennom systemets ANSI-kodeside. Spesifikasjonen beskriver å ta den lave byten av hvert UTF-16-tegn, noe som stemmer for ASCII. Uten Excel-prøver beskyttet av ikke-ASCII-passord finnes det ingen sannhetsgrunn for resten, så hold deg til ASCII-passord for XOR-filer

På lesesiden lar TXLSWorkbook.OnPassword deg spørre etter et passord når Open treffer en FILEPASS-post. Hendelsen er en TXLSPasswordEvent med en var PassWord: WideString og en var Retry: Boolean; sett Retry til True for å prøve igjen, opptil tre forsøk:

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: passord kreves men ikke oppgitt; -1005: feil passord
  if Rc <> 1 then
    raise Exception.CreateFmt('Cannot open %s (code %d)', [FileName, Rc]);
  ShowMessage(VarToStr(Wb.Sheets[1].Range['A1', 'A1'].Value));
end;

Vet du allerede passordet, hopper Open(FileName, APassWord) fullstendig over hendelsen

Er XOR-obfuskering sikker nok til noe som helst?

BIFF XOR-obfuskering er ikke kryptering og beskytter ingenting mot en motivert leser. Passordsjekken er en 16-bits verifier, nøkkelen er 16 biter, og 16-byte-matrisen gjentas gjennom hele strømmen, så forutsigbart BIFF-postinnhold avslører matrisebyter uten noe passord i det hele tatt. HotXLS skriver XOR bare fordi Excel 5.0/95-filer ikke har noe annet alternativ, og grunnen til å lage slike filer i dag er en eldre konsument som ikke kan lese noe nyere

Den klassiske motoren velger ordningen gjennom TXLSWorkbook.EncryptionType, og kombinasjonen med lagringsformatet sjekkes strengt:

  • xletAuto (standard) skriver RC4 CryptoAPI for xlExcel97 og XOR for xlExcel5, i tråd med det Excel selv skrev for hvert format
  • xletXor er gyldig bare for BIFF5; med xlExcel97 reiser lagringen et unntak i stedet for å falle stille tilbake
  • xletRC4 og xletRC4CryptoAPI er kun BIFF8, og å be om dem ved BIFF5-lagring reiser også et unntak

RC4 er også gammeldags, og detaljene i å få den til å interoperere er dekket i hvorfor Excel avviser en kryptert arbeidsbok med riktig passord. Kan mottakeren lese XLSX, bruk XLSX-motoren i stedet: TXLSXWorkbook.SaveAsEncryptedAgile skriver Agile Encryption (SHA-512 passord-hashing med en spin count på 100 000 og AES-256-CBC), formatet Excel 2010 og senere skriver som standard, mens SaveAsEncrypted skriver den eldre AES-128 Standard Encryption. Avveiningene mellom de to finner du i kryptering av XLSX-filer med AES i Delphi, og lesesiden er dekket i lesing av Agile-krypterte Excel-filer med HotXLS

Hurtigreferanse: BIFF XOR-obfuskering Excel 16 godtar

  • FILEPASS ($002F) følger den globale BOF; i BIFF5 er kroppen 4 byte: nøkkel, deretter verifier
  • Nøkkel = CreateXorKey_Method1(password) per [MS-OFFCRYPTO] §2.3.7.2, aldri tilfeldig; for secret er den $014D
  • Matrise = passord-byte + polstring, XOR med lav nøkkelbyte på partallsposisjoner og høy nøkkelbyte på oddetallsposisjoner, deretter XorRor (rotasjon høyre 1)
  • Krypter en byte: rotasjon venstre 5, deretter XOR; dekrypter: XOR, deretter rotasjon høyre 5 (§2.3.7.3)
  • XorArrayIndex = (byte-strøm-offset + postdatalengde) mod 16; hodet og klare prefikser teller med i offseten
  • BOUNDSHEET beholder sine første 4 byte i klartekst; BOF, FILEPASS og INTERFACEHDR forblir helt klare
  • Passord: ASCII, maks 15 tegn
  • HotXLS: indeksen rettet i v2.384.47, nøkkelen og XorRor rettet i v2.384.54, eldre HotXLS XOR-filer gjenkjent ved nøkkelmismatch
  • For ekte beskyttelse: bruk BIFF8 RC4 CryptoAPI som minimum, eller XLSX Agile Encryption

HotXLS håndterer BIFF5- og BIFF8-passordbeskyttelse, XLSX Standard og Agile Encryption, og lesesidens passord-callback fra ett Delphi- og C++Builder-bibliotek, med interoperasjonsdetaljene over håndtert for deg. Se HotXLS Delphi regnearkkomponent for utgaver, plattformer og en prøveversjon