PC微信逆向之发送消息

PC微信逆向之发送消息

    • 写在前面
    • 工具
    • 定位CALL地址
    • 调用
    • 生成DLL
    • 注入与外部调用
    • 注入部分代码
    • 写在后面

写在前面

最近在搞微信的发送消息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 
#include 
#include 
#include 

#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注入的部分,网上也有很多教程,此处不做过多讲解,思路如下:

  1. OpenProcess开启远程进程
  2. VirtualAllocEx在进程内开辟内存空间
  3. WriteProcessMemory在指定内存写入DLL的绝对路径
  4. CreateRemoteThread创建一个远程线程,让目标进程调用LoadLibrary
  5. 适当的时候卸载DLL(参考2-4,先GetModuleHandle,再FreeLibrary)

关键说一下怎么在外部调用发送消息的接口,在编译的DLL中,导出了SendWxMessageAPI这个函数,其实也可以不导出,只要提前算好该函数相对DLL基地址的偏移即可,具体思路如下:

  1. DLL注入微信
  2. CreateRemoteThread调用GetModuleHandle获取DLL在微信的基地址
  3. 加上算好的偏移,得到SendWxMessageAPI的地址
  4. WriteProcessMemory将消息内容和接收人写入远程进程,得到两处地址
  5. 组装结构体,保存第四步的两处地址
  6. WriteProcessMemory将结构体写入远程进程,得到结构体地址
  7. CreateRemoteThread调用SendWxMessageAPI,参数是第6步得到的结构体地址
  8. SendWxMessageAPI解引用指针,再从目标地址获取消息内容地址和接收人地址
  9. SendWxMessageAPI调用SendWxMessage,完成消息发送

看起来比较复杂,可能有人会问为什么不直接调用SendWxMessage,我理解的是,CreateRemoteThread只能传递一个LPVOID类型的参数,所以要用结构体来保存所有的参数,但是结构体中不能有指针,否则把结构体写入远程进程的时候,只是写了一些冰冷的数字进去,访问的时候还会出现NULL指针错误。

注入部分代码

injert.h

#pragma once
#include 
#include "stdlib.h"
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

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;
}

写在后面

感觉越来越有判头了。

你可能感兴趣的:(软件逆向,微信)