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_Method1i [MS-OFFCRYPTO] §2.3.7.2, drevet av to konstante tabeller (InitialCode, 15 ord, ogXorMatrix, 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
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
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:
| Valg | Alternativ A | Alternativ B |
|---|---|---|
| Matriserotasjon | XorRor (rotasjon høyre 1) | Rotasjon venstre 2 |
| Matriseindeks | (offset + postlengde) mod 16 | offset mod 16 |
| Byte-transformasjonsrekkefølge | Rotasjon venstre 5, deretter XOR | XOR, 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
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_Method1leser 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 forxlExcel97og XOR forxlExcel5, i tråd med det Excel selv skrev for hvert formatxletXorer gyldig bare for BIFF5; medxlExcel97reiser lagringen et unntak i stedet for å falle stille tilbakexletRC4ogxletRC4CryptoAPIer 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; forsecreter 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