Protected OOXML Text Documents, (Mon, Sep 2nd)

SANS Internet Storm Center, InfoCON: green 2024-09-03

Just like "Protected OOXML Spreadsheets", Word documents can also be protected:

You have to look into the word/settings.xml file, and search for element w:documentProtection:

The hash algorithm is the same as for OOXML spreadsheets. However, you will not be able to use hashcat to crack protected Word document hashes, because the password is encoded differently before it is repeatedly hashed.

A legacy algorithm is used to preprocess the password, and I found a Python implementation here.

# https://stackoverflow.com/questions/65877620/open-xml-document-protection-implementation-documentprotection-classdHighOrderWordLists = [    [0xE1, 0xF0],    [0x1D, 0x0F],    [0xCC, 0x9C],    [0x84, 0xC0],    [0x11, 0x0C],    [0x0E, 0x10],    [0xF1, 0xCE],    [0x31, 0x3E],    [0x18, 0x72],    [0xE1, 0x39],    [0xD4, 0x0F],    [0x84, 0xF9],    [0x28, 0x0C],    [0xA9, 0x6A],    [0x4E, 0xC3]]dEncryptionMatrix = [    [[0xAE, 0xFC], [0x4D, 0xD9], [0x9B, 0xB2], [0x27, 0x45], [0x4E, 0x8A], [0x9D, 0x14], [0x2A, 0x09]],    [[0x7B, 0x61], [0xF6, 0xC2], [0xFD, 0xA5], [0xEB, 0x6B], [0xC6, 0xF7], [0x9D, 0xCF], [0x2B, 0xBF]],    [[0x45, 0x63], [0x8A, 0xC6], [0x05, 0xAD], [0x0B, 0x5A], [0x16, 0xB4], [0x2D, 0x68], [0x5A, 0xD0]],    [[0x03, 0x75], [0x06, 0xEA], [0x0D, 0xD4], [0x1B, 0xA8], [0x37, 0x50], [0x6E, 0xA0], [0xDD, 0x40]],    [[0xD8, 0x49], [0xA0, 0xB3], [0x51, 0x47], [0xA2, 0x8E], [0x55, 0x3D], [0xAA, 0x7A], [0x44, 0xD5]],    [[0x6F, 0x45], [0xDE, 0x8A], [0xAD, 0x35], [0x4A, 0x4B], [0x94, 0x96], [0x39, 0x0D], [0x72, 0x1A]],    [[0xEB, 0x23], [0xC6, 0x67], [0x9C, 0xEF], [0x29, 0xFF], [0x53, 0xFE], [0xA7, 0xFC], [0x5F, 0xD9]],    [[0x47, 0xD3], [0x8F, 0xA6], [0x0F, 0x6D], [0x1E, 0xDA], [0x3D, 0xB4], [0x7B, 0x68], [0xF6, 0xD0]],    [[0xB8, 0x61], [0x60, 0xE3], [0xC1, 0xC6], [0x93, 0xAD], [0x37, 0x7B], [0x6E, 0xF6], [0xDD, 0xEC]],    [[0x45, 0xA0], [0x8B, 0x40], [0x06, 0xA1], [0x0D, 0x42], [0x1A, 0x84], [0x35, 0x08], [0x6A, 0x10]],    [[0xAA, 0x51], [0x44, 0x83], [0x89, 0x06], [0x02, 0x2D], [0x04, 0x5A], [0x08, 0xB4], [0x11, 0x68]],    [[0x76, 0xB4], [0xED, 0x68], [0xCA, 0xF1], [0x85, 0xC3], [0x1B, 0xA7], [0x37, 0x4E], [0x6E, 0x9C]],    [[0x37, 0x30], [0x6E, 0x60], [0xDC, 0xC0], [0xA9, 0xA1], [0x43, 0x63], [0x86, 0xC6], [0x1D, 0xAD]],    [[0x33, 0x31], [0x66, 0x62], [0xCC, 0xC4], [0x89, 0xA9], [0x03, 0x73], [0x06, 0xE6], [0x0D, 0xCC]],    [[0x10, 0x21], [0x20, 0x42], [0x40, 0x84], [0x81, 0x08], [0x12, 0x31], [0x24, 0x62], [0x48, 0xC4]]]def WordEncodePassword(password):  password_bytes = password.encode('utf-8')  password_bytes = password_bytes[:15]  password_length = len(password_bytes)  if password_length > 0:    high_order_word_list = dHighOrderWordLists[password_length - 1].copy()  else:    high_order_word_list = [0x00, 0x00]  for i in range(password_length):    password_byte = password_bytes[i]    matrix_index = i + len(dEncryptionMatrix) - password_length    for j in range(len(dEncryptionMatrix[0])):      # Only perform XOR operation using the encryption matrix if the j-th bit is set      mask = 1 << j      if (password_byte & mask) == 0:        continue      for k in range(len(dEncryptionMatrix[0][0])):        high_order_word_list[k] = high_order_word_list[k] ^ dEncryptionMatrix[matrix_index][j][k]  low_order_word = 0x0000  for i in range(password_length - 1, -1, -1):    password_byte = password_bytes[i]    low_order_word = (      (((low_order_word >> 14) & 0x0001) | ((low_order_word << 1) & 0x7fff))      ^ password_byte    )  low_order_word = (    (((low_order_word >> 14) & 0x0001) | ((low_order_word << 1) & 0x7fff))    ^ password_length    ^ 0xce4b  )  low_order_word_list = [(low_order_word & 0xff00) >> 8, low_order_word & 0x00ff]  key = high_order_word_list + low_order_word_list  key.reverse()  # `key_str` is a hex string with uppercase hexadecimal letters, e.g. '7EEDCE64'  key_str = ''.join(f'{c:X}' for c in key)  return key_str

This password preprocessing code can then be used with the same hashing function as for Excel, like this:

def CalculateHash(password, salt):    passwordBytes = password.encode('utf16')[2:]    buffer = salt + passwordBytes    hash = hashlib.sha512(buffer).digest()    for iter in range(100000):        buffer = hash + struct.pack('<I', iter)        hash = hashlib.sha512(buffer).digest()    return hashdef WordCalculateHash(password, salt):    return CalculateHash(WordEncodePassword(password), binascii.a2b_base64(salt))

Using password "P@ssword" and the salt seen in the screenshot above, we can calculate the hash:

This calculated hash (BASE64 representation) is the same as the stored hash, thus the password is indeed "P@ssw0rd".

 

Didier Stevens Senior handler blog.DidierStevens.com

(c) SANS Internet Storm Center. https://isc.sans.edu Creative Commons Attribution-Noncommercial 3.0 United States License.