HotXLS zapisuje Excel 5.0/95 (BIFF5) radne knjige zaštićene XOR obfuskacijom koje Excel 16 otvara samo kad se tri detalja točno poklapaju s [MS-OFFCRYPTO]: FILEPASS ključ mora biti CreateXorKey_Method1(password), 16-bajtno XOR polje mora se graditi s XorRor (rotacija udesno za jedan bit), a svaki bajt mora koristiti XorArrayIndex = (pomak toka + duljina zapisa) mod 16. HotXLS je indeks pogodio u v2.384.47, a ključ i rotaciju u v2.384.54. Prije toga, svaka datoteka BIFF5 zaštićena lozinkom koju je proizveo otvarala se lijepo u HotXLS-u i padala u Excelu
Ta posljednja rečenica jest cijela priča u malom. Čitač i pisač koji dijele istu krivu ideju savršeno se slažu jedan s drugim, pa testovi povratnog ciklusa ostaju zeleni dok jedini potrošač koji je važan govori ne. Excel 16 je rekao ne dvaput, s dvije različite poruke, a svaka je poruka pokazivala na drugi sloj sheme. Ovaj članak prolazi kroz te slojeve redoslijedom kojim ih Excel provjerava, s detaljima na razini bajta koji vam koriste bez obzira zovete li HotXLS ili pišete vlastiti BIFF čitač
Što BIFF XOR obfuskacija uopće pohranjuje?
BIFF XOR obfuskacija u datoteci pohranjuje samo dvije 16-bitne riječi, a sve ostalo preračunava se iz lozinke. Zapis FILEPASS ($002F) sjedi odmah iza globals BOF-a radne knjige, a u BIFF5 datoteci njegovo je tijelo točno 4 bajta: XOR ključ iza kojeg slijedi verifier lozinke. Nema soli, nema identifikatora algoritma nema ni šifriranog verifier bloba kakav nose RC4 i AES sheme
Iz te dvije riječi čitač ponovno gradi tri stvari:
- Verifier, 16-bitni hash bajtova lozinke XOR-ovanih s
$CE4B. Usporedba s pohranjenom riječi jest provjera lozinke, i jedina - XOR ključ, 16-bitna vrijednost iz
CreateXorKey_Method1u [MS-OFFCRYPTO] §2.3.7.2, kojeg pokreću dvije konstantne tablice (InitialCode, 15 riječi, iXorMatrix, 105 riječi) - XOR polje, 16 bajtova od bajtova lozinke dopunjenih fiksnim 16-bajtnim punjenjem, svaki XOR-ovan s nižim bajtom ključa (parne pozicije) ili višim bajtom ključa (neparne pozicije), pa rotiran udesno za jedan bit
Zaglavlja zapisa ostaju u običnom tekstu, a tako i gomilica cijelih zapisa koje shema izuzima, među njima BOF, FILEPASS i INTERFACEHDR. Svako drugo tijelo zapisa transformira se bajt po bajt: rotacija ulijevo za 5 bitova, pa XOR s jednim unosom 16-bajtnog polja. Dešifriranje, koje [MS-OFFCRYPTO] §2.3.7.3 izlaže kao DecryptData_Method1, jest zrcalna slika: prvo XOR, pa rotacija udesno za 5
U HotXLS-u ničega od ovoga ne dirate izravno. Postavite lozinku, odaberite format, a SaveAs emitira FILEPASS i transformira tok:
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 podržava samo XOR obfuskaciju; i xletAuto bi odabrao nju
Wb.EncryptionType := xletXor;
// Držite se ASCII-a i najviše 15 znakova (vidi niže)
Wb.EncryptionPassword := 'secret';
if Wb.SaveAs(FileName, xlExcel5) <> 1 then
raise Exception.Create('BIFF5 save failed');
end;
Zašto Excel govori da je lozinka kriva kad se verifier poklapa?
Excel odbija lozinku jer ne vjeruje pohranjenom ključu: Excel izvodi ključ iz upisane lozinke s CreateXorKey_Method1 i uspoređuje ga s FILEPASS ključnom riječi, pa datoteka čiji je ključ bilo što drugo pada na provjeri lozinke i kad je verifier ispravan. Specifikacija opisuje ključ kao izlaz lozinke, a ne kao slobodan parametar, i Excel 16 provodi to čitanje
HotXLS pisač prije v2.384.54 punio je ključnu riječ s dva slučajna bajta. Na papiru to izgleda bezopasno, jer je verifier dokumentirana provjera lozinke, a polje se gradi iz kojeg god ključa datoteka deklarira. HotXLS je sam čitao te datoteke bez problema, jer je njegov čitač ključ iz FILEPASS-a uzimao zadanom činjenicom. Excel 16, primivši istu datoteku i ispravnu lozinku, odgovorio je da lozinka nije ispravna. Od v2.384.54 ključ se izvodi, pa FILEPASS za lozinku secret uvijek drži ključ $014D i verifier $DAA7, vrijednosti unakrižno provjerene neovisnom implementacijom specifikacije
Samo izvođenje kratko je čim su dvije tablice na mjestu. Prođite lozinkom unatrag, gledajte bit 6 svakog bajta sedam puta uz pomak ulijevo i XOR-ujte jedan XorMatrix unos svaki put kad je bit postavljen. Slijedi načelna skica koja reproducira algoritam iz specifikacije i poklapa se s HotXLS implementacijom; nije to HotXLS API:
// Načelna skica [MS-OFFCRYPTO] 2.3.7.2 CreateXorKey_Method1
// i CreateXorArray_Method1 (samo ilustracija, nije 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; // ključ vidi samo 15 bajtova
if Len = 0 then
Exit;
Result := InitialCode[Len - 1];
Element := $68; // posljednji XorMatrix unos
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)); // rotacija udesno za jedan 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;
Za secret ovo daje polje 1F 32 17 B9 14 BA 7B 7F 59 DD 59 7F 7A C0 A6 DF, zgodan fixture ako testirate vlastiti čitač
Zašto pravi ključ i dalje proizvodi oštećenu datoteku?
Ispravan ključ i dalje proizvodi oštećenu datoteku kad se XOR polje rotira krivim smjerom: [MS-OFFCRYPTO] definira korak polja kao XorRor, rotaciju udesno za jedan bit, a polje rotirano ulijevo za dva dešifrira svako tijelo zapisa u šum. Popravak ključa gurnuo je Excel 16 preko upita za lozinku ravno u drugu grešku, izvještaj da datoteka ima problem i ne može se otvoriti
Stari HotXLS kod rotirao je svaki bajt polja ulijevo za 2 bita, forma koja kruži u nekoliko BIFF implementacija. Budući da je HotXLS koristio istu rotaciju na obje strane, vlastiti mu čitač nikad nije primijetio. Excel 16 više ne može spremati Excel 5.0/95 datoteke, a XOR ne nudi pri spremanju BIFF8, pa nije bilo nativnog Excel uzorka za usporedbu. Dokazi su morali doći iz drugog smjera: zapisati jedan običan BIFF5 tok, ponovno ga kodirati na osam načina i pustiti Excel 16 da otvori svaku varijantu. Osam je varijanti ukrstilo tri neovisne odluke:
| Odluka | Opcija A | Opcija B |
|---|---|---|
| Rotacija polja | XorRor (rotacija udesno 1) | Rotacija ulijevo 2 |
| Indeks polja | (offset + record length) mod 16 | offset mod 16 |
| Redoslijed transformacije bajta | Rotacija ulijevo 5, pa XOR | XOR, pa rotacija ulijevo 5 |
Excel 16 otvorio je točno dvije od osam: XorRor s rotiraj-pa-XOR i indeksom po duljini zapisa, te jednu varijantu koja se samo čini drugačijom. Rotacija-ulijevo-2 s XOR-pa-rotiraj i istim indeksom ista je funkcija samo prerušena. Rotacija se distribuirá preko XOR-a, pa je rol5(p xor rol2(b)) jednako rol5(p) xor rol7(b), a na 8-bitnoj vrijednosti rotacija ulijevo za 7 jest rotacija udesno za 1. Ukratko, rol5 ∘ rol2 = ror1, pa izolirano gledano polje s rotacijom-ulijevo-2 izgleda vjerodostojno: točno je samo uz obrnuti redoslijed transformacije. U paru s redoslijedom iz specifikacije pokvari svaki transformirani bajt
Isti eksperiment riješio je i drugo pitanje. Varijante koje su izbacile duljinu zapisa iz indeksa sve su pale, čime je potvrđeno pravilo indeksa koje je HotXLS usvojio izdanje ranije na temelju samog teksta specifikacije
Kako se XorArrayIndex računa za svaki bajt?
XorArrayIndex za bajt jest njegov pomak u toku radne knjige plus duljina cijelih podataka zapisa kojem pripada, mod 16. Indeks se zato za svaki zapis ponovno pokreće od vrijednosti koja ovisi o zapisu i povećava za jedan po bajtu unutar njega. Pseudokod iz specifikacije imenuje ulaze FileOffset i Data.Length, što se lako pogrešno pročita kao sam pomak početka zapisa, a to pogrešno čitanje točno je ono što je HotXLS isporučivao do v2.384.47
Tri detalja odlučuju poravnaju li se vaši indeksi s Excelom:
- 4-bajtno zaglavlje zapisa nikad se ne transformira, ali i dalje zauzima pozicije toka, pa prvi bajt tijela zapisa sjedi na pomaku zaglavlja + 4
- Član duljine jest puna duljina podataka zapisa, ne broj bajtova koji su stvarno transformirani
- BOUNDSHEET je djelomično običan: njegova prva 4 bajta,
lbPlyPos, pomak toka BOF-a lista, ostaju čitljivi da parser može locirati listove. Tih 4 bajta transformacija preskače ali se i dalje računaju i u pomak i u duljinu zapisa
Složeno, transformacija po zapisu jest nekoliko redaka. Opet, ovo je skica pravila, ne nešto što trebate zvati:
function Rol8(B: Byte; N: Integer): Byte;
begin
Result := Byte((B shl N) or (B shr (8 - N)));
end;
// Obfuskiraj tijelo jednog zapisa na mjestu. BodyPos jest pomak toka od
// Body[0], tj. pomak zaglavlja zapisa + 4. PlainPrefix je 4 za
// BOUNDSHEET, puna duljina za BOF / FILEPASS, 0 za većinu zapisa
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;
// Čitanje je zrcalna slika: B := Body[I] xor Arr[...];
// pa rotacija udesno za 5, tj. Rol8(B, 3)
Prije v2.384.47 HotXLS čitač računao je indeks iz same pozicije toka, a pisač koristio je obični bajtni pomak. Oboje je ignoriralo duljinu zapisa, pa su opet dvije polovice pristajale jedna na drugu i ni s kim drugim. Neovisno napisan dekoder točno je čitao v2.384.47 izlaz, a stariji izlaz kao smeće, pa je test s osam varijanti u Excelu 16 kasnije potvrdio pravilo protiv stvarnog cilja
Što je s XOR datotekama koje su zapisale starije HotXLS verzije?
HotXLS i dalje čita vlastite XOR datoteke prije v2.384.54 provjerom FILEPASS ključa: kad je pohranjeni ključ jednak ključu izvedenom iz lozinke, čitač gradi XorRor polje iz specifikacije, a kad se razlikuje, čitač datoteku tretira kao stariju HotXLS datoteku i ponovno gradi polje s rotacijom-ulijevo-2. Excelom zapisane datoteke uvijek nose izvedeni ključ, pa uvijek idu putem iz specifikacije
Test je heuristika s točnom stopom pogreške. Stara bi se datoteka čiji se slučajni ključ slučajno poklopio s izvedenim ključem čitala krivim poljem, a šansa za to jest 1 u 65,536. Fallback pokriva samo rotaciju polja; pravilo indeksa ne prebacuje se, pa datoteke koje spašava one su zapisane između v2.384.47 i v2.384.53. Ako još držite BIFF5 XOR datoteke iz tog prozora, otvorite ih s trenutnim HotXLS-om i spremite ih ponovno da dobijete datoteku koju Excel prihvaća
Dva detalja o lozinkama vrijede za svaku datoteku, staru ili novu:
- Duljina.
CreateXorKey_Method1čita samo prvih 15 bajtova lozinke, što je granica iz specifikacije. HotXLS primjenjuje taj strop na ključ i drži verifier i polje na njihovim uobičajenim pravilima pune duljine i 16 bajtova, dosljedno na obje strane. Excel sam odbija lozinke duže od 15 znakova za ovaj format, pa smatrajte 15 stvarnim maksimumom - Skup znakova. HotXLS pretvara lozinku u bajtove kroz sistemsku ANSI kodnu stranicu. Specifikacija opisuje uzimanje nižeg bajta svakog UTF-16 znaka, što se slaže za ASCII. Bez Excel uzoraka zaštićenih ne-ASCII lozinkama nema utemeljene istine o ostalom, pa se držite ASCII lozinki za XOR datoteke
Na strani čitanja TXLSWorkbook.OnPassword vam dopuštra da zatražite lozinku kad Open naiđe na FILEPASS zapis. Događaj je TXLSPasswordEvent s var PassWord: WideString i var Retry: Boolean; postavite Retry na True za novi pokušaj, do tri ponovna pokušaja:
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: lozinka je potrebna ali nije dana; -1005: kriva lozinka
if Rc <> 1 then
raise Exception.CreateFmt('Cannot open %s (code %d)', [FileName, Rc]);
ShowMessage(VarToStr(Wb.Sheets[1].Range['A1', 'A1'].Value));
end;
Ako lozinku već znate, Open(FileName, APassWord) potpuno preskače događaj
Je li XOR obfuskacija dovoljno sigurna za išta?
BIFF XOR obfuskacija nije šifriranje i ne štiti ništa od motiviranog čitača. Provjera lozinke jest 16-bitni verifier, ključ ima 16 bitova, a 16-bajtno se polje ponavlja kroz cijeli tok, pa predvidljivi sadržaji BIFF zapisa izlažu bajtove polja bez ikakve lozinke. HotXLS piše XOR samo jer Excel 5.0/95 datoteke nemaju drugu opciju, a razlog za proizvodnju takvih datoteka danas jest naslijeđeni potrošač koji ne čita ništa novije
Klasični motor bira shemu kroz TXLSWorkbook.EncryptionType, a kombinacija sa formatom spremanja provjerava se strogo:
xletAuto(zadano) piše RC4 CryptoAPI zaxlExcel97i XOR zaxlExcel5, u skladu s onim što je Excel sam zapisivao za svaki formatxletXorvrijedi samo za BIFF5; uzxlExcel97spremanje podiže iznimku umjesto da tiho padne natragxletRC4ixletRC4CryptoAPIsamo su BIFF8, a traženje njih pri spremanju BIFF5 također podiže iznimku
RC4 je isto zastario, a detalji njegove interoperabilnosti pokriveni su u zašto Excel odbija šifriranu radnu knjigu s ispravnom lozinkom. Ako primatelj može čitati XLSX, koristite umjesto toga XLSX motor: TXLSXWorkbook.SaveAsEncryptedAgile piše Agile Encryption (SHA-512 hashiranje lozinke sa spin countom od 100.000 iteracija i AES-256-CBC), format koji Excel 2010 i noviji zapisuju po zadanom, dok SaveAsEncrypted piše starije AES-128 Standard Encryption. Komromise između njih dvoje pokriva šifriranje XLSX datoteka s AES-om u Delphiju, a stranu čitanja pokriva čitanje Agile šifriranih Excel datoteka s HotXLS-om
Brza referenca: BIFF XOR obfuskacija koju Excel 16 prihvaća
- FILEPASS (
$002F) slijedi globals BOF; u BIFF5 njegovo je tijelo 4 bajta: ključ, pa verifier - Ključ =
CreateXorKey_Method1(password)po [MS-OFFCRYPTO] §2.3.7.2, nikad slučajan; zasecretto je$014D - Polje = bajtovi lozinke + punjenje, XOR s nižim bajtom ključa na parnim pozicijama i višim na neparnim pozicijama, pa XorRor (rotacija udesno 1)
- Šifriraj bajt: rotacija ulijevo 5, pa XOR; dešifriraj: XOR, pa rotacija udesno 5 (§2.3.7.3)
- XorArrayIndex = (pomak bajta u toku + duljina podataka zapisa) mod 16; zaglavlja i obični prefiksi računaju se u pomak
- BOUNDSHEET drži svoja prva 4 bajta običnima; BOF, FILEPASS i INTERFACEHDR ostaju potpuno obični
- Lozinke: ASCII, najviše 15 znakova
- HotXLS: indeks popravljen u v2.384.47, ključ i XorRor u v2.384.54, starije HotXLS XOR datoteke otkrivaju se nepoklapanjem ključa
- Za stvarnu zaštitu koristite barem BIFF8 RC4 CryptoAPI, ili XLSX Agile Encryption
HotXLS obrađuje zaštitu lozinkom za BIFF5 i BIFF8, XLSX Standard i Agile Encryption te callback lozinke na strani čitanja iz jedne Delphi i C++Builder knjižnice, s gore navedenim detaljima interoperabilnosti riješenima umjesto vas. Pogledajte HotXLS Delphi proračunsku komponentu za izdanja, platforme i probno preuzimanje