关于微信的hook思考

开发初衷

微信当今成为现代国人离不开的通讯的工具,已然成为了作为人的“一部分”,随之弊端也愈演愈烈,工作、社交、日常生活都渗透在微信中,一时间不关注它就生怕错过了什么。我便思考:有没有什么助手可以帮助我托管微信,就像玩斗地主或者打游戏,我不得不离开电脑或手机时,它可以临时的替代我机械的“在线”。我在网上找各种各样的助手,但是没有合适的,我便想着能不能自己做一款。

思路

微信软件本身的思路非常简单,信息的接收无非是通讯消息,这一块的处理方式我们只需要劫持通讯信息,获取通讯信息后再将信息放回原来的通讯即可;难点就在于发送消息,发送消息该如何做?这一定要渗透到微信的内存和进程内部才行,就不得不使用注入技术。

探寻底层原理

如果要做获取微信本身信息的操作,则一定是获取微信占用的内存进行读取,比如:

  • 获取联系人,则是遍历一块特定的内存空间;
  • 获取当前登录微信信息,则是获取一块特定的内存空间;
  • 获取当前微信的设置项,也是获取一块特别的内存空间;

如果要做一些“行为”,则一定是获取微信的进程,调用进程中的模块,比如:

  • 需要发送消息时,模拟微信发送消息,组装好消息体,调用微信发送消息的模块;
  • 通过好友验证,则是组装好验证信息,调用微信的验证模块;
  • 转发信息,则是拿到劫持的信息获取指定ID,组装转发信息,调用微信转发模块;

如果要做一些需要调用微信协议获取的信息,依然是调用模块,自然会通过模块触发协议,比如:

  • 获取指定好友的全部信息,需要组装好请求体,直接调用获取好友详情模块即可。

打个比方,这就需要我们派一个间谍打入微信内部,通过电报(RPC)和间谍(DLL)对外进行消息交换:

DLL 负责拦截、伪装,这就需要拦截技术(Hook);
RPC 负责传送消息,涉及到跨进程间通信,本项目使用的是远程过程调用(Remote Procedure Call);

当微信收到消息时,间谍(DLL)把消息通过 RPC 传给 外部;
需要发送消息时,会将信息通过 RPC 传递给间谍(DLL) “假传圣旨”发送出去

这一切的实现都要基于注入技术,也就是进入微信的进程才能达到目的。

注入原理

首先介绍一下注入(Inject)技术。

注入技术通常都跟恶意软件有关,一般是为了在目标进程中执行自定义代码。注入技术有很多,本项目选取了最经典的一种:将 DLL 的路径写入微信进程的虚拟地址空间,然后通过在微信进程中创建一个远程线程来加载DLL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 1. 获取目标进程,并在目标进程的内存里开辟空间
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
LPVOID pRemoteAddress = VirtualAllocEx(hProcess, NULL, 1, MEM_COMMIT, PAGE_READWRITE);

// 2. 把 dll 的路径写入到目标进程的内存空间中
if (pRemoteAddress) {
WriteProcessMemory(hProcess, pRemoteAddress, dllPath, wcslen(dllPath) * 2 + 2, &dwWriteSize);
} else {
MessageBox(NULL, L"DLL 路径写入失败", L"InjectDll", 0);
return -1;
}

// 3. 创建一个远程线程,让目标进程调用 LoadLibrary
hThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)LoadLibrary, pRemoteAddress, NULL, NULL);
if (hThread) {
WaitForSingleObject(hThread, -1);
} else {
MessageBox(NULL, L"LoadLibrary 调用失败", L"InjectDll", 0);
return -2;
}
CloseHandle(hThread);
VirtualFreeEx(hProcess, pRemoteAddress, 0, MEM_RELEASE);
CloseHandle(hProcess);

拦截伪装

通过注入技术,成功将 DLL(间谍)打入了微信内部,下一步要做的事情便是让DLL(间谍)能“劫持”微信消息和“假传圣旨”,这需要使用拦截、伪装技术。

拦截

拦截技术通常被称为 Hook。

为了介绍拦截技术,需要先说一说运行的流程。运行大体需要经历:

  1. 创建程序进程,加载程序代码、数据,创建、映射虚拟地址空间
  2. 创建主线程,运行程序
  3. 在编译阶段,编译器便把代码里的指令安放到代码段。当程序被加载到虚拟地址空间的时候,代码段便被映射过去。于是,我们程序里的函数,便可以用一个地址(是不是想起了指针?)代替。

当微信接收到一条新消息,需要展示给用户的时候,可以想象,肯定会调用某个函数,把消息展示出来。如果我们把这个函数换成咱们的函数,就可以拦截微信的消息了。 前面提到,在程序运行的时候,所谓函数不过是个地址指向,所以我们只要把这个地址指向咱们自己的函数,便实现了拦截。

举个例子:

1
2
3
# 打个比方,当微信收到消息的时候,假设调用下面的函数
# 地址 机器码 反汇编
0F8F0F6C E8 FF525200 call WeChatWi.0FD26250

我们只要把 0F8F0F6C 里的 call WeChatWi.0FD36350,替换成 call 咱们自己的函数,便可以对消息进行拦截了。同时,为了不影响原有的功能,我们还需要在 咱们自己的函数 的最后,调用 WeChatWi.0FD26250

我们把 0F8F0F6C 叫做 Hook 地址,把 WeChatWi.0FD26250 叫做 Call 地址。这里 0F8F0F6CFF525200 都是“相对”地址——相对 WeChatWin.dll 的地址;而 WeChatWin.dll 的地址称为 基址(Base)

伪装

当我们需要在微信上发送一条新消息的时候,可以想象,微信肯定会调用某个函数,把消息发送出去。如果我们找到这个函数,组装好发送内容,调用它,就可以发送微信的消息了。

下面举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
0F44FBF3    8D46 38         lea eax,dword ptr ds:[esi+0x38]
0F44FBF6 6A 01 push 0x1
0F44FBF8 50 push eax ; At members
0F44FBF9 57 push edi ; Message
0F44FBFA 8D55 90 lea edx,dword ptr ss:[ebp-0x70] ; Receiver wxid
0F44FBFD 8D8D 50FCFFFF lea ecx,dword ptr ss:[ebp-0x3B0] ; Buffer
# 打个比方,当微信发送消息的时候,假设使用下面的函数
0F44FC03 E8 28213700 call WeChatWi.0F7C1D30 ; Send Msg
0F44FC08 83C4 0C add esp,0xC
0F44FC0B C645 FC 05 mov byte ptr ss:[ebp-0x4],0x5
0F44FC0F 8B85 70FCFFFF mov eax,dword ptr ss:[ebp-0x390]
0F44FC15 0B85 74FCFFFF or eax,dword ptr ss:[ebp-0x38C]
0F44FC1B 75 10 jnz short WeChatWi.0F44FC2D

于是,当我们需要发送消息的时候,只要调用 0x521D30(0x0F7C1D30 - 0x0F2A0000)即可。

RPC

前面我们成功打入微信内部,并且也可以拦截消息并“假传圣旨”,那么,我们怎么把消息传出去或者传进来呢?

微信和我们的应用,在不同的进程。如果我们的应用需要和微信通信,则涉及到进程间通信(Inter Process Communication)。

Windows 支持的 IPC 方式包括:

  • 剪贴板
  • COM
  • 数据复制
  • DDE
  • 文件映射
  • Mailslots
  • 管道
  • RPC
  • Windows 套接字

RPC 指远程过程调用(Remote Procedure Call)。这里的远程指的是不在同一个进程,可以是一台电脑上的不同进程;也可以是不个电脑上的不同进程。使用 RPC,可以创建高性能紧密耦合的分布式应用程序。

通过 RPC,进程间通信就变得很简单。RPC 工具使用户看起来就像客户端直接调用位于远程服务器程序中的过程一样。客户端和服务器各自有自己的地址空间;也就是说,每个资源都有自己的内存资源分配给过程使用的数据。