HotXLS เขียน workbook แบบ XOR-obfuscated ของ Excel 5.0/95 (BIFF5) ที่ Excel 16 จะเปิดได้ก็ต่อเมื่อรายละเอียดสามจุดตรงกับ [MS-OFFCRYPTO] เป๊ะ ๆ: key ของ FILEPASS ต้องเป็น CreateXorKey_Method1(password) XOR array 16 ไบต์ต้องสร้างด้วย XorRor (หมุนขวาหนึ่งบิต) และทุกไบต์ต้องใช้ XorArrayIndex = (stream offset + record length) mod 16 HotXLS ทำ index ถูกใน v2.384.47 และทำ key กับการหมุนถูกใน v2.384.54 ก่อนหน้านั้น ไฟล์ BIFF5 ที่มีรหัสผ่านทุกไฟล์ที่มันผลิตออกมาเปิดใน HotXLS ได้ปกติแต่พังใน Excel
แบบนี้แหละคือเรื่องทั้งหมดในเวอร์ชันย่อส่วน reader กับ writer ที่เข้าใจผิดแบบเดียวกันจะเห็นพ้องกันเป๊ะ เทสต์ round-trip จึงยังเขียวสวิงทั้งที่ consumer ตัวเดียวที่สำคัญบอกว่าไม่ Excel 16 บอกไม่สองรอบ ด้วยข้อความสองแบบ และแต่ละข้อความชี้ไปคนละชั้นของสคีมา บทความนี้เดินผ่านชั้นพวกนั้นตามลำดับที่ Excel เช็ก พร้อมรายละเอียดระดับไบต์ที่ใช้ได้ทั้งคนเรียก HotXLS และคนเขียน BIFF reader เอง
BIFF XOR obfuscation เก็บอะไรลงไฟล์กันแน่
BIFF XOR obfuscation เก็บในไฟล์แค่คำ 16 บิตสองคำ ที่เหลือคำนวณใหม่จากรหัสผ่านได้หมด record FILEPASS ($002F) นั่งต่อจาก workbook globals BOF ทันที และในไฟล์ BIFF5 body ของมันคือ 4 ไบต์เป๊ะ: XOR key ตามด้วย password verifier ไม่มี salt ไม่มีตัวระบุอัลกอริทึม และไม่มี blob verifier เข้ารหัสแบบที่สคีมา RC4 กับ AES แบกมา
จากคำสองคำนี้ reader สร้างคืนสามอย่าง:
- verifier แฮช 16 บิตของไบต์รหัสผ่านที่ XOR กับ
$CE4Bการเทียบมันกับคำที่เก็บไว้คือการเช็กรหัสผ่าน และเป็นการเช็กอย่างเดียวเท่านั้น - XOR key ค่า 16 บิตจาก
CreateXorKey_Method1ใน [MS-OFFCRYPTO] §2.3.7.2 ขับเคลื่อนด้วยตารางค่าคงที่สองตัว (InitialCode15 คำ กับXorMatrix105 คำ) - XOR array 16 ไบต์ที่ประกอบจากไบต์รหัสผ่านเติมด้วย pad คงที่ 16 ไบต์ แต่ละตัว XOR กับไบต์ต่ำของ key (ตำแหน่งคู่) หรือไบต์สูง (ตำแหน่งคี่) แล้วหมุนขวาหนึ่งบิต
record header อยู่เป็น plain text และ record ทั้งอันที่สคีมายกเว้นก็มีไม่กี่ตัวเช่นกัน ในนั้นมี BOF, FILEPASS กับ INTERFACEHDR ที่เหลือ body ถูกแปลงทีละไบต์: หมุนซ้าย 5 บิต แล้ว XOR กับหนึ่ง entry ของ array 16 ไบต์ การถอดรหัสที่ [MS-OFFCRYPTO] §2.3.7.3 เขียนไว้เป็น DecryptData_Method1 คือภาพสะท้อน: XOR ก่อน แล้วหมุนขวา 5 บิต
ใน HotXLS คุณไม่ต้องแตะอะไรพวกนี้ตรง ๆ เลย ตั้งรหัสผ่าน เลือกฟอร์แมต แล้ว SaveAs จะปล่อย FILEPASS กับแปลง stream ให้เอง:
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 รองรับแค่ XOR obfuscation; xletAuto ก็จะเลือกแบบนี้เช่นกัน
Wb.EncryptionType := xletXor;
// ใช้ ASCII และไม่เกิน 15 ตัวอักษร (ดูด้านล่าง)
Wb.EncryptionPassword := 'secret';
if Wb.SaveAs(FileName, xlExcel5) <> 1 then
raise Exception.Create('BIFF5 save failed');
end;
ทำไม Excel ถึงบอกว่ารหัสผ่านผิดทั้งที่ verifier ตรง
Excel ปฏิเสธรหัสผ่านเพราะมันไม่เชื่อ key ที่เก็บมา: Excel ได้ key มาจากรหัสผ่านที่พิมพ์ผ่าน CreateXorKey_Method1 แล้วเทียบกับคำ key ของ FILEPASS ไฟล์ที่ key เป็นค่าอื่นจึงตกการเช็กรหัสผ่านไปเลย ทั้งที่ verifier ถูกต้องก็ตาม สเปกระบุว่า key เป็น output ของรหัสผ่าน ไม่ใช่พารามิเตอร์อิสระ และ Excel 16 บังคับตามการอ่านแบบนี้
ตัวเขียน HotXLS ก่อน v2.384.54 เติมคำ key ด้วยไบต์สุ่มสองไบต์ บนกระดาษดูไม่มีพิษภัย เพราะ verifier คือการเช็กรหัสผ่านที่เอกสารระบุไว้ และ array ก็สร้างจาก key ที่ไฟล์ประกาศมาโดยดู HotXLS เองก็อ่านไฟล์พวกนั้นได้ไม่มีสะดุด เพราะ reader ของมันรับ key จาก FILEPASS เป็นค่ากำหนดให้ ส่วน Excel 16 ได้ไฟล์เดียวกันกับรหัสผ่านที่ถูกไป กลับตอบมาว่ารหัสผ่านไม่ถูก ตั้งแต่ v2.384.54 key ถูก derive มา ไฟล์ FILEPASS ของรหัสผ่าน secret จึงถือ key $014D กับ verifier $DAA7 เสมอ โดยค่าพวกนี้ถูกตรวจสอบข้ามกับการ implement สเปกอีกชุดหนึ่ง
ตัวการ derive เองสั้นพอควรเมื่อมีตารางสองตัวพร้อมแล้ว เดินรหัสผ่านย้อนหลัง มองบิตที่ 6 ของแต่ละไบต์เจ็ดรอบระหว่างเลื่อนไปทางซ้าย แล้ว XOR เข้า entry หนึ่งตัวของ XorMatrix ทุกครั้งที่บิตนั้นเปิดอยู่ โค้ดต่อไปนี้เป็นภาพร่างหลักการที่ทำซ้ำอัลกอริทึมตามสเปกและตรงกับ implementation ของ HotXLS ไม่ใช่ API ของ HotXLS:
// ภาพร่างหลักการของ [MS-OFFCRYPTO] 2.3.7.2 CreateXorKey_Method1
// กับ CreateXorArray_Method1 (แค่ภาพประกอบ ไม่ใช่ 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; // key เห็นแค่ 15 ไบต์
if Len = 0 then
Exit;
Result := InitialCode[Len - 1];
Element := $68; // entry สุดท้ายของ 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)); // หมุนขวาหนึ่งบิต
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;
สำหรับ secret โค้ดนี้ผลิต array 1F 32 17 B9 14 BA 7B 7F 59 DD 59 7F 7A C0 A6 DF ออกมา เป็น fixture ที่ใช้มือได้ถ้าคุณกำลังเทสต์ reader ของตัวเอง
ทำไม key ถูกแล้วไฟล์ยังเสียอยู่ดี
key ถูกแล้วไฟล์ยังเสียอยู่ เมื่อ XOR array ถูกหมุนสวยทาง: [MS-OFFCRYPTO] นิยามขั้นของ array เป็น XorRor คือหมุนขวาหนึ่งบิต และ array ที่หมุนซ้ายสองบิตจะถอดรหัส body ทุก record ให้กลายเป็นขยะ แก้ key เสร็จ Excel 16 ผ่านหน้าถามรหัสผ่านไปแล้วเด้งเข้า error ต่างหากทันที คือรายงานว่าไฟล์มีปัญหาและเปิดไม่ได้
โค้ด HotXLS รุ่นเก่าหมุนไบต์ของ array แต่ละตัวไปทางซ้าย 2 บิต รูปแบบที่หมุนเวียนอยู่ใน implementation ของ BIFF หลายชุด เพราะ HotXLS ใช้การหมุนแบบเดียวกันทั้งสองฝั่ง reader ของตัวเองจึงไม่เคยรู้ตัว Excel 16 ไม่มีทางเซฟไฟล์ Excel 5.0/95 ได้อีกแล้ว และตอนเซฟ BIFF8 ก็ไม่เสนอ XOR จึงไม่มี sample จาก Excel แท้ ๆ ให้เอามา diff หลักฐานต้องมาจากทางอื่น: เขียน stream BIFF5 เป็นข้อความธรรมดาหนึ่งชุด เข้ารหัสใหม่แปดแบบ แล้วให้ Excel 16 เปิดทีละแบบ แปดแบบนี้ไขว้กันจากสามทางเลือกที่เป็นอิสระจากกัน:
| ทางเลือก | ทางเลือก A | ทางเลือก B |
|---|---|---|
| การหมุนของ array | XorRor (หมุนขวา 1) | หมุนซ้าย 2 |
| index ของ array | (offset + record length) mod 16 | offset mod 16 |
| ลำดับการแปลงไบต์ | หมุนซ้าย 5 แล้ว XOR | XOR แล้วหมุนซ้าย 5 |
Excel 16 เปิดได้แค่สองจากแปดแบบเป๊ะ: XorRor กับหมุนแล้วค่อย XOR กับ index ที่ใช้ความยาว record และอีกหนึ่งแบบที่ดูต่างเป็นแค่เปลือก Rotate-left-2 กับ XOR แล้วค่อยหมุนกับ index เดียวกันคือฟังก์ชันเดิมสวมเสื้อคลุมใหม่ การหมุนกระจายผ่าน XOR ได้ rol5(p xor rol2(b)) จึงเท่ากับ rol5(p) xor rol7(b) และบนค่า 8 บิต การหมุนซ้าย 7 คือการหมุนขวา 1 ย่อ ๆ ว่า rol5 ∘ rol2 = ror1 นี่คือเหตุที่ array แบบ rotate-left-2 ดูผ่านได้ถ้ามองโดด ๆ มันถูกแค่ตอนจับคู่กับลำดับการแปลงสวนทาง ถ้าจับกับลำดับตามสเปก มันจะทำลายไบต์ที่ถูกแปลงทุกตัว
การทดลองชุดเดียวกันปิดคำถามที่สองด้วย แบบที่ตัดความยาว record ออกจาก index พังหมด จึงยืนยันกฎ index ที่ HotXLS เอามาใช้ตั้งแต่รีลีสก่อนหน้าโดยยึดข้อความในสเปกเพียงอย่างเดียว
XorArrayIndex ของแต่ละไบต์คำนวณอย่างไร
XorArrayIndex ของไบต์หนึ่งคือ offset ของมันใน workbook stream บวกความยาว record data ทั้งชุดที่มันอยู่ mod 16 index จึงเริ่มใหม่ที่ค่าที่ขึ้นกับ record สำหรับทุก record แล้วเพิ่มทีละหนึ่งต่อไบต์ข้างใน pseudocode ในสเปกเรียก input เหล่านี้ว่า FileOffset กับ Data.Length ซึ่งอ่านผิดเป็น offset เริ่มของ record อย่างเดียวได้ง่าย และการอ่านผิดแบบนั้นก็คือสิ่งที่ HotXLS ส่งมอบมาจนถึง v2.384.47
รายละเอียดสามข้อตัดสินว่า index ของคุณจะตรงกับ Excel หรือไม่:
- record header 4 ไบต์ไม่ถูกแปลง แต่มันยังกินตำแหน่งใน stream ไบต์ body ตัวแรกของ record จึงนั่งที่ header offset + 4
- ตัวเลขความยาวคือความยาว record data เต็ม ๆ ไม่ใช่จำนวนไบต์ที่ถูกแปลงจริง
- BOUNDSHEET เป็น plain บางส่วน: 4 ไบต์แรกของมัน คือ
lbPlyPosstream offset ของ sheet BOF ยังอ่านได้เพื่อให้ parser หา sheet เจอ 4 ไบต์นี้ถูกข้ามจากการแปลงแต่ยังนับเข้าทั้ง offset และความยาว record
เอามารวมกันแล้ว การแปลงราย record ก็แค่ไม่กี่บรรทัด ย้ำอีกที นี่เป็นภาพร่างของกฎ ไม่ใช่สิ่งที่คุณต้องเรียกใช้:
function Rol8(B: Byte; N: Integer): Byte;
begin
Result := Byte((B shl N) or (B shr (8 - N)));
end;
// ทำ obfuscate record body หนึ่งอันในที่ BodyPos คือ stream offset ของ
// Body[0] ก็คือ record header offset + 4 PlainPrefix เป็น 4 สำหรับ
// BOUNDSHEET เต็มความยาวสำหรับ BOF / FILEPASS และ 0 สำหรับ record ส่วนใหญ่
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;
// การอ่านคือภาพสะท้อน: B := Body[I] xor Arr[...];
// แล้วหมุนขวา 5 บิต ก็คือ Rol8(B, 3)
ก่อน v2.384.47 reader ของ HotXLS คำนวณ index จากตำแหน่งใน stream อย่างเดียว ส่วน writer ใช้ byte offset แบบ plain ทั้งคู่เมินความยาว record สองฝั่งจึงเห็นพ้องกันอีกครั้ง แต่ไม่เห็นพ้องกับใครอื่น decoder ที่เขียนแยกอ่าน output ของ v2.384.47 ได้ถูกต้อง และอ่าน output รุ่นเก่าเป็นขยะ แล้วเทสต์แปดแบบกับ Excel 16 ในภายหลังก็ยืนยันกฎกับเป้าหมายจริง
ไฟล์ XOR ที่เขียนโดย HotXLS รุ่นเก่าเป็นอย่างไร
HotXLS ยังอ่านไฟล์ XOR รุ่นก่อน v2.384.54 ของตัวเองได้ด้วยการเช็ก key ของ FILEPASS: ถ้า key ที่เก็บไว้เท่ากับ key ที่ derive จากรหัสผ่าน reader จะสร้าง XorRor array ตามสเปก และถ้าต่างกัน reader จะถือว่าไฟล์นั้นเป็นไฟล์ HotXLS รุ่นเก่าแล้วสร้าง array แบบ rotate-left-2 คืน ไฟล์ที่ Excel เขียนถือ key ที่ derive เสมอ จึงเดินเส้นทางตามสเปกตลอด
การเช็กนี้เป็น heuristic ที่มีอัตราพลาดที่วัดได้เป๊ะ ไฟล์เก่าที่ key แบบสุ่มบังเอิญเท่ากับ key ที่ derive จะถูกอ่านด้วย array ที่ผิด โอกาสนั้นคือ 1 ใน 65,536 fallback ครอบคลุมแค่การหมุนของ array กฎ index ไม่ถูกสลับ ไฟล์ที่มันช่วยไว้จึงเป็นไฟล์ที่เขียนระหว่าง v2.384.47 ถึง v2.384.53 ถ้าคุณยังมีไฟล์ BIFF5 XOR จากช่วงนั้นอยู่ เปิดด้วย HotXLS ปัจจุบันแล้วเซฟใหม่จะได้ไฟล์ที่ Excel ยอมรับ
รายละเอียดของรหัสผ่านสองข้อใช้กับทุกไฟล์ ทั้งเก่าและใหม่:
- ความยาว
CreateXorKey_Method1อ่านไบต์รหัสผ่านแค่ 15 ไบต์แรก ซึ่งเป็นขีดจำกัดตามสเปก HotXLS ใช้เพดานนี้กับ key และคง verifier กับ array ตามกฎเดิมทั้งความยาวเต็มกับ 16 ไบต์ เท่ากันทั้งสองฝั่ง ตัว Excel เองปฏิเสธรหัสผ่านที่ยาวเกิน 15 ตัวอักษรสำหรับฟอร์แมตนี้ จึงควรถือว่า 15 คือเพดานจริง - ชุดอักขระ HotXLS แปลงรหัสผ่านเป็นไบต์ผ่าน system ANSI code page สเปกระบุว่าให้หยิบไบต์ต่ำของอักขระ UTF-16 แต่ละตัว ซึ่งตรงกันในกรณี ASCII โดยไม่มี sample ของ Excel ที่ใช้รหัสผ่าน non-ASCII ก็ไม่มีค่าอ้างอิงสำหรับกรณีอื่น ยึดรหัสผ่าน ASCII กับไฟล์ XOR ไว้จึงปลอดภัยสุด
ฝั่งการอ่าน TXLSWorkbook.OnPassword ให้คุณถามรหัสผ่านเมื่อ Open เจอ record FILEPASS event เป็น TXLSPasswordEvent ที่มี var PassWord: WideString กับ var Retry: Boolean ตั้ง Retry เป็น True เพื่อลองใหม่ ได้สูงสุดสามรอบ:
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: ต้องใช้รหัสผ่านแต่ไม่ได้ส่งมา; -1005: รหัสผ่านผิด
if Rc <> 1 then
raise Exception.CreateFmt('Cannot open %s (code %d)', [FileName, Rc]);
ShowMessage(VarToStr(Wb.Sheets[1].Range['A1', 'A1'].Value));
end;
ถ้าคุณรู้รหัสผ่านอยู่แล้ว Open(FileName, APassWord) ข้าม event ไปทั้งตัว
XOR obfuscation ปลอดภัยพอสำหรับอะไรสักอย่างหรือเปล่า
BIFF XOR obfuscation ไม่ใช่การเข้ารหัสลับ และไม่คุ้มกันอะไร reader ที่ตั้งใจจะเจาะเลย เช็กรหัสผ่านด้วย verifier 16 บิต key ก็ 16 บิต และ array 16 ไบต์ทำซ้ำตลอด stream เนื้อหา record BIFF ที่ทำนายได้จึงเปิดเผยไบต์ของ array ได้โดยไม่ต้องมีรหัสผ่านเลย HotXLS เขียน XOR เพราะไฟล์ Excel 5.0/95 ไม่มีทางเลือกอื่นเท่านั้น เหตุผลที่ยังต้องผลิตไฟล์แบบนี้วันนี้คือ consumer รุ่นเก่าที่อ่านอะไรใหม่กว่านี้ไม่ได้
เอนจินคลาสสิกเลือกสคีมาผ่าน TXLSWorkbook.EncryptionType และการจับคู่กับฟอร์แมตเซฟถูกเช็กเข้มงวด:
xletAuto(ค่าเริ่มต้น) เขียน RC4 CryptoAPI สำหรับxlExcel97กับ XOR สำหรับxlExcel5ตรงกับที่ Excel เองเขียนสำหรับแต่ละฟอร์แมตxletXorใช้ได้กับ BIFF5 เท่านั้น กับxlExcel97การเซฟจะ throw exception แทนที่จะหลุดไปใช้ตัวอื่นเงียบ ๆxletRC4กับxletRC4CryptoAPIเป็นของ BIFF8 เท่านั้น ขอใช้กับการเซฟ BIFF5 ก็ throw exception เหมือนกัน
RC4 เองก็เก่าแล้ว รายละเอียดการทำให้มัน interoperate ได้เล่าไว้ในทำไม Excel ถึงปฏิเสธ workbook เข้ารหัสทั้งที่รหัสผ่านถูก ถ้าผู้รับอ่าน XLSX ได้ ใช้เอนจิน XLSX แทนเลย TXLSXWorkbook.SaveAsEncryptedAgile เขียน Agile Encryption (hash รหัสผ่านด้วย SHA-512 พร้อม spin count 100,000 รอบกับ AES-256-CBC) ซึ่งเป็นฟอร์แมตที่ Excel 2010 ขึ้นไปเขียนเป็นค่าเริ่มต้น ส่วน SaveAsEncrypted เขียน Standard Encryption แบบ AES-128 รุ่นเก่ากว่า การถ่วงน้ำหนักระหว่างสองแบบอยู่ในการเข้ารหัสไฟล์ XLSX ด้วย AES ใน Delphi และฝั่งการอ่านเล่าไว้ในการอ่านไฟล์ Excel ที่เข้ารหัสแบบ Agile ด้วย HotXLS
สรุป: BIFF XOR obfuscation ที่ Excel 16 ยอมรับ
- FILEPASS (
$002F) ต่อจาก globals BOF ใน BIFF5 body ของมันคือ 4 ไบต์: key ตามด้วย verifier - Key =
CreateXorKey_Method1(password)ตาม [MS-OFFCRYPTO] §2.3.7.2 ห้ามสุ่ม สำหรับsecretคือ$014D - Array = ไบต์รหัสผ่าน + pad XOR ไบต์ต่ำของ key ที่ตำแหน่งคู่ ไบต์สูงที่ตำแหน่งคี่ แล้ว XorRor (หมุนขวา 1)
- เข้ารหัสหนึ่งไบต์: หมุนซ้าย 5 แล้ว XOR ถอดรหัส: XOR แล้วหมุนขวา 5 (§2.3.7.3)
- XorArrayIndex = (byte stream offset + ความยาว record data) mod 16 header กับ prefix ที่เป็น plain นับเข้า offset
- BOUNDSHEET คง 4 ไบต์แรกเป็น plain, BOF, FILEPASS กับ INTERFACEHDR เป็น plain ทั้งอัน
- รหัสผ่าน: ASCII ไม่เกิน 15 ตัวอักษร
- HotXLS: index แก้ใน v2.384.47, key กับ XorRor แก้ใน v2.384.54, ไฟล์ XOR ของ HotXLS รุ่นเก่าตรวจจับได้จาก key ไม่ตรง
- ถ้าต้องการคุ้มกันจริงใช้ BIFF8 RC4 CryptoAPI ขึ้นไป หรือ XLSX Agile Encryption
HotXLS จัดการ password protection แบบ BIFF5 กับ BIFF8, XLSX Standard กับ Agile Encryption และ callback รหัสผ่านฝั่งอ่าน จากไลบรารี Delphi กับ C++Builder ชุดเดียว โดยรายละเอียด interop ข้างต้นจัดการให้คุณเรียบร้อย ดูHotXLS Delphi spreadsheet component สำหรับรุ่น แพลตฟอร์ม และดาวน์โหลดรุ่นทดลอง