VBGood网站全文搜索 Google

搜索VBGood全站网页(全文搜索)

VB爱好者乐园(VBGood)

 找回密码
 立即注册
搜索
查看: 16900|回复: 29

[原创] 【暑假礼物】挂钩实现通过API名字空间调用任意函数

[复制链接]
发表于 2012-6-29 16:23:45 | 显示全部楼层 |阅读模式
本帖最后由 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函数的。为此,我们编写了下面的代码。
  1. Declare Function MessageBoxA Lib "user32" ( _
  2.         ByVal a As Long, _
  3.         ByVal b As Long, _
  4.         ByVal c As Long, _
  5.         ByVal d As Long) As Long

  6. Sub Main()
  7.         Call MessageBoxA(0, 0, 0, 0)
  8. 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调用来确定。
  1. Declare Function MessageBoxA Lib "user32" ( _
  2.     ByVal a As Long, _
  3.     ByVal b As Long, _
  4.     ByVal c As Long, _
  5.     ByVal d As Long) As Long

  6. Declare Function MessageBoxW Lib "user32" ( _
  7.     ByVal a As Long, _
  8.     ByVal b As Long, _
  9.     ByVal c As Long, _
  10.     ByVal d As Long) As Long

  11. Sub Main()
  12.     Call MessageBoxA(0, 0, 0, 0)
  13.     Call MessageBoxW(0, 0, 0, 0)
  14. End Sub
复制代码
我们分别找到两个API调用的桩函数,比较他们的地址。



可以看到,两个记录的地址分别为4022CC和4022D8,相差3项。因此,这个记录的长度恰好为3项。

-

让我们来回顾一下调用API时的逻辑。在桩函数中,首先检查记录中是否已经获取到了API函数的地址:如果有,就直接跳转过去;如果没有,就调用DllFunctionCall来获取。由于桩函数不好定位,我们将思路转向挂钩VB运行时中的DllFunctionCall。

我们将调用DllFunctionCall时作为参数的结构体称为Descriptor(描述符),将描述符中指向的记录称为Context(上下文),结构体的VB定义如下:
  1. Private Type DllFunctionContext
  2.     Unknown As Long
  3.     ModuleHandle As Long
  4.     FunctionPtr As Long
  5. End Type

  6. Private Type DllFunctionDescriptor
  7.     ModuleNamePtr As Long
  8.     FunctionNamePtr As Long
  9.     Unknown As Long
  10.     ContextPtr As Long
  11. End Type
复制代码
在描述符中有两个ANSI字符串,我们仍用Long类型来定义。这是因为,如果定义为String,根据VB的ABI,在内存中的结构为BSTR,即在字符串指针之前储存着字符串的字节长度。而这里的字符串不满足这一约定。(虽然经过测试,这样做程序不会崩溃,然而这样做是不正确的,)为此,我们编写了函数,从指定的内存地址读取字符串。
  1. Private Function GetStringByPtr(ByVal Ptr As Long) As String
  2.     Dim length As Long
  3.     Dim result As String
  4.    
  5.     length = MultiByteToWideChar(CP_ACP, 0, Ptr, -1, 0, 0)
  6.     result = Space(length - 1)
  7.     Call MultiByteToWideChar(CP_ACP, 0, Ptr, -1, StrPtr(result), length)
  8.     GetStringByPtr = result
  9. End Function
复制代码
这段代码调用了两次MultiByteToWideChar函数:第一次获取指定ANSI字符串的Unicode长度(含结束符),然后使用Space来创建相应大小的字符串作为缓冲区;第二次传入这段缓冲区的地址StrPtr(result)获取数据。

有了这些构建块,我们可以开始编写我们的hook函数。由于VB程序一般是单线程的,为了简单,我们将hook之前的代码保存起来。每次需要调用原始函数时,先还原hook,调用后再恢复。(更好的办法是创建一个桩函数,调用它就像调用原始函数一样,通常是将原始函数的开头几条指令复制过来,执行后再跳转过去)
  1. Public Sub EnableHook()
  2.     Dim hMod As Long
  3.     Dim flOldProtect As Long
  4.    
  5.     If Not g_HookInitialized Then
  6.         hMod = GetModuleHandle("msvbvm60")
  7.         g_pfnDllFunctionCall = GetProcAddress(hMod, "DllFunctionCall")
  8.         Call CopyMemory(g_OriginalCode(0), ByVal g_pfnDllFunctionCall, 8)
  9.         g_HookedCode(0) = &HCCCCC0C7: g_HookedCode(1) = &HE0FFFFFF
  10.         Call PutMem4(VarPtr(g_HookedCode(0)) + 2, AddressOf MyDllFunctionCall)
  11.         g_HookInitialized = True
  12.     End If
  13.    
  14.     Call VirtualProtect(g_pfnDllFunctionCall, 8, PAGE_EXECUTE_READWRITE, flOldProtect)
  15.     Call CopyMemory(ByVal g_pfnDllFunctionCall, g_HookedCode(0), 8)
  16.     Call VirtualProtect(g_pfnDllFunctionCall, 8, flOldProtect, flOldProtect)
  17. 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。
  1. Public Sub DisableHook()
  2.     Dim flOldProtect As Long
  3.    
  4.     If g_HookInitialized Then
  5.         Call VirtualProtect(g_pfnDllFunctionCall, 8, PAGE_EXECUTE_READWRITE, flOldProtect)
  6.         Call CopyMemory(ByVal g_pfnDllFunctionCall, g_OriginalCode(0), 8)
  7.         Call VirtualProtect(g_pfnDllFunctionCall, 8, flOldProtect, flOldProtect)
  8.     End If
  9. End Sub
复制代码
-

通过上面的代码,我们实现了对DllFunctionCall函数的挂钩,所有的调用会被转到我们的MyDllFunctionCall中。
  1. Private Function MyDllFunctionCall( _
  2.     ByRef dfd As DllFunctionDescriptor) As Long
  3.    
  4.     Dim modName As String
  5.     Dim funcName As String
  6.     Dim funcPtr As Long
  7.    
  8.     Call DisableHook
  9.    
  10.     modName = LCase(GetStringByPtr(dfd.ModuleNamePtr))
  11.     funcName = GetStringByPtr(dfd.FunctionNamePtr)
  12.    
  13.     On Error GoTo NotFound
  14.     funcPtr = g_FunctionMap(modName & "?" & funcName)
  15.     On Error GoTo 0
  16.    
  17.     Call PutMem4(dfd.ContextPtr + 8, funcPtr)
  18.     MyDllFunctionCall = funcPtr
  19.     Call EnableHook
  20.     Exit Function
  21.    
  22. NotFound:
  23.     Call Err.Clear
  24.     MyDllFunctionCall = DllFunctionCall(VarPtr(dfd))
  25.     Call EnableHook
  26. End Function
复制代码
在函数中,我们首先还原hook,以避免重入。接着,我们查看g_FunctionMap中是否已经存在指定的函数。如果存在,就将地址写入到Context中的指定位置,同时返回这个地址;如果不存在,转而调用原始的DllFunctionCall(注意hook已经还原,不会发生重入)。离开函数之前,恢复hook。

我们给用户提供一个接口,用于把函数注册到API名字空间:
  1. Public Sub RegisterApi(ByVal ModuleName As String, ByVal FunctionName As String, ByVal Address As Long)
  2.     Call g_FunctionMap.Add(Address, LCase(ModuleName) & "?" & FunctionName)
  3. End Sub
复制代码
-

经过上面的处理,我们已经实现了编译后的程序通过API声明调用指定地址的函数。然而,在线调试时却不行。为了解决这个问题,我们使用OllyDbg载入VB本身,并运行上述的MessageBox例子。



通过类似的方法,我们找到了这段桩函数,位于堆内存中。少了一些OllyDbg自动添加的符号注释,所以看起来会比较陌生。仔细查看不难发现其实是一样的。调用的函数是VBA6.DllFunctionCall,难怪我们的挂钩无效。

为此,我们在程序中加入了IDE的判断:如果在编译环境,就挂钩MSVBVM60.DllFunctionCall,否则挂钩VBA6.DllFunctionCall。
  1. Private Function SetIDE() As Boolean
  2.     g_InIDE = True
  3.     SetIDE = True
  4. End Function

  5. Public Sub EnableHook()
  6.     ...
  7.    
  8.     If Not g_HookInitialized Then
  9.         Debug.Assert SetIDE
  10.         
  11.         If Not g_InIDE Then
  12.             hMod = GetModuleHandle("msvbvm60")
  13.         Else
  14.             hMod = GetModuleHandle("vba6")
  15.         End If
  16.         g_pfnDllFunctionCall = GetProcAddress(hMod, "DllFunctionCall")
  17.                 ...
复制代码
原理是这样的,在IDE中运行时,Debug.Assert语句有效,从而SetIDE函数被调用;在编译环境下,整句都被忽略,不产生任何代码。

同样地,在hook函数中,我们也需要调用正确的函数。
  1. Private Function MyDllFunctionCall( _
  2.     ByRef dfd As DllFunctionDescriptor) As Long
  3.    
  4.     ...
  5. NotFound:
  6.     Call Err.Clear
  7.     If Not g_InIDE Then
  8.         MyDllFunctionCall = DllFunctionCall(VarPtr(dfd))
  9.     Else
  10.         MyDllFunctionCall = DllFunctionCall2(VarPtr(dfd))
  11.     End If
  12.     Call EnableHook
  13. End Function
复制代码
其中DllFunctionCall2是VBA6.DllFunctionCall的别名。

-

程序中用到的API定义如下:
  1. Private Declare Function GetModuleHandle Lib "kernel32" Alias "GetModuleHandleA" ( _
  2.     ByVal ModuleName As String) As Long

  3. Private Declare Function GetProcAddress Lib "kernel32" ( _
  4.     ByVal ModuleHandle As Long, _
  5.     ByVal ProcName As String) As Long
  6.    
  7. Private Declare Function VirtualProtect Lib "kernel32" ( _
  8.     ByVal lpAddress As Long, _
  9.     ByVal dwSize As Long, _
  10.     ByVal flNewProcect As Long, _
  11.     flOldProtect As Long) As Long

  12. Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" ( _
  13.     Destination As Any, _
  14.     Source As Any, _
  15.     ByVal length As Long)
  16.    
  17. Private Declare Function MultiByteToWideChar Lib "kernel32" ( _
  18.     ByVal CodePage As Long, _
  19.     ByVal dwFlags As Long, _
  20.     ByVal lpMultiByteStr As Long, _
  21.     ByVal cchMultiByte As Long, _
  22.     ByVal lpWideCharStr As Long, _
  23.     ByVal cchWideChar As Long) As Long
  24.    
  25. Private Declare Function DllFunctionCall Lib "msvbvm60" ( _
  26.     ByVal lpDescriptor As Long) As Long
  27.    
  28. Private Declare Function DllFunctionCall2 Lib "vba6" Alias "DllFunctionCall" ( _
  29.     ByVal lpDescriptor As Long) As Long
  30.    
  31. Private Declare Sub PutMem4 Lib "msvbvm60" ( _
  32.     ByVal Ptr As Long, _
  33.     ByVal NewVal As Long)

  34. Private Const CP_ACP = 0
  35. Private Const PAGE_EXECUTE_READWRITE = &H40
复制代码
全局变量定义如下:
  1. Private g_InIDE As Boolean
  2. Private g_HookInitialized As Boolean
  3. Private g_pfnDllFunctionCall As Long
  4. Private g_OriginalCode(0 To 1) As Long
  5. Private g_HookedCode(0 To 1) As Long
  6. Private g_FunctionMap As New Collection
复制代码
程序中有若干地方没有做错误处理,有兴趣的读者可以进行完善。

完整程序代码:

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?立即注册

x

点评

强,这就是传说中的API Hook么?  发表于 2012-6-29 16:40

评分

参与人数 19威望 +240 金钱 +120 人气 +58 收起 理由
inat + 10 + 2 很给力
yesong + 8 + 1 很给力!
天尽头的风 + 1 很给力!
wtywtykk + 10 + 2 神马都是浮云
sunfrank + 13 + 3 很给力!
海南老陈 + 8 + 2 赞一个!
仙剑魔 + 12 + 3 很好玩!
Apple_0 + 17 + 3 必须加分
weisi + 8 + 1 赞一个!
shanhan + 2 + 1 神马都是浮云
水晶葡萄 + 4 + 2 不在我的知识体系之内,高人的经验贴必须支.
dolphins + 16 + 3 神马都是浮云
reker + 60 + 100 + 15 专门赶过来加分的
h907308901 + 10 + 2 支持
JuncoJet + 10 + 3 赞一个!
acme_pjz + 15 + 3 精品文章
gujin162 + 4 + 3 很给力!
19900603 + 13 + 3 很给力!
yimins + 20 + 20 + 5 很给力!

查看全部评分

本帖被以下淘专辑推荐:

发表于 2012-6-29 16:34:15 | 显示全部楼层
抢 ib 的沙发!!

(我是xqq)
回复 支持 反对

使用道具 举报

发表于 2012-6-29 16:38:30 | 显示全部楼层
板凳~
回复 支持 反对

使用道具 举报

发表于 2012-6-29 18:37:45 | 显示全部楼层
呵呵,支持个。。。VB的娱乐性还是很强的,。。
回复 支持 反对

使用道具 举报

发表于 2012-6-29 20:52:40 | 显示全部楼层
大牛!!
回复 支持 反对

使用道具 举报

发表于 2012-6-29 22:09:24 | 显示全部楼层
本来想抢沙发的  结果发现一个论坛升级出现的问题...

提示信息关闭
道具所需文件 ./source/class/magic/magic_sofa.php 不存在

点评

……  发表于 2012-6-30 16:11
回复 支持 反对

使用道具 举报

发表于 2012-6-30 09:31:18 | 显示全部楼层
回复 支持 反对

使用道具 举报

发表于 2012-7-1 10:40:11 | 显示全部楼层
判断 IDE 环境 我觉得用 App.LogMode 方便些,  IDE环境=0   编译环境=1

点评

文不对题?  发表于 2012-7-2 11:52
回复 支持 反对

使用道具 举报

发表于 2012-7-1 15:12:41 | 显示全部楼层
本帖最后由 ccl1314520 于 2012-7-1 15:19 编辑

我是菜鸟不知道这有什么用
模块会多个MYDLL。DLL吗


Option Explicit


Function MyApiFunctionImpl() As Long
    MyApiFunctionImpl = 70514

End Function

Sub Main()
   
    MsgBox MyApiFunctionImpl
  
End Sub
跟这个有什么不同

点评

你要直接调用汇编代码的时候就知道了  发表于 2012-7-4 16:24
回复 支持 反对

使用道具 举报

发表于 2012-7-4 19:48:32 | 显示全部楼层
图文并茂,讲的挺详细,支持一下。
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

文字版|手机版|小黑屋|VBGood  

GMT+8, 2022-6-26 04:43

VB爱好者乐园(VBGood)
快速回复 返回顶部 返回列表