前言

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

思路呢?

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

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

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

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

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

 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 包了么?

0.png

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

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

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

注入进程

这里使用常见的映像劫持,劫持掉 winhttp.dll
关于劫持怎么写,
可以去看我之前发的

简单改下入口就行了

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

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

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

// 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();
}
// 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");
    }
}

编译生成

配置格式:

{
  "//": " 进程名 : 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 管道通信


文章免责声明

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

网站免责声明