# PC微信逆向：发送与接收消息的分析与代码实现

**URL:** <https://forum.90sec.com/t/topic/265>\
**Category:** 信安之路\
**Created:** [2019 年8 月 3 日 09:47 UTC](https://forum.90sec.com/t/topic/265 "2019-08-03T09:47:41Z")\
**Posts on this page:** 1\
**Page:** 1

<div class="post-metadata">

作者： ![xazlsec](https://forum.90sec.com/user_avatar/forum.90sec.com/xazlsec/32/659_2.png) [@xazlsec](https://forum.90sec.com/u/xazlsec)\
发布日期： [2019 年8 月 3 日 09:47 UTC](https://forum.90sec.com/t/topic/265/1 "2019-08-03T09:47:41Z")

</div>

> 本文作者： **鬼手56** （信安之路病毒分析小组成员）
> 
> 成员招募：[信安之路病毒分析小组寻找志同道合的朋友](http://mp.weixin.qq.com/s?__biz=MzI5MDQ2NjExOQ==&mid=2247488359&idx=1&sn=98507e4fc7d4b15476c286e4aa9b8465&chksm=ec1e234fdb69aa590793a34d7bd7c2c77de3fe0c9d2051d27a5ba74328b3a0a880036bc06644&scene=21#wechat_redirect)

我们先来定位一下消息接收函数，这对我们后面分析消息发送函数会有所帮助

**定位消息接收函数的相关思路**

与接收消息函数最直接相关的东西肯定是消息本身，所以消息本身的内容就是我们的切入点。我们可以首先找到存放消息内容的地址，然后对地址下断，通过栈回溯最终定位到接收消息的函数

**定位消息内容的地址**

首先用另外一个微信给自己发一条消息，在不点开消息的状态下用 CE 搜索消息内容

 ![image](https://forum.90sec.com/uploads/default/original/2X/4/4fddad068ba837e2f6b839acd93ca3694d7ecf6c.jpeg)

然后再发送一条消息

 ![image](https://forum.90sec.com/uploads/default/original/2X/c/c72007d5a27374f784fd4b17d3e7e3f40c2f992e.jpeg)

此时有效结果只剩下 3 个，把这三个地址加入到下方地址栏，右键-\>更改记录-\>类型

 ![image](https://forum.90sec.com/uploads/default/original/2X/f/fb78da6324bd6145fbb9110b8e247bc6b272798f.png)

将显示范围调大

 ![image](https://forum.90sec.com/uploads/default/original/2X/7/78943c7b7f6e21df928ab452aa640874c1df92c8.png)

其中有一个是最原始的未经处理的消息，也是显示的最全的那一条，剩下的两条是经过处理的。我们需要中间那个未经任何处理的消息

**定位接收消息函数的地址**

既然消息内容的地址找到了，那么接下来就通过这个内容来找到接收消息的函数

 ![image](https://forum.90sec.com/uploads/default/original/2X/0/07070850a6e09cad4f9abefa93d9a531b4ef7f5f.jpeg)

在 OD 中找到这个地址，下内存写入断点。为什么是写入不是访问？因为这个是最原始的的消息，要想对这条消息进行处理就必须改写当前的这条消息，所以在这个位置下内存写入断点，当客户端对这条原始消息进行处理时，断点就会断下。

此时，再次发送一条消息，程序断下，删除内存写入断点，这个时候堆栈的返回地址里面一定有一个函数是用来接收消息的。我们点击 K 查看调用堆栈。

 ![image](https://forum.90sec.com/uploads/default/original/2X/6/6a38216db348e8dfa74f0236619c178774d7f25e.jpeg)

经过排查确认是这个 call，大家可以根据我图中的函数特征直接找到这个 call。我们在这个函数下断点，让程序再次断下，分析附近的代码。

### 分析接收消息函数

#### 好友消息

 ![image](https://forum.90sec.com/uploads/default/original/2X/e/e4176af8e0c66e3cac1c9726df7ce58a1544a76e.jpeg)

此时我们点击查看堆栈中 esp 寄存器的值，数据窗口跟随

 ![image](https://forum.90sec.com/uploads/default/original/2X/c/c17fdbb5eced264e5f67b3bca72dcf2da6fd6263.jpeg)

此时 `[esp+0x40]` 的位置是发送者的微信 ID， `[esp+0x68]` 的位置是消息内容( **通过这个 call 我们还可以拿到文件助手的 ID 是 filehelper，这对后面分析消息发送会有用，大家可以去试验一下** )

 ![image](https://forum.90sec.com/uploads/default/original/2X/a/abc579feede83a2eeeae656de2321895deb42a08.jpeg)

`[esp+0x114]` 的位置是 0， `[esp+0x128]` 的位置是一串未知数据。

 ![image](https://forum.90sec.com/uploads/default/original/2X/2/2f2d00966c29bf70ddc910d59b9914ce410ac576.jpeg)

#### 群消息

然后我们再发送一条群消息，看看有什么区别

 ![image](https://forum.90sec.com/uploads/default/original/2X/0/009309479a89b0077d75eea59d4affdbfba8ff27.jpeg)

此时 `[esp+0x40]` 的位置是群 ID， `[esp+0x68]` 的位置是消息内容

 ![image](https://forum.90sec.com/uploads/default/original/2X/e/ebb38c7b9120c2a702db25940ca8dea39578c4ef.jpeg)

`[esp+0x114]` 的地址不再是零，而是消息发送者的 ID， `[esp+0x128]` 的位置依旧是一串未知数据。大家可以用同样的方式分析处图片和表情在内存中的表现形式。

那么我们只要记录下这个 call 的地址+偏移，然后写一个 dll 注入到微信进程空间中，HOOK 这个函数，就能拦截所有的消息，并显示到我们的程序中。

### 总结

总结一下思路，寻找切入点-\>找地址-\>下断-\>栈回溯分析。就是这么简单粗暴

#### 代码实现

注入之后接收消息的代码如下：

```auto
void RecieveMsg()
{
  wstring receivedMessage = L"";
  BOOL isFriendMsg = FALSE;
  //[[esp]]
  //信息块位置
  DWORD** msgAddress = (DWORD * *)r_esp;

  //消息类型[[esp]]+0x30
  //[01文字] [03图片] [31转账XML信息] [22语音消息] [02B视频信息]
  //感谢：.順唭_自嘫ɑ、unravel提供类型消息。
  DWORD msgType = *((DWORD*)(**msgAddress + 0x30));
  receivedMessage.append(L"消息类型:");
  switch (msgType)
  {
  case 0x01:
    receivedMessage.append(L"文字 ");
    break;
  case 0x03:
    receivedMessage.append(L"图片 ");
    break;

  case 0x22:
    receivedMessage.append(L"语音 ");
    break;
  case 0x25:
    receivedMessage.append(L"好友确认 ");
    break;
  case 0x28:
    receivedMessage.append(L"POSSIBLEFRIEND_MSG ");
    break;
  case 0x2A:
    receivedMessage.append(L"名片 ");
    break;
  case 0x2B:
    receivedMessage.append(L"视频 ");
    break;
  case 0x2F:
    //石头剪刀布
    receivedMessage.append(L"表情 ");
    break;
  case 0x30:
    receivedMessage.append(L"位置 ");
    break;
  case 0x31:
    //共享实时位置
    //文件
    //转账
    //链接
    receivedMessage.append(L"共享实时位置、文件、转账、链接 ");
    break;
  case 0x32:
    receivedMessage.append(L"VOIPMSG ");
    break;
  case 0x33:
    receivedMessage.append(L"微信初始化 ");
    break;
  case 0x34:
    receivedMessage.append(L"VOIPNOTIFY ");
    break;
  case 0x35:
    receivedMessage.append(L"VOIPINVITE ");
    break;
  case 0x3E:
    receivedMessage.append(L"小视频 ");
    break;
  case 0x270F:
    receivedMessage.append(L"SYSNOTICE ");
    break;
  case 0x2710:
    //系统消息
    //红包
    receivedMessage.append(L"红包、系统消息 ");
    break;
  case 0x2712:
    receivedMessage.append(L"撤回消息 ");
    break;
  default:
    break;
  }
  receivedMessage.append(L"\r\n");

  //dc [[[esp]] + 0x114]
  //判断是群消息还是好友消息
  //相关信息
  wstring msgSource2 = L"<msgsource />\n";
  wstring msgSource = L"";
  msgSource.append(GetMsgByAddress(**msgAddress + 0x168));

  if (msgSource.length() <= msgSource2.length())
  {
    receivedMessage.append(L"收到好友消息:\r\n");
    isFriendMsg = TRUE;
  }
  else
  {
    receivedMessage.append(L"收到群消息:\r\n");
    isFriendMsg = FALSE;
  }

  //好友消息
  if (isFriendMsg == TRUE)
  {
    receivedMessage.append(L"好友wxid：\r\n")
      .append(GetMsgByAddress(**msgAddress + 0x40))
      .append(L"\r\n\r\n");
  }
  else
  {
    receivedMessage.append(L"群号：\r\n")
      .append(GetMsgByAddress(**msgAddress + 0x40))
      .append(L"\r\n\r\n");

    receivedMessage.append(L"消息发送者：\r\n")
      .append(GetMsgByAddress(**msgAddress + 0x114))
      .append(L"\r\n\r\n");

    receivedMessage.append(L"相关信息：\r\n");
    receivedMessage += msgSource;
    receivedMessage.append(L"\r\n\r\n");
  }

  receivedMessage.append(L"消息内容：\r\n")
    .append(GetMsgByAddress(**msgAddress + 0x68))
    .append(L"\r\n\r\n");

  //文本框输出信息
  USES_CONVERSION;
  SetWindowText(GetDlgItem(hWinDlg, IDC_MSG), W2A(receivedMessage.c_str()));
}

```

定位微信的消息发送函数

**定位消息发送函数的相关思路**

首先思考一下消息发送函数背后的编程逻辑，一个发消息的函数，至少需要三个参数。第一个是发送给谁，第二个是发送的内容，第三个是消息的类型。所以我们可以从参数入手，然后通过栈回溯的方式找到发送消息的 call。

至于突破口我们可以从发送的消息内容和消息的接收者的微信 ID 入手，比如文件传输助手的微信 ID 是 filehelper，这个可以在接收消息的 call 中拿到。以这个微信 ID 为突破口会比从文本来追溯方便。

在拿到接收者的微信 ID 之后，对这个地址下内存访问断点，然后通过栈回溯的方式就能找到发送消息的 call

#### 过滤当前聊天窗口的微信 ID

首先将当前聊天窗口设置为文件传输助手，搜索 filehelper

 ![image](https://forum.90sec.com/uploads/default/original/2X/4/43612b313a32272e8bd2c5c4877b741f74a89b34.jpeg)

除了文件传输助手，我们还知道个人的微信ID都是以 `wxid_` 开头的，所以将窗口切换到微信好友，搜索 `wxid_`

 ![image](https://forum.90sec.com/uploads/default/original/2X/f/fe2e2e7ba9e055dfc82c7939afb32bd9a9702cd1.jpeg)

接着我们选中所有地址，加入到下方地址栏

 ![image](https://forum.90sec.com/uploads/default/original/2X/f/fbc7802b51ff5d8cb8a25ad49318eba978de2567.jpeg)

然后选择全部地址右键-\>更改记录-\>类型

 ![image](https://forum.90sec.com/uploads/default/original/2X/c/c40c52d53c191291585d09324c4f803dc0f34a32.png)

将长度修改为 50 以显示更多的内容

 ![image](https://forum.90sec.com/uploads/default/original/2X/5/59734b5ad020fc7d3f68ee3c59eefdee226cb599.png)

此时你会看到微信好友的 ID，记录下这个 ID，待会有用

 ![image](https://forum.90sec.com/uploads/default/original/2X/4/46965406a24542f904a3e175690e020ff8571e05.jpeg)

再将窗口切回文件助手，下方地址栏的 ID 会发生变化，将数值不是 filehelper 的全部剔除掉。剩下的地址中的某一个是当前窗口的微信 ID，它会随着你当前微信窗口 ID 进行变换。

#### 定位当前聊天窗口的 ID

这个当前聊天窗口的 ID 到底有什么作用呢？我们来测试一下

 ![image](https://forum.90sec.com/uploads/default/original/2X/0/0e257e4efe334a7a957996030adbd9f25a1735cc.jpeg)

选中所有地址，右键-\>更改记录-\>数值，将当前聊天窗口的 ID 改为 filehelper，然后在当前好友的聊天窗口发送一条消息，你会发现此时消息发到了文件传输助手

**当前聊天窗口的 ID 是谁 谁就会接收到这条消息** ，利用这个特性我们来找出那个唯一的当前窗口 ID

 ![image](https://forum.90sec.com/uploads/default/original/2X/8/8bec1d4f714f666b484a66dcdc5806bbe219c732.png)

选中一半地址，将其更改为 filehelper，然后在当前窗口发送消息。如果消息发给了 filehelper，那么选中的地址里面就有真正的当前聊天窗口的 ID。重复这个步骤，可以找到真正的当前窗口 ID

#### 定位发送消息的函数

 ![image](https://forum.90sec.com/uploads/default/original/2X/4/477c6807649310f6de397d17a1fe33bd2a15aee7.jpeg)

接着载入 OD，在找到的当前窗口 ID 的地址中下一个内存访问断点。为什么是内存访问断点而不是内存写入呢？因为当前微信窗口的 ID 肯定会被发送消息的当作参数传入到堆栈中，所以必定会访问这个 ID，而不是写入 ID。

给好友发送一条消息，点击发送，内存访问断点断下。

 ![image](https://forum.90sec.com/uploads/default/original/2X/c/c6aec1904325c2e1a94194b2ae53d2fa70bd4d1c.jpeg)

此时 eax 指向当前窗口 ID，接着删除内存访问断点。点击 K 查看调用堆栈，在堆栈的返回地址中逐个排查每一个函数，这个函数必须有两个以上的参数，其中一个参数是消息内容，另外一个参数是消息 ID

 ![image](https://forum.90sec.com/uploads/default/original/2X/5/52ba37ee705f797b9a602b3df8ed8e267f0490ec.jpeg)

经过排查，可以在调用堆栈的第二层找到一个疑似发消息的 call。在这个地方下断点，让程序断下，分析附近代码

#### 分析发送消息的函数

##### 普通消息

 ![image](https://forum.90sec.com/uploads/default/original/2X/c/c1c8699ed3a150e0414ad6feeb6f9717da4510df.jpeg)

此时 edx 指向微信 ID， `[edx+4]` 保存的是微信 ID 的长度

 ![image](https://forum.90sec.com/uploads/default/original/2X/7/712307f3ba57925cd34e1acb7a6dcaa82303827d.jpeg)

ebx 指向消息内容， `[ebx+4]` 保存的是消息内容的长度。那么这个很有可能就是我们要找的发送消息的 call。

找到了发送消息的函数，那么怎么验证呢？利用微信 ID。将 edx 指向的微信 ID 的地址和我们之前在 CE 中找到的当前窗口的微信 ID 对比，你会发现两个地址是一样的。

 ![image](https://forum.90sec.com/uploads/default/original/2X/6/660c188f58043eb793d0bb9d29e6623c4a04b158.jpeg)

改变这个地址的微信 ID 和内容，就能直接改变消息的接收者和内容，这个刚才我们已经实验过了。再结合这个函数传入的参数有当前消息的内容，就可以确定这个 call 就是微信发送消息的函数。

##### 艾特某人消息

除了以普通文本的方式发送消息以外，还可以以艾特某人的方式发送消息。那么当发送的消息是艾特某人的时候，这个函数和发送普通文本消息有什么区别呢？区别就在于 eax 寄存器的值

先发送一条普通消息程序断下

 ![image](https://forum.90sec.com/uploads/default/original/2X/d/d789eb5a0be112f2cb4f51bf82ed5e6e56fb53c1.jpeg)

此时 eax 的值为 0，然后再发送一条艾特某人的消息

 ![image](https://forum.90sec.com/uploads/default/original/2X/1/11219731f71531ff10dd3adfab78095255e7e743.jpeg)

此时 eax 是有值的，数据窗口跟随，看看这个 `14704C40` 的地址保存的是什么内容

 ![image](https://forum.90sec.com/uploads/default/original/2X/b/bb22eddc12ba662292fa3ae2075194587d8efc0c.jpeg)

里面的被艾特的人的微信 ID， **普通消息与艾特消息的区别就在于 eax 是否保存了被艾特人的微信 ID** 。大家可以用同样的方式分析处图片和表情在内存中的表现形式。

接下来我们只要记录下当前发送消息函数的地址+偏移，就能写一个 dll 注入到微信进程空间中，直接调用发送消息的函数，就能实现用自己写的程序给任何人发送消息。

#### 总结

总结一下思路，寻找切入点-\>找地址-\>下断-\>栈回溯分析。跟接收消息的步骤是一致的。 **找call的关键在于你能不能找到一个好的切入点，并且利用切入点与call之间的关系。**

#### 代码实现

调用发送消息的函数代码如下：

```auto
void SendTextMessage(wchar_t* wxid, wchar_t* msg)
{
  //拿到发送消息的call的地址
  DWORD dwSendCallAddr = GetWeChatWinAddr() + 0x2EB4E0;

  //微信ID/群ID
  wxMsg id = {0};
  id.pMsg = wxid;
  id.msgLen = wcslen(wxid);
  id.buffLen = wcslen(wxid)*2;

  //消息内容
  wxMsg text = { 0 };
  text.pMsg = msg;
  text.msgLen = wcslen(msg);
  text.buffLen = wcslen(msg)*2;

  //取出微信ID和消息的地址
  char* pWxid = (char*)&id.pMsg;
  char* pWxmsg = (char*)&text.pMsg;

  char buff[0x81C] = { 0 };
  //调用微信发送消息call
  __asm {
    mov edx, pWxid;
    push 1;
    mov eax, 0;
    push eax;
    mov ebx, pWxmsg;
    push ebx;
    lea ecx, buff;
    call dwSendCallAddr;
    add esp, 0xC;
  }
}

```

最终效果

发送消息

 ![image](https://forum.90sec.com/uploads/default/original/2X/5/5a6d60e16759c909b33c9a9393ef4878c739d5c0.jpeg)

接收消息

 ![image](https://forum.90sec.com/uploads/default/original/2X/b/b813533c25870fd8e6a835ed51a352eeff2b935e.jpeg)

成品就暂时不发了，下回补上，有个 bug 调了好久没调出来，拜拜~
