HotXLS skriver Excel 5.0/95 (BIFF5)-workbooks med XOR-obfuscation, som Excel 16 kun åbner, når tre detaljer matcher [MS-OFFCRYPTO] præcist: FILEPASS-nøglen skal være CreateXorKey_Method1(password), det 16-byte store XOR-array skal bygges med XorRor (rotér én bit mod højre), og hver byte skal bruge XorArrayIndex = (stream-offset + record-længde) mod 16. HotXLS fik indekset rigtigt i v2.384.47 og nøglen og rotationen rigtigt i v2.384.54. Før det åbnede hver kodeordsbeskyttede BIFF5-fil, den producerede, fint i HotXLS og fejlede i Excel
Den sidste sætning er hele historien i miniature. En læser og en skriver, der deler den samme forkerte idé, er fuldkommen enige med hinanden, så round-trip-tests forbliver grønne, mens den eneste forbruger, der tæller, siger nej. Excel 16 sagde nej to gange, med to forskellige beskeder, og hver besked pegede på et forskelligt lag af skemaet. Denne artikel går gennem de lag i den rækkefølge, Excel tjekker dem, med detaljer på byteniveau, du kan bruge, uanset om du kalder HotXLS eller skriver din egen BIFF-læser
Hvad gemmer BIFF XOR-obfuscation egentlig?
BIFF XOR-obfuscation gemmer kun to 16-bit ord i filen, og alt andet beregnes ud fra adgangskoden. FILEPASS-recorden ($002F) sidder umiddelbart efter workbook globals BOF, og i en BIFF5-fil er dens body præcis 4 bytes: XOR-nøglen efterfulgt af password-verificatoren. Der er intet salt, ingen algoritme-identifikator og ingen krypteret verifier-blob af den slags, RC4- og AES-skemaerne bærer
Ud fra de to ord genopbygger en læser tre ting:
- Verificatoren, en 16-bit hash af adgangskode-byterne XORet med
$CE4B. At sammenligne den med det gemte ord er adgangskodetjekket, og det eneste - XOR-nøglen, en 16-bit værdi fra
CreateXorKey_Method1i [MS-OFFCRYPTO] §2.3.7.2, drevet af to konstanttabeller (InitialCode, 15 ord, ogXorMatrix, 105 ord) - XOR-arrayet, 16 bytes bestående af adgangskode-byterne fyldt op med en fast 16-byte-pad, hver XORet med den lave nøglebyte (lige positioner) eller den høje nøglebyte (ulige positioner) og derefter roteret én bit mod højre
Record-headere forbliver i klartekst, og det gør en håndfuld hele records også, som skemaet fritager, blandt andet BOF, FILEPASS og INTERFACEHDR. Alle andre record-bodies transformeres byte for byte: rotér 5 bit mod venstre, og XOR derefter med én indgang af det 16-byte store array. Dekryptering, som [MS-OFFCRYPTO] §2.3.7.3 beskriver som DecryptData_Method1, er spejlbilledet: XOR først, og rotér derefter 5 mod højre
I HotXLS rører du intet af dette direkte. Sæt en adgangskode, vælg formatet, og SaveAs sender FILEPASS ud og transformerer streamen:
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 understøtter kun XOR-obfuscation; xletAuto ville vælge den samme
Wb.EncryptionType := xletXor;
// Hold den ASCII og på højst 15 tegn (se nedenfor)
Wb.EncryptionPassword := 'secret';
if Wb.SaveAs(FileName, xlExcel5) <> 1 then
raise Exception.Create('BIFF5 save failed');
end;
Hvorfor siger Excel, at adgangskoden er forkert, når verificatoren matcher?
Excel afviser adgangskoden, fordi den ikke stoler på den gemte nøgle: Excel udleder nøglen fra den indtastede adgangskode med CreateXorKey_Method1 og sammenligner den med FILEPASS-nøgleordet, så en fil, hvis nøgle er noget som helst andet, fejler adgangskodetjekket, selv når verificatoren er korrekt. Specifikationen beskriver nøglen som et output af adgangskoden, ikke som en fri parameter, og Excel 16 håndhæver den læsning
HotXLS-skriveren før v2.384.54 fyldte nøgleordet med to tilfældige bytes. Det ser harmløst ud på papir, eftersom verificatoren er den dokumenterede adgangskodetjek, og arrayet bygges ud fra den nøgle, filen deklarerer. HotXLS selv læste de filer uden problemer, fordi dens læser tog nøglen fra FILEPASS som givet. Excel 16 fik samme fil og den rigtige adgangskode og svarede, at adgangskoden ikke var korrekt. Siden v2.384.54 udledes nøglen, så FILEPASS for adgangskoden secret altid indeholder nøglen $014D og verificatoren $DAA7, værdier krydstjekket mod en uafhængig implementering af specifikationen
Selve udledningen er kort, når de to tabeller er på plads. Gennemløb adgangskoden baglæns, se på bit 6 af hver byte syv gange, mens den skiftes mod venstre, og XOR én XorMatrix-indgang ind, hver gang biten er sat. Det følgende er en principskitse, der gengiver specifikationens algoritme og matcher HotXLS-implementeringen; det er ikke et HotXLS-API:
// Principskitse over [MS-OFFCRYPTO] 2.3.7.2 CreateXorKey_Method1
// og CreateXorArray_Method1 (kun illustration, 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øglen ser kun 15 bytes
if Len = 0 then
Exit;
Result := InitialCode[Len - 1];
Element := $68; // sidste XorMatrix-indgang
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ér én bit mod højre
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 giver det arrayet 1F 32 17 B9 14 BA 7B 7F 59 DD 59 7F 7A C0 A6 DF, en praktisk fixture, hvis du tester din egen læser
Hvorfor giver den rigtige nøgle stadig en beskadiget fil?
En korrekt nøgle giver stadig en beskadiget fil, når XOR-arrayet roteres den forkerte vej: [MS-OFFCRYPTO] definerer array-trinnet som XorRor, en rotation én bit mod højre, og et array roteret to mod venstre dekrypterer hver record-body til støj. Nøglefixet fik Excel 16 forbi adgangskodeprompten og direkte ind i en anden fejl, en melding om, at filen har et problem og ikke kan åbnes
Den gamle HotXLS-kode roterede hver array-byte 2 bit mod venstre, en form, der cirkulerer i adskillige BIFF-implementeringer. Fordi HotXLS brugte den samme rotation på begge sider, bemærkede dens egen læser det aldrig. Excel 16 kan ikke længere gemme Excel 5.0/95-filer, og den tilbyder ikke XOR ved gemning af BIFF8, så der var ingen nativ Excel-prøve at diff-e imod. Beviset måtte komme fra den anden retning: skriv én BIFF5-stream i klartekst, omkod den otte veje, og lad Excel 16 åbne hver variant. De otte varianter krydsede tre uafhængige valg:
| Valg | Mulighed A | Mulighed B |
|---|---|---|
| Array-rotation | XorRor (rotér 1 mod højre) | Rotér 2 mod venstre |
| Array-indeks | (offset + record-længde) mod 16 | offset mod 16 |
| Byte-transformeringsrækkefølge | Rotér 5 mod venstre, derefter XOR | XOR, derefter rotér 5 mod venstre |
Excel 16 åbnede præcis to af de otte: XorRor med rotér-derefter-XOR og record-længde-indekset, og én variant, der kun ser anderledes ud. Rotér-2-mod-venstre med XOR-og-derefter-rotér og det samme indeks er den samme funktion i forklædning. Rotation distribuerer over XOR, så rol5(p xor rol2(b)) er lig med rol5(p) xor rol7(b), og på en 8-bit værdi er en rotation 7 mod venstre en rotation 1 mod højre. Kort sagt: rol5 ∘ rol2 = ror1, og det er derfor, rotér-2-mod-venstre-arrayet ser plausibelt ud i isolation: det er kun korrekt sammen med den omvendte transformeringsrækkefølge. Parret med specifikationens rækkefølge korrumperer det hver transformerede byte
Det samme eksperiment afgjorde et andet spørgsmål. De varianter, der droppede record-længden fra indekset, fejlede alle, hvilket bekræftede indeksreglen, HotXLS havde vedtaget en udgivelse tidligere alene på specifikationsteksten
Hvordan beregnes XorArrayIndex for hver byte?
XorArrayIndex for en byte er dens offset i workbook-streamen plus længden af den samlede record-data, den hører til, mod 16. Indekset starter derfor ved en record-afhængig værdi for hver record og tæller én op pr. byte indeni. Specifikationens pseudokode navngiver inputtene FileOffset og Data.Length, hvilket er let at mislæse som kun record-start-offsettet, og den fejerlæsning er præcis, hvad HotXLS shippede indtil v2.384.47
Tre detaljer afgør, om dine indekser stemmer med Excel:
- Record-headeren på 4 bytes transformeres aldrig, men den optager stadig stream-positioner, så en records første body-byte ligger ved header-offset + 4
- Længdeleddet er hele record-datalængden, ikke antallet af bytes, der faktisk transformeres
- BOUNDSHEET er delvist klartekst: dens første 4 bytes,
lbPlyPos, stream-offsettet for sheet-BOF'en, forbliver læsbare, så en parser kan finde arkene. De 4 bytes springes over af transformeringen, men tælles stadig med i både offsettet og record-længden
Sat sammen er transformeringen pr. record et par linjer. Igen: dette er en skitse af reglen, ikke noget, du behøver at kalde:
function Rol8(B: Byte; N: Integer): Byte;
begin
Result := Byte((B shl N) or (B shr (8 - N)));
end;
// Obfuskér én record-body in place. BodyPos er stream-offsettet for
// Body[0], altså record-header-offset + 4. PlainPrefix er 4 for
// BOUNDSHEET, hele længden for BOF / FILEPASS, 0 for de fleste records
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;
// Læsning er spejlbilledet: B := Body[I] xor Arr[...];
// og rotér derefter 5 mod højre, altså Rol8(B, 3)
Før v2.384.47 beregnede HotXLS-læseren indekset ud fra stream-positionen alene, og skriveren brugte det rene byte-offset. Begge ignorerede record-længden, så igen var de to halvdele enige med hinanden og med ingen andre. En uafhængigt skrevet dekoder læste v2.384.47-outputtet korrekt og det ældre output som skrald, og otte-variant-testen mod Excel 16 bekræftede senere reglen over for det rigtige mål
Hvad sker der med XOR-filer skrevet af ældre HotXLS-versioner?
HotXLS læser fortsat sine egne XOR-filer fra før v2.384.54 ved at tjekke FILEPASS-nøglen: når den gemte nøgle er lig den fra adgangskoden udledte nøgle, bygger læseren specifikationens XorRor-array, og når den afviger, behandler læseren filen som en ældre HotXLS-fil og genopbygger rotér-2-mod-venstre-arrayet. Excel-skrevne filer bærer altid den udledte nøgle, så de følger altid specifikationsvejen
Tjekket er en heuristik med en præcis fejlrate. En gammel fil, hvis tilfældige nøgle tilfældigvis lignede den udledte nøgle, ville blive læst med det forkerte array, og chancen for det er 1 ud af 65.536. Fallbacken dækker kun array-rotationen; indeksreglen skiftes ikke, så de filer, den redder, er dem skrevet mellem v2.384.47 og v2.384.53. Har du stadig BIFF5-XOR-filer fra det vindue, så åbn dem med den aktuelle HotXLS og gem dem igen for at få en fil, Excel accepterer
To adgangskodedetaljer gælder for hver fil, gammel som ny:
- Længde.
CreateXorKey_Method1læser kun de første 15 adgangskode-bytes, hvilket er specifikationens grænse. HotXLS anvender det loft på nøglen og holder verificatoren og arrayet på deres sædvanlige regler for fuld længde og 16 bytes, konsekvent på begge sider. Excel selv nægter adgangskoder længere end 15 tegn til dette format, så behandl 15 som det reelle maksimum - Tegnsæt. HotXLS konverterer adgangskoden til bytes gennem systemets ANSI-codepage. Specifikationen beskriver at tage den lave byte af hvert UTF-16-tegn, hvilket stemmer for ASCII. Uden Excel-prøver beskyttet med ikke-ASCII-adgangskoder er der ingen ground truth for resten, så hold dig til ASCII-adgangskoder til XOR-filer
På læsesiden lader TXLSWorkbook.OnPassword dig bede om en adgangskode, når Open støder på en FILEPASS-record. Eventet er en TXLSPasswordEvent med en var PassWord: WideString og en var Retry: Boolean; sæt Retry til True for at prøve igen, op til tre forsøg:
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: adgangskode påkrævet, men ingen angivet; -1005: forkert adgangskode
if Rc <> 1 then
raise Exception.CreateFmt('Cannot open %s (code %d)', [FileName, Rc]);
ShowMessage(VarToStr(Wb.Sheets[1].Range['A1', 'A1'].Value));
end;
Kender du allerede adgangskoden, springer Open(FileName, APassWord) eventet helt over
Er XOR-obfuscation sikker nok til noget?
BIFF XOR-obfuscation er ikke kryptering og beskytter intet mod en motiveret læser. Adgangskodetjekket er en 16-bit verificator, nøglen er 16 bit, og det 16-byte store array gentages gennem hele streamen, så forudsigelige BIFF-record-indhold afslører array-bytes uden nogen adgangskode overhovedet. HotXLS skriver kun XOR, fordi Excel 5.0/95-filer ikke har andre muligheder, og grunden til at producere sådanne filer i dag er en legacy-forbruger, der ikke kan læse noget nyere
Den klassiske motor vælger skemaet gennem TXLSWorkbook.EncryptionType, og kombinationen med gemmeformatet tjekkes strengt:
xletAuto(default) skriver RC4 CryptoAPI tilxlExcel97og XOR tilxlExcel5, i overensstemmelse med, hvad Excel selv skrev for hvert formatxletXorer kun gyldig til BIFF5; medxlExcel97udløser gemningen en exception i stedet for lydløst at falde tilbagexletRC4ogxletRC4CryptoAPIer kun BIFF8, og at bede om dem ved en BIFF5-gemning udløser også en exception
RC4 er også dateret, og detaljerne om at få den til at interoperere er dækket i hvorfor Excel afviser en krypteret workbook med en korrekt adgangskode. Kan modtageren læse XLSX, så brug XLSX-motoren i stedet: TXLSXWorkbook.SaveAsEncryptedAgile skriver Agile Encryption (SHA-512-adgangskodehashing med en spin count på 100.000 iterationer og AES-256-CBC), det format, Excel 2010 og senere skriver som default, mens SaveAsEncrypted skriver den ældre AES-128 Standard Encryption. Afvejningerne mellem de to står i kryptering af XLSX-filer med AES i Delphi, og læsesiden er dækket i læsning af Agile-krypterede Excel-filer med HotXLS
Hurtig reference: BIFF XOR-obfuscation, som Excel 16 accepterer
- FILEPASS (
$002F) følger globals BOF; i BIFF5 er dens body 4 bytes: nøglen, derefter verificatoren - Nøgle =
CreateXorKey_Method1(password)iht. [MS-OFFCRYPTO] §2.3.7.2, aldrig tilfældig; forsecreter den$014D - Array = adgangskode-bytes + pad, XOR den lave nøglebyte ved lige positioner og den høje nøglebyte ved ulige positioner, derefter XorRor (rotér 1 mod højre)
- Kryptér en byte: rotér 5 mod venstre, derefter XOR; dekryptér: XOR, derefter rotér 5 mod højre (§2.3.7.3)
- XorArrayIndex = (byteens stream-offset + record-datalængde) mod 16; headere og klartekst-præfikser tælles med i offsettet
- BOUNDSHEET holder sine første 4 bytes i klartekst; BOF, FILEPASS og INTERFACEHDR forbliver helt i klartekst
- Adgangskoder: ASCII, højst 15 tegn
- HotXLS: indeks rettet i v2.384.47, nøgle og XorRor rettet i v2.384.54, ældre HotXLS-XOR-filer genkendt ved nøgle-mismatch
- Til reel beskyttelse: brug mindst BIFF8 RC4 CryptoAPI, eller XLSX Agile Encryption
HotXLS håndterer BIFF5- og BIFF8-adgangskodebeskyttelse, XLSX Standard og Agile Encryption og read-side-adgangskode-callback fra ét Delphi- og C++Builder-bibliotek, med interoperabilitetsdetaljerne ovenfor håndteret for dig. Se HotXLS Delphi spreadsheet-komponenten for udgaver, platforme og en trial-download