- 微信2.6.8.68
- sha1(WeChatWin.dll) = 2E9417F4276B12FE32CA7B4FEE49272A4A2AF334
打开log
参考: https://s7so.com/article/100.html
需要准备:
- x64dbg/ida等等debug工具
- 微信Mars源码 https://github.com/Tencent/mars
按照参考文章,需要sg_consolelog_open <- true
以及xlogger_SetLevel(0);
定位sg_consolelog_open <- true
先找到函数ConsoleLog(void, void);关键字%s[%s, %s, %d][%s。
然后搜索调用ConsoleLog()的地方,共有4处,找到此段源码的位置(如图),然后在把cmp所指内存改成true即可!
定位xlogger_SetLevel()调用位置
找到后,把push 2改成push 0即可。(图里面的push 0是我改完的)
写一个简单的dll,替换掉ConsoleLog()函数中的snprintf
按照Mars源码中记载,此处是snprintf。
如图,数一下传参是snprintf(eax, 0x4000, "%s[%s, %s, %d][%s", ...);
用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 gotostn_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, callingLongLinkTaskManager::fun_callback_(...);
- Step 4a. 无论是longlink还是shortlink的task_manager,到了StartTask()这里都会被扔进lst_cmd_后,调
- 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_
,大喜!
于是千辛万苦来到了这里,刚开始是猜 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__,不开源,散了吧。)
可能还需要找一个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,看看这个函数的地址写在哪里了,找到以后顺势找到相邻函数指针变量。
图中对应于上述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所在内存改掉,然后我们定位到了下述汇编代码:
按照上述办法再次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个的地方有这么一个代码
- 进第一个call发现从内存中直接读一个指针,返回ecx给第二个call做this指针
- 大胆推测这个类是singleton的,我们命名为class SceneCenter()
- 在第二个call前下断点,看传入的两个参数像是两个class *
- 下图为第一个class ,惊喜的发现其vtable中含有我们之前标记过的函数。按照上一章的分析,第一个参数应该是NetSceneClass
想法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);
- 上图跟着往下走,这段我们发现了他执行了
[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解一下果然是合理合法
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跳到钩子“里面”了