破解Excel打开密码:从加密结构到纯数字密码暴力破解实践

破解Excel打开密码:从加密结构到纯数字密码暴力破解实践 简介这是一套面向Python初学者的Excel加密文件密码破解实战源码聚焦纯数字密码场景讲解如何利用Python脚本对Excel文件进行密码探测与解锁适合需要处理个人文件密码遗忘或学习Excel安全机制的技术人员。资源包共2个文件包含一个可运行的Python脚本和一段演示操作视频脚本逻辑清晰视频能直观展示运行效果与注意事项。压缩包整体仅14.55MB轻量易下载。目前已有1856人学习使用兼具实用性与教学价值。通过这份资料读者能掌握破解纯数字Excel密码的基本思路学会使用Python调用相关库逐位穷举或字典探测并能在演示视频的引导下快速调通环境、上手运行为更深层的办公自动化或密码恢复需求打下基础。1. 破解Excel打开密码先理解加密包再谈跑字典处理“Excel打开密码纯数字”这种需求时第一反应是写个for循环从000000试到999999但真动手就会发现Excel根不给你“试错”的机会错误密码不会返回“密码错误”的提示而是直接拒绝整个文件。绕过这个障碍的办法是先把加密文件拆开找到验证密码的那一段数据再在本地把候选密码重放一遍。这篇会走一遍完整流程从xlsx的加密结构、msoffcrypto-tool提取加密参数到纯数字密码的生成与多进程加速最后落在结果验证和几个容易翻车的坑上。适合要做数据恢复、处理内部加密报表、或想搞懂ECMA-376加密协议的人前提是处理的文件本身有合法访问权限。2. 拆开加密文件从EncryptionInfo里拿到密码验证器2.1 Agile加密下密码到底怎么被固定成二进制现代xlsx/xlsm文件Office 2007默认走的是ECMA-376 Agile Encryption规范打开密码不参与文件内容的直接加解密。加密包的物理结构是CFB复合文档里面有两个关键流EncryptionInfo和EncryptedPackage。EncryptionInfo是“密码验证器”的所在地它包含算法标识、salt、迭代次数、密钥长度以及encryptedVerifierHashInput和encryptedVerifierHashValue两个密文段。你的密码经过派生后必须能正确解出这两段才算验证通过。密码派生走的是固定循环把密码以UTF-16LE编码和salt一起反复做SHA-512每轮结果作为下一轮输入共迭代10万次左右实际次数记录在spinCount里。最终得到的hash截断成密钥长度再用这个密钥去解验证段。也就是说破解的判定逻辑是确定的一个候选密码派生出的密钥如果能同时解开那两段数据它就是文件密码。这个结构决定了我们不需要碰EncryptedPackage只要解析EncryptionInfo就能在自己的代码里重构出验证过程。纯数字密码空间最大也就10的n次方比起盲目调库解密手动重放验证逻辑能快一到两个数量级。2.2 用msoffcrypto-tool提取加密参数msoffcrypto-tool是处理这类文件的标准工具它把CFB里的EncryptionInfo解析成对象属性省去手动翻二进制。安装依赖pycryptodome和olefile然后执行import msoffcrypto import io with open(encrypted.xlsx, rb) as f: off msoffcrypto.OfficeFile(f) print(是否加密:, off.is_encrypted()) # 先加载一个占位密码触发内部encryption_info解析 try: off.load_key(password000000, verify_passwordFalse) except Exception as e: # 此时结构已经加载密码不对会抛错但不影响观察属性 pass enc off.encryption_info print(算法:, enc.cipher_algorithm) print(salt长度:, len(enc.salt)) print(迭代次数:, enc.iterations) print(密钥位数:, enc.key_bits)这段代码的核心目的是拿到加密参数对象。关键参数含义如下参数作用说明salt密码派生时掺入的随机数据每个文件唯一不可省略iterationsSHA-512循环次数Agile默认值约100000取自spinCountkey_bits派生密钥长度常见128/256决定AES密钥字节数encrypted_verifier_hash_input验证明文段解密后是“验证输入”对其做SHA-512encrypted_verifier_hash_value验证比对段解密后应与上面计算的hash一致注意load_key在传入密码时会立即做派生验证如果密码错误且verify_passwordTrue默认抛出的异常会中断后续观察。这里传verify_passwordFalse跳过自动验证仅加载结构。2.3 Hash校验路径为什么离线爆破比循环decrypt快一个量级直接走msoffcrypto.decrypt()把整个EncryptedPackage解出来每次要处理整个文件体积的数据还要对每个候选密码做相同的AES解密。而手动校验只需要解密两个很小的密文块一个验证输入段通常几十字节一个验证hash段。解密量相差上千倍瓶颈从磁盘和AES大块解密转移到了SHA-512派生循环本身。这也决定了破解脚本的设计思路候选密码生成器和验证器分开。生成器负责按数字空间递推验证器只做派生和小块AES解密命中后再用decrypt()完整解密一次避免误报。实际测试中完整的decrypt()路径每秒只能跑几个候选而手动校验路径单核每秒能处理150个以上8进程可以到1000。3. 纯数字候选空间的剪枝设计与核心循环3.1 位数未知时的空间估算与剪枝原则纯数字密码最常见的场景是用户设了固定位数但没人记得是4位还是6位。盲目从1位跑到8位会浪费大量时间在低概率空段上所以先估算成本位数候选数量单核估算耗时150次/秒8进程估算1-4位11110约74秒约10秒5位90000约10分钟约75秒6位900000约1.7小时约13分钟7位9000000约16.7小时约2小时8位90000000约7天约21小时实际剪枝原则三条。应优先按“记忆中可能用到的位数”分隔区间而不是一次性全跑。应优先跑4-6位这是绝大多数纯数字密码的重灾区。应把零开头的密码视为正常候选比如“0123”Excel允许这种密码很多脚本写range()时顺手丢掉了前导零。3.2 核心实现用hashlib重放派生过程完成校验下面这段是可运行的判定器手工重放ECMA-376的SHA-512派生循环验证候选密码import hashlib import itertools import msoffcrypto import io def derive_key(password: str, salt: bytes, iterations: int, key_bytes: int) - bytes: # Excel AgileSHA-512(salt UTF-16LE密码)逐轮迭代 h hashlib.sha512(salt password.encode(utf-16-le)).digest() for _ in range(iterations - 1): h hashlib.sha512(h).digest() return h[:key_bytes] def verify_candidate(enc_info, candidate: str) - bool: key derive_key(candidate, enc_info.salt, enc_info.iterations, enc_info.key_bits // 8) # 解密VerifierHashInput from Crypto.Cipher import AES iv enc_info.salt[:16] aes AES.new(key, AES.MODE_CBC, iv) verifier_input aes.decrypt(bytes(enc_info.encrypted_verifier_hash_input)) verifier_input verifier_input[:64] # SHA-512输出长度 # 对明文做SHA-512作为比对基准 digest hashlib.sha512(verifier_input).digest() # 解密VerifierHashValue与digest比较 aes2 AES.new(key, AES.MODE_CBC, iv) verifier_hash aes2.decrypt(bytes(enc_info.encrypted_verifier_hash_value)) return digest verifier_hash[:len(digest)] def brute_force(enc_info, max_digits: int): # 纯数字候选利用itertools.product保留前导零 for length in range(1, max_digits 1): for digits_tuple in itertools.product(0123456789, repeatlength): candidate .join(digits_tuple) if verify_candidate(enc_info, candidate): return candidate return None逻辑分三层。derive_key严格按照规范把密码转UTF-16LE与salt拼接后做SHA-512循环哈希最后一轮截断到key_bytesverify_candidate用派生密钥以CBC模式解密两段小数据比较哈希值brute_force用itertools.product生成带前导零的数字串。注意encrypted_verifier_hash_input在部分版本库里是bytes类型如果取出来是bytearray用bytes()包一下即可。这个脚本依赖enc_info的属性名不同版本的msoffcrypto-tool对字段命名有差异。动手前先跑2.2节的代码把enc.__dict__打印出来核对键名属性名不一致就直接改verify_candidate里的引用。3.3 参数怎么调迭代次数、内存约束、多文件批量iterations不是所有文件都一样旧版Office生成的xlsx可能只有1万次新版默认10万次。验证器不需要死用固定值直接从enc_info.iterations读取这会让老文件跑得更快。内存方面itertools.product不提前生成完整列表每个候选用完即释放跑8位数空间内存占用也稳定在百MB以内。批量处理多个文件时把enc_info的加载做成函数复用def load_enc_info(path: str): with open(path, rb) as f: off msoffcrypto.OfficeFile(f) off.load_key(password000000, verify_passwordFalse) return off.encryption_info这里有个关键点如果文件实际没有打开密码load_key会报“文件未加密”的错。批量前先用off.is_encrypted()过滤一遍避免无意义地中止任务。4. 多进程提速与长任务断点续跑4.1 按整数区间切分候选交给多个worker上一章的brute_force是单线程6位密码跑到十几分钟还能忍7位以上就必须切分并发。纯数字候选可以按十进制整数区间切分每个worker拿到[start, end)就独立跑互不依赖。这里用ProcessPoolExecutor而不是线程池原因是SHA-512循环是CPU密集型多进程能绕开GIL限制。from concurrent.futures import ProcessPoolExecutor def hunt_range(enc_info, start: int, end: int, digits: int): for number in range(start, end): candidate str(number).zfill(digits) if verify_candidate(enc_info, candidate): return candidate return None def parallel_brute(enc_info, digits: int, processes: int 8, batch_step: int 100000): limit 10 ** digits with ProcessPoolExecutor(max_workersprocesses) as pool: futures [] start 0 while start limit: end min(start batch_step, limit) futures.append(pool.submit(hunt_range, enc_info, start, end, digits)) start end for future in futures: result future.result() if result: # 命中后取消未完成任务 for other in futures: other.cancel() return result return None每个worker一次领10万个候选跑完再领下一批。这样做的考虑是如果密码恰好落在某个区间其他worker的未完成批次可以被cancel()掉整体响应更快。需要留意的是enc_info对象通过参数传给子进程时会被pickle其内部包含的bytes字段序列化开销不大但最好在worker函数内部重新加载文件而不是传递整个对象否则大salt和密文段反复拷贝会影响性能。4.2 实测吞吐量参考与耗时推算按前面代码的纯手工校验路径单进程每秒大约能完成150次派生比较这个数字会随CPU主频和pycryptodome的AES实现有浮动。多进程的吞吐量和核数基本线性相关进程数每秒候选数100万候选耗时1000万候选耗时1约150约1.8小时约18小时4约550约30分钟约5小时8约1000约16分钟约2.7小时如果机器的hashlib底层带了SHA-NI硬件加速单进程能冲到200以上。但注意只有派生循环用到了hashlibAES解开那两个小数据段仍是固定开销所以“验证一次”的成本不会降到0。4.3 断点续跑与日志设计跑7位以上密码时进程崩溃或手动停掉是常事。断点续跑的思路很简单worker每处理完一个batch就把batch结束位置写进本地文件重启时读取最后位置继续。顺手在同一个文件里记录尝试过的最大数字方便事后核对覆盖范围。import os PROGRESS_FILE progress.txt def load_progress(): if os.path.exists(PROGRESS_FILE): with open(PROGRESS_FILE, r) as f: return int(f.read().strip()) return 0 def save_progress(position: int): with open(PROGRESS_FILE, w) as f: f.write(str(position))写盘频率不要太高每10万batch写一次即可避免磁盘IO拖慢主循环。也可以在hunt_range返回None时更新进度命中的时候删除进度文件避免下次误读旧状态。日志建议记录启动时间、当前区间、累计尝试数、每秒吞吐量而不是每个候选都打一行否则输出本身会成为新瓶颈。5. 结果验证与排错把试出来的密码真正用起来5.1 候选密码的完整解密验证暴力循环命中后先用msoffcrypto做一次完整解密再确认解出来的文件是合法zip压缩包。这一步能过滤极小概率的哈希碰撞误报def full_decrypt(path: str, password: str) - bytes: with open(path, rb) as f: off msoffcrypto.OfficeFile(f) off.load_key(passwordpassword) output io.BytesIO() off.decrypt(output) return output.getvalue() data full_decrypt(encrypted.xlsx, found_password) assert data[:2] bPK, 解密结果不是zip格式密码疑似误报 with open(decrypted.xlsx, wb) as f: f.write(data)注意如果原始文件同时有打开密码和工作表保护这段代码只能解除打开密码工作表的写保护需要在解出的xlsx里继续处理。5.2 打开密码、写保护密码与老xls格式的区分排错时先确认你手里的文件到底带的是哪种锁。打开密码会体现在文件流里is_encrypted()返回True上面整套方案都适用。写保护密码存在xl/workbook.xml里的workbookProtection标签中is_encrypted()返回False直接用zipfile读文件再改XML即可解除。两者同时存在时先用本方法解打开密码再用zipfile改写XML。老版本.xls文件Office 2003及以前的加密机制完全不同走的是RC4 自有salt结构msoffcrypto虽然能解析部分老文件但salt、iterations等属性可能不存在。报KeyError或AttributeError时先确认文件扩展名和实际格式必要时用file命令查看二进制头不要把时间浪费在错误协议上。5.3 用一个标定样本文件校验整套脚本最容易被忽略的环节是验证脚本自身是否正确。写一段自检代码生成一个密码已知的加密文件跑完整流程from msoffcrypto.format.ooxml import OOXMLFile # 实际接口随版本略有差异 # 用msoffcrypto官方示例方式生成加密文件 # 设置密码为 0427确保是纯数字且含前导零 # 再用 brute_force 跑预期几秒内返回 0427如果自检文件跑不出预期密码问题大概率出在enc_info属性名或derive_key的迭代方式上而不是目标文件本身。这个方法在换msoffcrypto大版本后尤其有用——我踩过verifier_hash_input被改名导致破解程序静默失败的坑自检样本帮我把问题隔离到了升级动作上。正式跑目标文件前先把自检样本的日志留存一份确认“脚本在当前环境是可验证的”再切换目标文件这个顺序能省下最贵的调试时间。本文还有配套的精品资源点击获取