A HotXLS Excel 5.0/95 (BIFF5) XOR-obfuszkált munkafüzeteket ír, amiket az Excel 16 csak akkor nyit meg, ha három részlet pontosan egyezik az [MS-OFFCRYPTO]-val: a FILEPASS kulcs CreateXorKey_Method1(password) kell legyen, a 16 bájtos XOR tömb XorRorral épül (egébites jobra forgatás), és minden bájt XorArrayIndex-e (stream offszet + rekordhossz) mod 16. A HotXLS az indexet v2.384.47-ben, a kulcsot meg a forgatást v2.384.54-ben hozta helyre. Azelőtt minden jelszóval védett BIFF5 fájl, amit produkált, HotXLS-ben szépen nyílt, Excelben nem
Az az utolsó mondat a teljes történet miniatürizálva. Egy olvasó és egy író, ami ugyanazt a rossz gondolatot osztja meg, tökéletesen egyetért egymással, így a körbevisz tesztek zöldek maradnak, miközben az egyetlen fogyasztó, akinek számít, nemet mond. Az Excel 16 kétszer mondott nemet, két különböző üzenettel, és mindegyik üzenet a séma másik rétegére mutatott. Ez a cikk azokat a rétegeket járja abban a sorrendben, ahogy az Excel ellenőriz, bájtszintű részlettel, ami akkor is használható, ha HotXLS-t hívsz, akkor is, ha saját BIFF olvasót írsz
Mit tárol valójában a BIFF XOR obfuszkáció?
A BIFF XOR obfuszkáció csak két 16 bites szót tárol a fájlban, minden mást a jelszóból számolnak újra. A FILEPASS rekord ($002F) közvetlenül a workbook globals BOF után ül, és egy BIFF5 fájlban a teste pontosan 4 bájt: az XOR kulcs, majd a jelszóverifikátor. Nincs só, nincs algoritmusazonosító és nincs olyan titkosított verifikátor blob, amilyet az RC4 és AES sémák hordoznak
Ebből a két szóból egy olvasó három dolgot épít újra:
- A verifikátort, a jelszóbájtok 16 bites hashét
$CE4B-vel XOR-olva. Az összehasonlítása a tárolt szóval a jelszóteszt, és az egyetlen - Az XOR kulcsot, egy 16 bites értéket a [MS-OFFCRYPTO] §2.3.7.2-es
CreateXorKey_Method1-éből, két konstans tábla vezérli (InitialCode, 15 szó, meg azXorMatrix, 105 szó) - Az XOR tömböt, 16 bájtot a jelszóbájtokból egy rögzített 16 bájtos paddal kiegészítve, mindegyiket az alsó kulcsbájttal (páros pozíciók) vagy a felső kulcsbájttal (páratlan pozíciók) XOR-olva, majd egébittel jobra forgatva
A rekordfejek sima szövegben maradnak, ahogy egy maréknyi teljes rekord is, amiket a séma felment, köztük a BOF, FILEPASS és INTERFACEHDR. Minden más rekord teste bájtonként transzformálódik: 5 bittel balra forgatás, majd XOR az egyik 16 bájtos tömbelemmel. A visszafejtés, amit a [MS-OFFCRYPTO] §2.3.7.3-a DecryptData_Method1-ként ír le, a tükörképe: előbb XOR, aztán 5-tel jobra forgatás
HotXLS-ben mindezzel soha nem érintkezel közvetlenül. Állíts be jelszót, válaszd ki a formátumot, és a SaveAs FILEPASS-t bocsát ki, meg átalakítja a streamet:
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;
// A BIFF5 csak XOR obfuszkációt támogat; az xletAuto is ezt választaná
Wb.EncryptionType := xletXor;
// Tartsd ASCII-nek és legfeljebb 15 karakternek (lásd lent)
Wb.EncryptionPassword := 'secret';
if Wb.SaveAs(FileName, xlExcel5) <> 1 then
raise Exception.Create('BIFF5 save failed');
end;
Miért mondja az Excel, hogy rossz a jelszó, amikor a verifikátor egyezik?
Az Excel azért utasítja el a jelszót, mert nem bízik a tárolt kulcsban: az Excel a beütött jelszóból származtatja a kulcsot a CreateXorKey_Method1-gyel, és összeveti a FILEPASS kulcsszóval, így egy fájl, aminek a kulcsa bármi más, elbukja a jelszótesztet akkor is, ha a verifikátor helyes. A specifikáció a kulcsot a jelszó kimeneteként írja le, nem szabad paraméterként, és az Excel 16 érvényre juttatja ezt az olvasatot
A v2.384.54 előtti HotXLS író a kulcsszót két véletlen bájttal töltötte fel. Ez papíron ártalmatlannak néz ki, hiszen a verifikátor a dokumentált jelszóteszt, a tömb pedig abból a kulcsból épül, amit a fájl deklarál. A HotXLS maga gond nélkül olvasta azokat a fájlokat, mert az olvasója a FILEPASS-ból vette a kulcsot, adottnak. Az Excel 16 ugyanazt a fájlt és a helyes jelszót kapva azt felelte, hogy a jelszó nem helyes. v2.384.54 óta a kulcs származtatott, így a secret jelszóhoz a FILEPASS mindig $014D kulcsot és $DAA7 verifikátort hordoz, értékek, amiket a specifikáció egy független implementációjával ellenőriztek
A származtatás maga rövid, ha a két tábla a helyén van. Járd végig a jelszót visszafelé, nézd meg minden bájt 6. bitjét hétszer, miközben balra tolod, és minden beállított bitnél XOR-olj bele egy XorMatrix elemet. Az alábbi elvi vázlat, ami reprodukálja a specifikáció algoritmusát, és egyezik a HotXLS implementációjával; nem HotXLS API:
// A [MS-OFFCRYPTO] 2.3.7.2 CreateXorKey_Method1 elvi vázlata
// meg a CreateXorArray_Method1 (csak illusztráció, nem 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; // a kulcs csak 15 bájtot lát
if Len = 0 then
Exit;
Result := InitialCode[Len - 1];
Element := $68; // utolsó XorMatrix bejegyzés
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)); // egébittel jobra forgatás
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;
A secret-hez ez a 1F 32 17 B9 14 BA 7B 7F 59 DD 59 7F 7A C0 A6 DF tömböt adja, ami kéznél lévő fixture, ha a saját olvasódat tesztelitek
Miért ad a jó kulcs is sérült fájlt?
Egy helyes kulcs is sérült fájlt ad, ha a XOR tömb rossz irányba forgatódik: a [MS-OFFCRYPTO] a tömblépést XorRorként definiálja, egébites jobra forgatásként, és egy kettővel balra forgatott tömb minden rekordtestet zajmá fejt vissza. A kulcs javítása az Excel 16-ot a jelszóprompton túl egy másik hibáig tolt, egy jelentésig, hogy a fájlnak baja van, és nem nyitható meg
A régi HotXLS kód minden tömbbájtot 2 bittel balra forgatott, egy olyan forma, ami több BIFF implementációban kering. Mivel a HotXLS mindkét oldalon ugyanazt a forgatást használta, a saját olvasója soha nem vette észre. Az Excel 16 már nem tud Excel 5.0/95 fájlokat menteni, BIFF8 mentésnél pedig nem kínál XOR-t, így nem volt natív Excel minta, amit diffelni lehetett volna. A bizonyítéknak a másik irányból kellett jönnie: írj ki egy sima szöveges BIFF5 streamet, kódold vissza nyolcféleképpen, és hagyd, hogy az Excel 16 nyissa meg mindegyiket. A nyolc változat három független választást keresztezett:
| Választás | A opció | B opció |
|---|---|---|
| Tömbforgatás | XorRor (egébittel jobra) | 2-vel balra forgatás |
| Tömbindex | (offset + record length) mod 16 | offset mod 16 |
| Bájttranszformáció sorrendje | 5-tel balra forgatás, aztán XOR | XOR, aztán 5-tel balra forgatás |
Az Excel 16 pontosan kettőt nyitott meg a nyolcból: az XorRor-t forgatás-aztán-XORral meg a rekordhosszos indexszel, meg egy változatot, ami csak úgy néz ki másképp. A balra-2 forgatás XOR-aztán-forgatással és ugyanazzal az indexszel ugyanaz a függvény álcában. A forgatás eloszlik a XOR felett, így a rol5(p xor rol2(b)) egyenlő a rol5(p) xor rol7(b)-vel, és egy 8 bites értéken a 7-tel balra forgatás egébittel jobra forgatás. Röviden, rol5 ∘ rol2 = ror1, ezért néz ki hihetően önmagában a balra-2 tömb: csak együtt helyes az ellentétes transzformációs sorrenddel. A specifikáció sorrendjével párosítva minden transzformált bájtot tönkretesz
Ugyanez a kísérlet egy második kérdést is eldöntött. Azok a változatok, amik kihagyták a rekordhosszt az indexből, mind elbuktak, ami megerősítette azt az indexszabályt, amit a HotXLS egy kiadással korábban vett fel pusztán a specifikáció szövege alapján
Hogyan számolódik XorArrayIndex minden bájtra?
Egy bájt XorArrayIndex-e a workbook streambeli offszetje plusz annak a teljes rekordadatnak a hossza, amihez tartozik, mod 16. Az index ezért rekordonként rekordfüggő értékről indul újra, és rekordon belül bájtonként eggyel nő. A specifikáció pszeudokódja FileOffset-nek és Data.Length-nek nevezi a bemeneteket, ami könnyen úgy olvasható félre, hogy csak a rekordkezdő offszetről van szó, és pontosan az a félreolvasás, amit a HotXLS v2.384.47-ig szállított
Három részlet dönti el, hogy az indexeid sorban vannak-e az Excellel:
- A 4 bájtos rekordfej sosem transzformálódik, de streampozíciókat foglal, így egy rekord első testbájtja a fej offszetje + 4 helyen ül
- A hossztag a teljes rekordadat hossza, nem a ténylegesen transzformált bájtok száma
- A BOUNDSHEET részben sima: az első 4 bájtja, a
lbPlyPos, a sheet BOF-jának stream offszetje olvasható marad, hogy a parser megtalálja a sheeket. Az a 4 bájt kimarad a transzformációból, de az offszetbe és a rekordhosszba is beleszámít
Együtt a rekordonkénti transzformáció néhány sor. Megint csak a szabály vázlata ez, nem kell hívnod:
function Rol8(B: Byte; N: Integer): Byte;
begin
Result := Byte((B shl N) or (B shr (8 - N)));
end;
// Egy rekord testének obfuszkálása helyben. A BodyPos a Body[0] stream
// offszetje, azaz a rekordfej offszetje + 4. A PlainPrefix 4 a
// BOUNDSHEET-hez, BOF / FILEPASS-nál a teljes hossz, a legtöbb rekordnál 0
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;
// Az olvasás a tükörképe: B := Body[I] xor Arr[...];
// aztán 5-tel jobra forgatás, azaz Rol8(B, 3)
v2.384.47 előtt a HotXLS olvasója az indexet a streampozícióból számolta önmagában, az író pedig a sima bájt offszetet használta. Mindkettő ignorálta a rekordhosszt, tehát megint a két fél értett egymással, és senki mással. Egy függetlenül írt dekódór a v2.384.47-es kimenetet helyesen olvasta, az öregebbet szemétként, a nyolcváltozatos Excel 16-os teszt pedig később a valódi céllal szemben erősítette meg a szabályt
Mi történik az öregebb HotXLS verziók XOR fájljaival?
A HotXLS továbbra is olvassa a saját v2.384.54 előtti XOR fájljait a FILEPASS kulcs megnézésével: ha a tárolt kulcs egyenlő a jelszóból származtatott kulccsal, az olvasó a specifikáció szerinti XorRor tömböt építi, ha eltér, a fájlt öregebb HotXLS fájlként kezeli, és a balra-2 tömböt építi újra. Az Excel által írt fájlok mindig a származtatott kulcsot hordozzák, így mindig a specifikációs útra kerülnek
A teszt heurisztika, pontos bukási aránnyal. Egy régi fájl, aminek a véletlen kulcsa véletlenül egyezett a származtatottal, rossz tömbbel olvasódott volna, ennek esélye 65 536-ban 1. A fallback csak a tömbforgatást fedi le; az indexszabály nem vált, így azok a fájlok menekülnek meg, amik a v2.384.47 és v2.384.53 közt íródtak. Ha még tartsz BIFF5 XOR fájlokat abból az ablakból, nyisd meg őket a jelenlegi HotXLS-szel, és mentsd újra, hogy olyan fájlt kapj, amit az Excel elfogad
Két jelszórészlet minden fájlra érvényes, régre és újra egyaránt:
- Hossz. A
CreateXorKey_Method1csak az első 15 jelszóbájtot olvassa, ami a specifikációs korlát. A HotXLS ezt a sapkát a kulcsra alkalmazza, a verifikátort meg a tömböt a szokásos teljes hosszú és 16 bájtos szabályaikon tartja, mindkét oldalon következetesen. Maga az Excel 15 karakternél hosszabb jelszavakat utasít el ennél a formátumnál, így kezeld a 15-öt valódi maximumként - Karakterkészlet. A HotXLS a jelszót a rendszer ANSI kódlapján át váltja bájtokká. A specifikáció minden UTF-16 karakter alsó bájtjának levételét írja le, ami ASCII-nél egyezik. Nem ASCII jelszavakkal védett Excel minták nélkül nincs referenciánk a többire, ezért ragaszkodj ASCII jelszavakhoz XOR fájloknál
Az olvasó oldalon a TXLSWorkbook.OnPassword jelszót kérdezhetsz vele, amikor az Open FILEPASS rekordba botlik. Az esemény egy TXLSPasswordEvent, var PassWord: WideString-gel és var Retry: Boolean-nal; állítsd a Retry-t True-ra az újrapróbáláshoz, legfeljebb három próbáig:
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: kell jelszó, de nem érkezett; -1005: rossz jelszó
if Rc <> 1 then
raise Exception.CreateFmt('Cannot open %s (code %d)', [FileName, Rc]);
ShowMessage(VarToStr(Wb.Sheets[1].Range['A1', 'A1'].Value));
end;
Ha a jelszót már ismered, az Open(FileName, APassWord) teljesen kihagyja az eseményt
Biztonságos-e a XOR obfuszkáció bármire?
A BIFF XOR obfuszkáció nem titkosítás, és motivált olvasóval szemben semmit sem véd. A jelszóteszt egy 16 bites verifikátor, a kulcs 16 bites, és a 16 bájtos tömb az egész streamen át ismétlődik, így a kiszámítható BIFF rekordtartalmak tömbbájtokat fednek fel jelszó nélkül is. A HotXLS csak azért ír XOR-t, mert az Excel 5.0/95 fájloknak nincs más opciója, és az ok, hogy ma ilyen fájlt produkálj, egy olyan örökölt fogyasztó, ami semmi újabbat nem tud olvasni
A klasszikus engine a TXLSWorkbook.EncryptionType-on át választja a sémát, és a mentési formátummal való kombinációt szigorúan ellenőrzi:
- Az
xletAuto(alapérték) RC4 CryptoAPI-t írxlExcel97-hez és XOR-txlExcel5-höz, egyezve azzal, amit maga az Excel írt mindegyik formátumhoz - Az
xletXorcsak BIFF5-re érvényes;xlExcel97-gel a mentés kivételt dob, csendes visszaesés helyett - Az
xletRC4ésxletRC4CryptoAPIcsak BIFF8-as, és BIFF5 mentésnél ugyancsak kivételt ad
Az RC4 is idős, és az interoperabilitásának részleteit a miért utasítja el az Excel a titkosított munkafüzetet helyes jelszóval cikk fedi le. Ha a címzett tud XLSX-et olvasni, használd helyette az XLSX enginet: a TXLSXWorkbook.SaveAsEncryptedAgile Agile Encryptiont ír (SHA-512 jelszóhash 100 000 iterációs spin counttal és AES-256-CBC-vel), azt a formátumot, amit az Excel 2010 és újabb alapból ír, míg a SaveAsEncrypted az öregebb AES-128 Standard Encryptiont. A kettő közti kompromisszumok a XLSX fájlok titkosítása AES-szel Delphiben cikkben vannak, az olvasó oldalát pedig a Agile-titkosított Excel fájlok olvasása HotXLS-szel fedi le
Gyorsreferencia: BIFF XOR obfuszkáció, amit az Excel 16 elfogad
- A FILEPASS (
$002F) a globals BOF-ot követi; BIFF5-ben a teste 4 bájt: kulcs, aztán verifikátor - Kulcs =
CreateXorKey_Method1(password)a [MS-OFFCRYPTO] §2.3.7.2 szerint, soha nem véletlen; asecret-hez$014D - Tömb = jelszóbájtok + pad, páros pozíciókon az alsó kulcsbájt, páratlan pozíciókon a felső kulcsbájt XOR-olva, aztán XorRor (egébittel jobra)
- Bájt titkosítása: 5-tel balra forgatás, aztán XOR; visszafejtés: XOR, aztán 5-tel jobra forgatás (§2.3.7.3)
- XorArrayIndex = (bájt stream offszet + rekordadat hossz) mod 16; a fejek és sima prefixek beleszámítanak az offszetbe
- A BOUNDSHEET megtartja az első 4 bájtját simán; a BOF, FILEPASS és INTERFACEHDR teljesen sima marad
- Jelszavak: ASCII, legfeljebb 15 karakter
- HotXLS: index javítva v2.384.47-ben, kulcs és XorRor javítva v2.384.54-ben, öregebb HotXLS XOR fájlok kulcseltérés alapján felismerve
- Valódi védelemhez legalább BIFF8 RC4 CryptoAPI, vagy XLSX Agile Encryption
A HotXLS kezeli a BIFF5 és BIFF8 jelszavakat, az XLSX Standard és Agile Encryptiont, meg az olvasó oldali jelszócallbacket egyetlen Delphi és C++Builder libraryből, a fenti interoperabilitási részletekkel együtt kezelve. Kiadásokért, platformokért és próbaverzióért lásd a HotXLS Delphi spreadsheet component oldalt