HotXLS écrit des classeurs BIFF5 (Excel 5.0/95) obfusqués par XOR qu’Excel 16 n’ouvre que si trois détails collent exactement à [MS-OFFCRYPTO] : la clé FILEPASS doit être CreateXorKey_Method1(password), le tableau XOR de 16 octets doit être construit avec XorRor (rotation à droite d’un bit), et chaque octet doit utiliser XorArrayIndex = (décalage de flux + longueur d’enregistrement) mod 16. HotXLS a eu l’index juste dès v2.384.47, puis la clé et la rotation dès v2.384.54. Avant, chaque fichier BIFF5 protégé par mot de passe qu’il produisait s’ouvrait parfaitement dans HotXLS et échouait dans Excel
Cette dernière phrase résume toute l’histoire en miniature. Un lecteur et un écrivain qui partagent la même idée fausse se donnent parfaitement raison mutuellement, si bien que les tests aller-retour restent au vert pendant que le seul consommateur qui compte dit non. Excel 16 a dit non deux fois, avec deux messages différents, et chaque message pointait une couche différente du schéma. Cet article parcourt ces couches dans l’ordre où Excel les vérifie, avec des détails au niveau de l’octet utilisables que vous appeliez HotXLS ou que vous écriviez votre propre lecteur BIFF
Que stocke réellement l’obfuscation XOR de BIFF ?
L’obfuscation XOR de BIFF ne stocke que deux mots de 16 bits dans le fichier, et tout le reste est recalculé à partir du mot de passe. L’enregistrement FILEPASS ($002F) siège immédiatement après le BOF des globals du classeur, et dans un fichier BIFF5 son corps fait exactement 4 octets : la clé XOR suivie du vérificateur de mot de passe. Pas de sel, pas d’identifiant d’algorithme, pas de blob de vérificateur chiffré du genre que portent les schémas RC4 et AES
À partir de ces deux mots, un lecteur reconstruit trois choses :
- Le vérificateur, un hash 16 bits des octets du mot de passe XORés avec
$CE4B. Le comparer au mot stocké est la vérification du mot de passe, et la seule - La clé XOR, une valeur 16 bits issue de
CreateXorKey_Method1dans [MS-OFFCRYPTO] §2.3.7.2, pilotée par deux tables constantes (InitialCode, 15 mots, etXorMatrix, 105 mots) - Le tableau XOR, 16 octets faits des octets du mot de passe complétés par un pad fixe de 16 octets, chacun XORé avec l’octet bas de la clé (positions paires) ou l’octet haut (positions impaires), puis pivoté à droite d’un bit
Les en-têtes d’enregistrements restent en clair, ainsi qu’une poignée d’enregistrements entiers que le schéma exempt, parmi eux BOF, FILEPASS et INTERFACEHDR. Tout autre corps d’enregistrement est transformé octet par octet : rotation à gauche de 5 bits, puis XOR avec une entrée du tableau de 16 octets. Le déchiffrement, que [MS-OFFCRYPTO] §2.3.7.3 détaille sous le nom DecryptData_Method1, est l’image miroir : XOR d’abord, puis rotation à droite de 5
Dans HotXLS, vous ne touchez jamais tout cela directement. Réglez un mot de passe, choisissez le format, et SaveAs émet FILEPASS et transforme le flux :
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 ne supporte que l’obfuscation XOR ; xletAuto la choisirait aussi
Wb.EncryptionType := xletXor;
// Restez en ASCII et 15 caractères au plus (voir plus bas)
Wb.EncryptionPassword := 'secret';
if Wb.SaveAs(FileName, xlExcel5) <> 1 then
raise Exception.Create('BIFF5 save failed');
end;
Pourquoi Excel dit-il que le mot de passe est faux alors que le vérificateur correspond ?
Excel rejette le mot de passe parce qu’il ne fait pas confiance à la clé stockée : Excel dérive la clé du mot de passe tapé avec CreateXorKey_Method1 et la compare au mot clé de FILEPASS, donc un fichier dont la clé est autre chose échoue à la vérification du mot de passe même quand le vérificateur est correct. La spécification décrit la clé comme une sortie du mot de passe, pas comme un paramètre libre, et Excel 16 impose cette lecture
L’écrivain HotXLS avant v2.384.54 remplissait le mot de clé avec deux octets aléatoires. Cela semble inoffensif sur le papier, puisque le vérificateur est la vérification de mot de passe documentée et que le tableau est construit à partir de la clé que le fichier déclare. HotXLS lui-même relisait ces fichiers sans problème, parce que son lecteur prenait la clé de FILEPASS comme donnée. Excel 16, devant le même fichier et le bon mot de passe, répondait que le mot de passe n’était pas correct. Depuis v2.384.54 la clé est dérivée, donc FILEPASS pour le mot de passe secret porte toujours la clé $014D et le vérificateur $DAA7, valeurs recoupées contre une implémentation indépendante de la spécification
La dérivation elle-même est courte une fois les deux tables en place. Parcourez le mot de passe à rebours, regardez le bit 6 de chaque octet sept fois en le décalant à gauche, et XORez une entrée de XorMatrix à chaque fois que le bit est à 1. Ce qui suit est une esquisse de principe qui reproduit l’algorithme de la spécification et colle à l’implémentation de HotXLS ; ce n’est pas une API HotXLS :
// Esquisse de principe de [MS-OFFCRYPTO] 2.3.7.2 CreateXorKey_Method1
// et CreateXorArray_Method1 (illustration seule, pas une API HotXLS)
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; // la clé ne voit que 15 octets
if Len = 0 then
Exit;
Result := InitialCode[Len - 1];
Element := $68; // dernière entrée de XorMatrix
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)); // rotation à droite d’un 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;
Pour secret, cela produit le tableau 1F 32 17 B9 14 BA 7B 7F 59 DD 59 7F 7A C0 A6 DF, une fixture bien pratique si vous testez votre propre lecteur
Pourquoi la bonne clé produit-elle quand même un fichier abîmé ?
Une clé correcte produit quand même un fichier abîmé quand le tableau XOR est pivoté dans le mauvais sens : [MS-OFFCRYPTO] définit l’étape tableau comme XorRor, une rotation à droite d’un bit, et un tableau pivoté à gauche de deux déchiffre chaque corps d’enregistrement en bruit. La correction de la clé a fait passer Excel 16 l’invite de mot de passe et l’a propulsée dans une autre erreur, un rapport selon lequel le fichier a un problème et ne peut pas être ouvert
L’ancien code HotXLS pivotait chaque octet du tableau à gauche de 2 bits, une forme qui circule dans plusieurs implémentations BIFF. Comme HotXLS utilisait la même rotation des deux côtés, son propre lecteur ne s’en est jamais aperçu. Excel 16 ne sait plus sauvegarder des fichiers Excel 5.0/95, et il ne propose pas XOR à la sauvegarde BIFF8, donc il n’y avait aucun échantillon Excel natif à comparer. La preuve devait venir de l’autre direction : écrire un flux BIFF5 en clair, le réencoder de huit façons, et laisser Excel 16 ouvrir chaque variante. Les huit variantes croisaient trois choix indépendants :
| Choix | Option A | Option B |
|---|---|---|
| Rotation du tableau | XorRor (rotation à droite de 1) | Rotation à gauche de 2 |
| Index du tableau | (décalage + longueur d’enregistrement) mod 16 | décalage mod 16 |
| Ordre de transformation d’octet | Rotation à gauche de 5, puis XOR | XOR, puis rotation à gauche de 5 |
Excel 16 en a ouvert exactement deux sur les huit : XorRor avec rotation-puis-XOR et l’index à longueur d’enregistrement, et une variante qui n’en a que l’air. Rotation-à-gauche-2 avec XOR-puis-rotation et le même index est la même fonction déguisée. La rotation se distribue sur le XOR, donc rol5(p xor rol2(b)) égale rol5(p) xor rol7(b), et sur une valeur 8 bits une rotation à gauche de 7 est une rotation à droite de 1. Bref, rol5 ∘ rol2 = ror1, voilà pourquoi le tableau rotation-à-gauche-2 paraît plausible isolément : il n’est correct qu’accompagné de l’ordre de transformation opposé. Associé à l’ordre de la spécification, il corrompt chaque octet transformé
La même expérience a tranché une seconde question. Les variantes qui retiraient la longueur d’enregistrement de l’index ont toutes échoué, ce qui a confirmé la règle d’index que HotXLS avait adoptée une version plus tôt sur la seule foi du texte de la spécification
Comment le XorArrayIndex est-il calculé pour chaque octet ?
Le XorArrayIndex d’un octet est son décalage dans le flux du classeur plus la longueur de la totalité des données de l’enregistrement auquel il appartient, mod 16. L’index repart donc d’une valeur dépendant de l’enregistrement à chaque enregistrement et s’incrémente d’un par octet à l’intérieur. Le pseudocode de la spécification nomme les entrées FileOffset et Data.Length, qu’il est facile de mal lire comme le seul décalage de début d’enregistrement, et cette méslecture est exactement ce que HotXLS livrait jusqu’à v2.384.47
Trois détails décident si vos index s’alignent avec Excel :
- L’en-tête d’enregistrement de 4 octets n’est jamais transformé, mais il occupe quand même des positions de flux, donc le premier octet de corps d’un enregistrement siège au décalage d’en-tête + 4
- Le terme de longueur est la longueur totale des données de l’enregistrement, pas le nombre d’octets réellement transformés
- BOUNDSHEET est partiellement en clair : ses 4 premiers octets,
lbPlyPos, le décalage de flux du BOF de la feuille, restent lisibles pour qu’un analyseur puisse localiser les feuilles. Ces 4 octets sont sautés par la transformation mais comptent quand même à la fois dans le décalage et dans la longueur d’enregistrement
Assemblée, la transformation par enregistrement tient en quelques lignes. Là encore, c’est une esquisse de la règle, pas quelque chose que vous devez appeler :
function Rol8(B: Byte; N: Integer): Byte;
begin
Result := Byte((B shl N) or (B shr (8 - N)));
end;
// Obfusque un corps d’enregistrement sur place. BodyPos est le décalage de flux de
// Body[0], c’est-à-dire le décalage de l’en-tête d’enregistrement + 4. PlainPrefix vaut 4 pour
// BOUNDSHEET, la longueur complète pour BOF / FILEPASS, 0 pour la plupart des enregistrements
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;
// La lecture est l’image miroir : B := Body[I] xor Arr[...];
// puis rotation à droite de 5, soit Rol8(B, 3)
Avant v2.384.47, le lecteur HotXLS calculait l’index à partir de la seule position de flux et l’écrivain utilisait le décalage d’octet en clair. Tous deux ignoraient la longueur d’enregistrement, donc là encore les deux moitiés se donnaient raison l’une l’autre et à personne d’autre. Un décodeur écrit indépendamment relisait correctement la sortie v2.384.47 et l’ancienne sortie en charabia, et le test à huit variantes sur Excel 16 a ensuite confirmé la règle face à la vraie cible
Que deviennent les fichiers XOR écrits par les anciennes versions de HotXLS ?
HotXLS continue de lire ses propres fichiers XOR d’avant v2.384.54 en vérifiant la clé FILEPASS : quand la clé stockée égale la clé dérivée du mot de passe, le lecteur construit le tableau XorRor de la spécification, et quand elle diffère, le lecteur traite le fichier comme un vieux fichier HotXLS et reconstruit le tableau rotation-à-gauche-2. Les fichiers écrits par Excel portent toujours la clé dérivée, donc ils prennent toujours le chemin de la spécification
Le test est une heuristique avec un taux d’échec précis. Un vieux fichier dont la clé aléatoire tomberait juste égale à la clé dérivée serait lu avec le mauvais tableau, et la chance d’un tel cas est de 1 sur 65 536. Le repli ne couvre que la rotation du tableau ; la règle d’index n’est pas commutée, donc les fichiers qu’il sauve sont ceux écrits entre v2.384.47 et v2.384.53. Si vous détenez encore des fichiers XOR BIFF5 de cette fenêtre, ouvrez-les avec le HotXLS actuel et sauvegardez-les à nouveau pour obtenir un fichier qu’Excel accepte
Deux détails de mot de passe s’appliquent à chaque fichier, ancien ou nouveau :
- Longueur.
CreateXorKey_Method1ne lit que les 15 premiers octets du mot de passe, c’est la limite de la spécification. HotXLS applique ce plafond à la clé et garde le vérificateur et le tableau sur leurs règles habituelles pleine longueur et 16 octets, de façon cohérente des deux côtés. Excel lui-même refuse les mots de passe de plus de 15 caractères pour ce format, donc considérez 15 comme le vrai maximum - Jeu de caractères. HotXLS convertit le mot de passe en octets via la page de codes ANSI du système. La spécification décrit la prise de l’octet bas de chaque caractère UTF-16, ce qui concorde pour l’ASCII. Sans échantillons Excel protégés par des mots de passe non ASCII, il n’y a pas de vérité terrain pour le reste, donc tenez-vous-en aux mots de passe ASCII pour les fichiers XOR
Côté lecture, TXLSWorkbook.OnPassword permet de demander un mot de passe quand Open rencontre un enregistrement FILEPASS. L’événement est un TXLSPasswordEvent avec un var PassWord: WideString et un var Retry: Boolean ; mettez Retry à True pour réessayer, jusqu’à trois tentatives :
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 : mot de passe requis mais aucun fourni ; -1005 : mot de passe erroné
if Rc <> 1 then
raise Exception.CreateFmt('Cannot open %s (code %d)', [FileName, Rc]);
ShowMessage(VarToStr(Wb.Sheets[1].Range['A1', 'A1'].Value));
end;
Si vous connaissez déjà le mot de passe, Open(FileName, APassWord) saute complètement l’événement
L’obfuscation XOR est-elle assez sûre pour quoi que ce soit ?
L’obfuscation XOR de BIFF n’est pas du chiffrement et ne protège de rien face à un lecteur motivé. La vérification du mot de passe est un vérificateur 16 bits, la clé fait 16 bits, et le tableau de 16 octets se répète sur tout le flux, si bien que des contenus d’enregistrements BIFF prévisibles exposent les octets du tableau sans aucun mot de passe. HotXLS n’écrit du XOR que parce que les fichiers Excel 5.0/95 n’ont pas d’autre option, et la raison de produire de tels fichiers aujourd’hui est un consommateur historique qui ne sait rien lire de plus récent
Le moteur classique choisit le schéma via TXLSWorkbook.EncryptionType, et la combinaison avec le format de sauvegarde est vérifiée strictement :
xletAuto(défaut) écrit RC4 CryptoAPI pourxlExcel97et XOR pourxlExcel5, à l’image de ce qu’écrivait Excel lui-même pour chaque formatxletXorn’est valable que pour BIFF5 ; avecxlExcel97la sauvegarde lève une exception au lieu de replier silencieusementxletRC4etxletRC4CryptoAPIsont réservés au BIFF8, et les demander sur une sauvegarde BIFF5 lève une exception également
RC4 est daté lui aussi, et les détails de son interopérabilité sont traités dans pourquoi Excel rejette un classeur chiffré avec un mot de passe correct. Si le destinataire sait lire le XLSX, utilisez plutôt le moteur XLSX : TXLSXWorkbook.SaveAsEncryptedAgile écrit Agile Encryption (hash de mot de passe SHA-512 avec un spin count de 100 000 itérations et AES-256-CBC), le format qu’écrivent par défaut Excel 2010 et ultérieur, tandis que SaveAsEncrypted écrit le Standard Encryption AES-128 plus ancien. Les compromis entre les deux sont dans le chiffrement AES des fichiers XLSX en Delphi, et le côté lecture est couvert dans la lecture des fichiers Excel chiffrés Agile avec HotXLS
Aide-mémoire : l’obfuscation XOR BIFF qu’accepte Excel 16
- FILEPASS (
$002F) suit le BOF des globals ; en BIFF5 son corps fait 4 octets : la clé, puis le vérificateur - Clé =
CreateXorKey_Method1(password)selon [MS-OFFCRYPTO] §2.3.7.2, jamais aléatoire ; poursecretelle vaut$014D - Tableau = octets du mot de passe + pad, XOR avec l’octet bas de la clé aux positions paires et l’octet haut aux impaires, puis XorRor (rotation à droite de 1)
- Chiffrer un octet : rotation à gauche de 5, puis XOR ; déchiffrer : XOR, puis rotation à droite de 5 (§2.3.7.3)
- XorArrayIndex = (décalage de flux de l’octet + longueur des données d’enregistrement) mod 16 ; les en-têtes et les préfixes en clair comptent dans le décalage
- BOUNDSHEET garde ses 4 premiers octets en clair ; BOF, FILEPASS et INTERFACEHDR restent entièrement en clair
- Mots de passe : ASCII, 15 caractères au plus
- HotXLS : index corrigé en v2.384.47, clé et XorRor corrigés en v2.384.54, vieux fichiers XOR HotXLS détectés par la divergence de clé
- Pour une vraie protection, utilisez au minimum RC4 CryptoAPI BIFF8, ou Agile Encryption XLSX
HotXLS gère la protection par mot de passe BIFF5 et BIFF8, le Standard et l’Agile Encryption XLSX et le callback de mot de passe côté lecture depuis une seule bibliothèque Delphi et C++Builder, avec les détails d’interopérabilité ci-dessus pris en charge pour vous. Voir le composant tableur HotXLS Delphi pour les éditions, les plateformes et un essai à télécharger