写在前面
最近在搞微信的发送消息CALL,跟着网上的教程,一步一步走,很容易定位到CALL的地址,在适当的地方用OD断下,修改压入的参数内容,消息内容或接收人成功改变,但在使用C++调用的时候,因为不懂汇编指令,所以踩了一些坑。
工具
微信 3.5.0.46
Windows10 Pro
OllyICE 1.10
Cheat Engine 7.0
Visual Studio 2019
定位CALL地址
这部分感觉自己讲不太明白,而且网上有很多现成的教程,找这个CALL的思路还是比较简单的,推荐阅读下面这篇文章:
CSDN:PC微信逆向:发送与接收消息的分析与代码实现
下面是我定位到的内容:
787D42EE 8D46 38 lea eax, dword ptr [esi+38] ; 取at结构体
787D42F1 6A 01 push 1 ; 0x1
787D42F3 50 push eax ; 群消息at好友,非at消息为0
787D42F4 57 push edi ; 消息内容,[edi]
787D42F5 8D95 7CFFFFFF lea edx, dword ptr [ebp-84] ; 接收人,[edx]
787D42FB 8D8D 58FCFFFF lea ecx, dword ptr [ebp-3A8] ; 缓冲区,据说是类本身
787D4301 E8 7A793300 call 78B0BC80 ; 发送消息CALL
787D4306 83C4 0C add esp, 0C ; 平衡堆栈
CALL的偏移:0x78B0BC80 - 0x78670000 = 0x49BC80
0x78670000是WeChatWin.dll的基地址,可以在OD中查看可执行模块获取
调用
拿到了汇编代码,下一步自然是调用,不过在这之前要搞清楚接收人和消息内容的结构,不然发送的东西CALL看不明白,微信可能就崩了。
消息内容结构:
0ABBFC1C 122B2CF8 UNICODE "123456"
0ABBFC20 00000006
0ABBFC24 00000006
0ABBFC28 00000000
0ABBFC2C 00000000
地址122B2CF8指向消息内容本身,所以结构体第一个元素是消息文本的指针,后面第一个6是消息文本的长度,第二个是消息最大长度,一般分配消息文本长度两倍大小,会多耗费一点内存,CALL执行完就回收了;再往后面就是0了,为了保证安全,可以在结构体末尾添加DWORD类型占位字符。
接收人结构:
012FE504 12290278 UNICODE "filehelper"
012FE508 0000000A
012FE50C 0000000A
012FE510 00000000
012FE514 00000000
可以看到该结构体跟消息内容基本一致,都是字符串地址加长度,在OD中还可以看到接收人结构体后面跟了一个消息内容结构体,似乎没什么用。
最终结构体应该是这个样子:
struct WxString
{
// 存字符串
wchar_t* buffer;
// 存字符串长度
DWORD length;
// 字符串最大长度
DWORD maxLength;
// 补充两个占位符
DWORD fill1;
DWORD fill2;
};
因为是调用已有的函数,不需要加Hook,直接使用内联汇编调用就可以了:
void SendWxMessage(wchar_t* wsWxId,wchar_t* wsTextMsg) {
// 1、构造参数
// 构造接收者结构
WxString wxWxid = { 0 };
wxWxid.buffer = wsWxId;
wxWxid.length = wcslen(wsWxId);
// OD显示与字符串长度一致,但可以给大一点
wxWxid.maxLength= wcslen(wsWxId) * 2;
// 构造消息结构
WxString wxTextMsg = { 0 };
wxTextMsg.buffer = wsTextMsg;
wxTextMsg.length = wcslen(wsTextMsg);
// OD显示与字符串长度一致,但可以给大一点
wxTextMsg.maxLength = wcslen(wsTextMsg) * 2;
// 取出消息地址
wchar_t** pWxmsg = &wxTextMsg.buffer;
// 构造空buffer
char buffer[0x3A8] = { 0 };
WxString wxNull = { 0 };
// 2、获取DLL模块基址
// 模块基址
DWORD dllBaseAddress = (DWORD)GetModuleHandle(L"WeChatWin.dll");
// 3、计算函数的内存地址
// 函数偏移,不同的微信版本会有变化
DWORD callOffset = 0x49BC80;
// 函数内存地址
DWORD callAddress = dllBaseAddress + callOffset;
__asm {
lea eax, wxNull;
// 参数5:1
push 0x1;
// 参数4:空结构
push eax;
// 参数3:发送的消息,传递消息内容的地址
mov edi, pWxmsg;
push edi;
// 参数2:接收人,传递结构体地址,要特别注意lea和mov的区别
lea edx, wxWxid;
// 参数1:空buffer
lea ecx, buffer;
// 调用函数
call callAddress;
// 堆栈平衡,否则会崩溃
add esp, 0xC;
}
}
上面容易踩坑的地方是edi和edx两处,一个是mov赋值,一个是用lea取有效地址,mov传递字符串地址,lea取结构体地址,如果lea取字符串地址,就变成了字符串地址的地址,这里比较绕,其实就是指针那点破事儿,对于汇编也不太了解,如有错误欢迎指正。
可以把mov替换成lea,那么edi也直接取结构体地址就行了,已验证通过,但不知道会不会出什么问题。
生成DLL
写好了调用函数,就要编译成DLL,然后注入到微信的内存空间,测试一下了,这部分直接给完整的代码:
pch.h
// pch.h: 这是预编译标头文件。
// 下方列出的文件仅编译一次,提高了将来生成的生成性能。
// 这还将影响 IntelliSense 性能,包括代码完成和许多代码浏览功能。
// 但是,如果此处列出的文件中的任何一个在生成之间有更新,它们全部都将被重新编译。
// 请勿在此处添加要频繁更新的文件,这将使得性能优势无效。
#ifndef PCH_H
#define PCH_H
// 添加要在此处预编译的标头
#include "framework.h"
#include <string>
#include <iostream>
#include <io.h>
#include <fcntl.h>
#endif //PCH_H
#define DLLEXPORT extern "C" __declspec(dllexport)
DLLEXPORT void SendWxMessage(wchar_t* wsWxId, wchar_t* wsTextMsg);
BOOL CreateConsole(void);
// 外部调用的入口,必须export,不然无法计算函数地址
DLLEXPORT void SendWxMessageAPI(LPVOID lpParameter);
pch.cpp
// pch.cpp: 与预编译标头对应的源文件
#include "pch.h"
using namespace std;
#define STRUCT_OFFSET(stru_name, element) (unsigned long)&((struct stru_name*)0)->element
// 当使用预编译的头时,需要使用此源文件,编译才能成功。
struct WxString
{
// 存字符串
wchar_t* buffer;
// 存字符串长度
DWORD length;
//字符串最大长度
DWORD maxLength;
// 补充两个占位符
DWORD fill1;
DWORD fill2;
};
// 外部调用时使用
struct RemoteParam
{
DWORD wxid;
DWORD wxmsg;
};
// 启动一个控制台窗口,以便调试
BOOL CreateConsole(void) {
if (AllocConsole()) {
AttachConsole(GetCurrentProcessId());
FILE* retStream;
freopen_s(&retStream, "CONOUT$", "w", stdout);
if (!retStream) throw std::runtime_error("Stdout redirection failed.");
freopen_s(&retStream, "CONOUT$", "w", stderr);
if (!retStream) throw std::runtime_error("Stderr redirection failed.");
return 0;
}
return 1;
}
// 测试用的函数
void testMessage(DWORD edx_, DWORD edi_,int s) {
wcout.imbue(locale("chs"));
printf("s->%d,edi->0x%08X,edx->0x%08X\n",s,edi_,edx_);
unsigned long offset = STRUCT_OFFSET(WxString, buffer);
// edx是通过lea取的结构体地址,所以直接强制类型转换
WxString* wxWxid = (WxString*)edx_;
// edi是结构体成员buffer地址,要反推结构体首地址,再进行强制类型转换
printf("wxTextMsg结构体首地址为: 0x%08X\n", edi_ - offset);
WxString* wxTextMsg = (WxString*)(edi_ - offset);
// edi实际上是wchar_t**变量
wcout << L"接收人wxid:" << wxWxid->buffer << "," << L"消息内容:" << *(wchar_t**)edi_ << endl;
wcout << wxWxid->buffer << ",";
printf("%d,%d,%d,%d\n", wxWxid->length, wxWxid->maxLength, wxWxid->fill1, wxWxid->fill2);
wcout << wxTextMsg->buffer << ",";
printf("%d,%d,%d,%d\n", wxTextMsg->length, wxTextMsg->maxLength, wxTextMsg->fill1, wxTextMsg->fill2);
}
void SendWxMessageAPI(LPVOID lpParameter) {
RemoteParam* rp = (RemoteParam*)lpParameter;
wchar_t* wsWxId = (WCHAR*)rp->wxid;
wchar_t* wsTextMsg = (WCHAR*)rp->wxmsg;
SendWxMessage(wsWxId, wsTextMsg);
}
void SendWxMessage(wchar_t* wsWxId,wchar_t* wsTextMsg) {
// 1、构造参数
// 构造接收者结构
WxString wxWxid = { 0 };
wxWxid.buffer = wsWxId;
wxWxid.length = wcslen(wsWxId);
// OD显示与字符串长度一致,但可以给大一点
wxWxid.maxLength= wcslen(wsWxId) * 2;
// 构造消息结构
WxString wxTextMsg = { 0 };
wxTextMsg.buffer = wsTextMsg;
wxTextMsg.length = wcslen(wsTextMsg);
// OD显示与字符串长度一致,但可以给大一点
wxTextMsg.maxLength = wcslen(wsTextMsg) * 2;
//取出消息地址
wchar_t** pWxmsg = &wxTextMsg.buffer;
// 构造空buffer
char buffer[0x3A8] = { 0 };
WxString wxNull = { 0 };
// 2、获取DLL模块基址
// 模块基址
DWORD dllBaseAddress = (DWORD)GetModuleHandle(L"WeChatWin.dll");
// 3、计算函数的内存地址
// 函数偏移,不同的微信版本会有变化
DWORD callOffset = 0x49BC80;
// 函数内存地址
DWORD callAddress = dllBaseAddress + callOffset;
// printf("发消息CALL地址:0x%08X\n", callAddress);
// 一段测试代码,参数是按从右到左的顺序压入的
/*__asm {
push 0x1;
mov edi, pWxmsg;
push edi;
lea edx, wxWxid;
push edx;
call testMessage;
add esp, 0xC;
}*/
// 4、编写调用函数的代码
/*
787D42EE 8D46 38 lea eax, dword ptr [esi+38] ; 取at结构体
787D42F1 6A 01 push 1 ; 0x1
787D42F3 50 push eax ; 群消息at好友,非at消息为0
787D42F4 57 push edi ; 消息内容,[edi]
787D42F5 8D95 7CFFFFFF lea edx, dword ptr [ebp-84] ; 接收人,[edx]
787D42FB 8D8D 58FCFFFF lea ecx, dword ptr [ebp-3A8] ; 缓冲区,据说是类本身
787D4301 E8 7A793300 call 78B0BC80 ; 发送消息CALL
787D4306 83C4 0C add esp, 0C ; 平衡堆栈
*/
__asm {
lea eax, wxNull;
// 参数5:1
push 0x1;
// 参数4:空结构
push eax;
// 参数3:发送的消息,传递消息内容的地址
mov edi, pWxmsg;
push edi;
// 参数2:接收人,传递结构体地址,要特别注意lea和mov的区别
lea edx, wxWxid;
// 参数1:空buffer
lea ecx, buffer;
// 调用函数
call callAddress;
// 堆栈平衡,否则会崩溃
add esp, 0xC;
}
printf("over\n");
}
dllmain.cpp
// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "pch.h"
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
{
// DLL注入后,会执行到此处
// CreateConsole();
wchar_t* wsWxId = (WCHAR*)L"filehelper";
wchar_t* wsTextMsg = (WCHAR*)L"发送的消息";
SendWxMessage(wsWxId, wsTextMsg);
DWORD pfunc = (DWORD)SendWxMessageAPI;
printf("发送消息的函数地址:0x%08X\n",pfunc);
}
break;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
注入与外部调用
DLL注入的部分,网上也有很多教程,此处不做过多讲解,思路如下:
- OpenProcess开启远程进程
- VirtualAllocEx在进程内开辟内存空间
- WriteProcessMemory在指定内存写入DLL的绝对路径
- CreateRemoteThread创建一个远程线程,让目标进程调用LoadLibrary
- 适当的时候卸载DLL(参考2-4,先GetModuleHandle,再FreeLibrary)
关键说一下怎么在外部调用发送消息的接口,在编译的DLL中,导出了SendWxMessageAPI这个函数,其实也可以不导出,只要提前算好该函数相对DLL基地址的偏移即可,具体思路如下:
- DLL注入微信
- CreateRemoteThread调用GetModuleHandle获取DLL在微信的基地址
- 加上算好的偏移,得到SendWxMessageAPI的地址
- WriteProcessMemory将消息内容和接收人写入远程进程,得到两处地址
- 组装结构体,保存第四步的两处地址
- WriteProcessMemory将结构体写入远程进程,得到结构体地址
- CreateRemoteThread调用SendWxMessageAPI,参数是第6步得到的结构体地址
- SendWxMessageAPI解引用指针,再从目标地址获取消息内容地址和接收人地址
- SendWxMessageAPI调用SendWxMessage,完成消息发送
看起来比较复杂,可能有人会问为什么不直接调用SendWxMessage,我理解的是,CreateRemoteThread只能传递一个LPVOID类型的参数,所以要用结构体来保存所有的参数,但是结构体中不能有指针,否则把结构体写入远程进程的时候,只是写了一些冰冷的数字进去,访问的时候还会出现NULL指针错误。
注入部分代码
injert.h
#pragma once
#include <iostream>
#include "stdlib.h"
#include <tchar.h>
#include <Windows.h>
#include <stdio.h>
#include <windows.h>
#include <TlHelp32.h>
#include <atlconv.h>
#include <tchar.h>
#include <sys/stat.h>
using namespace std;
bool Inject(DWORD dwId, WCHAR* szPath);
bool isFileExists_stat(string& name);
string wstring2string(wstring wstr);
main.cpp
#include "injert.h"
// DLL中有一个同样的结构体
struct RemoteParam
{
DWORD wxid;
DWORD wxmsg;
};
// 参数1:申请的远程进程句柄,参数2:要调用的函数地址
void SendWxMessage(HANDLE hProcess, DWORD addrsend) {
DWORD dwId = 0;
DWORD dwWriteSize = 0;
RemoteParam RemoteData;
ZeroMemory(&RemoteData, sizeof(RemoteParam));
LPVOID wxidaddr = VirtualAllocEx(hProcess, NULL, 1, MEM_COMMIT, PAGE_READWRITE);
LPVOID wxmsgaddr = VirtualAllocEx(hProcess, NULL, 1, MEM_COMMIT, PAGE_READWRITE);
RemoteParam* paramAndFunc = (RemoteParam*)::VirtualAllocEx(hProcess, 0, sizeof(RemoteData), MEM_COMMIT, PAGE_READWRITE);
if (!wxidaddr || !wxmsgaddr || !paramAndFunc || !addrsend)
return;
DWORD dwTId = 0;
// wxid和wxmsg写入远程线程
WCHAR* wxid = (WCHAR*)L"filehelper";
if (wxidaddr)
WriteProcessMemory(hProcess, wxidaddr, wxid, wcslen(wxid) * 2 + 2, &dwWriteSize);
WCHAR* wxmsg = (WCHAR*)L"发送的消息";
if (wxmsgaddr)
WriteProcessMemory(hProcess, wxmsgaddr, wxmsg, wcslen(wxmsg) * 2 + 2, &dwWriteSize);
// 结构体存储wxid和wxmsg的地址
RemoteData.wxid = (DWORD)wxidaddr;
RemoteData.wxmsg = (DWORD)wxmsgaddr;
// 远程线程写入结构体
if (paramAndFunc != NULL)
printf("wxid地址:0x%08X,wxmsg地址:0x%08X\n", (DWORD)wxidaddr, (DWORD)wxmsgaddr);
if (paramAndFunc) {
if (!::WriteProcessMemory(hProcess, paramAndFunc, &RemoteData, sizeof(RemoteData), &dwTId))
{
printf("写入paramAndFunc失败!error:0x%08X\n", GetLastError());
}
else {
printf("写入paramAndFunc成功!要写入的size:%d,实际写入的size:%d\n", sizeof(RemoteData), dwTId);
}
}
else {
printf("申请内存空间paramAndFunc失败!error:0x%08X\n", GetLastError());
}
printf("写入的结构体首地址:0x%08X\n", (DWORD)paramAndFunc);
// 在此处打断点,然后用CE验证指针中的数据是否正确
// system("pause");
HANDLE hThread = ::CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)addrsend, (LPVOID)paramAndFunc, 0, &dwId);
if (hThread) {
WaitForSingleObject(hThread, INFINITE);
CloseHandle(hThread);
}
else {
printf("调用消息发送函数失败!\n");
}
// 释放内存,释放后可以再次查看CE
VirtualFreeEx(hProcess, wxidaddr, 0, MEM_RELEASE);
VirtualFreeEx(hProcess, wxmsgaddr, 0, MEM_RELEASE);
VirtualFreeEx(hProcess, paramAndFunc, 0, MEM_RELEASE);
}
bool Inject(DWORD dwId, WCHAR* szPath)//参数1:目标进程PID 参数2:DLL路径
{
//一、在目标进程中申请一个空间
/*
【1.1 获取目标进程句柄】
*/
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwId);
printf("目标窗口的句柄为:%d\n", (int)hProcess);
/*
【1.2 在目标进程的内存里开辟空间】
*/
LPVOID pRemoteAddress = VirtualAllocEx(hProcess,NULL,1,MEM_COMMIT,PAGE_READWRITE);
//二、 把dll的路径写入到目标进程的内存空间中
DWORD dwWriteSize = 0;
/*
【写一段数据到刚才给指定进程所开辟的内存空间里】
*/
if (pRemoteAddress)
{
WriteProcessMemory(hProcess, pRemoteAddress, szPath, wcslen(szPath) * 2 + 2, &dwWriteSize);
}
else {
printf("写入失败!\n");
return 1;
}
//三、 创建一个远程线程,让目标进程调用LoadLibrary
HANDLE hThread = CreateRemoteThread(hProcess,NULL,0,(LPTHREAD_START_ROUTINE)LoadLibrary,pRemoteAddress,NULL,NULL);
if (hThread) {
WaitForSingleObject(hThread, -1); //当句柄所指的线程有信号的时候,才会返回
}
else {
printf("调用失败!\n");
return 1;
}
CloseHandle(hThread);
WCHAR* dllname = (WCHAR*)L"DllSendMessage.dll";
WriteProcessMemory(hProcess, pRemoteAddress, dllname, wcslen(dllname) * 2 + 2, &dwWriteSize);
// 调用GetModuleHandleW
DWORD dwHandle, dwID;
LPVOID pFunc = GetModuleHandleW;
hThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)pFunc, pRemoteAddress, 0, &dwID);
if (hThread) {
WaitForSingleObject(hThread, INFINITE);
// 获取远程线程的返回值
GetExitCodeThread(hThread, &dwHandle);
}
else {
printf("GetModuleHandleW调用失败!\n");
return 1;
}
CloseHandle(hThread);
// 获取发送消息接口函数地址
HMODULE hd = LoadLibrary(szPath);
DWORD addrsend = 0;
// 计算对应函数的地址,已经算好偏移就不需要加载DLL进本进程了
if (hd) {
DWORD localsendaddr = (DWORD)GetProcAddress(hd, "SendWxMessageAPI");
printf("模块基址:0x%08X,函数地址:0x%08X,偏移:0x%08X\n", (DWORD)hd, localsendaddr, localsendaddr - (DWORD)hd);
addrsend = dwHandle + localsendaddr - (DWORD)hd;
printf("目标进程发送消息函数地址:0x%08X\n", addrsend);
// 当前进程卸载DLL
FreeLibrary(hd);
}
SendWxMessage(hProcess, addrsend);
// 四、 【释放申请的虚拟内存空间】
VirtualFreeEx(hProcess, pRemoteAddress, 0, MEM_RELEASE);
// 释放console窗口,不然关闭console的同时微信也会退出
pFunc = FreeConsole;
hThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)pFunc, NULL, 0, &dwID);
if (hThread) {
WaitForSingleObject(hThread, INFINITE);
CloseHandle(hThread);
}
else {
printf("FreeConsole调用失败!\n");
return 1;
}
// 使目标进程调用FreeLibrary,卸载DLL
pFunc = FreeLibrary;
hThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)pFunc, (LPVOID)dwHandle, 0, &dwID);
if (hThread) {
WaitForSingleObject(hThread, INFINITE);
CloseHandle(hThread);
}
else {
printf("FreeLibrary调用失败!\n");
return 1;
}
CloseHandle(hProcess);
return 0;
}
bool isFileExists_stat(string& name) {
struct stat buffer;
return (stat(name.c_str(), &buffer) == 0);
}
string wstring2string(wstring wstr)
{
std::string result;
//获取缓冲区大小,并申请空间,缓冲区大小事按字节计算的
int len = WideCharToMultiByte(CP_ACP, 0, wstr.c_str(), wstr.size(), NULL, 0, NULL, NULL);
char* buffer = new char[len + 1];
//宽字节编码转换成多字节编码
WideCharToMultiByte(CP_ACP, 0, wstr.c_str(), wstr.size(), buffer, len, NULL, NULL);
buffer[len] = '\0';
//删除缓冲区并返回值
result.append(buffer);
delete[] buffer;
return result;
}
int _tmain(int nargv,WCHAR* argvs[])
{
wchar_t* wStr = (WCHAR*)L"";
if (nargv == 1) {
return 0;
}
else {
wStr = argvs[1];
}
string name = wstring2string((wstring)wStr);
DWORD dwId = 0;
if (!isFileExists_stat(name)) {
wstring info = L"注入失败!请检查DLL路径!";
MessageBox(NULL, info.c_str(), _T("警告"), MB_ICONWARNING);
return 0;
}
// 参数1:NULL
// 参数2:目标窗口的标题
// 返回值:目标窗口的句柄
HWND hCalc = FindWindow(NULL, L"微信");
printf("目标窗口的句柄为:%d\n", (int)hCalc);
DWORD dwPid = 0;
//参数1:目标进程的窗口句柄
//参数2:把目标进程的PID存放进去
DWORD dwRub = GetWindowThreadProcessId(hCalc, &dwPid);
printf("目标窗口的进程PID为:%d\n", dwPid);
//参数1:目标进程的PID
//参数2:想要注入DLL的路径
Inject(processID, wStr);
return 0;
}
写在后面
感觉越来越有判头了。