Engintime系列逆向分析(3)--工具开发

坚决抵制盗版行为,本文仅作 OSX 软件逆向分析及 OSX 软件安全相关的研究目的。


前言

前面已经对这个软件的验证进行了比较清晰的分析,
接下来就该写出一个完整工具了。

思路呢?

首先总不能把fiddler作为工具的一部分吧。

那么需要模拟收发包才行,
在开始分析时就提过了原程序使用的cpprestsdk,故 cpprestsdk将成为突破口。

方案一:
cpprestsdk内部使用的是 WinHttpxxx 函数收发包,
由于 cpprestsdk 利用的是异步收发,
在尝试了 HOOK WinHttpxxx 系列函数直接返回后,
总会出现莫名其妙的崩溃,
就算尝试交付其完成函数也是一个结果。
为了测试这种方案,耗费了数个小时,有兴趣的可以试试。
里面涉及多线程及锁重入问题,就不再细说了。

方案二:
既然从底层劫持收发包不行,那能不能从库本身入手呢?
答案是可以的。

下面是 cpprestsdk 在编写客户端时常用的写法,

1
2
3
4
5
http_client client(U("http://www.bing.com/"));
// Build request URI and start the request.
uri_builder builder(U("/search"));
builder.append_query(U("q"), U("cpprestsdk github"));
return client.request(methods::GET, builder.to_string());

如果我们能替换掉 “http://www.bing.com/" 为指向本地的通信,不就能替换掉 Response 包了么?

![[Engintime_protocol_analysis_3/image-20221010175446393.png]]

cpprestsdk的导出函数下断可以轻松定位到相关代码。

首先尝试修改了 uri 给定的参数,
但发现程序会出现失败提示,
原因是,在调用uri时,传入的字符串只是一个副本,
修改副本并不能改变本体,在其他地方可能不会使用这个副本,所以通信会失败。
通过继续分析,发现真正的本体会经过 mf140u.#1663,就是上图画红线那个函数调用。

于是继续尝试编写代码 HOOK 之并替换。
需要注意的是,替换的串最好不要长于原始串,
由于没有分析原始串内存是位于哪里,溢出可能会崩溃。

注入进程

这里使用常见的映像劫持,劫持掉 winhttp.dll
关于劫持怎么写,
在 [巧用DLL劫持做黑盒分析] 一文中已详细描述。

简单改下入口就行了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
BOOL APIENTRY DllMain(HMODULE hModule, DWORD dwReason, PVOID pvReserved)
{
if (dwReason == DLL_PROCESS_ATTACH)
{
DisableThreadLibraryCalls(hModule);

if (Load() && Init())
{
// 功能入口
CloseHandle(CreateThread(NULL, NULL, Do, NULL, NULL, NULL));
}
}
else if (dwReason == DLL_PROCESS_DETACH)
{
Free();
}
return TRUE;
}

HOOK与转向

这段代码使用了 Detour 框架来 hook

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
static PVOID cpprest_uri = 0;
static const wchar_t* newUri = L"http://127.0.0.1:13303/";

static wchar_t** sstr = NULL;
static bool bfix = false;
inline void FixUrl() {
if(!wcscmp(*sstr, L"https://www.codecode.net/")) {
memcpy(*sstr, newUri, (wcslen(newUri) + 1) * 2);
bfix = true;
}
}

void __declspec(naked) FixFunc() {

__asm {
mov sstr,ecx
pushad
pushfd
}
if (!bfix) FixUrl();

__asm {
popfd
popad
jmp cpprest_uri
}
}

bool dropHook() {
DetourTransactionBegin();
DetourUpdateThread(GetCurrentThread());

if (cpprest_uri) DetourDetach((PVOID*)&cpprest_uri, FixFunc);

return DetourTransactionCommit() == NO_ERROR;
}

bool setHook() {
auto moduleBase = LoadLibraryA("mfc140u.dll");
if (!moduleBase)
return FALSE;
cpprest_uri = GetProcAddress(moduleBase, (char*)1663);
if (!cpprest_uri)
return FALSE;
// _CIP<IBindStatusCallback, &_GUID const IID_IBindStatusCallback>::operator IBindStatusCallback * (void)mfc140u


DetourTransactionBegin();
DetourUpdateThread(GetCurrentThread());

if (cpprest_uri) DetourAttach((PVOID*)&cpprest_uri, FixFunc);

return DetourTransactionCommit() == NO_ERROR;
}

DWORD WINAPI Do(LPVOID lpThreadParameter) {
setHook();
// 后面才用到
startServer();
return 0;
}

注意 FixUrl 的用处是防止 release 编译时给优化了,inline 在此并不会起作用,可有可无。
dropHook并未调用,有兴趣可以自己试试。

http simple server

我直接考虑使用第三方框架,而没有在以前写的 server 上改
开始时打算使用程序自带的 cpprestsdk,后面感觉还是用更具备通用性的框架比较好。

对于这个第三方的server库有几点要求

  1. 轻量级,易嵌入的,不需要过于复杂的功能,不需要考虑并发,异步
  2. 最好支持 route,便于编写
  3. 如果能支持https以及代理转发就更好了

翻来覆去看了几个框架,最终选择了基于mongoose的简易server

在这份代码的基础上进行简单修改,使其能符合使用需求。

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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
// startServer.cpp
static char szCurName[MAX_PATH];
static std::string jkey;
static const char* head =
"Content-Type: application/json; charset=utf-8\r\n"
"Connection: close\r\n"
"Cache-Control : no-store\r\n"
"Pragma : no-cache\r\n";

bool handle_token(std::string url, std::string body, mg_connection* c, OnRspCallback rsp_callback)
{
static const char* token =
"{\"access_token\": \"\"}";

mg_send_response_line(c, 200, head);
mg_printf(c, "%s", token);
c->flags |= MG_F_SEND_AND_CLOSE;

return true;
}

bool handle_key(std::string url, std::string body, mg_connection* c, OnRspCallback rsp_callback)
{
mg_send_response_line(c, 200, head);
mg_printf(c, "%s", jkey.c_str());
c->flags |= MG_F_SEND_AND_CLOSE;

return true;
}

void startServer()
{
GetModuleFileNameA(NULL, szCurName, MAX_PATH);
PathStripPathA(szCurName);

{ // 利用 RAII 回收相关资源
std::string defkey = "[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15]";

using nlohmann::json;
std::ifstream in("winhttp_3deskey.json");
if (in) // 有该文件
{
std::string sjson((std::istreambuf_iterator<char>(in)),
std::istreambuf_iterator<char>());
try {
auto js = json::parse(sjson)[szCurName];

// 不依赖库异常,自己拆开判断一下
if (!js.empty()) defkey = js.get<std::string>();
}
catch (json::exception&) {
// do nothing
}
}

jkey =
std::string("{"
"\"message\":\"0\","
"\"number\" : 0,"
"\"ban_count\":0,"
"\"email\":\"\","
"\"name\":\"\","
"\"username\":\"Welcome\","
"\"inforId\":\"\","
"\"inforContent\":\"Welcome!\","
"\"threedeskey\":") +
defkey +
std::string(","
"\"auth_state\":true"
"}");

}

auto http_server = std::shared_ptr<HttpServer>(new HttpServer);
http_server->Init("13303");
// add handler
http_server->AddHandler("/oauth/token", handle_token);
http_server->AddHandler("/api/v4/clientauthorizedcode", handle_key);
http_server->Start();
}

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
// http_server.cpp

void HttpServer::HandleHttpEvent(mg_connection* connection, http_message* http_req)
{
std::string req_str = std::string(http_req->message.p, http_req->message.len);

// 先过滤是否已注册的函数回调
std::string url = std::string(http_req->uri.p, http_req->uri.len);
std::string body = std::string(http_req->body.p, http_req->body.len);
auto it = s_handler_map.find(url);
if (it != s_handler_map.end())
{
ReqHandler handle_func = it->second;
handle_func(url, body, connection, &HttpServer::SendHttpRsp);
}
// 其他请求 正常应该是转发逻辑,这里就临时这样实现一下
else if (route_check(http_req, (char*)"/")) // index page
{
mg_send_response_line(connection, 200, "Content-Type: text/html\r\n"
"Connection: close\r\n");
mg_printf(connection, "%s", "<script>location.href = 'https://www.codecode.net';</script>");
connection->flags |= MG_F_SEND_AND_CLOSE;
}
else
{
SendHttpRsp(connection, "welcome to jsonapi");
}
}

编译生成

  • [Engintime.rar]

配置格式:

1
2
3
4
{
"//": " 进程名 : 3deskey 当不填key时会使用默认的key 0-15,在需要key的几款软件中会崩溃",
"CP Lab.exe" : "[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15]"
}

验证:
其中 CP,CS,DS等均能正常工作,但并未彻底测试。

由于使用了 SMC 所有授权的key都是一样的 不存在身份泄露风险
当然这个key只有在相应的版本(强制更新版本)下才有意义
另外这个key不能轻易更换,否则会导致大量用户需要升级,根据包内容看,并没有强制升级功能
如果你愿意分享key来供研究学习测试
欢迎在评论区留言 软件 + 版本号 + key

当前版本号:

Engintime ASM Lab Setup_3.0.9.msi
Engintime C&C++ Lab Setup_3.0.16.msi
Engintime CP Lab Setup_3.0.9.msi
Engintime CS Lab Setup_3.0.7.msi
Engintime Dream Logic 2019 Setup_3.0.24.msi
Engintime DS Lab Setup_3.0.13.msi
Engintime Eclipse Setup_windows_x64_3.0.1.msi
Engintime Linux Lab Setup_3.0.12.msi
Engintime OS Lab Setup_3.0.8.msi

总结

这个系列到这里就算完整的结束了。
中间遇到了许多细微的细节问题,由于篇幅就不一一道来。

工程本身就不公开了,中了夹杂了很多测试代码没整理,贸然开源反而混乱。
上文中的代码片段包含了所有经过整理的核心代码,更具备参考价值。

总结成一句话,逆向分析靠猜想和验证来驱动,正向开发靠需求驱动。

其他内容

另外在分析完成之后,还简单分析了一下它是如何实现调试的。
利用了 GDB MI 接口。
关键词:
GDB MI CreateProcessW CreatePipe ReadFile WriteFile 管道通信


文章免责声明

本系列文章仅供研究学习,切勿用做非法之事。
如果喜欢本系列软件并准备长期使用,请购买正版,支持软件开发者继续改进和增强本软件的功能。