通过 Winsock Hook 监听英雄联盟客户端RTMP通信

英雄联盟和 RTMP 的关系

很多人看到这个标题可能会一脸懵逼,RTMP 不是用来搞流媒体传输的吗?赫赫,这你就不懂辣。

从 2016 年拳头发布的 LCU 客户端更新技术细节 中可以看到,在 2008 年 Adobe AIR 的易用性与表达能力远超当时的 HTML,得到了拳头的青睐。而 RTMP 也是 Adobe 主导开发的可靠传输协议,与前者共同处于 Adobe Flash 框架之下,理所当然的被拳头用于客户端数据传输。

时至 2023 年末,拳头和腾讯已经尽可能将 RTMP 协议上的服务迁移到 HTTP,但是仍然有部分通讯由 RTMP 实现(如登录、Teambuilder)。

前人项目经验

远在 LCU 客户端更新之前的 2015 年,在 GitHub 上就已经有了一个基于 C#的 RTMP 监听实现,名为 frostycpu/FinalesFunkeln

原理

它的主要原理大概如下:

  1. 程序会在127.0.0.1:2099启动一个 RTMP 服务器用于转发消息,并同时将消息显示在程序 GUI

  2. 在游戏客户端启动时,计算出WSAConnect函数的地址

    这个是可行的,因为ws2_32.dll的基地址在不同进程空间的地址不变,只需要提前获取函数的偏移即可计算出内存地址。

    因为函数的偏移基本也是固定的,所以可以说 WSAConnect在所有 32 位进程中的地址是一样的。

  3. 将已经设计好的字节码直接写入WSAConnect的地址,从而加入自己的逻辑。

    这里有一个冷门的知识点,32 位的 WindowsAPI 函数汇编第一句都是 mov edi,edi是有意为之的无意义指令。

    而 FinalesFunkeln 正是利用了这一点,实现了简易的热补丁。

    详见 Why do Windows functions all begin with a pointless MOV EDI, EDI instruction? - The Old New Thing

  4. 在写入的逻辑中会检查WSAConnect的第二个参数,也就是那个sockaddr*指针,如果连接的远程服务器是基于AF_INET的,并且端口是 2099(RTMP 服务默认端口),则将其服务器地址更改为127.0.0.1,这样我们就成功把消息劫持到了本地的 RTMP 服务

已不可用

由于年久失修,这个项目已经不可用状态,原因主要有:

  • SSL 验证机制的变化
  • 游戏客户端从 32 位升级为 64 位,硬编码的的 32 位 Hook 汇编代码失效

那么对应的思路也是有的:

  • 关闭客户端与 RTMP 服务器之间的 SSL 验证
  • 使用 C++重新实现关键函数的 Hook

缝缝补补

原来的 FinalesFunkeln 项目中,本地服务器的搭建和对客户端的 Hook 是在同一个项目中的,为了方便,这里我会把原项目中的 Hook 部分独立使用 C++重新编写。

原项目的修补

这部分是我一个朋友做的,所以暂时没有办法放出源码,我大概讲一下做什么

  1. 删除原有的 AIR 客户端验证逻辑,已经彻底没用了

  2. 将 RTMP 服务器地址硬编码,原先的逻辑不适用

    腾讯运营的区服中,RTMP 服务器地址一般是区名-cloud-feapp.lol.qq.com,如艾欧尼亚的是hn1-cloud-feapp.lol.qq.com

  3. 删除证书验证逻辑,因为我们打算直接关闭 SSL 验证

    相应的,需要在system.yaml中的server项下添加如下内容,以关闭 SSL 校验

    1
    2
    3
    4
    5
    server:
    # ....
    lcds:
    ssl: false
    # ....

Winsock Hook

我的逆向工程水平连入门都称不上,Intel 指令集手册让我翻冒烟了我都没做到用 64 位汇编重新实现 Hook 功能,所以我决定从 C++层面实现。这里使用的是我从 GitHub 上找到的一个极简的Hook 库 ,非常好用👍。

我将使用传统派的思路,编写一个 Payload DLL,随后使用远程线程注入。

Payload

这里我引入了上面提到的 Hook 库,代码很简单,甚至没检查协议,只需要检查端口是不是 2099 就行了(实测在本端口只有这一处调用)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#include "WinSock2.h"
#include "WS2tcpip.h"
#include "Hook.h"
#include "Windows.h"
#pragma comment(lib, "ws2_32.lib")

typedef int (WSAAPI* LPWSACONNECT)(
SOCKET s,
const sockaddr* name,
int namelen,
LPWSABUF lpCallerData,
LPWSABUF lpCalleeData,
LPQOS lpSQOS,
LPQOS lpGQOS
);

divert div_hook;
LPWSACONNECT fpOriginal;

static int HookedWSAConnect(
SOCKET s,
const sockaddr* name,
int namelen,
LPWSABUF lpCallerData,
LPWSABUF lpCalleeData,
LPQOS lpSQOS,
LPQOS lpGQOS
)
{

sockaddr_in* data = (sockaddr_in*)(name);
if (data->sin_port == htons(2099)) {
data->sin_addr.s_addr = inet_addr("127.0.0.1");
}

div_hook.unhook();
int ret = fpOriginal(s, name, namelen, lpCallerData, lpCalleeData, lpSQOS, lpGQOS);
div_hook.hook(fpOriginal, &HookedWSAConnect);

return ret;

}

BOOL APIENTRY DllMain(HMODULE hModule, DWORD dwReason, LPVOID) {
if (dwReason == DLL_PROCESS_ATTACH) {

fpOriginal = reinterpret_cast<LPWSACONNECT>(helper::get_module_export("ws2_32.dll", "WSAConnect"));

if (fpOriginal) {
div_hook.hook(fpOriginal, &HookedWSAConnect);
MessageBoxA(NULL, "WSAConnect Hooked", "Success", MB_OK);
}
}
return TRUE;
}

Injector

需要注意的是,国服的LeagueClient.exe尽管是带 ACE 的,但是在最初启动的前两秒左右是未被保护的,我们可以趁这个时候将 Payload 载入到目标进程里。

这里从 KooroshRZ/Windows-DLL-Injector 借用一下提权的逻辑,使用AdjustTokenPrivileges将进程提升到SeDebugPrivilege级别,以防权限不够。

远程线程注入方法是十分简单的,大概可以分为如下几步

  • 将 DLL 地址写入目标进程
  • 获取LoadLibraryA的地址(W 也行,看自己需要)
  • 调用CreateRemoteThread在目标进程执行LoadLibraryA从而加载我们准备好的 Payload

唯一需要注意的点就是,这个 Payload 的路径是相对目标进程的,而非相对注入器,所以我直接在下面写了绝对路径,省事一些。

主体代码也比较简单,不到 40 行

main.cc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <Windows.h>
#include <iostream>
#include <tlhelp32.h>
#include "Injector.h"

int main() {
const wchar_t* processName = L"LeagueClient.exe";
bool processFound = 0;
int32_t pid = 0;

int epResult = EscalatePrivilege();
if (epResult == 0)
printf("Successfully Escalated privileges to SYSTEM level...\n");

while (!processFound) {
pid = GetProcessIdByName(processName);
if (pid == 0) {
Sleep(100); // Sleep for 100ms
continue;
}
processFound = 1;
printf("Found LeagueClient.exe PID: %d\n", pid);
// This path should be relative to `LeagueClient.exe` or use absolute path directly
InjectPayload("D:\\myMsg.dll", OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid));
}

return 0;
}

运行实测

基于如上的改动,我们基于如下顺序操作启动游戏

  1. 首先启动 FinalesFunkeln 和 注入器

  2. 修改客户端的system.yaml以关闭 SSL 校验

  3. 运行游戏

    WeGame 校验不通过的话,直接用 TCLS 登录,或者其他你想得到的办法

效果图如下:

ss

Author

BakaFT

Posted on

2023-12-28

Updated on

2023-12-28

Licensed under

Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×