PC微信逆向之发送消息


写在前面

最近在搞微信的发送消息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注入的部分,网上也有很多教程,此处不做过多讲解,思路如下:

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

写在后面

感觉越来越有判头了。