HotXLS skriver Excel 5.0/95-arbetsböcker (BIFF5) med XOR-obfuskering som Excel 16 bara öppnar när tre detaljer stämmer exakt med [MS-OFFCRYPTO]: FILEPASS-nyckeln måste vara CreateXorKey_Method1(password), 16-bytes-XOR-matrisen måste byggas med XorRor (rotera höger en bit), och varje byte måste använda XorArrayIndex = (strömoffset + postlängd) mod 16. HotXLS fick indexet rätt i v2.384.47 och nyckeln och rotationen rätt i v2.384.54. Innan dess öppnades varje lösenordsskyddad BIFF5-fil som den producerade felfritt i HotXLS och misslyckades i Excel
Den sista meningen är hela historien i miniatyr. En läsare och en skrivare som delar samma felaktiga idé håller med varandra perfekt, så rundturntesterna förblir gröna medan den enda konsument som spelar roll säger nej. Excel 16 sa nej två gånger, med två olika meddelanden, och varje meddelande pekade på ett annat lager i schemat. Den här artikeln går igenom de lagren i den ordning Excel kontrollerar dem, med detaljer på bytenivå som du kan använda oavsett om du anropar HotXLS eller skriver din egen BIFF-läsare
Vad lagrar BIFF XOR-obfuskering egentligen?
BIFF XOR-obfuskering lagrar bara två 16-bitarsord i filen, och allt annat räknas om från lösenordet. FILEPASS-posten ($002F) sitter direkt efter arbetsbokens globals-BOF, och i en BIFF5-fil är dess kropp exakt 4 byte: XOR-nyckeln följt av lösenordsverifieraren. Det finns inget salt, ingen algoritmidentifierare och ingen krypterad verifier-blob av det slag som RC4- och AES-scheman bär på
Från de två orden bygger en läsare upp tre saker:
- En verifierare, en 16-bitars hash av lösenordsbytena XOR:ade med
$CE4B. Att jämföra den med det lagrade ordet är lösenordskontrollen, och den enda - En XOR-nyckel, ett 16-bitarsvärde från
CreateXorKey_Method1i [MS-OFFCRYPTO] §2.3.7.2, driven av två konstanta tabeller (InitialCode, 15 ord, ochXorMatrix, 105 ord) - En XOR-matris, 16 byte bestående av lösenordsbytena utfyllda med ett fast 16-byte-fyllnadsmönster, vart och ett XOR:at med lågt nyckelbyte (jämna positioner) eller högt nyckelbyte (udda positioner), därefter roterat höger en bit
Posthuvuden förblir i klartext, och det gör också en handfull hela poster som schemat undantar, bland dem BOF, FILEPASS och INTERFACEHDR. Varje annan postkropp transformeras byte för byte: rotera vänster 5 bitar, sedan XOR med en post i 16-byte-matrisen. Dekryptering, som [MS-OFFCRYPTO] §2.3.7.3 stavar ut som DecryptData_Method1, är spegelbilden: XOR först, sedan rotera höger 5
I HotXLS rör du aldrig något av detta direkt. Sätt ett lösenord, välj formatet, och SaveAs avger FILEPASS och transformerar 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ödjer bara XOR-obfuskering; xletAuto skulle välja det också
Wb.EncryptionType := xletXor;
// Håll det i ASCII och högst 15 tecken (se nedan)
Wb.EncryptionPassword := 'secret';
if Wb.SaveAs(FileName, xlExcel5) <> 1 then
raise Exception.Create('BIFF5 save failed');
end;
Varför säger Excel att lösenordet är fel när verifieraren stämmer?
Excel avvisar lösenordet för att det inte litar på den lagrade nyckeln: Excel härleder nyckeln från det inskrivna lösenordet med CreateXorKey_Method1 och jämför den med FILEPASS-nyckelordet, så en fil vars nyckel är något annat faller på lösenordskontrollen även när verifieraren är korrekt. Specifikationen beskriver nyckeln som en utdata från lösenordet, inte som en fri parameter, och Excel 16 driver igenom den läsningen
HotXLS-skrivaren före v2.384.54 fyllde nyckelordet med två slumpmässiga byte. Det ser harmlöst ut på papperet, eftersom verifieraren är den dokumenterade lösenordskontrollen och matrisen byggs från vilken nyckel filen än deklarerar. HotXLS själv läste de filerna utan problem, för dess läsare tog nyckeln från FILEPASS som given. Excel 16, med samma fil och rätt lösenord, svarade att lösenordet inte var korrekt. Sedan v2.384.54 härleds nyckeln, så FILEPASS för lösenordet secret innehåller alltid nyckeln $014D och verifieraren $DAA7, värden korskontrollerade mot en oberoende implementation av specifikationen
Härledningen i sig är kort när de två tabellerna är på plats. Gå lösenordet baklänges, titta på bit 6 i varje byte sju gånger medan du skiftar den vänster, och XOR:a in en XorMatrix-post varje gång biten är satt. Följande är en principiell skiss som reproducerar specifikationens algoritm och stämmer med HotXLS-implementationen; det är inte ett HotXLS-API:
// Principiell skiss av [MS-OFFCRYPTO] 2.3.7.2 CreateXorKey_Method1
// och CreateXorArray_Method1 (endast illustration, inte ett 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; // nyckeln ser bara 15 byte
if Len = 0 then
Exit;
Result := InitialCode[Len - 1];
Element := $68; // sista XorMatrix-posten
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)); // rotera höger en 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;
För secret ger detta matrisen 1F 32 17 B9 14 BA 7B 7F 59 DD 59 7F 7A C0 A6 DF, ett praktiskt fast testvärde om du testar din egen läsare
Varför ger rätt nyckel ändå en skadad fil?
En korrekt nyckel ger ändå en skadad fil när XOR-matrisen roteras åt fel håll: [MS-OFFCRYPTO] definierar matrissteget som XorRor, en rotation höger en bit, och en matris roterad vänster två dekrypterar varje postkropp till brus. Att fixa nyckeln flyttade Excel 16 förbi lösenordsfrågan och rakt in i ett annat fel, ett meddelande om att filen har ett problem och inte kan öppnas
Den gamla HotXLS-koden roterade varje matrisbyte vänster 2 bitar, en variant som cirkulerar i flera BIFF-implementationer. Eftersom HotXLS använde samma rotation på båda sidor märkte dess egen läsare aldrig något. Excel 16 kan inte längre spara Excel 5.0/95-filer, och det erbjuder inte XOR när BIFF8 sparas, så det fanns inget inhemskt Excel-exempel att diffa mot. Bevisen fick komma från andra hållet: skriv en BIFF5-ström i klartext, koda om den på åtta sätt, och låt Excel 16 öppna varje variant. De åtta varianterna korsade tre oberoende val:
| Val | Alternativ A | Alternativ B |
|---|---|---|
| Matrisrotation | XorRor (rotera höger 1) | Rotera vänster 2 |
| Matrisindex | (offset + postlängd) mod 16 | offset mod 16 |
| Ordning för byte-transformation | Rotera vänster 5, sedan XOR | XOR, sedan rotera vänster 5 |
Excel 16 öppnade exakt två av de åtta: XorRor med rotera-sedan-XOR och postlängdsindexet, samt en variant som bara ser annorlunda ut. Rotera-vänster-2 med XOR-sedan-rotera och samma index är samma funktion i förklädnad. Rotation distribuerar över XOR, så rol5(p xor rol2(b)) är lika med rol5(p) xor rol7(b), och på ett 8-bitarsvärde är en rotation vänster 7 en rotation höger 1. Kort sagt är rol5 ∘ rol2 = ror1, vilket är därför matrisen med rotera-vänster-2 ser rimlig ut i isolering: den är korrekt bara tillsammans med den omvända transformeringsordningen. Tillsammans med specifikationens ordning förstör den varje transformerad byte
Samma experiment avgjorde en andra fråga. Varianterna som släppte postlängden ur indexet misslyckades alla, vilket bekräftade indexregeln HotXLS hade antagit en utgåva tidigare enbart med specifikationstexten som stöd
Hur beräknas XorArrayIndex för varje byte?
XorArrayIndex för en byte är dess offset i arbetsboksströmmen plus längden på hela den postdata den tillhör, mod 16. Indexet startar alltså om på ett postspecifikt värde för varje post och ökar med ett per byte inuti den. Specifikationens pseudokod namnger ingångarna FileOffset och Data.Length, vilket är lätt att missläsa som enbart postens startoffset, och det misset är exakt vad HotXLS levererade fram till v2.384.47
Tre detaljer avgör om dina index stämmer med Excel:
- Postheadern på 4 byte transformeras aldrig, men den upptar ändå strömmens positioner, så en posts första kroppsbyte ligger på headerns offset + 4
- Längdtermen är hela postdatalängden, inte antalet byte som faktiskt transformeras
- BOUNDSHEET är delvis klartext: dess första 4 byte,
lbPlyPos, strömoffseten för bladets BOF, förblir läsbara så att en parser kan hitta bladen. Dessa 4 byte hoppas över av transformationen men räknas ändå med i både offseten och postlängden
Satt ihop är transformationen per post några rader. Även detta är en skiss av regeln, inte något du behöver anropa:
function Rol8(B: Byte; N: Integer): Byte;
begin
Result := Byte((B shl N) or (B shr (8 - N)));
end;
// Obfuskera en postkropp på plats. BodyPos är strömoffseten för
// Body[0], dvs. postheaderns offset + 4. PlainPrefix är 4 för
// BOUNDSHEET, hela längden för BOF / FILEPASS, 0 för de flesta 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;
// Läsning är spegelbilden: B := Body[I] xor Arr[...];
// sedan rotera höger 5, dvs. Rol8(B, 3)
Före v2.384.47 beräknade HotXLS-läsaren indexet enbart från strömpositionen och skrivaren använde den kryptofria byte-offseten. Båda ignorerade postlängden, så återigen höll de två halvorna med varandra och med ingen annan. En oberoende skriven dekoder läste v2.384.47-utdata korrekt och den äldre utdatan som skräp, och åtta-variant-testet mot Excel 16 bekräftade senare regeln mot det verkliga målet
Vad händer med XOR-filer skrivna av äldre HotXLS-versioner?
HotXLS fortsätter läsa sina egna XOR-filer från före v2.384.54 genom att kontrollera FILEPASS-nyckeln: när den lagrade nyckeln är lika med nyckeln härledd från lösenordet bygger läsaren specifikationens XorRor-matris, och när den skiljer sig behandlar läsaren filen som en äldre HotXLS-fil och bygger upp matrisen med rotera-vänster-2. Filer skrivna av Excel bär alltid den härledda nyckeln, så de tar alltid specifikationens väg
Testet är en heuristik med en exakt felfrekvens. En gammal fil vars slumpmässiga nyckel råkade vara lika den härledda skulle läsas med fel matris, och sannolikheten för det är 1 på 65 536. Fallbacken täcker bara matrisrotationen; indexregeln byts inte, så de filer den räddar är de som skrevs mellan v2.384.47 och v2.384.53. Har du fortfarande BIFF5 XOR-filer från det fönstret, öppna dem med aktuella HotXLS och spara dem igen för att få en fil Excel accepterar
Två lösenordsdetaljer gäller varje fil, gammal som ny:
- Längd.
CreateXorKey_Method1läser bara de första 15 byte av lösenordet, vilket är specifikationens gräns. HotXLS tillämpar det taket på nyckeln och håller verifieraren och matrisen på sina vanliga regler för full längd och 16 byte, konsekvent på båda sidor. Excel själv vägrar lösenord längre än 15 tecken för det här formatet, så betrakta 15 som det verkliga maximet - Teckenuppsättning. HotXLS konverterar lösenordet till byte via systemets ANSI-kodsida. Specifikationen beskriver att man tar det låga bytet av varje UTF-16-tecken, vilket stämmer för ASCII. Utan Excel-exempel skyddade av icke-ASCII-lösenord finns ingen sanning att jämföra med för resten, så håll dig till ASCII-lösenord för XOR-filer
På lässidan låter TXLSWorkbook.OnPassword dig fråga efter ett lösenord när Open stöter på en FILEPASS-post. Eventet är en TXLSPasswordEvent med en var PassWord: WideString och en var Retry: Boolean; sätt Retry till True för att försöka igen, upp till tre försö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: lösenord krävs men inget angivet; -1005: fel lösenord
if Rc <> 1 then
raise Exception.CreateFmt('Cannot open %s (code %d)', [FileName, Rc]);
ShowMessage(VarToStr(Wb.Sheets[1].Range['A1', 'A1'].Value));
end;
Känner du redan lösenordet hoppar Open(FileName, APassWord) över eventet helt
Är XOR-obfuskering säker nog för något alls?
BIFF XOR-obfuskering är inte kryptering och skyddar inget mot en motiverad läsare. Lösenordskontrollen är en 16-bitars verifierare, nyckeln är 16 bitar, och 16-byte-matrisen upprepas genom hela strömmen, så förutsägbara BIFF-postinnehåll avslöjar matrisens byte utan något lösenord alls. HotXLS skriver XOR bara för att Excel 5.0/95-filer inte har något annat alternativ, och skälet att producera sådana filer idag är en äldre konsument som inte kan läsa något nyare
Den klassiska motorn väljer schema via TXLSWorkbook.EncryptionType, och kombinationen med sparaformatet kontrolleras strikt:
xletAuto(standard) skriver RC4 CryptoAPI förxlExcel97och XOR förxlExcel5, i linje med vad Excel själv skrev för varje formatxletXorär giltigt bara för BIFF5; medxlExcel97kastar sparandet ett undantag i stället för att tyst falla tillbakaxletRC4ochxletRC4CryptoAPIär bara för BIFF8, och att begära dem vid ett BIFF5-sparande kastar också ett undantag
RC4 är daterat också, och detaljerna för att få det att samverka tas upp i varför Excel avvisar en krypterad arbetsbok med ett korrekt lösenord. Kan mottagaren läsa XLSX, använd XLSX-motorn i stället: TXLSXWorkbook.SaveAsEncryptedAgile skriver Agile Encryption (SHA-512-lösenordshashning med en spin count på 100 000 iterationer och AES-256-CBC), formatet Excel 2010 och senare skriver som standard, medan SaveAsEncrypted skriver den äldre AES-128 Standard Encryption. Avvägningarna mellan dem finns i kryptering av XLSX-filer med AES i Delphi, och lässidan tas upp i läsning av Agile-krypterade Excel-filer med HotXLS
Snabbreferens: BIFF XOR-obfuskering som Excel 16 accepterar
- FILEPASS (
$002F) följer globals-BOF; i BIFF5 är dess kropp 4 byte: nyckeln, sedan verifieraren - Nyckel =
CreateXorKey_Method1(password)enligt [MS-OFFCRYPTO] §2.3.7.2, aldrig slumpmässig; försecretär den$014D - Matris = lösenordsbyte + fyllnadsmönster, XOR med lågt nyckelbyte på jämna positioner och högt på udda, sedan XorRor (rotera höger 1)
- Kryptera en byte: rotera vänster 5, sedan XOR; dekryptera: XOR, sedan rotera höger 5 (§2.3.7.3)
- XorArrayIndex = (bytes strömoffset + postdatalängd) mod 16; huvuden och klartextprefix räknas med i offseten
- BOUNDSHEET håller sina första 4 byte i klartext; BOF, FILEPASS och INTERFACEHDR förblir helt i klartext
- Lösenord: ASCII, högst 15 tecken
- HotXLS: index fixat i v2.384.47, nyckel och XorRor fixat i v2.384.54, äldre HotXLS XOR-filer upptäcks via nyckelmismatch
- För verkligt skydd använd BIFF8 RC4 CryptoAPI som minimum, eller XLSX Agile Encryption
HotXLS hanterar BIFF5- och BIFF8-lösenordsskydd, XLSX Standard och Agile Encryption och lässidans lösenordsåteranrop från ett enda Delphi- och C++Builder-bibliotek, med interop-detaljerna ovan åtgärdade åt dig. Se HotXLS Delphi spreadsheet component för utgåvor, plattformar och en nedladdningsbar testversion