wechatwin.dll细则若干

@vrqq  August 17, 2019
  • 微信2.6.8.68
  • sha1(WeChatWin.dll) = 2E9417F4276B12FE32CA7B4FEE49272A4A2AF334

打开log

参考: https://s7so.com/article/100.html
需要准备:

按照参考文章,需要sg_consolelog_open <- true以及xlogger_SetLevel(0);

定位sg_consolelog_open <- true
先找到函数ConsoleLog(void, void);关键字%s[%s, %s, %d][%s
然后搜索调用ConsoleLog()的地方,共有4处,找到此段源码的位置(如图),然后在把cmp所指内存改成true即可!
sg_consolelog_open.PNG

定位xlogger_SetLevel()调用位置
找到后,把push 2改成push 0即可。(图里面的push 0是我改完的)
xlogger_setlevel.PNG

写一个简单的dll,替换掉ConsoleLog()函数中的snprintf
按照Mars源码中记载,此处是snprintf。
如图,数一下传参是snprintf(eax, 0x4000, "%s[%s, %s, %d][%s", ...);
hacker.PNG

用VS写hook程序

  • 需要一个Injector,用以操控线程。(详见下方简明样例)
  • 由于不能执行远程的代码(即wechat.exe不能运行injector内部的代码),我们需要制作一个dll来存我们的代码。
    简单的injector,包括inject dll和操纵目标的内存空间。Part1和Part2不相互依赖

    
    hWHND = OpenProcess(PROCESS_ALL_ACCESS, NULL, <PID of DWORD>);
    
    // --- Part 1: load a DLL into target memory. ---
    // 1. locate Kernel32.dll's memory address.
    HMODULE hModule = GetModuleHandle(L"Kernel32.dll"); 
    // 2. find the function in kernel32.dll
    FARPROC funcLoad = GetProcAddress(hModule, "LoadLibraryA"); 
    // 3. open a new space to save the dll's filepath, as the parameter to LoadLibraryA();
    LPVOID allocRes = VirtualAllocEx(h, NULL, sizeof(dllPath)*2 , MEM_COMMIT, PAGE_READWRITE);
    // 4. copy the dll path into memory we allocated.
    WriteProcessMemory(h, allocRes, dllPath, strlen(dllPath), NULL);
    // 5. create a new remote thread to call LoadLibraryA(dllPath).
    CreateRemoteThread(h, NULL, 0, (LPTHREAD_START_ROUTINE)funcLoad, allocRes, 0, NULL);
    // --- Part 1 END ---
    
    // --- Part 2 Memory operation  ---
    dllBaseAddress = LoadLibrary("WeChatWin.dll");
    ReadProcessMemory(h, dllBaseAddress + offset, memData, memDataLen, NULL);
    WriteProcessMemory(h, dllBaseAddress + offset, memData, sizeof(memData), NULL);
    // --- Part 2 End ---

如果需要执行我们写的代码,那就把我们的代码编入dll,然后attach上去,再修改原先的代码用以跳转。
使用微软detours库节约时间

Mars库的消息传递机制

翻看网上的intro及sample,Mars::STN库提供的是 一条longlink + 一条shortlink 数据通路,数据包在longlink及shortlink中排队通过。(有关short/longlink的扩展阅读:mmtls加密过程)
整体的一览看这里:futu cocoa 微信终端跨平台组件Mars介绍
另外在github wiki中的几个参考文章真的很不错。
看了一些wiki和代码后几点提示给大家:

  • STN库在程序中仅有一个instance,是个单例Singleton,我们也可以认为是(伪)静态的。
  • 作为开发者,只能写一个总的callback,在源码中明确了使用 全局变量 存储回调函数。

    • 若是针对不同类型的task,需要自己在总的callback function中,用if分类解析。

作为开发者,需要完成的部分

  • 运行stn_logic.cc中的mars::stn::SetCallback(Callback* const callback),to finish these Interface:

    • bool Req2Buf(taskid, IN|void* const user_context, OUT|AutoBuffer& outbuffer, OUT|AutoBuffer& extend, int& error_code, const int channel_select)
    • int Buf2Resp(taskid, OUT|void* const user_context, IN|const AutoBuffer& inbuffer, IN|const AutoBuffer& extend, int& error_code, const int channel_select)
    • 还有OnTaskEnd/OnPush等等,注意 这些 function pointer 均声明在stc_logic.cc中,作为全局变量保存。
    • 也就是说 无论哪种task,都走的是同一个Req2Buf/Buf2Resp/...

消息传递流程
开发者做Step1 -> mars库调用Req2Buf() -> mars库调用Buf2Resp() -> mars库调用OnTaskEnd()
在STN中,每次发起请求相当于往stn里面扔一个Task,我们在Task中构建好:目标uri,连接方式,数据及protobuf编解码function,数据返回时候的callback等等。然后其内部按照以下步骤送出。

  • Step 1. Shoot a package: calling mars::stn::StartTask(mars::stn::Task);
  • Step 2. Then -> stn_logic.cc::StartTask(mars::stn::Task);
  • Step 3. Goto net_core.cc::NetCore::StartTask(mars::stn::Task); //Called by NetCore Singleton Instance
  • Step 3. In previous step, goto {long/short}link_task_manager_->StartTask(mars::stn::Task) normally,or goto stn_logic.cc::OnTaskEnd(taskid, void*, error_type, error_code) if error.
  • Step 4. 以Longlink_task_manager.cc举例,4a/4b/4c/4d/4e/4f/4g均为此文件内部实现,觉得乱可以跳过。

    • Step 4a. 无论是longlink还是shortlink的task_manager,到了StartTask()这里都会被扔进lst_cmd_后,调__RunLoop()唤醒thread。
    • Step 4b. 对外提供了StartTask(), StopTask()等接口给我们用来往网络中推Task用。
    • Step 4c. 其内部有一线程运行mars::LongLinkTaskManager::__RunOnStartTask()用以盯着mars::LongLinkTaskManager::lst_cmd_,若不合法则调mars::LongLinkTaskManager::__SingleRespHandle()休息一下。
    • Step 4d. 接上部,若合理合法则stn_logic.cc::Req2Buf()void* user_context打包,接下来longlink_->Send()送入网络。
    • Step 4e. 微信官方提供了mmtls做连接加密,其使用longlink_with_mmtls.cc,而在开源的mars中,提供的为longlink.cc为不带加密的版本。在Step 4c中的longlink_->send()是由longlink.cc提供的。
    • Step 4f. When data package cameback from server, LongTaskManager::__OnResponse() will be triggered, 这个函数内部会调用 stn_logic.cc::Buf2Resp()(开发者的decode function)。
    • Step 4g. After decode, it will calling __SingleRespHandle() when no error, then in that function, calling LongLinkTaskManager::fun_callback_(...);
  • Step 5. 至此我们离开了xxLinkTaskManager,转入了NetCore::__CallBack(),这里是送抵开发者手里的最后一步。
  • Step 6. 最终上述函数调用stn_logic.cc::OnTaskEnd(taskid, user_context, error_type, error_code),将server reply送抵开发者手中。

代码细节
翻看net_core.cc代码,mars::stn::NetCore::StartTask(Task);里面巧妙应用了ASYNC_BLOCK_START&ASYNC_BLOCK_END完成一个async队列。
STN的结构及默认调用
首先来看class mars::stn::NetCore
在net_core.h中提到了SINGLETON_INTRUSIVE(NetCore, new NetCore, __Release);,obviously它创建了一个singleton instance,并且写死成mars::stn::NetCore::Singleton,在Mars的wiki中他们提及到STN 不可以,也不建议在一个应用多个进程中使用

找寻官方hook小片段

在源码的大力辅助下,hook简直像切菜一般,本想hook这个net_core.cc里面的NetCore::__CallBack()函数截Task的返回数据,见源码,已经预留好了task_callback_hook_,大喜!
officialhook.PNG
于是千辛万苦来到了这里,刚开始是猜 jne下方的call是这个钩子,jne是判断钩子在不在用的,上面那个call <wechatwin.sub_581FB040>不在源码中,也许是编译脚本插入的。结果沿着这个jne分支往下读发现不对啊,怎么return 0了。。
于是尝试理解如果把第一个call当作是task_callback_hook_,因为这个函数指针 在编译期间已经预埋在代码中了,那个if(isnull)就被优化掉了!
真相大白。
在回过头来看一下源码task_callback_hook_(_from, _err_type, _err_code, _fail_handle, _task),推断官方hook也是(from, errtype, errcode, handler, void* data);
按图索骥看一下它的引用,大部分都集中在看网络状态上,比如onresponse,shortlinkmgr,longlink等等,推测是个后加的分析脚本。

定位NetCore::StartTask()等网络函数

主要有这么几个:

  • net_core.cc : NetCore::StartTask(Task); //start a task
  • stn_logic.cc: mars::stn::Req2Buf(taskid, void*, outbuffer, ...); // serialize data
  • stn_logic.cc: mars::stn::Buf2Resp(taskid, void*, inbuffer, ...); // analysis data
  • stn_logic.cc: mars::stn::OnTaskEnd(tasikd, void*, ...); //task end
  • stn_logic.cc: mars::stn::OnPush(channelid, cmdid, taskid, ...); // when receive from server
    这么选定位简单,传入参数也比较有用,避免了 FF15 Call dword[..]
    startTask和onTaskEnd比较好定位,特征字符串比较多。
    OnPush最简单,把窗口关后台,发消息,发现这个函数断点出触发了ok就是它了。
    (这lambda_14bb9350bf444fccbfd3497804194bb0来源于编译器的__funcsig__,不开源,散了吧。)
    onpush.PNG
    可能还需要找一个static uint32_t mars::stn::gs_taskid 这个更简单了,金山游侠嘛!
    x32dbg的插件类似cheatengine功能的 https://github.com/codecat/ClawSearch/releases
    Buf2Resp,Req2Buf,OnPush的定位技巧:
  • 按照sg_callback != NULL找邻近的一大片lambda function,然后下断点,然后当StartTask触发时,看前两者触发顺序。(但是发现不太对。。)
  • 根据stn_logic.cc,这些函数指针连续声明,推测他们临近保存。因此在已经找到的lambda函数下断点,停下来以后翻call stack,看看这个函数的地址写在哪里了,找到以后顺势找到相邻函数指针变量。
    finding_var.PNG
    图中对应于上述req2buf、buf2resp的指针。

深挖在mmtls前的加密

这段需要一些技巧,需要定位到Buf2Resp以后才可继续。
先看定义Buf2Resp(taskid, void*, outbuffer, outbuffer2, error...);

先想如果是我怎么补全这个程序,首先stn以及buf2resp是singleton/static的了。
先猜:整个加密及处理是完全静态代码,通过user_context传入待处理的值,通过这个函数统一处理。
这个不对,user_context==0。

再猜:在buf2resp里调一个singleton方法或者static function里面,map<taskid, mydata>去找。
这个对,见下文,先去找数据包到底在哪里处理的。
先去看下源码里Autobuffer的格式{char*, readpos, len, capacity, alloc_unit };
技巧是在Buf2Resp函数的开头,直到return之间step over执行,就可以轻松找到哪个call会把outbuffer所在内存改掉,然后我们定位到了下述汇编代码:
marsmgr_req2buf.PNG
按照上述办法再次step over找到call eax改变了outbuffer,这很显然是要根据taskid不同而不同,否则会写call wechatwin.someaddress,不可能现场计算地址。
x86调用规范讲:ecx存this指针,传参通过stack。

(为了简明易懂,下面的解释和推导过程相反。)

  • 上图阴影的push eax,下方依然有push eax,这说明了阴影处代码是保存寄存器变量,给下面的代码使用eax让位。这也决定了,传值的stack是从此处开始的。
  • 因此第一个call wechatwin.64E4B040最多只有两个传入参数,分别为(&address, taskid),我推测是map<taskid, func_of_constructor>
  • 进到这个函数里看一下,似乎就是简单的return 了static variable一枚。
  • 紧接着下方的mov ecx, eax给下方的call提供了一个新的instance,设想如果不是在class内的函数,就没有必要再mov ecx这样多此一举。
  • 接下来call wechat.651244c0完成了new,并把指针写入[ebp-14],这个应该是经优化过了,不然会有一个寻址过程。
  • 在接下来ecx = [ebp-14] 给我们的最终call eax提供一个instance。
  • call eax从而跳转至不同的req2buf。
    因此这个WinMarsMgr::Req2buf()推导为:

    static Req2buf(..) {
    TaskMgr *taskmgr = getTaskmgrSingletonInstance(); // call wechatwin.64E4B040
    NetScene *ns;
    taskmgr->findAndNew(&ns, taskid); // call wechat.651244c0
    if (ns == nullptr) {
      printf("req2Buf error, no find scene:%d");
      return ;
    }
    ns->Req2Buf(outbuffer); // call eax, ns.vtable[0x04](outbuffer)
    }
    __thiscall NetScene::Req2Buf(AutoBuffer outbuffer) {
    unsigned char* ss;
    size_t len = this->protoSerialize(ss);
    Autobuffer pre_ab(ss, len);
    this->proto2enc2buf(outbuffer, pre_ab);
    print("NetSceneNoBaseEx<class micromsg::NewSyncRequest,classmicromsg::NewSyncResponse>::req2Buf");
    }

继续找加密
call eax跳转后,我们可以发现大部分都调用了同一个通用的加密过程:
this->proto2enc2buf(Autobuffer *out, Autobuffer *protobuf_string);
按照上述汇编找到this指针(eax),即可手工加密。
但是这里的this指针用一次就没了,显然不是singleton class,因此我继续猜测一种模式:使用某管理器,存储一个NetSceneClass,其内部包含了生成task的所需信息,包括加密key等。
网上查了很多,已经有前任做过加解密了。我猜测现今版本一定会有一个key做对称加密(也许这个key会变),然后用非对称加密做密钥交换。微信的体量很大,mmtls文档里都说到了如果全部用rsa那服务器根本撑不住。

  • 找寻RSA的技巧:rsa加密需要(n, e),若使用openssl则有一步是BIGNUM(char*)。e常用010001,因此内存搜索"010001",找到以后下硬件断点,找到读他的汇编代码BIGNUM(e),上下看看,容易找到BIGNUM(n)。

hook思路

  • 拦截Starttask/ Req2Buf/ Buf2Resp等等。。
  • 完全借用程序的加密过程,hooker不记录密钥,不解密数据包。
  • hooker仿照上述思路,先获得处理加密的Instance,然后ecx = &instance,然后调用proto2enc2buf制作加密数据包,扔给Req2buf。
  • protoc.exe --decode_raw < file.bin可以直接硬解serialize后的数据
  • 然后发现new NetSceneClass()代价有点大,尝试找寻简单办法。

想法2 Hack StartTask()

按照上面的办法很容易就能找到StartTask,log信息称之为MMStartTask()名字米关系啦,所以我们目前找到了StartTask()。
小技巧:

  • 没有log也可以定位函数,边定位边命名,按照传入值和this(ecx)来猜,看上下文有没调出ecx,记下ecx,把哪些func是属于同一个this指针的摸出来。
  • 在内存中看下this指针第一个DWORD是不是一个pointer,指向vtable,技巧是*(ecx)然后看到是一大片DWORD,每个dword都是一个指向dll.rdata代码区的指针,那么很显然这是个vtable,顺便就把同一个class下的函数都摸了出来。
    惊喜的发现:在调用MMStartTask()的前身(我们称之为NetSceneClass)的地址,和Req2Buf的地址是一样的,验证了上一段的猜测。(每个NetSceneClass包含/生成一个Task)

在MMStartTask()处下断点,用手机触发/newsync,然后往前倒调用栈

  • 在MMStartTask()往前数4个的地方有这么一个代码
    singleton_sencecenter.PNG
  • 进第一个call发现从内存中直接读一个指针,返回ecx给第二个call做this指针
  • 大胆推测这个类是singleton的,我们命名为class SceneCenter()
  • 在第二个call前下断点,看传入的两个参数像是两个class *
  • 下图为第一个class ,惊喜的发现其vtable中含有我们之前标记过的函数。按照上一章的分析,第一个参数应该是NetSceneClass
    superised.PNG

想法3 提取界面资源文件

  • 从LoadLibrary入手,估摸着是WeChatResource.dll,那我们在LoadLibrary处下断点。
  • 单步执行发现分别执行了如下几个:

    h = LoadLibrary("<Path>\WeChatResource.dll")
    resInfo = FindResourceW(h, MAKEINTRESOURCEW(0x22B8), L"wxz");
    resData = LoadResource(h, resInfo);
    resSize = SizeofResource(h, resInfo);
    ptr = LockResource(resData);

ResLoaderWXZ_constructor.PNG
ghrida.PNG

  • 上图跟着往下走,这段我们发现了他执行了[0x] = new Class,代码中先分配了0xC大小的空间,接着把vftable粘了过来。因此这个class是singleton的,this便指向新分配的这块0xC空间。
  • 上图为Ghrida,class[0x4]是resourceInfo,class[0x8]似乎调用函数后又分配了一块空间的pointer,深入得知估计是一个解压缩的class。
  • 看vftable所在内存往上一个,像是RTTI_info表,从里面可以读出类名称ResLoaderWXZ。
  • RTTI表,参考了这个:https://www.cnblogs.com/zhyg6516/archive/2011/03/07/1971898.html
  • 大胆hook仅有的vftable[0]这个函数,发现就是它!this->getRC("Login\\LoginWnd.xml", outAddr);
  • 在这个getRC停下后,继续单步执行发现:

    char * data = outAddr;
    if (res.size < 0x2000)
     (data + 0x2000) = outAddr;
    else
     (data + 0x2000) = new char[res.size()];
    (data + 0x2004) = res.size();
  • 就是说如果xml过大会在上述0x2000位置返回个指针。。
  • 想读数据甚至不用Hook,仅仅是远程调用尔。
  • 想全dump出来,可以vftable[0]至自己的函数,注意这段是readonly的,需要VirtualProtectEx先加上Write,挂完钩子再还原。
  • 这个函数会pop stack,需要找一个有趣的hook点。
  • 当然亦可简单的hook这个函数。

看汇编是枯燥的,constructor function总会inline,推荐大家使用IDA以及Ghidra,然后x64dbg单步执行,对照静态分析看。有时候它傻,能看懂就得了,十分好用又提高效率。Ghidra开源的,分析比较慢,然后就好用了。IDA不用说了,但是IDAfree不带decompile。

技巧:比较短的代码,在x64dbg里带着执行更好分析,同时也要在静态分析工具里打标记,记参数,改名,生成struct/class。然后另开一个文本编辑器把确定的class记下来。

挖出服务器reply解密函数

按照前文,挖出了数据的传递流程:

  • 数据从mars网络返回 -> mars\stn\logic.cc::Buf2Resp()
  • -> WinMarsMgr::Buf2Resp() -> NetSceneBaseEx<...>::buf2Resp(AutoBuffer*)
  • -> 程序逻辑处理,加密解密。(待查)
    显然,我们应该去追踪NetSceneBaseEx::buf2Resp
    思路:
  • 先把传入参数netencbuffer的内容记下来
  • 发现在这段代码里两次调用了getRandomKey,跟踪这个函数,惊喜的发现两次结果一样的。。这个不是什么随机数生成器,只是这个key的名字叫random,然后get方法叫getRandomKey();
  • 在这里把randomkey_0x10记下来(截个图什么的就可以啦)
  • 在代码中段有三次operator new(0x14),猜测是三枚Autobuffer,我们在x64dbg中开三个内存窗口去跟踪他,看到哪个call修改了他。
  • 发现下图这个__fastcall (ecx, edx, stack_1, stack_2, stack_3);
  • 好的我们已经看到PNG了试着用protoc --decode_raw解一下果然是合理合法
    findecode.png

Key同步的两/三种可能:

  • 1】根据上述过程,若同步key的数据包在每一个Scene里面都会出现,那么同步key就会写在WinMarsMgr::Buf2Resp(..)或者在其之前的mars.Callback中。私以为在callback里面可能性不大,太dirty patch了。。这种情况好处理,构建NetSceneBase扔进SceneManager里面,然后只写一个virtual NetSceneBase::Buf2Resp(..)。对于WinMarsMgr::Req2Buf(..)同样方法处理即可。
  • 2】若是只有特定的Scene是同步key的,那我们只需要不碰这些Scene就可以了,偶尔触发几个onPush让它自动去同步。。
  • 3】这种是每个NetSceneBase::Buf2Resp()都会自己处理key sync。。不会吧,那这代码写的也太乱了。。。如果真是这样那gg,那就要模拟所有要用到的NetScene。。
  • 4】不过还有一种可能是1+2混合,比较节约数据包!如果是这样还是老老实实模拟Scene吧。。

Hook思路:构建NetSceneBase调用加解密

这个思路目前来看比较能行得通,首先Mars网络由于开源了,我们很容易就定位到了诸多callback,基本发包还有监听都拿来直接用了,困难点在数据包在送抵mars网络之前,会有加密。

  • 那么我们通过不断地抓上述NetSceneBaseEx在调用时候的ecx(this指针),去猜测整个class的结构
  • 然后用猜出来的结构带入加密函数/解密函数,只需要我们确保某些offset上面的数据有效就可以完成加解密
  • 通过拦截stn_logic.cc下面的callback函数,可以得到所有的密文
  • 拦截加密解密函数可以获得明文
  • 在上述几个位置下坑以后,便可以截获消息来处理。
  • 而发消息有两个思路,一个是构建Task,然后直接扔进mars网络,然后在mars.callback_functions把我们的task拦下。(因为没在NetSceneManager注册)
  • 第二个想法是直接模拟构建一个class NetSceneBase,然后扔进SceneCenter::doScene();这种复杂在要实现诸多virtual function并且还要把它安回vftable。
  • 如果想模仿全部数据包,包括同步key的包,还是模拟NetSceneBase比较舒服了,交给现成的一段程序来处理不更好吗!
  • 如何仿造scene?对于已有的NetSceneBase,似乎微信有一个singleton的NetSceneManager,可以直接调用find。或者我们就breakpoint->dump呗。。
  • Anyway,我们已经把微信所有的明文通信包和加密包都拦下来了,自己造一个客户端又何妨呢!

几个坑

用HANDLE + LPVOID加出奇妙东西
参考 https://docs.microsoft.com/en-us/windows/win32/winprog/windows-data-types#handle
而void + void 没有这个行为啊,不知道m$怎么实现的。转成ulong或者DWORD呗(32位地址)

Detour总说hook失败-9 但是我看了汇编觉得没问题啊。
可能是下断点了吧,参考:https://blog.vrqq.org/archives/469/

为啥Debug模式和Release模式的vector或者string占内存大小不一样啊?
这有个开关,debug模式会有额外的debug信息(感谢某群友指点)
https://docs.microsoft.com/en-us/cpp/standard-library/iterator-debug-level?view=vs-2019

printf(std::string)咋输出不对啊?咋后面的参数都瞎输出
同样我们来看下string的结构!注意Release模式下的string是 4Bytes 6 == 24Bytes* (去掉下面的帖子里第一个参数)
http://bbs.jatools.com/redirect.php?tid=2753&goto=lastpost

HOOK完各种抛异常

  • 编译器优化无穷无尽,虽然有调用规范,但没有说每个call非要保持栈平衡或者还原寄存器。
  • 在不hook的情况下拦截call的前一句和后一句,看一下调用完是否栈平衡。
  • 写的钩子是否包含save/recovery register(include EFLAGS)
  • 有jump跳到钩子“里面”了

添加新评论