逆向-用C语言实现一个简单的Hook
前言
上一篇文章是有关ShareCode的处理的,里面涉及到了把关键代码劫持到另一个位置进行自己的处理然后再返回的行为,也就是Hook. 当然上一篇文章的Hook非常简陋非常LowB,而且是依赖CE才能进行的,你也不希望每次开游戏都要先找地址再手写一段汇编吧.
所以这篇文章里我们会用最简单的内存读写的方式手写一个Hook.把Hook的二进制代码封装成一个应用程序,每次只需要启动该程序就可以自动完成代码劫持的功能,没错这就是一个简单的游戏修改器了.
实战
CE部分
我们还是用老朋友PVZ做实验,目标不是很高,我们希望把种植植物阳光减少修改为种植植物阳光增加(当然你也可以修改为种植不减少,但是那个只需要nop就行了).
还是先用CE找到阳光的基址,然后设置指针,调查访问地址情况找到反汇编部分,这个上两篇已经有过程了,这里直接展示结果:
明显下面的指令是含有一个减法的,就是阳光值减少的逻辑了:
程序实现
首先你要关注的是指令集问题,PVZ是个32位的应用程序,你编写的代码应该遵循32位标准,包括一些特定函数和寄存器名称.
首先我们要手写一段汇编代码保证我们的修改逻辑是正确的,直接在sub
处注入汇编,如下即可:
1 |
|
注入之后可以直接跳转到CE为我们开辟的新内存区域查看代码的字节码:
接下来就是写C程序的部分了.可以看到CE代替我们做了内存偏移计算,内存空间开辟和汇编翻译为字节码的工作,接下来只需要在程序中手动实现这些部分就行了.
1 |
|
除掉下面的异常处理部分,解释一下代码的大部分函数调用和变量.
两个char
数组的初始化,很明显两个char数组里装的就是汇编对应的字节码,一个是从sub
跳转后的汇编代码,一个是从原本sub
跳转过去的代码.
选择char
这个类型完全是因为C语言中这个类型的大小刚好是1字节且没有负值,刚好可以放入一个字节码(机器码是按照1字节进行翻译的).获取汇编转字节码方式很多,你既可以直接手写,也可以直接从CE那里注入之后复制过来用.
这里需要注意jmp_code
的后面填充了很多0x90
,也就是nop
,因为这里直接转移了sub
和mov
两条指令一共有8字节,但是jmp
只需要五个字节,剩下的三个字节最好进行填充.
1 |
|
WindowsAPI提供的一个函数,返回进程的句柄,ALL_ACESS
权限让我们能够直接操纵进程的内存.
1 |
|
在game
句柄代表的程序开辟一块1024字节大小的内存,要求该页内存可读可写可执行.
1 |
|
这里比较难理解,首先code
是个数组,LPVOID
是typedef的void *
类型,这里转换之后可以获得code
的指针,加上code
的整个字节大小就可以来到code
的最后一个字节所在位置,-0x4
就可以来到jmp
,也就是0xE9
的后面一个位置.
同时声明为四字节大小的unsigned
,我们就可以利用C语言指针的特点,对offset[0]赋值,把一个八位地址填充到0xE9
的后面.
1 |
|
这个更难理解,首先要知道在机器码层面,jmp
跳转接的对象是下一条指令的地址相对目标地址的偏移.如下:
先阐明三个关键,第一,VirtualAllocEx
分配的地址在上层,地址比应用程序代码在的地址要大;第二,因为jmp
指令本身有五个字节长度,所以最后会减去0x5
;第三,jmp的操作对象是一个有符号的数,从高向低跳是一个负数,从低向高是正数(然而实际上CPU根本不分正负,这只是人的理解).
0x0041BA7C
是要跳转回去的地址,(unsigned)code_mem
获得分配内存区域的第一个字节码的地址,加上size实际上已经超过了code的大小,来自code最后一个字节的下一个字节位置,即下一条指令的地址,二者相减就可以得到所要的偏移量了.需要注意的是这其实是一种做完运算后的形式,原来的形式应该是:
1 |
|
先-0x5
来到jmp
的位置,再+0x5
来到下一条指令的位置然后再做运算.
1 |
|
这一行是写入内存,写入进程为game
,写入位置为code_mem
指向的地址,写入内容为code
整个数组,写入大小为size.
1 |
|
这几行就是一样的了,修改jmp_code
数组内容赋予正确的跳转地址.这里的偏移计算也是一样的,其原本格式为:
1 |
|
目标地址code_mem
减去0x0041BA74
当前位置(这个位置会被替换成5个字节的jmp指令),加上5来到下一条指令的位置.
其实可以总结一个公式(任意情况都可以用):
偏移=目标地址-(jmp所在地址+5)
最后就是一些异常处理了.
看看效果:
题外话
虽然效果实现了,但其实这是最最lowB的一种hook方法,理论上我们应该还要做一下字节码保存机制让字节码可以恢复回去.而且这种hook直接修改内存,非常容易被CRC32这类计算内存区域的摘要算法手段检测出来(当然你再hook一下CRC32检测的DLL就又能跑了).
更好的hook方法有很多(甚至可以达到无痕hook),一种是依赖Windows下的异常处理机制,比如软件断点类型的hook,其将原有部分设置为int 3
强制中断跳转到异常处理函数;硬件断点直接设置dr0-dr3
调试寄存器强制开启异常处理(这种方法甚至不会修改原有地址的代码).
还有一种用的比较多的vt-ept hook,操作EPT,把要HOOK的函数的对应的页面的pte属性改成只能读写,不能执行,把函数页面复制一份,在复制出来的页面做HOOK,页面属性为只能执行,同样可以做到不修改原有代码而实现hook.