文章背景: 最近发现有些办公电脑打开一些excel文件(xls格式)时,会弹出一个对话框,显示""隐藏模块中出现编译错误:<module name>"。在网上查阅一些资料后,发现早期的VBA代码存在兼容性问题。
当代码与此应用程序的版本或体系结构不兼容(例如文档中的代码面向 32 位 Microsoft Office 应用程序,但它试图在 64 位 Office 上运行)时,通常会发生此错误。
office365有32位和64位两个版本。通过64位office软件打开早期的excel文件,如果代码中存在早期面向32位office编写的VBA代码,可能会存在上述的编译错误。
1 此错误的原因和解决方案
此错误的原因:
当受保护(隐藏)的模块内的 VBA 代码中存在编译错误时会引发此错误。由于模块是受保护状态,因此不会公开具体的编译错误。
可能的解决方案:
(1)如果您具有访问文档或项目中的 VBA 代码的权限,请先取消对该模块的保护,然后再次运行该代码以查看具体的错误。
(2)如果没有访问文档中 VBA 代码的权限,请与文档作者联系,让作者更新隐藏模块中的代码。
2 更新旧版本的VBA代码
对于在 Office 2010 版本之前(VBA 版本 6 和更早版本)编写的 VBA 代码,需要修改为在 64 位 Office 版本中运行,否则在 64 位平台上运行时会导致错误。这是因为,VBA 版本 6 和更早版本完全以 32 位平台为目标,而且通常包含 Declare 语句,这些语句调用的 Windows API 使用 32 位数据类型的指针和句柄。因为 VBA 版本 6 和更早版本没有用于指针或句柄的特定数据类型,所以,它使用 Long 数据类型(一种 32 位 4 字节的数据类型)来引用指针和句柄。64 位环境中的指针和句柄为 8 字节 64 位数。这些 64 位数不能包含在 32 位数据类型中。
只有在 64 位版本的 Microsoft Office 中运行 VBA 代码时,才需要修改 VBA 代码。
在 64 位 Office 中运行旧 VBA 代码的问题在于,将 64 位加载到 32 位数据类型中会截断 64 位数。这会导致内存溢出、代码中出现意外结果,并且可能导致应用程序故障。
为解决此问题,以使 VBA 代码能同时在 32 位和 64 位环境中正确运行,新版 VBA 中增加了几项语言功能。q其中有三个重要的新增功能,分别是:LongPtr 类型别名、LongLong 数据类型和 PtrSafe 关键字。
对于要在 64 位版本的 Office 中运行的代码,您需要找到并修改所有现有 Declare 语句以使用 PtrSafe 限定符。同时,还需要找到并修改这些 Declare 语句内所有引用句柄或指针的数据类型以使用新的 64 位兼容的 LongPtr 类型别名,并且需要使用新的 LongLong 数据类型保存 64 位整数的类型。此外,还必须更新任何包含指针或句柄以及 64 位整数的用户定义类型 (UDT),使之使用 64 位数据类型,同时,必须验证所有变量赋值是否正确,以防止发生类型不匹配错误。
3 VBA工程密码破解
当受保护(隐藏)的模块内的 VBA 代码中存在编译错误时,由于模块是受保护状态,因此不会公开具体的编译错误。此时,需要取消对该模块的保护。下面这种方法可以避开VBA工程的密码验证。具体步骤如下:
(1)新建一个工作簿(xlsm格式)(记为工具文件
),在工作簿内新建一个模块。将以下代码复制到模块内。
Option Explicit
Private Declare Sub MoveMemory Lib "kernel32" Alias "RtlMoveMemory" _
(Destination As Long, Source As Long, ByVal Length As Long)
Private Declare Function VirtualProtect Lib "kernel32" (lpAddress As Long, _
ByVal dwSize As Long, ByVal flNewProtect As Long, lpflOldProtect As Long) As Long
Private Declare Function GetModuleHandleA Lib "kernel32" (ByVal lpModuleName As String) As Long
Private Declare Function GetProcAddress Lib "kernel32" (ByVal hModule As Long, _
ByVal lpProcName As String) As Long
Private Declare Function DialogBoxParam Lib "user32" Alias "DialogBoxParamA" (ByVal hInstance As Long, _
ByVal pTemplateName As Long, ByVal hWndParent As Long, _
ByVal lpDialogFunc As Long, ByVal dwInitParam As Long) As Integer
Dim HookBytes(0 To 5) As Byte
Dim OriginBytes(0 To 5) As Byte
Dim pFunc As Long
Dim Flag As Boolean
Private Function GetPtr(ByVal Value As Long) As Long
'获得函数的地址
GetPtr = Value
End Function
Public Sub RecoverBytes()
'若已经hook,则恢复原API开头的6字节,也就是恢复原来函数的功能
If Flag Then MoveMemory ByVal pFunc, ByVal VarPtr(OriginBytes(0)), 6
End Sub
Public Function Hook() As Boolean
Dim TmpBytes(0 To 5) As Byte
Dim p As Long
Dim OriginProtect As Long
Hook = False
'VBE6.dll调用DialogBoxParamA显示VB6INTL.dll资源中的第4070号对话框(就是输入密码的窗口)
'若DialogBoxParamA返回值非0,则VBE会认为密码正确,所以我们要hook DialogBoxParamA函数
pFunc = GetProcAddress(GetModuleHandleA("user32.dll"), "DialogBoxParamA")
'标准api hook过程之一: 修改内存属性,使其可写
If VirtualProtect(ByVal pFunc, 6, &H40, OriginProtect) <> 0 Then
'标准api hook过程之二: 判断是否已经hook,看看API的第一个字节是否为&H68,
'若是则说明已经Hook
MoveMemory ByVal VarPtr(TmpBytes(0)), ByVal pFunc, 6
If TmpBytes(0) <> &H68 Then
'标准api hook过程之三: 保存原函数开头字节,这里是6个字节,以备后面恢复
MoveMemory ByVal VarPtr(OriginBytes(0)), ByVal pFunc, 6
'用AddressOf获取MyDialogBoxParam的地址
'因为语法不允许写成p = AddressOf MyDialogBoxParam,这里我们写一个函数
'GetPtr,作用仅仅是返回AddressOf MyDialogBoxParam的值,从而实现将
'MyDialogBoxParam的地址付给p的目的
p = GetPtr(AddressOf MyDialogBoxParam)
'标准api hook过程之四: 组装API入口的新代码
'HookBytes 组成如下汇编
'push MyDialogBoxParam的地址
'ret
'作用是跳转到MyDialogBoxParam函数
HookBytes(0) = &H68
MoveMemory ByVal VarPtr(HookBytes(1)), ByVal VarPtr(p), 4
HookBytes(5) = &HC3
'标准api hook过程之五: 用HookBytes的内容改写API前6个字节
MoveMemory ByVal pFunc, ByVal VarPtr(HookBytes(0)), 6
'设置hook成功标志
Flag = True
Hook = True
End If
End If
End Function
Private Function MyDialogBoxParam(ByVal hInstance As Long, _
ByVal pTemplateName As Long, ByVal hWndParent As Long, _
ByVal lpDialogFunc As Long, ByVal dwInitParam As Long) As Integer
If pTemplateName = 4070 Then
'有程序调用DialogBoxParamA装入4070号对话框,这里我们直接返回1,让
'VBE以为密码正确了
MyDialogBoxParam = 1
Else
'有程序调用DialogBoxParamA,但装入的不是4070号对话框,这里我们调用
'RecoverBytes函数恢复原来函数的功能,在进行原来的函数
RecoverBytes
MyDialogBoxParam = DialogBoxParam(hInstance, pTemplateName, _
hWndParent, lpDialogFunc, dwInitParam)
'原来的函数执行完毕,再次hook
Hook
End If
End Function
(2)在Project界面上,右击sheet1工作表
,选择查看代码
,将如下代码拷贝进去。
Option Explicit
Sub 破解()
If Hook Then
MsgBox "破解成功"
End If
End Sub
Sub 恢复()
RecoverBytes
MsgBox "恢复成功"
End Sub
(3) 打开需要破解VBA工程密码的工作簿。
(4) 回到工具文件
,运行宏命令sheet1.破解
。顺利的话,会弹出对话框,提示”破解成功“。
(5) 双击刚才要解密的VBA工程窗体,发现已经可以查看之前受保护的模块代码了。
两点注意事项:
1)经测试后,发现上述代码在32位office365上可以顺利运行,在64位office365上运行会出现问题。
2)上述代码仅用于学术研究,请勿用于非法途径。
参考资料:
[1] 隐藏模块中出现编译错误:<module name>(https://docs.microsoft.com/zh-cn/office/vba/language/reference/user-interface-help/compile-error-in-hidden-modulemodule-name)
[2] VBA在32位下没问题,到64位异常报错(https://www.cnblogs.com/duanyuerui/p/6958631.html)
[3] vba工程密码清除(https://www.cnblogs.com/Summer6/p/11245383.html)