我有一些 VBA 代码,用于将 COM vtable 中的函数指针之一与汇编代码的 NO-OP 位交换。 它可以在 twinBASIC 中运行,这是一个独立的 VBA 环境,所以我知道我已经非常接近了,但是在 Excel 中它会崩溃。
这是 minrepro,可以在 tB 中运行,理论上不能在 Excel VBA 中运行。
Class DummyThing
Sub DoNothing()
End Sub
End Class
Module VBA
Private Const MEM_COMMIT As Long = &H1000
Private Const MEM_RESERVE As Long = &H2000
Private Const PAGE_EXECUTE_READWRITE As Long = &H40
Declare PtrSafe Function VirtualAlloc Lib "kernel32" ( _
ByVal lpAddress As LongPtr, _
ByVal dwSize As Long, _
ByVal flAllocationType As Long, _
ByVal flProtect As Long) As LongPtr
Sub Main()
Dim code(1 To 4) As Byte
code(1) = CByte(&h48)
code(2) = CByte(&h31)
code(3) = CByte(&hC0) 'xor rax, rax to clear it - this is like setting the hresult to 0
code(4) = CByte(&hC3) 'ret
Dim buffer As LongPtr
buffer = VirtualAlloc(0&, UBound(code) - LBound(code) + 1, MEM_COMMIT Or MEM_RESERVE, PAGE_EXECUTE_READWRITE)
If buffer = 0 Then
Debug.Print "VirtualAlloc() failed (Err:" ; Err.LastDllError ; ")."
Exit Sub
End If
CopyMemory ByVal buffer, code(1), UBound(code) - LBound(code) + 1
Dim base As DummyThing
Set base = New DummyThing
Dim vtable As LongPtr
vtable = MemLongPtr(ObjPtr(base))
MemLongPtr(vtable + PTR_SIZE * 7) = buffer
base.DoNothing 'Excel VBA crashes here, tB prints the message below
Debug.Print vbNewLine ; "Done!"
End Sub
End Module
内存API来自这里https://github.com/cristianbuse/VBA-MemoryTools/blob/master/src/LibMemory.bas
它是一个超级简单的 64 位汇编代码,通过交换
Sub DoNothing()
的 vtable 并用指向一些可执行操作码的指针替换它来运行。汇编代码无非就是
48 31 C0 xor rax, rax ; Set return value to 0
C3 ret ; Return
可能导致崩溃的原因 - 也许 VBA 检查了 vtable 完整性并且它指向预期地址范围内的内存?但我以前从未遇到过重载 vtable 的问题。
找到答案
在VBA64中,对于VBA实现的类,它不使用vtable 直接函数指针。相反,它检测到该对象是 由 VBA(本身)和直接 pcode 的快捷方式实现 实现所请求的成员而不是使用本机 必须跳回 pcode 虚拟的函数指针 机器。这是一种优化,但实际上它是一种 微优化,但最终它会阻止简单的 vtable 修补 从工作中。
情况并非总是如此。在 VBA64 的早期版本(office 2010 年没有服务包),他们确实使用了本机 vtable 函数 调用,那时你可以使用正常的 vtable 修补。
https://discord.com/channels/927638153546829845/1157647794484543589/1157660431545024553
所以崩溃来自于 VBA 看到该类是一个 VBA 类,然后使用 vtable 中的指针直接指向 pcode 作为优化。
除非我们覆盖了函数指针,否则它在同一位置没有相应的 pcode,因此 VBA 开始尝试将随机内存解释为 pcode,从而导致崩溃。
解决方案是调用没有 vtable 修补的组件以避免优化 - 例如使用 dispcallfunc,或者使用 CoTaskMemAlloc 和手工制作的 vtable 手动实现整个 COM 对象。