|
本帖最后由 iceboy 于 2012-6-29 16:29 编辑
VB给我的童年增添了不少精彩的回忆,记得在初中那会儿曾经痴迷于各种各样的VB技术,每周末在家捣腾VB,视其他语言为无物。
现在想想,这也不过是所有编程初学者都曾患过的“单一语言综合征”而已。
随着年龄的增大,对计算机的使用逐渐从单纯的兴趣爱好转向解决实际问题,VB渐行渐远。需要生成高性能的本机代码时,会用C;需要快速编写脚本时,会用Python;需要快速构建系统原型时,会用.NET。在解决实际问题时,永远会选用最合适而不是最好玩的工具。
然而,作为一个玩具,VB是非常好玩的:VB有着简洁精练的运行时(虽然msvbvm60.dll无比巨大);在IDE和编译模式下执行方式完全不同而结果无限接近;从代码完成到执行所需的编译时间几乎为0,从而可以非常快地修改-测试-再修改。
由于网友咨询关于我以前写的关于TEA加密的一篇帖子的缘故,我再次浏览了vbgood社区,忽然童心大发,回想起小时候的种种,决定写篇帖子,给社区增添一点活力。
-
下面先来介绍一下,这篇帖子实现了什么。
从图中可以看出,我们使用Declare声明从mydll导入了一个叫做MyApiFunction的函数。然而,mydll并不是真实存在的。使用我们实现的RegisterApi,将VB函数MyApiFunctionImpl“注册”到API名字空间。我们可以在MyApiFunctionImpl中下断点,进入MyApiFunction之后,就停在了我们断点上。
那么,这个东西有什么用呢?
注意到RegisterApi的第三个参数接受一个函数指针。我们不一定要传入一个VB函数的指针,而可以传入任何东西。比如,可以传入一段硬编码机器码的首地址,从而以另一种方式实现“VB内嵌汇编”(比CallWindowProc快很多哦)。结合“从内存中加载dll”,我们可以把dll所有的导出函数进行注册,从而实现无缝调用。
下面我们将看到,API名字空间为我们提供了一种高效的“按名字调用”的方法:编译是静态的,名字查找只发生在首次调用。
那么,这个东西是怎么实现的呢?
(毫不惊奇地,)我们是通过hook DllFunctionCall来实现的。事实上,之前有很多大牛曾经做过类似的工作。比如老汉大牛在逆向msvbvm60时曾经关注过DllFunctionCall这个函数;陈辉大牛也曾实现过一个完整的版本,然而实现中硬编码太多,难于理解。在搜索帖子时,我发现我自己也曾经写过一篇关于这个的文章,重新阅读后发现表达得很不到位,对逆向的结果没有给出充足的理由,因此读者没法重现(也就不好玩了)。
-
我们首先来探索一下VB的运行时是怎么调用API函数的。为此,我们编写了下面的代码。- Declare Function MessageBoxA Lib "user32" ( _
- ByVal a As Long, _
- ByVal b As Long, _
- ByVal c As Long, _
- ByVal d As Long) As Long
- Sub Main()
- Call MessageBoxA(0, 0, 0, 0)
- End Sub
复制代码 创建一个工程,移除默认的窗体Form1,并添加一个模块Module1,将代码粘贴在模块中,编译运行,可以看到一个标题为“错误”消息框。这里需要解释的东西很多,我们就不展开了,我们只需要知道,MessageBoxA是user32中真实存在的导出函数,调用MessageBoxA(0, 0, 0, 0)就可以看到这个消息框。这是对VB运行时调用API函数的一个最简单的测试用例。
使用默认的编译选项编译成exe文件,然后使用OllyDbg(我的版本是2.01 alpha 4)加载,在MessageBoxA下断点,然后执行。到达断点时如图所示。
通过栈的返回地址,我们可以找到调用这段函数的代码。
进入CALL的地址,我们可以看到一段调用API的桩函数。我们在首地址下断点,并重新运行程序。
这段代码首先检查地址为4022D4处的数据,如果为0,就以4012DC为参数,调用DllFunctionCall函数,并跳转到它的返回值处。从图中可以看到,一开始4022D4处的数据为0。
让我们来检视一下位于4012DC处的数据,可以看到,这段数据紧跟在桩函数代码之前(4012DC-4012F4),因此最多只有六项。前两项分别指向库的名字“user32”和函数的名字“MessageBoxA”;第三项为40000,不知道是啥;第四项的内容为4022CC,与4012D4只差8个字节,几乎可以肯定它们位于同一个记录中;再后面的内容为0。
我们再来看4022CC处的内存。一开始全为0,如图所示。
单步执行,调用DllFunctionCall之后,内容发生了变化。
4022D0处被修改为user32库的首地址,4022D4处被修改为MessageBoxA函数的地址,并且返回值也为MessageBoxA。注意到4022D4正是前面检查的地址。后面的内容仍然为0。
可以确定,4022CC处的记录最少为三项,那么最多是多少项呢?我们通过增加第二个API调用来确定。- Declare Function MessageBoxA Lib "user32" ( _
- ByVal a As Long, _
- ByVal b As Long, _
- ByVal c As Long, _
- ByVal d As Long) As Long
- Declare Function MessageBoxW Lib "user32" ( _
- ByVal a As Long, _
- ByVal b As Long, _
- ByVal c As Long, _
- ByVal d As Long) As Long
- Sub Main()
- Call MessageBoxA(0, 0, 0, 0)
- Call MessageBoxW(0, 0, 0, 0)
- End Sub
复制代码 我们分别找到两个API调用的桩函数,比较他们的地址。
可以看到,两个记录的地址分别为4022CC和4022D8,相差3项。因此,这个记录的长度恰好为3项。
-
让我们来回顾一下调用API时的逻辑。在桩函数中,首先检查记录中是否已经获取到了API函数的地址:如果有,就直接跳转过去;如果没有,就调用DllFunctionCall来获取。由于桩函数不好定位,我们将思路转向挂钩VB运行时中的DllFunctionCall。
我们将调用DllFunctionCall时作为参数的结构体称为Descriptor(描述符),将描述符中指向的记录称为Context(上下文),结构体的VB定义如下:- Private Type DllFunctionContext
- Unknown As Long
- ModuleHandle As Long
- FunctionPtr As Long
- End Type
- Private Type DllFunctionDescriptor
- ModuleNamePtr As Long
- FunctionNamePtr As Long
- Unknown As Long
- ContextPtr As Long
- End Type
复制代码 在描述符中有两个ANSI字符串,我们仍用Long类型来定义。这是因为,如果定义为String,根据VB的ABI,在内存中的结构为BSTR,即在字符串指针之前储存着字符串的字节长度。而这里的字符串不满足这一约定。(虽然经过测试,这样做程序不会崩溃,然而这样做是不正确的,)为此,我们编写了函数,从指定的内存地址读取字符串。- Private Function GetStringByPtr(ByVal Ptr As Long) As String
- Dim length As Long
- Dim result As String
-
- length = MultiByteToWideChar(CP_ACP, 0, Ptr, -1, 0, 0)
- result = Space(length - 1)
- Call MultiByteToWideChar(CP_ACP, 0, Ptr, -1, StrPtr(result), length)
- GetStringByPtr = result
- End Function
复制代码 这段代码调用了两次MultiByteToWideChar函数:第一次获取指定ANSI字符串的Unicode长度(含结束符),然后使用Space来创建相应大小的字符串作为缓冲区;第二次传入这段缓冲区的地址StrPtr(result)获取数据。
有了这些构建块,我们可以开始编写我们的hook函数。由于VB程序一般是单线程的,为了简单,我们将hook之前的代码保存起来。每次需要调用原始函数时,先还原hook,调用后再恢复。(更好的办法是创建一个桩函数,调用它就像调用原始函数一样,通常是将原始函数的开头几条指令复制过来,执行后再跳转过去)- Public Sub EnableHook()
- Dim hMod As Long
- Dim flOldProtect As Long
-
- If Not g_HookInitialized Then
- hMod = GetModuleHandle("msvbvm60")
- g_pfnDllFunctionCall = GetProcAddress(hMod, "DllFunctionCall")
- Call CopyMemory(g_OriginalCode(0), ByVal g_pfnDllFunctionCall, 8)
- g_HookedCode(0) = &HCCCCC0C7: g_HookedCode(1) = &HE0FFFFFF
- Call PutMem4(VarPtr(g_HookedCode(0)) + 2, AddressOf MyDllFunctionCall)
- g_HookInitialized = True
- End If
-
- Call VirtualProtect(g_pfnDllFunctionCall, 8, PAGE_EXECUTE_READWRITE, flOldProtect)
- Call CopyMemory(ByVal g_pfnDllFunctionCall, g_HookedCode(0), 8)
- Call VirtualProtect(g_pfnDllFunctionCall, 8, flOldProtect, flOldProtect)
- End Sub
复制代码 如果是第一次调用,g_HookInitialized没有被设置,则首先使用GetModuleHandle和GetProcAddress获得DllFunctionCall的地址(由于msvbvm60必须是常驻的,因此不需要LoadLibrary),然后准备好g_OriginalCode和g_HookedCode。前者为函数的原始内容,后者为待修改内容。
最后,我们把g_HookedCode复制到DllFunctionCall的首部。注意我们需要用VirtualProtect来修改页面的保护属性,否则有可能触发内存访问异常。
我们的待修改内容如下:
C7 C0 CC CC FF FF FF E0
翻译成汇编代码就是
mov eax, FFFFCCCC
jmp eax
其中下划线部分被替换成MyDllFunctionCall函数的实际地址。之所以不使用相对地址jmp,是为了避免减法运算:VB并不原生支持不带溢出检查的加减法运算(尽管可以通过优化设置实现,但至少在线调试时不行)。不使用B8前缀的MOV是为了8字节对齐以及美观。
DisableHook用于还原hook。- Public Sub DisableHook()
- Dim flOldProtect As Long
-
- If g_HookInitialized Then
- Call VirtualProtect(g_pfnDllFunctionCall, 8, PAGE_EXECUTE_READWRITE, flOldProtect)
- Call CopyMemory(ByVal g_pfnDllFunctionCall, g_OriginalCode(0), 8)
- Call VirtualProtect(g_pfnDllFunctionCall, 8, flOldProtect, flOldProtect)
- End If
- End Sub
复制代码 -
通过上面的代码,我们实现了对DllFunctionCall函数的挂钩,所有的调用会被转到我们的MyDllFunctionCall中。- Private Function MyDllFunctionCall( _
- ByRef dfd As DllFunctionDescriptor) As Long
-
- Dim modName As String
- Dim funcName As String
- Dim funcPtr As Long
-
- Call DisableHook
-
- modName = LCase(GetStringByPtr(dfd.ModuleNamePtr))
- funcName = GetStringByPtr(dfd.FunctionNamePtr)
-
- On Error GoTo NotFound
- funcPtr = g_FunctionMap(modName & "?" & funcName)
- On Error GoTo 0
-
- Call PutMem4(dfd.ContextPtr + 8, funcPtr)
- MyDllFunctionCall = funcPtr
- Call EnableHook
- Exit Function
-
- NotFound:
- Call Err.Clear
- MyDllFunctionCall = DllFunctionCall(VarPtr(dfd))
- Call EnableHook
- End Function
复制代码 在函数中,我们首先还原hook,以避免重入。接着,我们查看g_FunctionMap中是否已经存在指定的函数。如果存在,就将地址写入到Context中的指定位置,同时返回这个地址;如果不存在,转而调用原始的DllFunctionCall(注意hook已经还原,不会发生重入)。离开函数之前,恢复hook。
我们给用户提供一个接口,用于把函数注册到API名字空间:- Public Sub RegisterApi(ByVal ModuleName As String, ByVal FunctionName As String, ByVal Address As Long)
- Call g_FunctionMap.Add(Address, LCase(ModuleName) & "?" & FunctionName)
- End Sub
复制代码 -
经过上面的处理,我们已经实现了编译后的程序通过API声明调用指定地址的函数。然而,在线调试时却不行。为了解决这个问题,我们使用OllyDbg载入VB本身,并运行上述的MessageBox例子。
通过类似的方法,我们找到了这段桩函数,位于堆内存中。少了一些OllyDbg自动添加的符号注释,所以看起来会比较陌生。仔细查看不难发现其实是一样的。调用的函数是VBA6.DllFunctionCall,难怪我们的挂钩无效。
为此,我们在程序中加入了IDE的判断:如果在编译环境,就挂钩MSVBVM60.DllFunctionCall,否则挂钩VBA6.DllFunctionCall。- Private Function SetIDE() As Boolean
- g_InIDE = True
- SetIDE = True
- End Function
- Public Sub EnableHook()
- ...
-
- If Not g_HookInitialized Then
- Debug.Assert SetIDE
-
- If Not g_InIDE Then
- hMod = GetModuleHandle("msvbvm60")
- Else
- hMod = GetModuleHandle("vba6")
- End If
- g_pfnDllFunctionCall = GetProcAddress(hMod, "DllFunctionCall")
- ...
复制代码 原理是这样的,在IDE中运行时,Debug.Assert语句有效,从而SetIDE函数被调用;在编译环境下,整句都被忽略,不产生任何代码。
同样地,在hook函数中,我们也需要调用正确的函数。- Private Function MyDllFunctionCall( _
- ByRef dfd As DllFunctionDescriptor) As Long
-
- ...
- NotFound:
- Call Err.Clear
- If Not g_InIDE Then
- MyDllFunctionCall = DllFunctionCall(VarPtr(dfd))
- Else
- MyDllFunctionCall = DllFunctionCall2(VarPtr(dfd))
- End If
- Call EnableHook
- End Function
复制代码 其中DllFunctionCall2是VBA6.DllFunctionCall的别名。
-
程序中用到的API定义如下:- Private Declare Function GetModuleHandle Lib "kernel32" Alias "GetModuleHandleA" ( _
- ByVal ModuleName As String) As Long
- Private Declare Function GetProcAddress Lib "kernel32" ( _
- ByVal ModuleHandle As Long, _
- ByVal ProcName As String) As Long
-
- Private Declare Function VirtualProtect Lib "kernel32" ( _
- ByVal lpAddress As Long, _
- ByVal dwSize As Long, _
- ByVal flNewProcect As Long, _
- flOldProtect As Long) As Long
- Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" ( _
- Destination As Any, _
- Source As Any, _
- ByVal length As Long)
-
- Private Declare Function MultiByteToWideChar Lib "kernel32" ( _
- ByVal CodePage As Long, _
- ByVal dwFlags As Long, _
- ByVal lpMultiByteStr As Long, _
- ByVal cchMultiByte As Long, _
- ByVal lpWideCharStr As Long, _
- ByVal cchWideChar As Long) As Long
-
- Private Declare Function DllFunctionCall Lib "msvbvm60" ( _
- ByVal lpDescriptor As Long) As Long
-
- Private Declare Function DllFunctionCall2 Lib "vba6" Alias "DllFunctionCall" ( _
- ByVal lpDescriptor As Long) As Long
-
- Private Declare Sub PutMem4 Lib "msvbvm60" ( _
- ByVal Ptr As Long, _
- ByVal NewVal As Long)
- Private Const CP_ACP = 0
- Private Const PAGE_EXECUTE_READWRITE = &H40
复制代码 全局变量定义如下:- Private g_InIDE As Boolean
- Private g_HookInitialized As Boolean
- Private g_pfnDllFunctionCall As Long
- Private g_OriginalCode(0 To 1) As Long
- Private g_HookedCode(0 To 1) As Long
- Private g_FunctionMap As New Collection
复制代码 程序中有若干地方没有做错误处理,有兴趣的读者可以进行完善。
完整程序代码: |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有帐号?立即注册
x
评分
-
查看全部评分
本帖被以下淘专辑推荐:
- · 精品贴|主题: 326, 订阅: 12
- · 俺的精品贴|主题: 413, 订阅: 11
- · 精品测试|主题: 137, 订阅: 8
- · 论坛精华贴|主题: 53, 订阅: 6
- · 大杂烩|主题: 185, 订阅: 5
- · 更多
|