手动解密微软Agile Encryption的ECMA-376文档

先给一些参考资料,如果有漏掉的部分,可以参考这里:

  1. [MS-OFFCRYPTO]: Office Document Cryptography Structure
  2. [MS-CFB]: Compound File Binary File Format
  3. Standard ECMA-376 Office Open XML File Formats
  4. Apache POI Encryption Support

一 解析CFB文件结构

先介绍一下CFB文件结构,以从中取出解密需要的信息:

  1. CFB文件被切分为等长的Sector,然后用如下方式组织起来:
    Header Sector + Sector #0 + Sector #1 + …
    由于有Header的存在,算Sector偏移的时候,Sector Number需要加一。
  2. 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

  3. FAT表也存储在Sector里,FAT表由DIFAT表关联,关联方式和FAT一致,唯一的区别是前109个DIFAT项,被直接存储在Header Sector里。

    例子:
    DIFAT[0]: 00 00 00 00
    DIFAT[1]: 07 00 00 00

  4. Sector内存储了各种类型的数据,包括一个简单的类似文件系统的树状结构。

    其中,Storage类似于文件夹,Stream类似于文件。
    Root Storage是唯一的根目录,下面挂其他的Storage或者Stream。

    由于Stream大小比较大,还提供了Mini Stream,用于存储比较小的数据。

  5. Header的结构参考[MS-CFB] 2.2 Compound File Header,一些关键信息如下:
    1. Byte Order – 确定字节序 (0xFFFE)
    2. Sector Shift – 单个Sector的大小 0x9 (512B) 或者 0xc (4096B)
    3. Mini Sector Shift – 单个Mini Sector的大小 0x6 (64B)
    4. Mini Stream Cutoff Size – 小于这个大小的数据,被放在Mini Stream里 (4096B)
    5. First Directory Sector Location – Directory Stream的起始Sector Number
    6. First Mini FAT Sector Location – Mini FAT表的起始Sector Number
    7. First DIFAT Sector – DIFAT表的起始Sector Number (如果Header里的109项已经够用了,则为链结尾 – 0xFFFFFFFE)
  6. 接下来解析Directory Stream所在的起始Sector
    1. 偏移:[(Sector Number + 1) * Sector Shift]
    2. 每个Directory Entry的大小是128B,如果Sector大小为512B,则每个Sector可以放四个Directory Entry
    3. Directory Entry的结构参考[MS-CFB] 2.6.1 Compound File Directory Entry,一些关键信息解释如下:
      1. Directory Entry Name – 项名称 一般第一个为Root Entry [UTF-16]
      2. Object Type – 0x0 未分配 0x1 Storage 0x2 Stream 0x5 Root Storage
      3. Child ID:子项Directory Entry的ID (如果没有子项,则为0xFFFFFFFF)
      4. Left Sibling ID: 左兄弟项Directory Entry的ID (如果没有左兄弟项,则为0xFFFFFFFF)
      5. Right Sibling ID: 右兄弟项Directory Entry的ID (如果没有右兄弟项,则为0xFFFFFFFF)
      6. Starting Sector Location – 对Stream而言,表示起始Sector Number;对Root Storage而言,则指示了Mini Stream的起始Sector Number
      7. Stream Size: 对Stream而言,表示数据大小;对Root Storage而言,则表示Mini Stream的大小
    4. 例子
      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
  7. 根据以上的内容,我们可以得到两个关键的Stream
    1. EncryptionInfo Stream,这个Stream包含一个明文的XML字符串,包含我们需要的解密相关信息
      1. 从提供的密码 和 EncryptedKeyValue里解密中间密钥 encryption/keyEncryptors/keyEncryptor/encryptedKey
        1. spinCount – 加密多少轮,例如10000
        2. saltSize
        3. blockSize
        4. keyBits – 决定AES128 或者 AES256
        5. hashSize
        6. cipherAlgorithm – 例如AES
        7. cipherChaining – 例如CBC
        8. hashAlgorithm – 例如SHA512
        9. saltValue – 用于加密中间密钥的盐值 (base64 编码)
        10. encryptedKeyValue – 加密后的中间密钥 (base64 编码)
      2. 用上面获得的中间密钥解密实际数据 encryption/keyData
        1. saltSize
        2. blockSize
        3. keyBits
        4. hashSize
        5. cipherAlgorithm
        6. cipherChaining
        7. hashAlgorithm
        8. saltValue
    2. EncryptedPackage Stream,这个Stream包含我们待解密的数据
      1. Stream Size : 8 bytes (无符号整数)
      2. Encrypted Data: variable size

二 解密中间密钥

我们用AES256 + SHA512的组合举例, AES256需要Key和初始向量IV

  1. 预先处理
    1. Salt Value 和 Encrypted Key Value 在XML里是base64编码的,我们需要先解码为二进制 base64.b64decode()
    2. 用户输入的密码也需要用utf-16小尾编码 “password”.encode(“utf-16le”)
  2. 获得Key
    1. SHA512编码 (encryptedKey/saltValue + 用户输入的密码)
      pwHash = hashlib.sha512()
      pwHash.update(saltValue)
      pwHash.update(password)
      key = pwHash.digest()
    2. 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()
    3. 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]
  3. 获得IV
    1. 就是encryptedKey/saltValue
    2. 如果大于blockSize,则按blockSize截断
    3. 反之,如果小于,则需要用0x36来append补足
  4. 解密encryptedKey/encryptedKeyValue得到中间密钥
    aes = AES.new(key, AES.MODE_CBC, iv)
    secretKey = aes.decrypt(encryptedKeyValue)

三 解密EncryptedPackage Stream

解密的时候,每4096B为一个解密单元,count从0开始的32位无符号整数。

  1. 获得IV H = H(keyData/saltValue + count)
    contentHash = hashlib.sha512()
    contentHash.update(saltValue)
    contentHash.update(struct.pack(“<I”, count))
    iv = contentHash.digest()
  2. 解密当前单元
    aes = AES.new(secretKey, AES.MODE_CBC, iv)
    content = aes.decrypt(encryptedContent)

 

《手动解密微软Agile Encryption的ECMA-376文档》有一个想法

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据