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.