逆向技术学习

1
2
3
4
5
6
7
8

██╗ ██╗███████╗██╗ ██╗ ██████╗ ██╗ ██╗ ██████╗ ██████╗ ██╗ ██████╗
██║ ██║██╔════╝██║ ██║ ██╔═══██╗ ██║ ██║██╔═══██╗██╔══██╗██║ ██╔══██╗
███████║█████╗ ██║ ██║ ██║ ██║ ██║ █╗ ██║██║ ██║██████╔╝██║ ██║ ██║
██╔══██║██╔══╝ ██║ ██║ ██║ ██║ ██║███╗██║██║ ██║██╔══██╗██║ ██║ ██║
██║ ██║███████╗███████╗███████╗╚██████╔╝ ╚███╔███╔╝╚██████╔╝██║ ██║███████╗██████╔╝
╚═╝ ╚═╝╚══════╝╚══════╝╚══════╝ ╚═════╝ ╚══╝╚══╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝╚═════╝

背景

最近出于需求需要, 学习了一下微信逆向 wxhelper 的代码实现, wxhelper 暴露了大量的微信接口, 使得能够轻易基于此实现一个微信机器人 🤖️。因为是第一次接触逆向相关技术, 出于好奇就记录一下

实现原理

1
2
3
4
5
6
                                      |----------------
-------------------------- 注入 | WeChat.exe |
| ConsoleApplication.exe |————————> |---------------- -------------- 访问 ---------
| | | wxhelper.dll |————————>| 启动http服务 | <----------| clent |
|-------------------------- |----------------- -------------- --------

如上的流程图

  1. 首先需要通过一些手段获取到微信中需要被暴露的关键 Call, 作者把自己常用的方法记录在 wxhelper/wiki 文档, 感兴趣可以查阅
  2. 编译 wxhelper dll 程序注入到微信进程
  3. 该 dll 会创建一个端口为 19088 的 http server 向外提供接口来开放调用关键 Call 的能力

下面讲一下该 wxhelper dll 如何实现 hook 微信的关键 Call, 如下 dll 入口函数主要是调用了 GlobalContext::GetInstance().initialize 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// src/dllMain.cc

BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call,
LPVOID lpReserved) {
switch (ul_reason_for_call) {
case DLL_PROCESS_ATTACH: {
DisableThreadLibraryCalls(hModule);
GlobalContext::GetInstance().initialize(hModule);
break;
}
case DLL_THREAD_ATTACH: {
break;
}
case DLL_THREAD_DETACH: {
break;
}
case DLL_PROCESS_DETACH: {
GlobalContext::GetInstance().finally();
break;
}
}
return TRUE;
}

initialize 函数主要是启动了一个 http server

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// src/global_context.cc

void GlobalContext::initialize(HMODULE module) {
state =GlobalContextState::INITIALIZING;
module_ = module;
#ifndef _DEBUG
Utils::Hide(module);
#endif
UINT64 base = Utils::GetWeChatWinBase();
config.emplace();
config->Initialize();
log.emplace();
log->Initialize();
http_server = std::unique_ptr<HttpServer>( new HttpServer(config->GetPort()));
http_server->HttpStart();
ThreadPool::GetInstance().Create(2, 8);
mgr = std::unique_ptr<Manager>(new Manager(base));
DB::GetInstance().init(base);
state =GlobalContextState::INITIALIZED;
}

http server 的请求处理函数 HttpDispatch 即表示了向外提供的 hook 微信关键 Call 的实现。以 /api/hookSyncMsg 路径的请求为例, 如果客户端想通过调用该接口来监听微信收到消息的事件

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
56
57
58
59
60
61
62
63
64
// src/http_server_callback.cc

std::string HttpDispatch(struct mg_connection *c, struct mg_http_message *hm) {
std::string ret;
// ...
if (mg_http_match_uri(hm, "/api/checkLogin")) {
INT64 success = wxhelper::GlobalContext::GetInstance().mgr->CheckLogin();
nlohmann::json ret_data = {
{"code", success}, {"data", {}}, {"msg", "success"}};
ret = ret_data.dump();
return ret;
} else if (mg_http_match_uri(hm, "/api/userInfo")) {
common::SelfInfoInner self_info;
INT64 success =
wxhelper::GlobalContext::GetInstance().mgr->GetSelfInfo(self_info);
nlohmann::json ret_data = {
{"code", success}, {"data", {}}, {"msg", "success"}};
if (success) {
nlohmann::json j_info = {
{"name", self_info.name},
{"city", self_info.city},
{"province", self_info.province},
{"country", self_info.country},
{"account", self_info.account},
{"wxid", self_info.wxid},
{"mobile", self_info.mobile},
{"headImage", self_info.head_img},
{"signature", self_info.signature},
{"dataSavePath", self_info.data_save_path},
{"currentDataPath", self_info.current_data_path},
{"dbKey", self_info.db_key},
};
ret_data["data"] = j_info;
}
ret = ret_data.dump();
return ret;
} else if (mg_http_match_uri(hm, "/api/sendTextMsg")) {
std::wstring wxid = GetWStringParam(j_param, "wxid");
std::wstring msg = GetWStringParam(j_param, "msg");
INT64 success =
wxhelper::GlobalContext::GetInstance().mgr->SendTextMsg(wxid, msg);
nlohmann::json ret_data = {
{"code", success}, {"data", {}}, {"msg", "success"}};
ret = ret_data.dump();
return ret;
} else if (mg_http_match_uri(hm, "/api/hookSyncMsg")) {
int port = GetIntParam(j_param, "port");
std::string ip = GetStringParam(j_param, "ip");
int enable = GetIntParam(j_param, "enableHttp");
std::string url = "";
int timeout = 0;
if (enable) {
url = GetStringParam(j_param, "url");
timeout = GetIntParam(j_param, "timeout");
}
INT64 success =
wxhelper::hooks::HookSyncMsg(ip, port, url, timeout, enable);
nlohmann::json ret_data = {
{"code", success}, {"data", {}}, {"msg", "success"}};
ret = ret_data.dump();
return ret;
}
// ...
}

收到该请求后核心是调用了 wxhelper::hooks::HookSyncMsg 函数。可以发现 HookSyncMsg 函数中出现了关键的DetourAttach 系统调用

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
// src/hooks.cc

int HookSyncMsg(std::string client_ip, int port, std::string url,
uint64_t timeout, bool enable) {
if (kMsgHookFlag) {
SPDLOG_INFO("recv msg hook already called");
return 2;
}
kEnableHttp = enable;
if (kEnableHttp) {
HttpClient::GetInstance().SetConfig(url, timeout);
}
if (client_ip.size() < 1) {
return -2;
}

kServerPort = port;
strcpy_s(kServerIp, client_ip.c_str());
UINT64 base = Utils::GetWeChatWinBase();
if (!base) {
SPDLOG_INFO("base addr is null");
return -1;
}

DetourRestoreAfterWith();
DetourTransactionBegin();
DetourUpdateThread(GetCurrentThread());
UINT64 do_add_msg_addr = base + offset::kDoAddMsg;
DetourAttach(&(PVOID&)R_DoAddMsg, &HandleSyncMsg);
LONG ret = DetourTransactionCommit();
if(ret == NO_ERROR){
kMsgHookFlag = true;
}
return ret;
}

DetourAttach 是 Windows 平台上的一个函数,它是微软研究院开发的 Detours 库中的一部分。Detours 库提供了一种用于修改和重定向函数调用的技术,常用于实现函数的钩子和函数的动态修改。
DetourAttach 函数的作用是将一个目标函数的地址与一个自定义的回调函数关联起来,从而在目标函数被调用时执行回调函数。具体来说,DetourAttach 函数会修改目标函数的代码,使其在执行前调用指定的回调函数。DetourAttach 函数的原型如下:

1
LONG DetourAttach(PVOID *ppPointer, PVOID pDetour);

现在总算明白了, 原来是通过 DetourAttach 系统调用把微信收到消息的处理函数 R_DoAddMsg 给代理了, 也就是把 R_DoAddMsg 调用重定向为了 HandleSyncMsg, 而 HandleSyncMsg 函数里就可以写你的业务逻辑, 最后 HandleSyncMsg 的尾部调用一下微信的原始处理函数 R_DoAddMsg 即可

用 JavaScript 解释 DetourAttach 的行为就是

1
2
3
4
5
6
const rawHandler = wx.R_DoAddMsg
const newHandler = (...args) => {
// do something
rawHandler()
}
wx.R_DoAddMsg = newHandler

最后的一个关键点就是如何获取微信原本的处理函数 R_DoAddMsg 的地址, 可以看到 R_DoAddMsg 地址其实就是该 dll 的地址 GetWeChatWinBase() + 之前通过逆向手段获取到的该函数的偏移量 offset::kDoAddMsg

1
2
3
4
// src/hooks.cc

static UINT64 (*R_DoAddMsg)(UINT64, UINT64, UINT64) = (UINT64(*)(
UINT64, UINT64, UINT64))(Utils::GetWeChatWinBase() + offset::kDoAddMsg);

获取该 dll 的地址核心是 GetModuleHandleA 系统调用

1
2
3
4
5
// src/utils.cc

UINT64 Utils::GetWeChatWinBase() {
return (UINT64)GetModuleHandleA("WeChatWin.dll");
}

GetModuleHandleA 是 Windows 平台上的一个函数,用于获取指定模块的句柄(handle)。获取到的模块句柄可以用于后续的操作,例如获取模块中的函数地址、修改模块中的数据等。函数原型如下:

1
HMODULE GetModuleHandleA(LPCSTR lpModuleName);

至于如何获取关键 Call 的偏移量见上面提到的 wxhelper/wiki 文档