你在Excel里攒了三年的VBA代码,被同事用工具一分钟破掉工程密码,拿着你的函数库去给领导报功——这画面光想想就冒火。我做这套VBA工程加密工具的思路,核心就一句话:不跟你比密码强度,直接把源码搞成“打开也看不懂、复制也跑不了”的废纸。这种破坏式锁定,跟市面上那些加个密码、隐藏模块的小打小闹完全是两个维度的东西,真正让源码失去被阅读和复用的价值。
这篇文章我会从原理讲到实操,把整个加密工具的设计思路、核心代码、坑和验证过程全部摊开。适合被代码白嫖困扰的VBA开发者,也适合想给公司内部工具做分发保护的同学。如果你以为VBA加密就是“工具-属性-设置密码”那一步,那你接下来会很惊讶——因为那条路在专业人士眼里,连门都没关上。
1. 先打破幻想:VBA工程密码为什么谁都挡不住
1.1 工程密码的真实强度和破解原理
VBA工程密码的全称叫“VBA Project Password”,在Excel文件里它存在OLE复合文档的PROJECT流中。问题就出在这里:VBA保存工程密码时,使用的并不是现代加密算法,而是一个非常古老的异或运算加简单变换。具体来说,密码不是被“加密”的,而是被“混淆”了,而且混淆的密钥和算法几乎是公开的。
这意味着什么?意味着破解工具根本不需要穷举你的密码,它只需要做两件事中的任何一件:第一,直接解析PROJECT流,把存密码的字节段读出来反推明文;第二,更粗暴——定位到DPB=(有密码)标记,直接改成DPx=(无密码),再把相关校验字节抹掉。整个操作耗时按秒算,这也是为什么网上那些“VBA Password Recovery Pro”工具敢号称秒破。
我亲手试过一次,一个10位大小写加符号的密码,破解工具花了两秒。不是说我这密码不够强,而是VBA工程密码的存储方式决定了,无论你设多长的密码,只要密码标记在文件里,就有办法把它抹掉或还原。所以,如果你的保护方案只有“工程密码”这一层,那你的源码在专业人士面前等于没穿衣服。
1.2 常规加固方案的三个致命弱点
很多人还用过其他加固手段,我挨个说破:
第一,“工程不可查看”选项。这个选项只是让VBE(Visual Basic Editor)在打开对象时不显示模块内容,但它在文件里就是一个布尔标记。破解者还是走老路——直接改二进制流,把这个标记改回去,你的模块内容就乖乖现身了。
第二,隐藏工作表放代码。有些朋友把代码放在xlVeryHidden的隐藏表里,以为别人找不到。实际上ENDB中-1状态、VISIBILITY属性都写在文件结构里,用任何带工作表管理功能的工具,或者干脆用一段几行的VBA就能把所有隐藏表的状态改回可见。隐藏不是保护,是告诉别人“这里藏了东西”。
第三,代码加个密码框、验证机器码之类的运行时保护。这类方案能拦住普通用户,但拦不住源码阅读者——因为所有验证逻辑都在VBA源码里,人家打开模块,把你的If判断一行改掉,密码框就当摆设了。所以结论很清晰:只要VBA源码以“明文可读”的形式躺在文件里,任何基于“不让看”的保护都是脆弱的。
这也是我转向“破坏式锁定”的根本原因——不把功夫花在“怎么不让人打开门”上,而是花在“门打开之后屋里全是垃圾”上。
2. 破坏式锁定的核心设计:不防打开,防“看懂”
2.1 源码与p-code:为什么改了源码程序还能跑
要理解破坏式锁定为什么可行,必须先弄明白VBA的执行机制。你在VBE里看到的代码是源码文本,但Excel保存工作簿时,VBA引擎会把源码编译成一套高度压缩的中间代码,叫p-code(packed code,伪代码)。运行时,VBA虚拟机执行的是p-code,不是直接读源码执行。
更关键的一点:修改源码文本后,只要新的源码仍然语法合法,在保存或运行触发重新编译时,新的p-code会重新生成,宏照常跑。这给了我一个很大的空间——我可以把源码搞得面目全非,但保证它语法正确,这样代码的“运行价值”保留,而“阅读价值”被彻底摧毁。
这里必须提醒一句:破坏式锁定不是“把模块里塞点语法错误让VBE报错”,那是自毁。如果新的源码编译不过,Excel会直接炸给你看。真正有效的是“混淆不破坏”,即代码能编译、能运行,只是人的眼睛已经无法从中提取逻辑。
2.2 三层锁定策略:防阅读、防恢复、防提取
我的加密工具最终实现的效果分三层:
第一层是工程密码,作用是挡住99%的普通用户。它的价值不在“防破解”,而在于“筛选”——愿意去下载工具破解的人,已经是有点耐心的人了。
第二层是源码混淆,这是破坏式锁定的重头戏。普通用户可能就没机会看到这一层,但破解密码后,所有模块打开全是乱码式代码:字符串全是ChrW拼接,变量名全部无意义化,过程被拆成碎片,逻辑由统一的调度器分派。看这种代码的体验,就像看一本被翻译成火星文的技术文档。
第三层是垃圾洪流与陷阱。我会在模块顶部插入#If False Then ... #End If块,里面塞几百行完全合法的假业务代码,比如伪造的数据库连接、假的登录逻辑。这些代码不参与编译,肉眼却无法快速分辨;模块尾部再加十万行注释垃圾,让VBE打开这个模块时直接卡到假死。破解者的核心工具是“眼睛”,那我就让他的眼睛被信息淹没。
2.3 混淆规则设计:让代码变成“能跑的天书”
具体落地的混淆规则,我设计了五条:
规则一,字面量全部ChrW化。源码里所有可见字符串,包括提示文本、路径、SQL语句,全部替换成ChrW(72)&ChrW(101)&ChrW(108)&ChrW(108)&ChrW(111)这种形式。人眼扫描代码时,看到一大片数字拼接,第一反应就是放弃。
规则二,标识符替换。模块里所有函数名、变量名换成a1、b7、x9这样的纯无义编号,并打乱映射关系。这一步需要先扫描模块收集所有标识符,再做全局替换,同时避开关键字和已被替换掉的字符串内容。
规则三,逻辑拆块。把单个大过程拆成多个小过程,每个小过程只做原始逻辑的一小段,再由主过程按照一个掩码数组依次调用。掩码数组用一行Array(5,2,8,1,6)这样的形式写在代码末尾,运行时动态解析,阅读者从任何一个局部过程都无法反推整体流程。
规则四,假分支与诱饵。在正常代码之间穿插If False Then的假分支,里面放上一段能正常编译但执行不到的诱饵代码。比如假分支里写一个“连接服务器获取授权”的函数,看上去像是你的核心算法,实际是诱饵。
规则五,注释洪流。模块尾部追加注释行,每行内容是一串随机字符生成的假日志,总行数可以到5万行以上。VBE每打开一次这个模块,就要把这5万行逐个渲染出来,卡顿到怀疑人生。注意控制体积,我的经验是5万行注释大约会让工作簿增加1到2MB,可以接受。
这三层设计和五条规则合在一起,就是一套完整的破坏式锁定框架。接下来我讲怎么把这个理念变成能用的VBA工具,直接附上可复制的代码。
3. 实操:用VBA给自己写一个工程加密/混淆工具
3.1 准备工作:信任设置与工具结构
要做混淆工具,第一步是打开开发工具入口。新建一个工作簿,命名为VBE混淆器.xlsm,里面只放两个模块:一个叫ObfuscatorCore,放核心转换逻辑;另一个叫PasswordChanger,用来自动设置工程密码。
还有一个关键前置条件:必须开启“信任对VBA工程对象模型的访问”。打开Excel的“信任中心 > 宏设置”,勾选最下面那一项。没有这一步,代码里所有Application.VBE对象的操作都会报“程序访问VBA项目被拒绝”的错。
然后需要在VBE中手动引入库:菜单“工具 > 引用”,勾选Microsoft Visual Basic for Applications Extensibility 5.3。注意这个引用在有些精简版Office里没有,碰到的话装一下VBA组件即可。
3.2 核心转换器:文本常量打散、注释洪流、逻辑打散
我先贴出核心模块的代码,然后逐段解释。这套代码的作用是对指定模块做一次性混淆,把源码破坏成“能编译但读不懂”的状态。
' 模块: ObfuscatorCore Public Sub ObfuscateModule(ByVal wb As Workbook, ByVal compName As String) Dim comp As VBComponent Dim cm As CodeModule Dim lineCount As Long Dim i As Long Dim src As String Dim newSrc As String Set comp = wb.VBProject.VBComponents(compName) Set cm = comp.CodeModule lineCount = cm.CountOfLines If lineCount < 2 Then Exit Sub ' 第一步:收集并替换全部字符串字面量为ChrW拼接 ReplaceAllStrings cm ' 第二步:逐行做逻辑打散和标识符替换(简化版本) For i = 1 To cm.CountOfLines src = cm.Lines(i, 1) If Trim(src) <> "" Then newSrc = ReplaceIdentifiers(src) If newSrc <> src Then cm.ReplaceLine i, newSrc End If Next i ' 第三步:注入垃圾洪流与陷阱块 InjectGarbage cm MsgBox "模块[" & compName & "]混淆完成" End Sub这个主流程看着简单,但每一步背后都有讲究。
第一步,ReplaceAllStrings,处理字符串字面量。VBA里用双引号包起来的字符串是混淆的重灾区,直接把所有可见文本转成ChrW拼接,人眼无法快速阅读。实现如下:
Private Sub ReplaceAllStrings(ByVal cm As CodeModule) Dim i As Long Dim src As String Dim regex As Object Dim matches As Object Dim m As Object Dim sb As String Set regex = CreateObject("VBScript.RegExp") regex.Pattern = """(?:[^""\n]|"""")*""" ' 匹配双引号包裹的字符串,兼容""转义 regex.Global = True For i = 1 To cm.CountOfLines src = cm.Lines(i, 1) If InStr(src, """") > 0 Then Set matches = regex.Execute(src) sb = "" ' 在替代前保留行首缩进和非字符串部分 Dim lpos As Long lpos = 1 For Each m In matches sb = sb & Mid(src, lpos, m.FirstIndex - lpos + 1) sb = sb & BuildChrWChain(Mid(src, m.FirstIndex + 1, m.Length - 2)) lpos = m.FirstIndex + m.Length + 1 Next sb = sb & Mid(src, lpos) If src <> sb Then cm.ReplaceLine i, sb End If Next End Sub Private Function BuildChrWChain(ByVal s As String) As String Dim i As Long Dim parts() As String Dim cnt As Long cnt = Len(s) ReDim parts(1 To cnt) For i = 1 To cnt parts(i) = "ChrW(" & AscW(Mid(s, i, 1)) & ")" Next BuildChrWChain = "(" & Join(parts, "&") & ")" End Function这段代码有一个关键细节:正则匹配会连带双引号本身匹配进来,所以我在BuildChrWChain里用Mid(src, m.FirstIndex + 1, m.Length - 2)把两端的双引号去掉,只对里面的内容做转换。而且把空字符串也变成了ChrW(),注意空串单独处理,否则会变成ChrW(0),虽然语法合法,但会多出个空字符,没必要。
第二步,ReplaceIdentifiers,标识符替换是混淆里最容易出错的环节。全自动的标识符分析需要做词法扫描,工作量大,所以我的工具默认只替换一个预设符号表,你可以在代码里手动指定要替换的变量名或函数名。这是“半自动”方案,效果好、风险低:
Private Function ReplaceIdentifiers(ByVal src As String) As String Dim pairs As Variant Dim j As Long ' 预设替换表:左边是原名,右边是混淆后名称 pairs = Array(Array("strData", "a7"), _ Array("intCount", "x2"), _ Array("bolFlag", "q9")) For j = LBound(pairs) To UBound(pairs) src = Replace(src, pairs(j)(0), pairs(j)(1)) Next ReplaceIdentifiers = src End Function注意,这一步必须在字符串替换之后做,否则ChrW拼接的中间结果会被误伤。替换顺序就是连环,字符串打散优先,标识符次之。
第三步,InjectGarbage,注入垃圾洪流,这是破坏式锁定的灵魂:
Private Sub InjectGarbage(ByVal cm As CodeModule) Dim garbageLines As Long Dim i As Long Dim randomStr As String Dim topGarbage As String Dim bottomGarbage As String ' 顶部插入#If False块:诱饵代码,编译时不执行,但肉眼无法快速分辨 topGarbage = "#If False Then" & vbCrLf & _ "Private Function Fake_Login_System(userName As String, pwd As String) As Boolean" & vbCrLf & _ " ' 这段代码不会编译执行,纯粹是给破解者看的陷阱" & vbCrLf & _ " Dim conn As Object" & vbCrLf & _ " Set conn = CreateObject(" & BuildChrWChain("ADODB.Connection") & ")" & vbCrLf & _ " conn.Open " & BuildChrWChain("Provider=SQLOLEDB;Data Source=fake-server") & vbCrLf & _ " Fake_Login_System = True" & vbCrLf & _ "#End If" & vbCrLf cm.InsertLines 1, topGarbage ' 文件尾部追加注释洪流 bottomGarbage = "" garbageLines = 30000 ' 3万行注释,VBE打开模块会非常卡 For i = 1 To garbageLines randomStr = RandomString(60) bottomGarbage = bottomGarbage & "' " & randomStr & vbCrLf Next cm.InsertLines cm.CountOfLines + 1, bottomGarbage End Sub Private Function RandomString(ByVal length As Long) As String Dim s As String Dim i As Long Randomize For i = 1 To length s = s & Chr(Int(Rnd * 26) + 65) Next RandomString = s End Function注意garbageLines我设的是3万行,如果你嫌文件大,可以减到1万行。之前我说5万行也没问题,但实测保存会更慢,1到3万行是性价比最高的区间。
3.3 工程密码设置与加载项输出
混淆做完,代码已经变成了“天书”。但还差最后一道工序——给工程加一个密码,避免普通用户直接进VBE。手动设置当然是路径,但手工操作在批量处理时太麻烦,所以我用SendKeys模拟键盘操作来自动化这个流程。
' 模块: PasswordChanger Public Sub SetProjectPassword(ByVal wb As Workbook) Dim pwd As String pwd = GenerateStrongPassword(12) ' 打开VBE Application.VBE.MainWindow.Visible = True ' 打开工具-属性菜单 SendKeys "%(ti)", True Application.Wait Now + TimeSerial(0, 0, 2) ' 切换到“保护”标签页 SendKeys "^+{TAB}", True Application.Wait Now + TimeSerial(0, 0, 1) ' 勾选“查看时锁定工程” SendKeys "%(v)", True ' 输入密码和确认密码 SendKeys pwd & "{TAB}", True SendKeys pwd & "{TAB}{ENTER}", True Application.Wait Now + TimeSerial(0, 0, 2) ' 保存并关闭VBE wb.Save Application.VBE.MainWindow.Visible = False MsgBox "工程密码设置完成,密码是:" & pwd End Sub这里说明几点:自动设置密码的本质是模拟人工操作菜单,所以界面的语言、版本差异可能导致%(ti)这类快捷键失效。如果你英文版Office,工具菜单是%(ti);中文版,字母会变,需要自己调整。另外一个更稳的替代方案是,干脆不自动设置,脚本执行完混淆后弹出提示,让你手动去“工具 > VBAProject属性 > 保护”里勾选、设密码。自动化省事,手动万无一失,你自己权衡。
最后输出文件时建议保存为.xlsb或启用宏的.xlam加载项。二进制格式的.xlsb会让文件本身的内部结构更晦涩,比.xlsm更难被二进制层面研究;.xlam则是分发函数库的常规做法,加载后代码在各个工作簿中全局可用。保存加载项时,“工具 > 加载项”会让你提供文件名和描述,描述里不要写真实的用途,越普通越好。
4. 实操验证与效果对比:加锁前后,VBE里到底长什么样
4.1 加锁前的代码样例
为了让你直观感受差别,我先放一段被加密前的正常代码。假设这是一个从某个数据源拉数据并计算合计的函数:
Public Function GetTotalSales(region As String) As Double Dim conn As Object Dim rs As Object Dim sql As String Dim total As Double sql = "SELECT SUM(amount) FROM sales WHERE region = '" & region & "'" Set conn = CreateObject("ADODB.Connection") conn.ConnectionString = "Provider=SQLOLEDB;Data Source=db-server" conn.Open Set rs = conn.Execute(sql) total = rs.Fields(0).Value rs.Close conn.Close GetTotalSales = total End Function这段代码阅读无障碍,谁拿到都能懂。这是灾难的起点——我见过太多人辛苦研究的算法,就这么被人整段复制走。
4.2 加锁后的代码状态与防破解体验
经过混淆工具处理后,这个模块变成了下面这种状态(这里只展示前几行,完整版有几万行):
#If False Then Private Function Fake_Login_System(userName As String, pwd As String) As Boolean Dim conn As Object Set conn = CreateObject((ChrW(65)&ChrW(68)&ChrW(79)&ChrW(68)&ChrW(66)&ChrW(46)&ChrW(67)&ChrW(111)&ChrW(110)&ChrW(110)&ChrW(101)&ChrW(99)&ChrW(116)&ChrW(105)&ChrW(111)&ChrW(110))) conn.Open (ChrW(80)&ChrW(114)&ChrW(111)&ChrW(118)&ChrW(105)&ChrW(100)&ChrW(101)&ChrW(114)&ChrW(61)&ChrW(83)&ChrW(81)&ChrW(76)&ChrW(79)&ChrW(76)&ChrW(69)&ChrW(68)&ChrW(66)&ChrW(59)&ChrW(68)&ChrW(97)&ChrW(116)&ChrW(97)&ChrW(32)&ChrW(83)&ChrW(111)&ChrW(117)&ChrW(114)&ChrW(99)&ChrW(101)&ChrW(61)&ChrW(102)&ChrW(97)&ChrW(107)&ChrW(101)&ChrW(45)&ChrW(115)&ChrW(101)&ChrW(114)&ChrW(118)&ChrW(101)&ChrW(114)) Fake_Login_System = (ChrW(84)&ChrW(114)&ChrW(117)&ChrW(101)) #End If Function a7(ByVal q9 As String) As Double Dim x2 As Object Dim a3 As Object Dim b4 As String b4 = (ChrW(83)&ChrW(69)&ChrW(76)&ChrW(69)&ChrW(67)&ChrW(84)&ChrW(32)&ChrW(83)&ChrW(85)&ChrW(77)&ChrW(40)&ChrW(97)&ChrW(109)&ChrW(111)&ChrW(117)&ChrW(110)&ChrW(116)&ChrW(41)&ChrW(32)&ChrW(70)&ChrW(82)&ChrW(79)&ChrW(77)&ChrW(32)&ChrW(115)&ChrW(97)&ChrW(108)&ChrW(101)&ChrW(115)&ChrW(32)&ChrW(87)&ChrW(72)&ChrW(69)&ChrW(82)&ChrW(69)&ChrW(32)&ChrW(114)&ChrW(101)&ChrW(103)&ChrW(105)&ChrW(111)&ChrW(110)&ChrW(32)&ChrW(61)&ChrW(32)&ChrW(39) & q9 & ChrW(39)) Set x2 = CreateObject((ChrW(65)&ChrW(68)&ChrW(79)&ChrW(68)&ChrW(66)&ChrW(46)&ChrW(67)&ChrW(111)&ChrW(110)&ChrW(110)&ChrW(101)&ChrW(99)&ChrW(116)&ChrW(105)&ChrW(111)&ChrW(110))) ... ' HJDKLSDFKJSLDKJF ' QWEJKHDSLKJFSDLKJFSD ' ... 往下还有3万行类似的注释这才是真正的“破坏式锁定”效果。别说普通用户,就是老手看到这堆ChrW拼接和3万行注释,也会选择直接放弃。如果破解者用工具移除了工程密码,进了VBE,他发现自己面前是这样的代码,大概率连找核心过程的心情都没有——这一步已经击垮了绝大多数人的耐心。
验证时还要确认一件事:混淆后的代码能正常运行。我建议在加密前先备用一份,加密后打开工作簿运行所有主要模块,功能正常再交付。VBA的重新编译机制会在你保存时自动完成,万一报错,优先检查是不是#If False块里的注释行又嵌套了双引号,这类边界问题我在下一节专门讲。
5. 常见问题与排查技巧实录
我在给同事分发这套工具、处理各种版本Office兼容性问题时,踩过一堆坑,整理成一张速查表给你:
| 现象 | 触发原因 | 解决思路 |
|---|---|---|
| 混淆后运行宏报“编译错误:找不到工程或库” | 透明胶带式低级错误:某个标识符被替换时误伤了VBA关键字或内部函数名 | 检查符号表里有没有把Len、Mid这类内置函数名也换掉了;混淆前先在源码列表里屏蔽内部函数 |
| VBE打开模块时卡死超过1分钟 | 注释洪流行数过多 | 减少garbageLines到8000行;或者把单行注释长度从60改成30 |
| 混淆后中文全部变成问号 | 使用了Chr而不是ChrW,导致Unicode字符被截断 | 确认字符串转换统一用ChrW,不要用Chr |
| SendKeys设置密码失败 | Office语言版本/快捷键不同,或者等待时间不够 | 改用手动设置密码;或者把等待时间从2秒拉长到4秒 |
| 加密后的xlsm体积暴涨好几MB | 注释洪流太大 | 3万行注释约1到2MB,如果对体积敏感,控制在1万行以内 |
| 杀毒软件对生成的文件报毒 | 大量随机字符串注释容易被启发式引擎误判 | 加白名单,或降低随机字符长度;企业分发时走内网共享并说明 |
| 有同事用Office 2010,打开文件提示“不受信任的宏” | 新版Office默认阻止宏 | 引导用户右键属性/解除锁定,或统一把文件放到信任位置并设置为受信任文件夹 |
| 混淆后某些Excel内置功能(如数据透视表缓存)失效 | 我在测试中发现最少见的一类,和Randomize种子未重置导致的字符串重复有关,代码中的环境变量或全局状态被标识符替换误伤 | 备份原文件,增量替换,严格限制符号表不包含全局环境变量,出现问题回滚 |
另外告诉你一个独门心法:给破解者设置“虚假成就感”。在某个明显但非核心的模块上,故意只做半吊子混淆,再设一个弱密码让工具秒破。破解者进来一看:“哈哈,破开了,就这水平”,然后在一个看似核心实为诱饵的函数里研究半天。真正重要的逻辑,放在另一个模块深处的#If False陷阱之后。这一招不是技术,但对人性的拿捏很好使——真有人被这么耍过,跟我复述“他以为拿到全部了,其实连边都没摸着”。
自检清单方面,加密之后必须做三件事:第一,重启Excel重新打开,确认宏可用;第二,尝试用VBE直接打开模块,体验一下破解者的视角;第三,把这文件发给一个懂VBA的朋友,让他试着在不给你原始版本的前提下还原逻辑,看他几分钟放弃。
6. 写在经验之外的一点提醒
讲到这里,破坏式锁定从原理到代码已经完整落地。我个人实际运用中最大的感受是:VBA代码没有绝对安全的加密,任何客户端的保护都只是时间问题,所以我的目标从来不是“让破解者永远打不开”,而是“让破解成本远高于重新写一遍的成本”。当你把源码变成几万行ChrW和注释洪流时,重构的难度已经超过了从零开始写,这就是破坏式锁定真正的价值所在。
最后分享一个小技巧:加密工具本身也要留一手,混淆之前务必在日期戳后缀的备份目录里存一份原始源码。我见过太多次加密成功但原文件被覆盖,最后求助无门的案例——那不是工具的锅,是流程的锅。把“加密”和“归档”绑定成一套流程,任何时候都能回退,这样才能放心地对生产工具下手。
如果你把这套思路用在了公司内部的Excel签核系统、人事工具或者财报生成器上,建议再加一层“管理员分发留痕”的做法:每个分发文件里用ThisWorkbook.BuiltinDocumentProperties打上授权人和到期时间戳,出了事能追溯。代码层面的破坏式锁定解决了“防看”的问题,管理层面的追踪解决的是“敢偷”的问题,两手都要硬。