先给一些参考资料,如果有漏掉的部分,可以参考这里:
- [MS-OFFCRYPTO]: Office Document Cryptography Structure
- [MS-CFB]: Compound File Binary File Format
- Standard ECMA-376 Office Open XML File Formats
- Apache POI Encryption Support
一 解析CFB文件结构
先介绍一下CFB文件结构,以从中取出解密需要的信息:
- CFB文件被切分为等长的Sector,然后用如下方式组织起来:
Header Sector + Sector #0 + Sector #1 + …
由于有Header的存在,算Sector偏移的时候,Sector Number需要加一。 - Sector之间由FAT表关联,形成多条Sector链
FAT[0] – Sector #0 的下一个Sector Number (4 bytes)
FAT[1] – Sector #1 的下一个Sector Number
…还有一些特殊的内容,比如:
FAT Sector (0xFFFFFFFD)
链结尾 (0xFFFFFFFE)
空的占位符 (0xFFFFFFFF)例子:
FAT[0]: fd ff ff ff
FAT[1]: 04 00 00 00 - FAT表也存储在Sector里,FAT表由DIFAT表关联,关联方式和FAT一致,唯一的区别是前109个DIFAT项,被直接存储在Header Sector里。
例子:
DIFAT[0]: 00 00 00 00
DIFAT[1]: 07 00 00 00 - Sector内存储了各种类型的数据,包括一个简单的类似文件系统的树状结构。
其中,Storage类似于文件夹,Stream类似于文件。
Root Storage是唯一的根目录,下面挂其他的Storage或者Stream。由于Stream大小比较大,还提供了Mini Stream,用于存储比较小的数据。
- Header的结构参考[MS-CFB] 2.2 Compound File Header,一些关键信息如下:
- Byte Order – 确定字节序 (0xFFFE)
- Sector Shift – 单个Sector的大小 0x9 (512B) 或者 0xc (4096B)
- Mini Sector Shift – 单个Mini Sector的大小 0x6 (64B)
- Mini Stream Cutoff Size – 小于这个大小的数据,被放在Mini Stream里 (4096B)
- First Directory Sector Location – Directory Stream的起始Sector Number
- First Mini FAT Sector Location – Mini FAT表的起始Sector Number
- First DIFAT Sector – DIFAT表的起始Sector Number (如果Header里的109项已经够用了,则为链结尾 – 0xFFFFFFFE)
- 接下来解析Directory Stream所在的起始Sector
- 偏移:[(Sector Number + 1) * Sector Shift]
- 每个Directory Entry的大小是128B,如果Sector大小为512B,则每个Sector可以放四个Directory Entry
- Directory Entry的结构参考[MS-CFB] 2.6.1 Compound File Directory Entry,一些关键信息解释如下:
- Directory Entry Name – 项名称 一般第一个为Root Entry [UTF-16]
- Object Type – 0x0 未分配 0x1 Storage 0x2 Stream 0x5 Root Storage
- Child ID:子项Directory Entry的ID (如果没有子项,则为0xFFFFFFFF)
- Left Sibling ID: 左兄弟项Directory Entry的ID (如果没有左兄弟项,则为0xFFFFFFFF)
- Right Sibling ID: 右兄弟项Directory Entry的ID (如果没有右兄弟项,则为0xFFFFFFFF)
- Starting Sector Location – 对Stream而言,表示起始Sector Number;对Root Storage而言,则指示了Mini Stream的起始Sector Number
- Stream Size: 对Stream而言,表示数据大小;对Root Storage而言,则表示Mini Stream的大小
- 例子
Directory Entry [0]:
Directory Entry Name: 52 00 6f 00 6f 00 74 00 20 00 45 00 6e 00 74 00 72 00 79 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 [UTF-16]
Directory Entry Name: Root Entry [UTF-16]
Directory Entry Name Length: 16 00
Object Type: 05
Color Flag: 00 [0x00 Red 0x01 Black]
Left Sibling ID: ff ff ff ff
Right Sibling ID: ff ff ff ff
Child ID: 0a 00 00 00
CLSID: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
State Bits: 00 00 00 00
Creation Time: 00 00 00 00 00 00 00 00
Modified Time: 00 3d 2a 3d 8e 12 d2 01
Starting Sector Location: 03 00 00 00
Stream Size: 00 08 00 00 00 00 00 00
- 根据以上的内容,我们可以得到两个关键的Stream
- EncryptionInfo Stream,这个Stream包含一个明文的XML字符串,包含我们需要的解密相关信息
- 从提供的密码 和 EncryptedKeyValue里解密中间密钥 encryption/keyEncryptors/keyEncryptor/encryptedKey
- spinCount – 加密多少轮,例如10000
- saltSize
- blockSize
- keyBits – 决定AES128 或者 AES256
- hashSize
- cipherAlgorithm – 例如AES
- cipherChaining – 例如CBC
- hashAlgorithm – 例如SHA512
- saltValue – 用于加密中间密钥的盐值 (base64 编码)
- encryptedKeyValue – 加密后的中间密钥 (base64 编码)
- 用上面获得的中间密钥解密实际数据 encryption/keyData
- saltSize
- blockSize
- keyBits
- hashSize
- cipherAlgorithm
- cipherChaining
- hashAlgorithm
- saltValue
- 从提供的密码 和 EncryptedKeyValue里解密中间密钥 encryption/keyEncryptors/keyEncryptor/encryptedKey
- EncryptedPackage Stream,这个Stream包含我们待解密的数据
- Stream Size : 8 bytes (无符号整数)
- Encrypted Data: variable size
- EncryptionInfo Stream,这个Stream包含一个明文的XML字符串,包含我们需要的解密相关信息
二 解密中间密钥
我们用AES256 + SHA512的组合举例, AES256需要Key和初始向量IV
- 预先处理
- Salt Value 和 Encrypted Key Value 在XML里是base64编码的,我们需要先解码为二进制 base64.b64decode()
- 用户输入的密码也需要用utf-16小尾编码 “password”.encode(“utf-16le”)
- 获得Key
- SHA512编码 (encryptedKey/saltValue + 用户输入的密码)
pwHash = hashlib.sha512()
pwHash.update(saltValue)
pwHash.update(password)
key = pwHash.digest() - SHA512编码spinCount轮 Hn = H(count + Hn-1)
其中count为从0开始的无符号32位数
for i in xrange(spinCount):
pwHash = hashlib.sha512()
pwHash.update(struct.pack(“<I”, i))
pwHash.update(key)
key = pwHash.digest() - SHA512编码 Hfinal = H(Hn + BlockKey)
如果Hfinal大于keyBits,则需要按keyBits截断;
反之,如果小于,则需要用0x36来append补足
pwHash = hashlib.sha512()
pwHash.update(key)
pwHash.update(struct.pack(‘<BBBBBBBB’, 0x14, 0x6e, 0x0b, 0xe7, 0xab, 0xac, 0xd0, 0xd6))
key = pwHash.digest()[:32]
- SHA512编码 (encryptedKey/saltValue + 用户输入的密码)
- 获得IV
- 就是encryptedKey/saltValue
- 如果大于blockSize,则按blockSize截断
- 反之,如果小于,则需要用0x36来append补足
- 解密encryptedKey/encryptedKeyValue得到中间密钥
aes = AES.new(key, AES.MODE_CBC, iv)
secretKey = aes.decrypt(encryptedKeyValue)
三 解密EncryptedPackage Stream
解密的时候,每4096B为一个解密单元,count从0开始的32位无符号整数。
- 获得IV H = H(keyData/saltValue + count)
contentHash = hashlib.sha512()
contentHash.update(saltValue)
contentHash.update(struct.pack(“<I”, count))
iv = contentHash.digest() - 解密当前单元
aes = AES.new(secretKey, AES.MODE_CBC, iv)
content = aes.decrypt(encryptedContent)
BlockKey是什么…