本帖最后由 树上的鱼儿 于 2021-2-12 18:14 编辑
浅谈64位进程远程hook技术及模块导出表的一些变化开源 本人使用易语言也有10几年时间,至今留下来的也只是一些怀念和情节,还记得上一次在本论坛发帖是在10年前,期间也只是零星来论坛看看。平常使用也纯属兴趣爱好,并非科班出身,一些见解如若有误,忘大神包含。
我们知道目前易语言是支持32位编译,后期估计也不会有所改善了,这似乎已经成为一门被放弃的失败品。既然如此面对如今64位系统的普及,易语言爱好者面对64位程序的操作层面上就显得有些无奈和悲哀。好在有着一些执着的爱好者不想放弃,依然在鼓励解决这些问题,如本论坛的一个开源模块WOW64Ext,就为易语言操作64位模块进程提供了一些基本的封装,本人也借此基础上封装了几个功能,作为进一步扩展,有兴许的朋友可以继续完善。
一:浅谈64位进程远程hook技术 关于HOOK这个话题,网络上铺天盖地并无新鲜,故在此我就不讲述什么是HOOK这些无聊话题了,本文主要阐述一些64位下远程HOOK与32位的主要区别。
首先我们来看看要实现一个远程HOOK的构成顺序:
1:在目标进程申请内存空间,存放我们截断后的穿插代码与HOOK原代码
2:修改HOOK目标位置的指令为跳转至1申请的内存空间中
3:穿插代码中把我们需要的寄存器或其他通过通讯手段传达到我们程序的回调接口中去,在这个过程中如果只需要取值,穿插代码不需要等待,如果需要修改生效,穿插代码需要等待回调接口的返回,并把修改内容写回。
4:穿插代码最后跳回到HOOK位置长度的下一跳指令,或指定的位置。
5:完成整个HOOK过程
了解了整个过程看上去似乎很简单,确实要做到这个过程是不难的,只是要做到相对完美要考虑的情况有很多。
比如对跳转使用的选择情况:HOOK跳转代码肯定是越短越好,像32位JMP跳转只需要5字节即可,但是在64位进程中情况确截然不同。32位进程寻址能力为4字节,而64位进程寻址能力变成了8字节,然而64位汇编中所有的跳转直接寻址只支持4字节,这在32位中当然不是什么问题,因为32位最大寻址本来就不会超越4字节,不存在超限的说法:
但64位中想要达到长转移,必须借用寄存器或地址偏移,那么一般在64位中HOOK的跳转代码在不影响寄存器的情况下一般使用如下办法FFFFFFFFFFFFFFFF作为跳转目标地址:
为了不影响寄存器必须提前压入一个寄存器
--------------------------
Push rax
Mov rax, FFFFFFFFFFFFFFFFJMP rax 或 call rax
在内部要取回rax ,这里注意JMP和call的区别,最后平栈
--------------------------
Push rax
Mov rax,FFFFFFFFFFFFFFFF
Push rax
Ret
在内部要取回rax ,最后平栈
有的朋友看到这要问了 我不能直接 JMP FFFFFFFFFFFFFFFF或者 push FFFFFFFFFFFFFFFF 啊,您要这么问,我实在不知如何回答你,表示无语,您还是直接下个源码玩玩算了。 其他类似的列子我就不一一举例了,总结也是差不多形态,以上列子共占用13字节长度,这还是堆栈放在了内部平,否则还要+1个字节长度,如果放弃其中一个寄存器可以-1个字节长度,所以一般网上现有的64位hook一般都在12字节以上,但是一个好用的hook要占用13字节的长度,对我而言无疑无法忍受,难道真的没有其他办法了吗,要保护寄存器且支持长转移,是不是还有其他办法,那么其实是有办法的,就是通过JMP [rip] 机器码形态为 FF 25 00 00 00 00 这句代码占用6字节,那么这是什么意思呢 FF 25 = jmp ,00 00 00 00为偏移长度 对一个支持2G的字节转移长度,JMP [rip]在调试器中可以解释为 jmp qword ptr ds:[0x地址],对了,也就是读取这个偏移位置中的8字节数值作为跳转地址转移过去,如果偏移为00 00 00 00 那么就代表 JMP [rip]的下一条指令处8字节数据。想到这你也许会问 那么这个意思不就是JMP [rip]6字节+8字节长度吗,对如果是连起来确实如此,但是我们可以给他个偏移啊,不就可以分开了吗,我们只需要搜索同模块的其他位置中00或CC等连续8字节无用代码位置,把跳转的地址写入其中,那么JMP [rip]就可以通过偏移读取到跳转地址了。我们也就能实现6字节的HOOK,这个方式的亮点是改写长度小,且不影响寄存器和rsp堆栈指针,也算是达到曲线救国的目的。 比如对穿插代码中数据传递的问题:我们要获得16个通用寄存器RAX—R15的每个值,这些值我们又如何传递过去。一般远程HOOK数据传递使用消息或者远线程,因为这两种方式汇编改写量小一点,相对容易实现,在这我们不讨论远线程,我们来看看消息传递,一般是两个函数的选择SendMessageA和PostMessageA,第一个会等待消息返回,第二个则不会等待,因为我们的穿插代码要写回寄存器的值,所以我们必须等待易语言回调接口执行完代码返回后才能对各寄存器赋值,否则就无法达到修改目的。这么一看似乎SendMessageA更为合适因为它本身就会等待,但是其实并非如此,不死心的你如果去试试看就会知道,假如第一个回调接口中代码正在处理,又有第二个回调进来,那么当第一个回调返回了而第二个回调还在处理中,这样假如第一个回调返回了,那么SendMessageA发送端不论有几个等待都会一起返回,也就是说SendMessageA如果生了张三李四两个儿子,张三 李四都出去了,当张三回家的时候,SendMessageA只会认为两个孩子都回家了。如果我的理解有误,请大佬指正,至少我测试结果如上。如果是这样其实 SendMessageA 也就并不适合我们了,而且使用SendMessageA在回调中使用延时延迟都会造成一定问题。那么现在的我们就没必要使用SendMessageA了,转而使用无需等待的PostMessageA,而我们没必须自己在穿插代码中构造一个循环,循环什么呢,循环一个返回标志,当回调接口处理完毕,在内存中写一个返回标记,当PostMessageA发送玩之后的代码循环检查这个返回标志判断回调接口处理情况,当得到可以继续的返回标记则代码继续往下。 然后这个各个寄存器的值又如何传递呢,一开始我想的是易语言在目标进程申请内存用来存放各寄存器,但是后来仔细想想这是个很笨的办法,在大量HOOK点经过的似乎这个方式容易混淆寄存器值。那么在穿插代码中申请内存呢?这样每个经过都独立一个寄存器存储空间了?仔细思索这是个不得已的办法,也是个笨办法。善用调试器的你能不想到应该用什么最方便?对了,那就是堆栈空间,每个经过堆栈空间也是独立的存在,又方便又好写还稳定,超限的情况应该不存在,我们用的并不多。
这个时候问题又来了 PostMessageA 一共四个参数
窗口句柄
消息号
消息值1
消息值2
在64进程中PostMessageA函数的4个参数也是整数型4字节传递,包括它发送出去的值也是一个整数型4字节。那么64进程堆栈指针是一个8字节长整数,也就是说我们需要拆分堆栈指针8字节为两个4字节,通过消息值1 消息值2 传递这个堆栈指针,在我们的程序中在合并它得到完整的堆栈指针。看上去这样也许就可以了,但是喜欢完美的我还想让PostMessageA在传递一个HOOK进程的句柄值,这样我们在易语言程序中就可以直接使用这个句柄做其他操作,而且句柄与堆栈地址对应,完美,所有需要传递的值都不会出问题。可惜看上去已经没有这个句柄的位置了,经过反复实验,我看上了参数二 消息号 这个参数特殊一点
我们可以看到正常的绑定得到的消息编号值是有取值范围的PostMessageA 参数二 的取值范围在 C000-FFFF之间,那么这给我燃气的希望,我能不能通过进程句柄加65535的值来传递呢,在易语言得到后如果大于65535就是我们发送的数据在减去65535,也就得到实际的进程句柄值了不是,经过测试 消息发送的消息编号最大可以自定义至十进制131071=十六进制1FFFF,在大就接收不到了。也就是说按照我的思路句柄值不能大于1FFFF-FFFF=10000=十进制65535,虽然句柄值一般都不大但是65535还是觉得不太可行,好在天无绝人之路,经过不停的打开进程句柄测试后发现,系统中句柄值是依次增大,打开一次增大4,也就是说假如你系统是完全没有打开过句柄,那么第一个句柄值应该是4,第二个应该是8,依次递增。我只是一个比喻,本身系统还是有不少占用的,且系统也有限制句柄最大数量。我们来换算一下65535可以打开多少次句柄,65535/4=16383 次,完全够用了,最后第二个参数=进程句柄\4+65535,接收的时候(第二个参数-65535)*4=进程句柄,这样就完成了我们想要的目的。
下面附上一张穿插代码的汇编原型,还是比较简单的,64汇编中取消了寄存器统一压栈保护,只能一个一个压进去,这个原型内没打注释,懂的自然懂,不懂的解释了也看不懂,大概是这样,依次将需要保护的寄存器写到堆栈中保护起来->抬栈->调用APIPostMessageA->平栈->循环判断返回标记->写回寄存器->从栈中取得跳回地址并写入->判断是否执行原代码->HOOK原代码->JMP[rip]。
0000000100401392这里HOOK位置安装改写跳转指令后的显示结果
模块的大概情况
在这里再次强调本模块是在WOW64Ext开源模块的基础上增加的一些功能,请保留版权信息,若用于其它目的而造成的一切纠纷与后果由您自行承担。 本来还想写写 64位模块导出表的变化,鉴于篇幅,暂时作罢,日后再说,源码也包含在内,自己看吧。同时构造了模块64_取函数入口() 和其他的一些常用子程序。 在此感谢朋友 小叶的帮助,这个功能完成他也花费不少心思,两人反复商讨多次,修改多次后终于完成现有我们还算满意的一个成品源码: 远程hook64指令_安装() 支持最小6字节,最大127字节,hook采用JMP转移,且为长转移(并非2G短转移),且支持最小6字节 支持hook任意指令处,支持获得当时16个通用寄存器+一个rip寄存器,的读取及修改 回调接口返回值支持是否执行Hook掉的原指令(0=执行,>0不执行) 支持任意堆栈操作,通过rsp 支持任意rip转移(支持大于2G长转移),通过rip 支持一个易语言程序同时控制多个64位目标进程 支持多个hook点同时回调至一个回调接口内 且所有回调尽阶独立,不论hook位置的复杂经过性皆不影响回调数据 每次hook的经过都会等到回调返回,假如回调接口未返回,则hook当时的经过为挂起等待状态
灵活多变的自由控制,可达到几乎所有的要求
朋友们可以自行扩展,如果正好帮助到你我也很高兴呵呵,有人说我上一个帖子截图不清楚,不知道这次怎么样,有些二喷子只会JJYY我上个帖子没发好,也不必激将我开源不开源,没时间浪费在帖子的编辑上,有那个鸟本事就自己写去也别下,这个功能在写之初就是准备开源的,不准备开源你喷了也无用,我在积德了请问喷子们给我点啥呢,有那本事别伸手,附:源码
64hook v1.3.e
(272.89 KB, 下载次数: 449)
|