简介:面向 Excel VBA 工程密码遗忘场景下的应急解锁工具说明包,适合有 VBA 基础、希望了解 Windows API 与内存机制的技术人员使用。文档以图文与代码注释结合的方式,讲解如何绕过工程密码验证,并特别处理“工程不可查看”的情况,核心思路是通过 hook user32.dll 中的 DialogBoxParamA 函数,配合 VirtualProtect 修改内存保护属性,再以 MoveMemory 替换与恢复函数入口字节。资源包含 1 个 docx 说明文档,压缩包约 15KB,篇幅精简,重点集中在 API 地址获取、函数前六个字节备份、Hook 与恢复过程等关键步骤,并附带完整 VBA 示例代码。目前已有 1862 人浏览学习。对想深入理解 VBA 底层调用、程序保护机制与内存修补原理的读者,是一份可对照练习的参考笔记;但文中所涉方法仅适用于本人拥有密码权限的文件,应用时需注意合法边界。
1. 解除VBA工程密码:先搞清楚你动的是哪一层保护
接手遗留的 Excel 宏文档时,打开 VBA 编辑器弹出一个密码框,而交接人早就联系不上,这是很多人第一次遇到“解除 VBA 工程密码”的真实场景。标题里的“VBA 工程密码”指的是 Office 里 VBA 项目的访问口令——设置了它,别人按下 Alt+F11 就只能看到工程名,看不到任何模块代码。解除它不是破解文件打开密码,也不是暴力猜口令,而是把工程数据里“有密码”的校验标记去掉,让代码重新可查看、可修改、可迁移。适合正在被历史宏卡住的人,也适合准备批量清理手头宏资源的开发者。整篇会从原理说到工具,再落到参数和坑。
2. VBA工程密码藏在哪里:从文件结构到可解除的原理
2.1 OOXML与OLE:vbaProject.bin才是真正的战场
很多人第一反应是去 Word 或 Excel 的 XML 里找密码,那是白费劲。Office 2007 之后,xlsm、docm、pptm 这类带宏的文件本质上是一个 ZIP 压缩包,里面装着各种 XML 部件;但 VBA 本身不放在这些 XML 里,而是作为一个整体二进制流存在,文件名固定叫 vbaProject.bin,常见位置是 xl/vbaProject.bin(Excel)或 word/vbaProject.bin(Word)。这个文件是 OLE 复合文档格式,里面装着 VB 工程的代码、模块信息、窗体、引用以及工程属性。
所以解除密码的第一步不是打开 Office,而是打开这个 ZIP 容器,把 vbaProject.bin 抽出来。理解这一点非常关键:很多人拿着 Office 自带的宏编辑器折腾半天,是因为找错了对象。而老版本 Office(2003 及更早)的 xls/doc 本身就是 OLE 复合文档,不需要解 ZIP,直接用十六进制编辑器打开原文件也能看到同样的结构。原理是一样的,区别只在于容器。
OLE 复合文档内部像一个迷你的文件系统,里面有目录流、存储和流对象。vbaProject.bin 里最重要的几个流是 PROJECT(文本工程属性)、dir(模块和工程信息)、以及真正存代码的流。密码信息就藏在 dir 流末尾的 PROJECTLOCK 结构里。理解了这一层,就能解释为什么它能被直接绕过。
2.2 DPB校验块:为什么改两个字节就能绕过密码
VBA 工程密码并不是把整个代码用密钥加密一遍。更准确地说,它是一道“查看权限”的开关:检查到正确的密码后,Office 才把代码流映到 VBA 编辑器里。这个开关的状态记录在 dir 流尾部的 PROJECTLOCK 结构中。未加密工程通常记录为 DPx 之类的标记,而加密工程记录为 DPB,后面附带一串校验信息。
Office 读取工程文件时会先解析 PROJECTLOCK:如果读到未加密标记,就直接放行;如果读到 DPB,就要求输入密码并校验。问题在于,这个校验并不是一个强签名,老版本 VBA 格式甚至允许“长度字段为零”时跳过整个保护结构。于是有了两种非常直接的解除方式:一是把 DPB 改成 DPx,让 Office 把工程当成未加锁;二是把 DPB 后面跟的二进制长度字段清零,让解析逻辑认为保护块不存在。这两种方式都不碰代码流,所以理论上代码不会丢。
为什么 Office 到今天还没有把这个口子完全堵死?主要原因是兼容性。VBA 工程格式从 Office 97 一直延续到现在,新版本要读取 20 年前生成的 vbaProject.bin,不能随便改变校验结构;而识别未加锁标记又是安全校验的第一步。所以补丁式的修改在很多版本上仍然有效。这个“改标记”的做法不是去计算密码,而是让 Office 压根不去验证,这也是它比跑字典快得多的原因。
需要补充一个边界:此方法不能解除文件打开密码,不能解除工作簿或文档结构保护,更不能让你合法访问别人有意保护的代码。它只解决“工程查看锁”这一件事。如果文件本身设置了打开密码,必须先拿到或解除文件层密码,才能走到这一步。每一次修改之前,都把原文件复制一份作为备份——这不是建议,而是底线,因为后续操作如果改错位置,可能让 Office 直接拒绝打开文件。
2.3 合法边界与通用流程:先备份、再定位、后修改
整个解除流程可以归纳为四步:复制备份、取出 vbaProject.bin、定位并修改 DPB 校验标记、回写重打包。看起来简单,但每一步都有细节决定成败。先说备份:不要把“解除前备份”和“解除后版本管理”混为一谈。我一般会在同一个目录下生成 original.xlsm 和 unlocked.xlsm 两个文件,全程只操作 unlocked.xlsm,原文件不动。这样即使改坏了也能一键回到起点。
取出 vbaProject.bin 有两个常规入口:命令行 unzip 或者图形化解压工具。对于不熟悉命令行的,用解压工具打开 xlsm 文件,找到 vbaProject.bin 单独拖出来即可。如果解压工具有“压缩方式”相关设置,尽量选“不修改压缩方式”的提取,这样回写时更容易保持原包结构。命令行方式会在第 3 章给出具体命令。
定位修改时需要用十六进制编辑器。这类工具很多,不用追求新版本,能显示二进制和 ASCII 就行。打开 vbaProject.bin 后,搜索字符串 DPB。如果搜到了,继续观察它周围字节;如果同时看到 DPx,说明这个工程当前没有加保护,什么都不用改。这里有个常见误解:很多人以为密码是以明文形式存在文件里的,所以搜“密码字符串”去找;实际上没有明文密码,只有经过混淆的校验数据。搜 DPB 是最直接有效的入口。
3. 手工解除:用十六进制编辑器改出无密码工程
3.1 从xlsm/docm里拆出vbaProject.bin
命令行的拆包命令如下。我以 Excel 宏文件为例,Word 的 docm 结构类似,只是路径从 xl 换成 word。
mkdir unlocked_dir unzip locked.xlsm -d unlocked_dir ls unlocked_dir/xl/vbaProject.binmkdir unlocked_dir创建解压目录;unzip locked.xlsm -d unlocked_dir不会把 ZIP 里的文件撒得满目录都是,而是按包内目录结构放到 unlocked_dir 下;最后确认 vbaProject.bin 确实在 xl/ 路径中。如果系统提示没装 unzip,也可以用 Python 一行完成解包,但不关键,工具顺手即可。
参数注意:如果包内还有 vbaProjectSignature.bin(签名文件),不要动它;只提取 vbaProject.bin 做修改。locked.xlsm 建议使用完整路径或先进入文件所在目录,否则解包路径会让人找半天。解包之后先不要关终端,后面回写还要用。
3.2 定位DPB块并解析长度字段
用十六进制编辑器打开 unlocked_dir/xl/vbaProject.bin。按下 Ctrl+F,在搜索类型里选 ASCII 字符串,输入 DPB。正常加密工程会在文件的某个偏移处找到这个三字节序列,通常在文件末尾附近的 dir 流区域内。记录这个偏移值,我习惯写成 0x???? 的格式,并截图或抄下来。
找到 DPB 后不要急着改,先观察它前后的字节。受保护结构一般长这样:前面是版本或标志字节,DPB 是三个连续 ASCII 字符,随后是 4 字节的小端长度值,表示后面加密数据的字节数。这 4 字节长度通常是个比较大的数字,比如文件剩余几千字节,那么它以小端序写成类似 xx 00 00 00 的形式。如果看到 DPB 后面不是这种结构,而是紧跟着可读文本,那很可能你搜到的是误报——文件名、模块名里也可能出现 DPB 这三个字母。
为了定位准确,建议在搜索结果里查看偏移,再用十六进制面板从 DPB 往后数 4 个字节,把这 4 个字节读出来。小端序换算公式是:值 = byte0 + byte1 * 256 + byte2 * 65536 + byte3 * 16777216。如果算出来的值加上 DPB 偏移接近文件大小,说明结构正确,可以动手了。如果你搜索时发现文件里只有 DPx 没有 DPB,那本来就无密码;如果 DPB 出现多处,以最后一次出现为准。因为 dir 流末尾的 PROJECTLOCK 是最后一个结构,前面的可能是编译缓存里的相似字节。
3.3 两种修改路线:DPB改名与长度清零
路线一:把 DPB 改成 DPx。在十六进制编辑器里定位到 DPB 的偏移,把第三个字节从 42(ASCII 的 B)改成 78(ASCII 的 x),保存。这样 PROJECTLOCK 读到的标记变成未加密形式,很多老版本工程会直接放行。这个做法优点是改动最小,缺点是部分新版 Office 会对“内容被篡改”做二次校验,反而弹出修复提示。
路线二:长度清零。保持 DPB 不变,把它后面 4 个字节全部改成 00 00 00 00。解析逻辑读到保护块长度为零时,会认为当前没有需要校验的密码数据,从而跳过整个口令验证。这个做法在我的实测里对新旧版本兼容性更好,但一定要确认长度字段确实指向保护数据,而不是代码流的长度。如果改错位置,代码区会被截断,工程可能直接损坏。
两种方法可以做一个对比:
| 路线 | 改什么 | 适合场景 | 主要风险 |
|---|---|---|---|
| DPB 改名 | 第 3 字节 B→x | 格式较老、结构简单 | 新版本二次校验可能报损坏 |
| 长度清零 | DPB 后 4 字节置 0 | 各种版本,推荐先试 | 偏移必须准确,改错伤代码 |
实际操作时我的习惯是:先用长度清零试一次;如果 Office 提示不可读取,再用 DPB 改名路线从备份重做。两种方法改完都直接保存 vbaProject.bin,不要顺手做“另存为其他格式”,保持文件原始格式。
3.4 回打包与打开验证:压缩方式与修复提示
修改完 vbaProject.bin 后,回到 unlocked_dir 目录把它重新打包成 xlsm。这里最大的坑是:ZIP 打包方式的差异会让 Office 拒绝识别。最稳妥的简单方式是保持不压缩:
cd unlocked_dir zip -0 -r ../unlocked.xlsm .-0表示不压缩,-r表示递归包含目录和文件。注意最后还有一个点,表示把当前目录内容打包,而不是把 unlocked_dir 这层目录也打进去。如果打包后文件体积明显大于原始文件,是正常的,因为没压缩;Office 对不压缩的 ZIP 完全兼容。如果你用的解压工具自带打包功能,选择“存储”级别也是同理。
回包之后,复制一份 unlocked.xlsm 用于验证。双击打开文件,按 Alt+F11 进入 VBA 编辑器,如果能看到模块和代码,说明成功。如果弹出“发现不可读取的内容”或“是否修复”,说明刚才的修改有风险:先点否,再回到备份重做一次,换另一种路线。千万不要在那个提示窗里点“修复”,因为修复过程可能把整个 VBA 工程清空。
另外,很多现代 Office 会记住“受保护的视图”状态,导致文件处于只读预览,VBA 编辑器看起来打不开。请先确认 Excel 底部或顶部没有“启用编辑”横幅;文件若处于受保护视图,工程也会被限制。点击“启用编辑”后再验证,才能得到真实结果。
4. 批量化与脚本化:用Python自动处理多个VBA工程
4.1 为什么这个需求适合脚本化
手工方法适合处理一个文件,但实际需求往往是一批:某部门交接下来几十个带宏的 xlsm,都要清理掉查看密码;或者是同一个模板派生的多个副本,设置密码时用了同一个口令,现在要批量去掉。面对这种场景,一个个用十六进制编辑器改会让人崩溃。VBA 工程密码解除的核心操作在二进制层面非常固定:搜 DPB,改标记。这正好适合脚本化。
尤其适合的典型场景有三个:定期从老宏目录收集代码、把外部宏文件整理进自有代码库、以及为团队批量重置丢失的工程密码。脚本要做的不是“猜出密码”,而是通过重打包生成一个新文件,原文件保持不动。这个思维和手工流程一模一样,只是把定位与修改交给代码,避免人工记偏移。
4.2 脚本骨架:定位、修改、回写
下面这个脚本是我常用的雏形,去掉了具体项目路径,只保留核心逻辑。它能处理 xlsm、docm、xlam 这类基于 ZIP 的 Office 文件。
import sys import zipfile def find_dpb_offset(data: bytes) -> int: """在二进制流中定位 DPB 标记,取最后出现位置""" return data.rfind(b'DPB') def patch_vba(data: bytes, mode: str = 'zerolen') -> bytes: """修改 vbaProject.bin 中的工程保护标记""" off = find_dpb_offset(data) if off == -1: print('[!] 未找到 DPB,文件可能未加密或结构不同') return data buf = bytearray(data) if mode == 'rename': # 路线一:DPB 改为 DPx,标记为未加锁工程 buf[off + 2] = 0x78 print(f'[*] 已把 DPB@{off:#x} 改为 DPx') elif mode == 'zerolen': # 路线二:DPB 后的 4 字节长度字段清零 for i in range(off + 3, off + 7): buf[i] = 0 print(f'[*] 已清零 DPB@{off:#x} 后的长度字段') else: raise ValueError('mode 必须是 rename 或 zerolen') return bytes(buf) def unpack_and_patch(src: str, dst: str, mode: str = 'zerolen') -> None: """读取源压缩包,替换 vbaProject.bin 后写出新文件""" with zipfile.ZipFile(src, 'r') as zin: with zipfile.ZipFile(dst, 'w') as zout: for item in zin.infolist(): raw = zin.read(item.filename) if item.filename.endswith('vbaProject.bin'): raw = patch_vba(raw, mode) # 保留原始压缩方式和元数据,避免 Office 兼容性问题 zout.writestr(item, raw) if __name__ == '__main__': src_file, dst_file = sys.argv[1], sys.argv[2] patch_mode = sys.argv[3] if len(sys.argv) > 3 else 'zerolen' unpack_and_patch(src_file, dst_file, patch_mode) print(f'[+] 完成:{dst_file}')逻辑说明:先读入源 ZIP 的每个条目,只对路径以 vbaProject.bin 结尾的条目做二进制修改,其他 XML 部件原样回写。这样不会破坏包内其他内容。find_dpb_offset 用 rfind 取最后一次出现的 DPB,对应 dir 流末尾的 PROJECTLOCK 结构,同时避开可能出现在模块名里的误报。patch_vba 按照前面说的两种模式修改。
参数说明:第一个参数是源文件路径,第二个是输出文件路径,第三个可选参数是修改模式,默认 zerolen,可以手动改成 rename。输出文件必须和源文件不同,我一般写 temp_unlocked.xlsm,验证成功后再覆盖正式命名。writestr 时没有手动设置 compress_type,而是沿用 item.compress_type,这让每个条目的压缩方式与原本一致,是避免 Office“文件损坏”提示的关键。
如果你要批量处理,只需要在外面套一层目录遍历:
import glob for f in glob.glob('*.xlsm'): unpack_and_patch(f, f.replace('.xlsm', '_unlocked.xlsm'), 'zerolen')这个循环会为当前目录下的每个 xlsm 生成一个带 _unlocked 后缀的新文件,原始文件保持不动。建议不要直接在原文件上覆盖,因为一旦脚本对结构判断错误,你还能从原文件从头再来。
4.3 脚本的边界:老版xls文件与解密后仍需修复
脚本对 xlsm、docm、pptm 这类 ZIP 容器有效,但对于老版 .xls、.doc、.ppt 无效,因为它们是 OLE 复合文档,不是一个 ZIP 包。处理老版文件时我一般直接复制一份,用十六进制编辑器打开,在全文里搜 DPB,然后手工改。网上虽然也有直接处理 OLE 的脚本,但需要额外解析 dir 流,踩坑成本比手工高,除非你确定要长期处理几十个老文件,否则不必为一次需求引入新依赖。
另外,脚本执行成功不等于文件就能打开。有些文件在 VBA 工程密码之外还有一层工作簿保护或文档保护,打开后 Excel 会提示某些区域被锁定,VBA 编辑器虽然能进,代码也可能已被隐藏。更常见的边界是:如果文件还开着,或者有外接程序占用,脚本读源文件时会报 PermissionError;请先关掉所有 Office 进程再运行。最后再强调一次,脚本不处理打开密码,有打开密码的文档要先在 Office 里解除,否则后面一切都不成立。
5. 解除VBA工程密码的常见问题与避坑指南
5.1 修改后Office提示文件损坏、拒绝打开
现象:按前面步骤改完并重打包,双击文件,Office 弹窗说“文件已损坏,是否修复”,点“是”后所有宏代码没了。
原因:常见三类。第一,DPB 偏移找错或长度清零多清了几字节,破坏了 dir 流;第二,重打包时用了默认压缩级别,导致 Office 的 ZIP 解析器不认;第三,改到中途保存时编辑器自动追加了 EOF 或换了编码。
解决:立即放弃当前文件,从备份重新解包。先检查偏移:DPB 后第 4 个字节的值应该小于文件长度,若反了就是找错位置。回包时用 zip -0 或者脚本方式,保留原始压缩方式。如果一定要在十六进制编辑器里保存,注意确认编辑器没有悄悄换成 UTF-8。养成“只操作副本,失败就回滚”的习惯,这一条能解决绝大部分损坏问题。
5.2 代码窗口一片空白
现象:能进 VBA 编辑器,能看见模块名称,但双击模块,代码区空无一物。
原因:最常见的是原工程勾选了“保护工程并查看工程属性”,但它不影响查看模块;真正造成空白的是修改时误伤到了 dir 流里的模块名表,或者原工程代码本身做了隐藏处理。还有一种情况:文件被压缩工具重新打包后模块流被“优化”掉了。
解决:先试试能不能从工程里导出模块;如果导出来是空的,回到原始加密文件,输入正确密码后正常导出。如果原始文件密码已不可得,就只能从备份或交接资料中找回代码。提醒一句:解除工程密码能绕过查看锁,但如果对方在代码层做了隐藏处理,解除密码只是一部分工作。
5.3 文件还有打开密码,VBA编辑器直接进不去
现象:输入文件打开密码才能看到内容,但按下 Alt+F11 还是提示“工程不可查看”。
原因:文件打开密码是一道比 VBA 工程密码更靠前的锁。ZIP 容器本身被 Office 应用层加密,VBA 工程数据也被卷入了文件级加密,直接改 vbaProject.bin 里的 DPB 标记没用,因为 Office 要先验证文件解密,才会把 vbaProject.bin 交给 VBA 引擎解析。
解决:先确认文档的打开密码是否已知。知道密码就正常打开,另存为无密码的副本,再对副本做 VBA 解除。不知道打开密码,那不是本文能解决的范围,请先与文件所有者确认授权。一定不要用“取消密码保护”的旁门左道去处理别人有打开密码的文档,既不稳定,也容易跨过安全边界。
5.4 同一份patch后的文件在不同Office版本下表现不一致
现象:同一个 xlsm,在某台机器的 Office 2010 上能正常打开,换到 Office 2019 打开却提示工程损坏,或者干脆看不到模块。
原因:老版本 Office 对 ZIP 结构和 dir 流末尾的 PROJECTLOCK 解析很宽松,改标记就放行;新版本加了更多一致性校验,遇到改动会认为文件被篡改。
解决:交叉验证时以原文件生成的 Office 版本为准。如果文件是老版本生成的,优先用 DPx 改名路线;如果新版本生成,优先用长度清零。实在不确定,就在备份上把两种路线各做一遍,然后用本机 Office 验证。之后你的归档文件应该明确记录“用哪个 Office 版本验证过”,避免换机器后误判。
5.5 改完宏能运行,但一打开工程属性又自动加回密码
现象:VBA 密码清掉后,使用几天又发现工程被锁定,密码框重新出现。
原因:文件里很可能有一段自保护宏,在 Workbook_Open 或 Document_Open 事件里执行锁定工程的代码;也可能来自外接程序注入。这不是修改没生效,而是运行时又被保护了。
解决:打开 VBA 编辑器后先检查 ThisWorkbook 里有没有 ActiveWorkbook.VBProject.Protection 相关的代码。先停用所有外接程序再测试。确认是文件自身逻辑后,删除或注释掉自锁代码,并另存一份不带该逻辑的干净版本。清密码前也建议先关闭其他外接程序,避免它们干扰验证结果。
6. 验证与进阶:解除后如何确认代码完整,并改造为团队可维护的方案
6.1 解除后必做的完整性验证
密码清掉只是第一步,确认代码完整才是真正收口。我的验证顺序固定为六步:
- 打开解除后的文件,启用编辑,按 Alt+F11 进入 VBA 编辑器。
- 查看工程资源管理器,对照资源清单核对模块数量,少了模块说明没改干净或代码隐藏。
- 逐一打开每个代码窗口,按 Ctrl+A 全选,看代码行数是否合理。
- 右键工程名称进入工程属性,切到保护页签,确认“查看工程属性”复选框未被选中。
- 在立即窗口输入 ?ThisWorkbook.VBProject.Protection 并回车,返回值应为 0。
- 运行一次无副作用的宏,比如弹一个 MsgBox,确认编译通过。
| 验证项 | 预期结果 | 失败处理 |
|---|---|---|
| 模块数量 | 与资源清单一致 | 检查 dir 流是否损坏,回到备份 |
| 代码窗口 | 每模块都有代码 | 检查是否被属性隐藏,尝试导出 |
| 工程属性保护页 | 复选框未选中 | 手动取消勾选并保存 |
| Protection 属性 | 0 | 说明仍有运行时保护逻辑 |
| 运行验证 | 无报错 | 检查缺失引用,打开引用对话框补引用 |
这里最有迷惑性的是第 5 项。即使你在工程属性里看到没勾选,运行时也可能动态修改 Protection 值,所以一步不能少。
6.2 从一次性解除到规范化管理:导出模块与版本管理
解除成功后,我习惯立刻把所有模块导出一份,放进代码仓库里。具体操作不复杂:在 VBA 编辑器的工程资源管理器里,右键每个模块选择“导出文件”,能导出 .bas(标准模块)、.cls(类模块)、.frm(窗体)三类。导出的文本文件可以直接纳入版本管理,日后再也不用从加密文件里救人。
这个习惯来自一次翻车经历。某次我图省事,直接在原文件上做修改,结果 Office 提示修复,我手快点了“是”,VBA 工程被清空,代码全丢。后来那套逻辑我对着旧打印稿重写了大半天。从此再处理这类工程,一定先复制副本、再操作、最后验证;解除密码只是获得访问权,把代码变成普通人可读的文本文件,才算真正把东西攥在手里。
如果你的团队还要长期维护这些宏,建议把“工程密码”从技术保护方案里去掉。VBA 工程密码本来就不是可靠的安全措施,它充其量是一道心理锁;真要防外部人改代码,应该走文件权限、宏签名和统一的版本发布流程。让每个人手里的副本都是可编辑的源码库,出问题时从库里恢复,比什么密码都有用。希望这套从原理到验证的流程能帮到你拿到代码、修通遗留工程。
本文还有配套的精品资源,点击获取