HotXLS rašo Excel 5.0/95 (BIFF5) XOR užkoduotas darbaknyges, kurias Excel 16 atidaro tik tada, kai trys detalės sutampa su [MS-OFFCRYPTO] iki taško: FILEPASS raktas turi būti CreateXorKey_Method1(password), 16 baitų XOR masyvas turi būti sudarytas su XorRor (sukimas į dešinę vienu bitu), o kiekvienas baitas turi naudoti XorArrayIndex = (srauto poslinkis + įrašo ilgis) mod 16. HotXLS indeksą sutvarkė v2.384.47, o raktą ir sukimą – v2.384.54. Iki to visi jo pagaminti su slaptažodžiu apsaugoti BIFF5 failai HotXLS atverdavo be nusiskundimų, o Excel – ne
Tas paskutinis sakinys – visa istorija miniatiūroje. Skaitytuvas ir rašytojas, dalijantys tą pačią klaidingą idėją, vienas su kitu sutaria puikiai, tad round-trip testai žaliuoja, kol vienintelis svarbus vartotojas sako ne. Excel 16 du kartus pasakė ne, su dviem skirtingais pranešimais, ir kiekvienas pranešimas rodė į kitą schemos sluoksnį. Šis straipsnis per tuos sluoksnius eina ta tvarka, kuria juos tikrina Excel, su baitų lygio detalėmis, kurios pravers ir kviečiant HotXLS, ir rašant savą BIFF skaitytuvą
Ką iš tikrųjų saugo BIFF XOR užkodavimas?
BIFF XOR užkodavimas faile saugo tik du 16 bitų žodžius, o viskas kita perskaičiuojama iš slaptažodžio. FILEPASS įrašas ($002F) stovi iškart po darbaknygės globals BOF, o BIFF5 faile jo kūnas yra tiksliai 4 baitai: XOR raktas, paskui slaptažodžio verifikatorius. Jokios druskos (salt), jokio algoritmo identifikatoriaus ir jokio užšifruoto verifikatoriaus bloko, kokius nešioja RC4 ir AES schemos
Iš tų dviejų žodžių skaitytuvas atkuria tris dalykus:
- verifikatorių – 16 bitų maišą iš slaptažodžio baitų, apdorotų XOR su
$CE4B. Jo palyginimas su saugomu žodžiu yra slaptažodžio patikra, ir vienintelė - XOR raktą – 16 bitų reikšmę iš [MS-OFFCRYPTO] §2.3.7.2
CreateXorKey_Method1, varomą dviejų konstantinių lentelių (InitialCode, 15 žodžių, irXorMatrix, 105 žodžiai) - XOR masyvą – 16 baitų iš slaptažodžio baitų, papildytų fiksuota 16 baitų paketaine, kiekvienas apdorotas XOR su žemu rakto baitu (lyginėse pozicijose) arba aukštu rakto baitu (nelyginėse), tada sukamas į dešinę vienu bitu
Įrašų antraštės lieka grynatekstės, kaip ir saujelė visų įrašų, kuriuos schema atleidžia, tarp jų BOF, FILEPASS ir INTERFACEHDR. Kiekvienas kitas įrašo kūnas transformuojamas baitas po baito: sukimas į kairę 5 bitais, tada XOR su vienu iš 16 baitų masyvo elementų. Iššifravimas, kurį [MS-OFFCRYPTO] §2.3.7.3 išraiškingai vadina DecryptData_Method1, yra veidrodinis atspindys: pirmiau XOR, tada sukimas į dešinę 5
HotXLS programoje prie viso to tiesiogiai neliečiate. Nustatote slaptažodį, pasirenkate formatą, ir SaveAs išduoda FILEPASS bei transformuoja srautą:
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 palaiko tik XOR užkodavimą; xletAuto pasirinktų tą patį
Wb.EncryptionType := xletXor;
// Laikykite ASCII ir ne ilgesnį kaip 15 simbolių (žr. žemiau)
Wb.EncryptionPassword := 'secret';
if Wb.SaveAs(FileName, xlExcel5) <> 1 then
raise Exception.Create('BIFF5 save failed');
end;
Kodėl Excel sako, kad slaptažodis netinka, kai verifikatorius sutampa?
Excel atmeta slaptažodį, nes nepasitiki saugomu raktu: Excel raktą išveda iš įvesto slaptažodžio su CreateXorKey_Method1 ir lygina su FILEPASS rakto žodžiu, tad failas, kurio raktas kitoks, žlugdo slaptažodžio patikrą net tada, kai verifikatorius teisingas. Specifikacija raktą apibūdina kaip slaptažodžio išvestį, o ne laisvą parametrą, ir Excel 16 to laikosi
HotXLS rašytojas iki v2.384.54 rakto žodį užpildydavo dviem atsitiktiniais baitais. Popieriuje tai atrodo nekenksminga, nes verifikatorius yra dokumentuota slaptažodžio patikra, o masyvas sudaromas iš to rakto, kurį failas deklaruoja. Pats HotXLS tuos failus skaitė be rūpesčių, nes jo skaitytuvas raktą iš FILEPASS ėmė kaip duotybę. Excel 16, gavęs tą patį failą ir teisingą slaptažodį, atsakydavo, kad slaptažodis netinka. Nuo v2.384.54 raktas išvedamas, tad FILEPASS slaptažodžiui secret visada laiko raktą $014D ir verifikatorių $DAA7, o šios reikšmės suskaičiuotos su nepriklausoma specifikacijos implementacija
Pati išvestis trumpa, kai abi lentelės vietoje. Eikite slaptažodį atgal, žiūrėkite į kiekvieno baito 6 bitą septynis kartus, jį stumdydami į kairę, ir kaskart, kai bitas nustatytas, įmaišykite XOR vieną XorMatrix elementą. Žemiau – principinis eskizas, atkuriantis specifikacijos algoritmą ir sutampantis su HotXLS implementacija; tai nėra HotXLS API:
// [MS-OFFCRYPTO] 2.3.7.2 CreateXorKey_Method1 principinis eskizas
// ir CreateXorArray_Method1 (iliustracija, ne 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; // raktas mato tik 15 baitų
if Len = 0 then
Exit;
Result := InitialCode[Len - 1];
Element := $68; // paskutinis XorMatrix elementas
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)); // sukimas į dešinę vienu bitu
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;
Slaptažodžiui secret tai duoda masyvą 1F 32 17 B9 14 BA 7B 7F 59 DD 59 7F 7A C0 A6 DF – patogus etalonas, jei bandote savą skaitytuvą
Kodėl teisingas raktas vis tiek duoda sugadintą failą?
Teisingas raktas vis tiek duoda sugadintą failą, kai XOR masyvas sukamas netinkama kryptimi: [MS-OFFCRYPTO] masyvo žingsnį apibrėžia kaip XorRor, sukimą į dešinę vienu bitu, o į kairę dviem bitais sukamas masyvas kiekvieną įrašo kūną iššifruoja į triukšmą. Rakto sutvarkymas Excel 16 peržengė pro slaptažodžio raginimą ir nukreipė tiesiai į kitokį įspėjimą – pranešimą, kad faile yra problema ir jo negalima atidaryti
Senas HotXLS kodas kiekvieną masyvo baitą sukdydavo į kairę 2 bitais – forma, kuri sklinda po kelias BIFF implementacijas. Kadangi HotXLS tą patį sukimą naudojo abiejose pusėse, jo paties skaitytuvas niekada nepastebėdavo. Excel 16 nebepasako Excel 5.0/95 failų, o rašydamas BIFF8 XOR nepasiūlo, tad savo Excel pavyzdžio, su kuo lyginti, nebuvo. Įrodymai teko keliauti iš kitos pusės: parašykite vieną grynatekstį BIFF5 srautą, užkoduokite jį aštuoniais būdais ir leiskite Excel 16 atidaryti kiekvieną variantą. Aštuoni variantai kombinavo tris nepriklausomus pasirinkimus:
| Pasirinkimas | Variantas A | Variantas B |
|---|---|---|
| Masyvo sukimas | XorRor (sukimas į dešinę 1) | Sukimas į kairę 2 |
| Masyvo indeksas | (offset + record length) mod 16 | offset mod 16 |
| Baito transformacijos tvarka | Sukimas į kairę 5, tada XOR | XOR, tada sukimas į kairę 5 |
Excel 16 atidarė tiksliai du iš aštuonių: XorRor su sukimu-tada-XOR ir įrašo ilgio indeksu bei vieną variantą, kuris tik atrodo kitoks. Sukimas-į-kairę-2 su XOR-tada-sukimu ir tuo pačiu indeksu yra ta pati funkcija persirengusi. Sukimas plinta per XOR, tad rol5(p xor rol2(b)) lygu rol5(p) xor rol7(b), o 8 bitų reikšmei sukimas į kairę 7 yra sukimas į dešinę 1. Trumpai, rol5 ∘ rol2 = ror1, todėl sukimo-į-kairę-2 masyvas izoliuotai atrodo tikėtinas: jis teisingas tik kartu su priešinga transformacijos tvarka. Suporuotas su specifikacijos tvarka, jis sugadina kiekvieną transformuotą baitą
Tas pats eksperimentas išsprendė ir antrą klausimą. Variantai, išmetę įrašo ilgį iš indekso, žlugdavo visi, kas patvirtino indekso taisyklę, kurią HotXLS priėmė leidimu anksčiau, remdamasis vien specifikacijos tekstu
Kaip skaičiuojamas XorArrayIndex kiekvienam baitui?
XorArrayIndex baitui yra jo poslinkis darbaknygės sraute, plius viso įrašo duomenų, kuriems jis priklauso, ilgis, mod 16. Indeksas todėl kiekvienam įrašui persišoka į nuo įrašo priklausomą reikšmę ir jame didėja po vienetą baitui. Specifikacijos pseudokodas įvestis vadina FileOffset ir Data.Length, kas lengva perskaityti kaip vien įrašo pradžios poslinkį, ir būtent toks perskaitymas HotXLS platino iki v2.384.47
Trys detalės sprendžia, ar jūsų indeksai sulygsta su Excel:
- 4 baitų įrašo antraštė niekada netransformuojama, bet vis tiek užima srauto pozicijas, tad pirmasis įrašo kūno baitas stovi antraštės poslinkis + 4
- Ilgio narys yra pilnas įrašo duomenų ilgis, o ne tikrųjų transformuotų baitų skaičius
- BOUNDSHEET iš dalies grynas: pirmieji jo 4 baitai,
lbPlyPos, lapo BOF srauto poslinkis, lieka skaitomi, kad parseris surastų lapus. Tie 4 baitai transformacijos praleidžiami, bet vis tiek skaitosi ir į poslinkį, ir į įrašo ilgį
Sudėta kartu, transformacija įrašui – kelios eilutės. Ir vėl: tai taisyklės eskizas, ne ką nors, ką reikia kviesti:
function Rol8(B: Byte; N: Integer): Byte;
begin
Result := Byte((B shl N) or (B shr (8 - N)));
end;
// Užkoduoja vieno įrašo kūną vietoje. BodyPos yra Body[0] srauto poslinkis,
// t. y. įrašo antraštės poslinkis + 4. PlainPrefix yra 4 BOUNDSHEET,
// visas ilgis BOF / FILEPASS, 0 daugumai įrašų
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;
// Skaitymas – veidrodinis atvaizdas: B := Body[I] xor Arr[...];
// tada sukimas į dešinę 5, t. y. Rol8(B, 3)
Iki v2.384.47 HotXLS skaitytuvas indeksą skaičiuodavo vien iš srauto pozicijos, o rašytojas naudodavo grynąjį baito poslinkį. Abu ignoruodavo įrašo ilgį, tad ir vėl abi pusės sutardavo tarpusavyje ir su niekuo kitu. Nepriklausomai parašytas dekoderis v2.384.47 išvestį skaitė teisingai, o senesnę – kaip šiukšles, o vėliau aštuonių variantų Excel 16 testas taisyklę patvirtino prieš tikrąjį tikslą
Kas nutinka senesnių HotXLS versijų rašytiems XOR failams?
HotXLS ir toliau skaito savo iki v2.384.54 buvusius XOR failus tikrindamas FILEPASS raktą: kai saugomas raktas lygus rakto išvestei iš slaptažodžio, skaitytuvas sudaro specifikacijos XorRor masyvą, o kai skiriasi – laiko failą senesnio HotXLS failu ir atkuria sukimo-į-kairę-2 masyvą. Excel rašyti failai visada neša išvestą raktą, tad visada eina specifikacijos keliu
Ta patikra – heuristika su tikslia nesėkmės tikimybe. Senas failas, kurio atsitiktinis raktas netyčia sutaptų su išvestu raktu, būtų skaitomas su neteisingu masyvu, o tokios galimybės tikimybė – 1 iš 65 536. Atsarginis kelias dengia tik masyvo sukimą; indekso taisyklė nepersijungia, tad gelbstimi tik tie failai, kurie parašyti tarp v2.384.47 ir v2.384.53. Jei vis dar turite BIFF5 XOR failų iš to laikotarpio, atidarykite juos su esama HotXLS versija ir išsaugokite dar kartą, kad gautumėte failą, kurį priima Excel
Du slaptažodžio niuansai taikomi kiekvienam failui, senam ar naujam:
- Ilgis.
CreateXorKey_Method1skaito tik pirmuosius 15 slaptažodžio baitų – tai specifikacijos riba. HotXLS tą lubas taiko raktui, o verifikatorių ir masyvą laiko ant įprastųjų pilno ilgio ir 16 baitų taisyklių, nuosekliai abiejose pusėse. Pats Excel šiam formatui atmeta ilgesnius kaip 15 simbolių slaptažodžius, tad laikykite 15 tikra riba - Simbolių rinkinys. HotXLS slaptažodį į baitus konvertuoja per sistemos ANSI kodavimo puslapį. Specifikacija aprašo kiekvieno UTF-16 simbolio žemo baito paėmimą, kas sutampa ASCII atveju. Neturėdami Excel pavyzdžių, apsaugotų ne ASCII slaptažodžiais, likusiai daliai tiesos šaltinio neturime, tad XOR failams laikykitės ASCII slaptažodžių
Skaitymo pusėje TXLSWorkbook.OnPassword leidžia paklausti slaptažodžio, kai Open sutinka FILEPASS įrašą. Įvykis yra TXLSPasswordEvent su var PassWord: WideString ir var Retry: Boolean; nustatykite Retry į True, kad bandytumėte dar kartą, iki trijų bandymų:
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: slaptažodis reikalingas, bet nepateiktas; -1005: netinkamas slaptažodis
if Rc <> 1 then
raise Exception.CreateFmt('Cannot open %s (code %d)', [FileName, Rc]);
ShowMessage(VarToStr(Wb.Sheets[1].Range['A1', 'A1'].Value));
end;
Jei slaptažodį jau žinote, Open(FileName, APassWord) įvykio visai nepaliečia
Ar XOR užkodavimas ką nors apsaugo?
BIFF XOR užkodavimas nėra šifravimas ir nuo užsidegusio skaitytojo nepriegauna nieko. Slaptažodžio patikra yra 16 bitų verifikatorius, raktas – 16 bitų, o 16 baitų masyvas kartojasi per visą srautą, tad nuspėjamas BIFF įrašų turinys atidengia masyvo baitus be jokio slaptažodžio. HotXLS rašo XOR tik todėl, kad Excel 5.0/95 failai neturi kitos išeities, o gaminti tokius failus šiandien verta dėl seno vartotojo, kuris nesugeba skaityti nieko naujesnio
Klasikinis variklis schemą pasirenka per TXLSWorkbook.EncryptionType, o jos derinys su išsaugojimo formatu tikrinamas griežtai:
xletAuto(numatytasis) rašo RC4 CryptoAPIxlExcel97ir XORxlExcel5– atitinkamai tai, ką pats Excel rašė kiekvienam formatuixletXorgalioja tik BIFF5; suxlExcel97išsaugojimas meta išimtį vietoj tylio atsitraukimoxletRC4irxletRC4CryptoAPItikri tik BIFF8, o jų prašymas BIFF5 išsaugojime meta išimtį taip pat
RC4 irgi sensta, o jo suderinamumo detalės aprašytos straipsnyje kodėl Excel atmeta užšifruotą darbaknygę su teisingu slaptažodžiu. Jei gavėjas moka skaityti XLSX, naudokite XLSX variklį: TXLSXWorkbook.SaveAsEncryptedAgile rašo Agile Encryption (SHA-512 slaptažodžio maišymą su 100 000 iteracijų spin count ir AES-256-CBC) – formatą, kurį pagal numatymą rašo Excel 2010 ir naujesnės, o SaveAsEncrypted rašo senesnį AES-128 Standard Encryption. Abiejų kompromisus rasite straipsnyje XLSX failų šifravimas su AES Delphi programoje, o skaitymo pusę dengia Agile šifruotų Excel failų skaitymas su HotXLS
Trumpa atmintinė: BIFF XOR užkodavimas, kurį priima Excel 16
- FILEPASS (
$002F) seka po globals BOF; BIFF5 jo kūnas – 4 baitai: raktas, tada verifikatorius - Raktas =
CreateXorKey_Method1(password)pagal [MS-OFFCRYPTO] §2.3.7.2, niekada atsitiktinis; slaptažodžiuisecretjis$014D - Masyvas = slaptažodžio baitai + paketainė, XOR su žemu rakto baitu lyginėse pozicijose ir aukštu nelyginėse, tada XorRor (sukimas į dešinę 1)
- Baito užkodavimas: sukimas į kairę 5, tada XOR; iškodavimas: XOR, tada sukimas į dešinę 5 (§2.3.7.3)
- XorArrayIndex = (baito srauto poslinkis + įrašo duomenų ilgis) mod 16; antraštės ir grynatekstiai priešdėliai skaitosi į poslinkį
- BOUNDSHEET palieka pirmuosius 4 baitus grynatekstius; BOF, FILEPASS ir INTERFACEHDR lieka visiškai grynatekščiai
- Slaptažodžiai: ASCII, ne ilgesni kaip 15 simbolių
- HotXLS: indeksas sutvarkytas v2.384.47, raktas ir XorRor – v2.384.54, senesni HotXLS XOR failai aptinkami pagal rakto nesutapimą
- Tikrai apsaugai naudokite bent BIFF8 RC4 CryptoAPI, o geriau – XLSX Agile Encryption
HotXLS apdoroja BIFF5 ir BIFF8 slaptažodžių apsaugą, XLSX Standard ir Agile Encryption bei skaitymo pusės slaptažodžio atgalinį kvietimą iš vienos Delphi ir C++Builder bibliotekos, o aukščiau minėtos suderinamumo detalės sutvarkytos už jus. Leidimus, platformas ir bandomąją versiją rasite puslapyje HotXLS Delphi skaičiuoklės komponentas