学习笔记之-window hook (x86 和 64 版本) 初探

本文作者因微信机器人开发需求,转而研究Hook技术。介绍了Hook的个人入门理解,包括目的用途、实现思路、截获方法和原则。给出具体实现流程,提供64位和32位环境下的代码及解释,还给出完整调试代码和参考界面,代码经测试成功。

缘由:本来想做个微信的机器人,发现wev方法已经歇菜了,微信貌似不让web版好好工作了,因此就顺带看了一下hook技术,调试了一个网上现成的代码。竟然调试成功,并且有了一点儿体会。

为了简单起见,介绍部分,主要来自各味前辈的总结。
文章主要参考
HOOK API入门之Hook自己程序的MessageBoxW(简单入门)
本文的目的是对其中的部分代码进行更小白的解释

Hook的本意是钩子,意思比较形象,就是对应用程序勾一下,干点儿别的。
下面进入我的个人理解的介绍:

hook的个人入门级理解##

1、hook的个人目的和用途
在现有程序中插一脚,瞄一眼,在不影响现有程序运行的情况下,干点儿事情,善意的或者恶意的。
2、实现思路
截获运行流程,令其转向,然后再返回。
3、怎么截获
原则上讲,水平高从程序的任何地方截获都可以,只要来的时候不漏痕迹就行了。然而,这是理论上而已,可行的方案是从调用入口截获,这样符合模块操作的风格,对原来的程序流程的影响容易做到最小。因此,入门级的截获就是截获已知函数。
4、截获原则
函数类型和参数等等等要和原来的函数一模一样。

具体实现思路

参考引文,实现对自己message box 的hook

很多问题参考原文即可。
下面整理一下具体过程的流程
1、定义自己的函数
2、获取自己的函数地址
3、获取Message box 在 User32.dll 中的地址
4、用自己的函数地址替换原函数地址
就上面的四个步骤,具体实现的时候,还有一些技巧,例如怎么替换,替换后的具体行为是什么等等。

代码和解释

原文是基于64位的win7环境,没有给出具体的32位做法,本文将给出两种环境下的代码。
64位:
1、准备工作

char szOldAPI[12] = {
   
   };
// mov         rax,13F371A3Ch
#ifdef  _M_X64
char NewCode[12] = {
   
    0x48,0xB8,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x50,0xC3 };
#endif

定义存放函数指针附近附近的数据,实际上是存贮机器码(这个如果不懂,先不管吧)。这个机器码就是调用关系,或者理解成汇编语言就OK了。

szOldAPI: 是存贮原函数的地址附近的数据,这个用来恢复原函数的调用关系用。
NewCode:是我们新的函数地址附近的数据,用来替换原函数的数据。
第一个字节和第二个字节是 汇编代码 mov rax xxxxxxx,如图示,后面的数字就是函数地址。将函数地址里的值放入到rax寄存器中。
下面的50C3就是最后的两个字节,汇编代码就是 push rax, 将rax 寄存器压入堆栈,然后返回。
在这里插入图片描述
篮筐内的数据时替换后我们的函数地址。上面解释未必准确,但是方向应该是对的。因为我对汇编并不熟悉。

为了对比,下面列出没有替换时的原函数地址附近的数据情况
在这里插入图片描述
注意首地址是一样的,我们替换了最前面的12个字节,完成了向我们函数的跳转。
注:moe rax指令是完成一种系统调用,具体参考
汇编语言基础:寄存器和系统调用
2、替换操作

	DWORD dwPid = ::GetCurrentProcessId();
	hProcess = OpenProcess(PROCESS_ALL_ACCESS, 0, dwPid);
	//获取原API入口地址
	HMODULE hmod = ::LoadLibrary(_T("User32.dll"));
	OldMsgBoxW = (MsgBoxW)::GetProcAddress(hmod, "MessageBoxW");
	pfOldMsgBoxW = (FARPROC)OldMsgBoxW;//注意这里有个转换,32位好像不用。

	if (pfOldMsgBoxW == NULL)
	{
   
   
		MessageBox(NULL, _T("获取原API入口地址出错"), _T("error!"), 0);
		return;
	}
#ifdef _M_X64
	DWORD64 dwJmpAddr = 0;
	dwJmpAddr = (DWORD64)MyMessageBoxW; //存下我们自己的函数的地址
	memcpy(NewCode + 2, &dwJmpAddr, 8); //把地址写到szNewAPI中间的8个0字节处
	ReadProcessMemory((void*)-1, pfOldMsgBoxW, szOldAPI, FUN_ADD_LEN, NULL); //读出原来的前FUN_ADD_LEN个字节
	//ReadProcessMemory(hProcess, pfOldMsgBoxW, OldCode, 12, NULL); //读出原来的前12个字节
#endif

上面是准备工作,还没有进行hook操作。
注意:12个字节长度的原因是因为利用上面的汇编语句,这个长度是最短的长度,并不是必须的,可以再长一些,只要记住被替换的部分,可以恢复即可。

3、hook 操作代码

	ASSERT(hProcess != NULL);
	//修改API函数入口前5个字节为jmp xxxxxx
	VirtualProtectEx(hProcess, pfOldMsgBoxW, FUN_ADD_LEN, PAGE_READWRITE, &dwOldProtect);
	WriteProcessMemory(hProcess, pfOldMsgBoxW, NewCode, FUN_ADD_LEN, 0);
	//WriteProcessMemory((void*)-1, pfOldMsgBoxW, NewCode, FUN_ADD_LEN, NULL); //写入我们处理后的FUN_ADD_LEN个字节 

	VirtualProtectEx(hProcess, pfOldMsgBoxW, FUN_ADD_LEN, dwOldProtect, &dwTemp);

替换操作的代码是

WriteProcessMemory(hProcess, pfOldMsgBoxW, NewCode, FUN_ADD_LEN, 0);

FUN_ADD_LEN 的值是12, 32位同。

执行完上述代码之后,调用关系就已经被修改了。
修改之前,的内存内容
在这里插入图片描述
修改之后

请读者自行比较前12个字节的变化,一起获得感性认识。

至此,原理以及解释已经结束。

下面介绍一下32位的做法
原理一样,只不过汇编语言不同。这里用了jmp 汇编,该汇编代码是无条件跳转命令,后面数字是地址偏移量,而不是绝对地址,因此后面的数值要进行计算。

这个偏移量的计算如下(直接引用参考网友的注释)

#ifdef _M_IX86
	DWORD32 dwJmpAddr = 0;
	dwJmpAddr = <
本人使用易语言 也有10几年时间,至今留下来的也只是一些怀念情节,还记得上一次在本论坛发帖是在10年前,期间也只是零星来论坛看看。平常使用也纯属兴趣爱好,并非科班出身,一些见解如若有误,忘大神包含。我们知道目前易语言是支持32位编译,后期估计也不会有所改善了,这似乎已经成为一门被放弃的失败品。既然如此面对如今64位系统的普及,易语言爱好者面对64位程序的操作层面上就显得有些无奈悲哀。好在有着一些执着的爱好者不想放弃,依然在鼓励解决这些问题,如本论坛的一个开源模块WOW64Ext,就为易语言操作64位模块进程提供了一些基本的封装,本人也借此基础上封装了几个功能,作为进一步扩展,有兴许的朋友可以继续完善。 一:浅谈64位进程远程hook技术 关于HOOK这个话题,网络上铺天盖地并无新鲜,故在此我就不讲述什么是HOOK这些无聊话题了,本文主要阐述一些64位下远程HOOK32位的主要区别。首先我们来看看要实现一个远程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 raxMov rax, FFFFFFFFFFFFFFFF JMP rax 或 call rax 在内部要取回rax ,这里注意JMPcall的区别,最后平栈 -------------------------- Push rax Mov rax,FFFFFFFFFFFFFFFF Push rax Ret 在内部要取回rax ,最后平栈 有的朋友看到这要问了 我不能直接 JMP FFFFFFFFFFFFFFFF或者 push FFFFFFFFFFFFFFFF 啊,您要这么问,我实在不知如何回答你,表示无语,您还是直接下个源码玩玩算了。 其他类似的列子我就不一一举例了,总结也是差不多形态,以上列子共占用13字节长度,这还是堆栈放在了内部平,否则还要+1个字节长度,如果放弃其中一个寄存器可以-1个字节长度,所以一般网上现有的64hook一般都在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数据传递使用消息或者远线程,因为这两种方式汇编改写量小一点,相对容易实现,在这我们不讨论远线程,我们来看看消息传递,一般是两个函数的选
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值