前言
上一章中介绍了vx的逆向分析,现在我们就要进行编码了,我的思路如下:
- 使用远程注入DLL方式将我们的代码注入到VX中
- 再在DLL中使用Inline hook hook上一章中找到的关键函数
- 最后在我们的hook代码中做业务处理
所以这里我使用了两个项目:
-
用于dll注入的程序WechatTest
https://github.com/bigGreenPeople/WechatTest -
hook的dll程序SharkDll
https://github.com/bigGreenPeople/SharkDll
DLL注入
#include <iostream>
#include "stdlib.h"
#include <tchar.h>
#include <Windows.h>
bool Inject(DWORD dwId, char * szPath)//参数1:目标进程PID 参数2:DLL路径
{
//一、在目标进程中申请一个空间
/*
【1.1 获取目标进程句柄】
参数1:想要拥有的进程权限(本例为所有能获得的权限)
参数2:表示所得到的进程句柄是否可以被继承
参数3:被打开进程的PID
返回值:指定进程的句柄
*/
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwId);
/*
【1.2 在目标进程的内存里开辟空间】
参数1:目标进程句柄
参数2:保留页面的内存地址,一般用NULL自动分配
参数3:欲分配的内存大小,字节单位
参数4:MEM_COMMIT:为特定的页面区域分配内存中或磁盘的页面文件中的物理存储
参数5:PAGE_READWRITE 区域可被应用程序读写
返回值:执行成功就返回分配内存的首地址,不成功就是NULL
*/
LPVOID pRemoteAddress = VirtualAllocEx(
hProcess,
NULL,
1,
MEM_COMMIT,
PAGE_READWRITE
);
//二、 把dll的路径写入到目标进程的内存空间中
DWORD dwWriteSize = 0;
/*
【写一段数据到刚才给指定进程所开辟的内存空间里】
参数1:OpenProcess返回的进程句柄
参数2:准备写入的内存首地址
参数3:指向要写的数据的指针(准备写入的东西)
参数4:要写入的字节数(东西的长度+0/)
参数5: 返回值。返回实际写入的字节
*/
WriteProcessMemory(hProcess,
pRemoteAddress,
szPath,
strlen(szPath) * 1 + 1,
&dwWriteSize);
//三、 创建一个远程线程,让目标进程调用LoadLibrary
/*
参数1:该远程线程所属进程的进程句柄
参数2:一个指向 SECURITY_ATTRIBUTES 结构的指针, 该结构指定了线程的安全属性
参数3:线程栈初始大小,以字节为单位,如果该值设为0,那么使用系统默认大小
参数4:在远程进程的地址空间中,该线程的线程函数的起始地址(也就是这个线程具体要干的活儿)
参数5:传给线程函数的参数(刚才在内存里开辟的空间里面写入的东西)
参数6:控制线程创建的标志。0(NULL)表示该线程在创建后立即运行
参数7:指向接收线程标识符的变量的指针。如果此参数为NULL,则不返回线程标识符
返回值:如果函数成功,则返回值是新线程的句柄。如果函数失败,则返回值为NULL
*/
HANDLE hThread = CreateRemoteThread(
hProcess,
NULL,
0,
(LPTHREAD_START_ROUTINE)LoadLibrary,
pRemoteAddress,
NULL,
NULL
);
if (hThread == NULL) {
printf("CreateRemoteThread fail:%d\n", (int)hThread);
}
else
{
printf("hThread:%d\n", (int)hThread);
}
WaitForSingleObject(hThread, -1); //当句柄所指的线程有信号的时候,才会返回
/*
四、 【释放申请的虚拟内存空间】
参数1:目标进程的句柄。该句柄必须拥有 PROCESS_VM_OPERATION 权限
参数2:指向要释放的虚拟内存空间首地址的指针
参数3:虚拟内存空间的字节数
参数4:MEM_DECOMMIT仅标示内存空间不可用,内存页还将存在。
MEM_RELEASE这种方式很彻底,完全回收。
*/
VirtualFreeEx(hProcess, pRemoteAddress, 1, MEM_DECOMMIT);
return 0;
}
int _tmain(int argc, _TCHAR * argv[])
{
//这里是在目标进程的路径 所以我们暂时使用绝对路径
char wStr[] = "D:\\source\\repos\\SharkDll\\Debug\\SharkDll.dll";
DWORD dwId = 0;
//参数1:(NULL
//参数2:目标窗口的标题
//返回值:目标窗口的句柄
//HWND hCalc = FindWindow("TfrmMain", NULL);
HWND hCalc = FindWindow(NULL,"微信");
//HWND hCalc = FindWindow("IEFrame", NULL);
printf("目标窗口的句柄为:%d\n", (int)hCalc);
DWORD dwPid = 0;
//参数1:目标进程的窗口句柄
//参数2:把目标进程的PID存放进去
DWORD dwRub = GetWindowThreadProcessId(hCalc, &dwPid);
printf("目标窗口的进程PID为:%d\n", dwPid);
//return 0;
//参数1:目标进程的PID
//参数2:想要注入DLL的路径
Inject(dwPid, wStr);
return 0;
}
这段代码是网上的注释写的非常清楚了我稍微说明一下:
- OpenProcess - 用于打开要寄生的目标进程。
- VirtualAllocEx/VirtualFreeEx - 用于在目标进程中分配/释放内存空间。
- WriteProcessMemory - 用于在目标进程中写入要加载的DLL名称。
- CreateRemoteThread - 远程加载DLL的核心内容,用于控制目标进程调用API函数。
- LoadLibrary - 目标进程通过调用此函数来加载DLL。
在此我只给出了简要的函数说明,关于函数的详细功能和介绍请参阅MSDN。
注意:dll路径我为了方便测试使用了绝对路径,请注意修改
HOOK
为了有操作界面我使用了MFC的DLL工程
项目代码我都上传到git上了,我们这看看关键代码
先看看我们要使用到的hook函数
hook.cpp
/**
* 安装Hook
* hookAddr 内存地址
* backCode 备份的地址
* HookImpl 跳转到的函数,该函数为裸函数
*/
int StartHook5(DWORD hookAddr, BYTE backCode[5], void(*FuncBeCall)()) {
DWORD jmpAddr = (DWORD)FuncBeCall - (hookAddr + 5);
BYTE jmpCode[5];
*(jmpCode + 0) = 0xE9;
*(DWORD *)(jmpCode + 1) = jmpAddr;
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, NULL, GetCurrentProcessId());
//备份被替换的
if (ReadProcessMemory(hProcess, (LPVOID)hookAddr, backCode, 5, NULL) == 0) {
return -1;
}
//写入jmp指令
if (WriteProcessMemory(hProcess, (LPVOID)hookAddr, jmpCode, 5, NULL) == 0) {
return -1;
}
return 0;
}
这段hook代码是网上的,写的并不是很清楚我说明一下
参数一:关键call的地址
参数二:保存原来call的函数地址
参数三:是hook后的函数
调用完此函数后可以将
call WeChatWi.56BA6F9A
修改为call _OnRevock
即修改为我们的业务函数
现在看看如何调用这个hook函数
SharkDllMenuDialog.cpp
//我们计算得到的相对地址,通过获得WeChatWin.dll的基址就可以得到本次运行的虚拟地址
//调用关键call的相对地址
#define REVOCK_CALL_RVA 0x52d358
//关键call的相对地址
#define REVOCK_CALL_TARGET_RVA 0x11d6f9a
//关键call调用时的地址
DWORD revockCallVA = 0;
//原来的函数调用
//55EFD358 E8 3D9CCA00 call WeChatWi.56BA6F9A
DWORD revockCallTargetVA = 0;
//原来的函数调用的下一句,调回的一句
//55EFD35D 8B07 mov eax,dword ptr ds:[edi]
DWORD revockCallJmpBackVA = 0;
DWORD wechatWinAddr = 0;
BYTE backCode[5];
HWND m_dialog_hwnd;
// SharkDllMenuDialog 消息处理程序
CEdit* pBoxOne;
void SharkDllMenuDialog::OnBnClickedOk()
{
// TODO: 在此添加控件通知处理程序代码
//CDialogEx::OnOK();
// 开启消息防撤回
CreateThread(NULL, 0, LPTHREAD_START_ROUTINE(DlgThread), 0, 0, NULL);
//将编辑框保存到pBoxOne
pBoxOne = (CEdit*)sharkDialog->GetDlgItem(IDC_EDIT1);
}
//定义用于连接的字符串
CString back_msg = NULL;
CString back_split = CString(L"\r\n");
CString back_msginfo = CString(L"撤回消息为:");
CString back_tips = NULL;
void OnRevock(DWORD esp) {
wchar_t *tips = *(wchar_t **)(esp + 0x4);
wchar_t *msg = *(wchar_t **)(esp + 0x0);
if (NULL != tips) {
//WCHAR buffer[0x8192];
char buff2[0x8192];
//因为hook的位置会有多次调用,所以需要判断tips和msg是否是我们需要的关键信息
wchar_t* pos = wcsstr(tips, L"<revokemsg>");
wchar_t* pos2 = wcsstr(msg, L"<revokemsg>");
//如果需要的话我们把撤回的消息显示在编辑框中
if (pos2==NULL && pos != NULL && NULL != msg) {
CString temp_tip(tips);
CString temp_msg(msg);
//排除空信息
if (temp_msg.IsEmpty() || temp_msg.GetLength() == 0 || !temp_msg) {
return;
}
//排除重复的信息
if (back_msg == temp_msg && back_tips == temp_tip) {
return;
}
//下面就是以下字符串处理了
back_msg = CString(msg);
back_tips = CString(tips);
//设置信息
CString last_result;
pBoxOne->GetWindowText(last_result);
last_result = last_result + back_tips + back_split + back_msginfo + back_msg + back_split;
pBoxOne->SetWindowText(last_result);
}
}
}
DWORD tEsp = 0;
_declspec(naked) void _OnRevock() {
//调用我们的业务函数前,请先保存现场
__asm {
mov tEsp, esp
pushad
}
OnRevock(tEsp);
//以下是调用原来的函数,执行正常的流程
__asm {
popad
call revockCallTargetVA
jmp revockCallJmpBackVA
}
}
//开启线程进行hook,不开线程会卡死
void DlgThread(HMODULE hInstance) {
//hook开始
MessageBoxA(NULL, "DLL Attached!\n", "Game Hacking", MB_OK | MB_TOPMOST);
//得到WeChatWin.dll的基址
wechatWinAddr = GetWxModuleAddress();
//计算出关键call调用时的地址
revockCallVA = wechatWinAddr + REVOCK_CALL_RVA;
//计算出关键call的地址
revockCallTargetVA = wechatWinAddr + REVOCK_CALL_TARGET_RVA;
//指向了 55EFD35D 8B07 mov eax,dword ptr ds:[edi]
//这里是通关计算得到调用call后的下一条指令,并没有使用到我们上一章的相对地址,因为上条指令刚好是5个字节所以+5
revockCallJmpBackVA = revockCallVA + 5;
StartHook5(wechatWinAddr + REVOCK_CALL_RVA, backCode, _OnRevock);
}
完整的代码可以到我的Github上看到。
上面代码的具体做了以下事情:
点击按钮时开启一个线程进行hook(这里不开线程会卡死),再获得WeChatWin.dll的基址在加上我们上一章得到的相对地址就可以计算出,调用关键call的地址和关键call的地址,以及他们的下一条汇编指令的地址,从而进行hook。将调用关键call的汇编改为_OnRevock函数,在_OnRevock中调用完业务处理OnRevock函数后,我们需要执行原来的流程也就是call revockCallTargetVA和jmp revockCallJmpBackVA。
运行
首先运行微信并且登录
运行WechatTest.exe
弹出以下窗口,这个窗口是属于微信的:
image.png
使用火绒剑可以看到我们的dll成功的注入了
image.png
点击防撤回开启,出现提示
image.png
现在进行使用微信发送并撤回一条消息:
image.png image.png
消息成功的被我们拦截到了
尾言
其实界面之类的可以做的更好看,也可以减少hook函数的调用次数,这些都需要去对微信进行进一步的逆向分析,找到界面有关的call函数,和分析其栈的变化情况。运用的技术点分别是远程注入和inline hook,在注入和hook方便我也是只略知一二这里我就不对其做详细的说明了,感兴趣的朋友可以查找其相关资料。
有了此文章即使微信进行更新,只要按照文章获得相应的相对地址并在代码中修改即可~
网友评论