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-class
dHighOrderWordLists = [
[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 hash
def 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