【发布时间】:2021-04-28 20:04:30
【问题描述】:
我正在尝试在 Python 中实现 MS Word (2019) 文档的 Open XML documentProtection 哈希保护,以测试哈希算法。所以我创建了一个 Word 文档,并使用以下密码保护它不被编辑:johnjohn。然后,以 ZIP/XML 格式打开文档,我在 documentProtection 部分看到以下内容:
<w:documentProtection w:edit="readOnly" w:enforcement="1" w:cryptProviderType="rsaAES" w:cryptAlgorithmClass="hash" w:cryptAlgorithmType="typeAny" w:cryptAlgorithmSid="14" w:cryptSpinCount="100000" w:hash="pVjR9ktO9vlxijXcMPlH+4PLwD4Xwy1aqbNQOFmWaSpvBjipNh//T8S3nBhq6HRoRVfWL6s/+NdUCPTxUr0vZw==" w:salt="pH1TDVHSfGBxkd3Q88UNhQ==" />
根据 Open XML 文档 (ECMA-376-1:2016 #17.15.1.29):
-
cryptAlgorithmSid="14"指向 SHA-512 算法 -
cryptSpinCount="100000"表示哈希必须在 100k 轮中完成,使用以下算法(引用上述标准):
指定哈希函数在尝试时迭代运行的次数(使用每次迭代的结果加上一个包含迭代次数的 4 字节值(从 0 开始,小端序)作为下一次迭代的输入)将用户提供的密码与 hashValue 属性中存储的值进行比较。
用于散列的 BASE64 编码盐 ("pH1TDVHSfGBxkd3Q88UNhQ==") 被添加到原始密码之前。目标 BASE64 编码哈希必须是“pVjR9ktO9vlxijXcMPlH+4PLwD4Xwy1aqbNQOFmWaSpvBjipNh//T8S3nBhq6HRoRVfWL6s/+NdUCPTxUr0vZw==”
所以我的 Python 脚本尝试使用如下所述的算法生成相同的哈希值:
import hashlib
import base64
import struct
TARGET_HASH = 'pVjR9ktO9vlxijXcMPlH+4PLwD4Xwy1aqbNQOFmWaSpvBjipNh//T8S3nBhq6HRoRVfWL6s/+NdUCPTxUr0vZw=='
TARGET_SALT = 'pH1TDVHSfGBxkd3Q88UNhQ=='
bsalt = base64.b64decode(TARGET_SALT)
def hashit(what, alg='sha512', **kwargs):
if alg == 'sha1':
return hashlib.sha1(what)
elif alg == 'sha512':
return hashlib.sha512(what)
# etc...
else:
raise Exception(f'Unsupported hash algorithm: {alg}')
def gethash(data, salt=None, alg='sha512', iters=100000, base64result=True, returnstring=True):
# encode password in UTF-16LE
# ECMA-376-1:2016 17.15.1.29 (p. 1026)
if isinstance(data, str): data = data.encode('utf-16-le')
# prepend salt if provided
if not salt is None:
if isinstance(salt, str): salt = salt.encode('utf-16-le')
ghash = salt + data
else:
ghash = data
# hash iteratively for 'iters' rounds
for i in range(iters):
try:
# next hash = hash(previous data) + 4-byte integer (previous round number) with LE byte ordering
# ECMA-376-1:2016 17.15.1.29 (p. 1020)
ghash = hashit(ghash, alg).digest() + struct.pack('<I', i)
except Exception as err:
print(err)
break
# remove trailing round number bytes
ghash = ghash[:-4]
# BASE64 encode if requested
if base64result:
ghash = base64.b64encode(ghash)
# return as an ASCII string if requested
if returnstring:
ghash = ghash.decode()
return ghash
但是当我跑步时
print(gethash('johnjohn', bsalt))
我得到以下不等于目标的哈希:
G47RT4/+JdE6pnrP6MqUKa3JyL8abeYSCX+E4+9J+6shiZqImBJ8M6bb+IMKEdvKd6+9dVnQ3oeOsgQz/aCdcQ==
我的实现是否有误,或者您认为低级哈希函数实现(Python 的 hashlib 与 Open XML)有什么不同?
更新
我意识到 Word 使用旧算法来预处理密码(为了与旧版本兼容)。该算法在ECMA-376-1:2016 第 4 部分(过渡迁移功能,#14.8.1 “Legacy Password Hash Algorithm”)中有详细描述。所以我设法制作了一个重现官方 ECMA 示例的脚本:
def strtobytes(s, trunc=15):
b = s.encode('utf-16-le')
# remove BOM symbol if present
if b[0] == 0xfeff: b = b[1:]
pwdlen = min(trunc, len(s))
if pwdlen < 1: return None
return bytes([b[i] or b[i+1] for i in range(0, pwdlen * 2, 2)])
def process_pwd(pwd):
# 1. PREPARE PWD STRING (TRUNCATE, CONVERT TO BYTES)
pw = strtobytes(pwd) if isinstance(pwd, str) else pwd[:15]
pwdlen = len(pw)
# 2. HIGH WORD CALC
HW = InitialCodeArray[pwdlen - 1]
for i in range(pwdlen):
r = 15 - pwdlen + i
for ibit in range(7):
if (pw[i] & (0x0001 << ibit)):
HW ^= EncryptionMatrix[r][ibit]
# 3. LO WORD CALC
LW = 0
for i in reversed(range(pwdlen)):
LW = (((LW >> 14) & 0x0001) | ((LW << 1) & 0x7FFF)) ^ pw[i]
LW = (((LW >> 14) & 0x0001) | ((LW << 1) & 0x7FFF)) ^ pwdlen ^ 0xCE4B
# 4. COMBINE AND REVERSE
return bytes([LW & 0xff, LW >> 8, HW & 0xff, HW >> 8])
所以当我执行 process_pwd('Example') 时,我会得到 ECMA (0x7EEDCE64) 中所说的内容。散列函数也被修改了(最初的 SALT + HASH 不应该包含在主迭代循环中,正如我在论坛上发现的那样):
def gethash(data, salt=None, alg='sha512', iters=100000, base64result=True, returnstring=True):
def hashit(what, alg='sha512'):
return getattr(hashlib, alg)(what)
# encode password with legacy algorithm if a string is given
if isinstance(data, str):
data = process_pwd(data)
if data is None:
print('WRONG PASSWORD STRING!')
return None
# prepend salt if provided
if not salt is None:
if isinstance(salt, str):
salt = process_pwd(salt)
if salt is None:
print('WRONG SALT STRING!')
return None
ghash = salt + data
else:
ghash = data
# initial hash (salted)
ghash = hashit(ghash, alg).digest()
# hash iteratively for 'iters' rounds
for i in range(iters):
try:
# next hash = hash(previous data + 4-byte integer (previous round number) with LE byte ordering)
# ECMA-376-1:2016 17.15.1.29 (p. 1020)
ghash = hashit(ghash + struct.pack('<I', i), alg).digest()
except Exception as err:
print(err)
return None
# BASE64 encode if requested
if base64result:
ghash = base64.b64encode(ghash)
# return as an ASCII string if requested
if returnstring:
ghash = ghash.decode()
return ghash
然而,我已经多次重新检查此代码,但我再也看不到任何错误。但是我仍然无法在测试Word文档中重现目标哈希:
myhash = gethash('johnjohn', base64.b64decode('pH1TDVHSfGBxkd3Q88UNhQ=='))
print(myhash)
print(TARGET_HASH == myhash)
我明白了:
wut2VOpT+X8pKXky6u/+YtwRX2inDv1WVC8FtZcdxKsyX0gHNBJGYwBgV8xzq7Rke/hWMfWe9JVvqDQAZ11A5w==
错误
【问题讨论】:
标签: python hash openxml sha hashlib