avatar

Catalog
一次简单的Hook(上)

这一次hook小实验的目标是hook出某社交软件中的实时聊天内容,主要目的是了解hook的原理以及在hook之前对程序的分析流程及其他准备工作,从而对于hook有个完整的认识。

准备工具

  • OllyDbg调试器:用于单步跟踪,动态调试等。

  • IDA pro调试器:用于静态分析函数代码结构等。

  • Mircosoft Visual Studio 201*(版本随意):用于dll代码编写

  • DbgView:捕获并查看程序中由OutputDebugString输出的信息。

Hook的分类

Hook无非只有两种类型,一种是通过修改数据(通常是引用的函数地址)进行的Hook,这如何理解,简单而言,很多大型程序,都需要通过调用各式各样的函数库来实现一些功能,而这种Hook就是通过修改程序调用的函数地址,改成自己写的函数,从而额外实现一些功能,实现对程序的Hook,例如IAT Hook;另一种是直接修改函数内的指令进行Hook,通过控制函数内的跳转,在函数执行时,实现一些自己的功能,然后再返回到函数使之正常运行,例如Inline Hook。这次Hook实验,采用的就是Inline Hook的手法实现。

基址与偏移

在逆向分析中,通过基址和偏移定位一个函数,会非常方便,同时可以对IDA的代码进行基址重定位,从而达到和OD的同步分析。

如何确定基址呢,一个程序需要调用一个外部的库函数,或者加载dll来实现某些功能,当dll被加载进程序时,会根据默认基址0x10000000进行载入dll,但是一个程序往往会加载多个dll,此时dll会根据重定位表来对dll进行重定位,基址也会有所变化。由于dll中的导出函数,距离基址的偏移是固定的,所以只要确定需求函数与dll基址的偏移,就可以轻松定位到这个函数。如图,将程序被OD附加时,通过点击上方的E按钮,进入模块列表,左边第一栏的Base就是dll的基址,例如IM.dll在此次加载时的基址为0x62840000。

右键View name查看IM.dll的导出函数,可以发现此次加载时PostTask_Session这个函数的地址为0x628E6EF9,相减可得偏移为0xA6EF9,以后每次只需确定IM.dll的基址,就可以通过偏移直接确定PostTask_Session的地址,这在hook代码中很有用。

程序分析流程

查找需要被Hook函数

根据我们需要Hook的功能来看,要截取实时聊天的数据,需要对接收聊天内容的函数做手脚,猜想这个函数的函数名一定与Message有关,例如GetMessage,RecvMessage等。将程序附加进OD,点击E查看Tim加载的主要模块(dll),找到一个KernelUtil.dll,为啥找这个dll呢?根据之前逆向,发现很多功能都是由这个dll实现的,从dll名也可以看出这是一个实现功能的dll。右键View name,查看dll的导出函数。

查看和Message有关的函数,最终会发现一个名为GetMsgAbstract的导出函数

为什么选这个函数呢?因为看名字猜测这是一个获取消息摘要的函数,也许和收发消息有一定的关联,因此可以拿出来进行试一试。

在函数的开头,下一个断点,然后在Tim内任何发送一条消息。发现程序断了下来,说明这个函数的确和消息有关。

寻找聊天内容

尽管确定了,这个函数和聊天消息有关,但是只有真正找到了消息内容,这个函数才能够被利用,否则还得换另一个函数分析。首先按照F8单步执行(不步入函数,为了节省时间并快速定位),每当一个函数(call语句)执行完后,都要观察堆栈和寄存器的变化,看看是否有重要内容。继续单步,直到发现一个函数GetMsgAbstractByElement,这个函数执行完后,会在堆栈[ebp-8]的位置出现刚刚发送的消息。

这时,我们可以进入GetMsgAbstarctByElement(这里Abstract拼错了,但只能按照他的函数名来),来寻找发送的消息是如何出现的。

函数入口

右图开始分析,在调用GetMsgAbstarctByElement之前,共push了5个参数进去,其中第一个push进去的参数0x548FA04,此时所指向的堆栈中的值为0,第三个push进去的0x548FA01,所指位置,截断了堆栈中部分数据,也无意义。第四个push进去的参数,值为0,同样也用不到。第五个push进去的参数,首先将[ebp-8]处的地址加载到eax,然后再push进去,根据前一次的步入,已知函数执行后,[ebp-8]的位置即是发送的消息,但该参数用于存放取完后的消息,并不是消息的源头。这样就只剩下第二个参数,右键ebx,Follow in dump,进入数据窗口,并没有发现消息内容的存在,因此可以猜测,这是一个包含了消息的结构体,至于如何解构结构体获取消息,还得步入GetMsgAbstarctByElement函数作进一步分析。

GetMsgAbstarctByElement函数

按F7步入函数

可以发现,栈顶足足降低了0x70字节,所以push进来的参数暂时只能靠[ebp+偏移]来访问,关注点仍然放在第二个参数的值上,目前除了ebx仍然为此值,push进入堆栈,目前位于[ebp+0x14]的位置,值也相同,由于[ebp+0x14]是作为参数push进来的,所以ebx的值多半用不到了,接下来只需关注[ebp+0x14]的值。

同一张图,距离函数开始不远,就有一条赋值语句将[ebp+0x14]的值赋给edi,这时edi的值也会成为第二个参数的值,同样需要关注。接着继续单步分析。

单步执行到这里时发现,在一次函数调用前,将edi作为参数push了,此外又将[ebp-0x20]处的位置清零,需要留意关注(这种操作通常是清空返回地址处的值),然后单步步过该函数观察变化。

该函数执行后发现,[ebp-0x20]处的位置多了个数,右键-Follow in dump,进入数据窗口发现,在0x18偏移的位置,正好是发送的消息。这下就可以确定,是此处的call dword ptr ds:[eax + 0x48]这条语句,将传入的参数结构体进行解构,找到了消息。

解构参数结构体的函数

接下来重新运行程序,再发送一条新的消息并断下。单步到call dword ptr ds:[eax + 0x48]处,按F7单步进入该函数。

这个函数并不长,但是实现了很关键的功能

Code
1
mov ebx,dword ptr ss:[ebp+0x8]

这一步,将[ebp+8]的值给到ebx,而这个值恰好就是外部传进来的需要解构的结构体,接下来需要关注涉及到ebx的指令。

其中接下来一条,将[ebx+0x38]的地址赋给eax,但是eax接下来就被重新赋值了,所以只需向下关注,其中这里方框框住的两行,实际上做了个减法运算,之后eax的值再一次被重置。接下来的一步,ecx也被清零了。

Code
1
mov eax,dword ptr ds:[ebx+0x28]

这是非常关键的一步,为什么说它关键呢,因为这时候已经脱离了ebx,说明ebx已经将结构体内部的信息传递给了eax上了,接着继续看eax,同时跟进[ebx+0x28]数据窗口查看。

Code
1
mov eax,dword ptr ds:[eax+ecx*4] => mov eax,dword ptr ds:[eax]

由于ecx的值为0,便可做一个优化。此时再去跟进[eax]数据窗口查看。

发现,刚好有一条发送出去但是还未接收到的消息。至此大功告成,已经确定GetMsgAbstractElement函数的解构过程

  • 首先将外部传入的第二个参数,赋值给edi寄存器暂存
  • 接着将edi寄存器内的值作为参数,传入到一个解析结构体的函数内
  • 进入函数,先将原先edi寄存器保存的参数,赋值给ebx寄存器
  • 然后将ebx+0x28所在地址的值赋值给eax寄存器
  • 最后eax寄存器将保存的值作为地址,进入后发现,在0x18处偏移,刚好就是保存的消息。

结论验证

通过上述分析可以发现,在执行GetMsgAbstractElement,就可以通过其第二个push进去的参数,获取到消息内容了。这时,我们再次运行重新,发送消息,并重新断下。通过之前推论得出的偏移来计算:

Code
1
[[ebx+0x28]]+18

该命令可以达到一个地址,而这个地址就应该存着消息。

验证成功。尽管我们是从GetMsgAbstract进来开始分析的,但是在分析的过程中发现GetMsgAbstractElement在执行前,就可以拿到消息内容,而GetMsgAbstract,执行的过程中总会执行GetMsgAbstractElement,因此可以在GetMsgAbstractElement函数执行前做手脚,注入我们的代码实现对消息内容的Hook。至于Hook代码的编写,限于篇幅,将在下一个部分呈现

Author: cataLoc
Link: http://cataloc.gitee.io/blog/2020/03/10/%E4%B8%80%E6%AC%A1%E7%AE%80%E5%8D%95%E7%9A%84Hook-%E4%B8%8A/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.
Donate
  • 微信
    微信
  • 支付寶
    支付寶