逆向WeChat (五)
本篇逆向mmmojo.dll,介绍如何使用mmmojo,wmpf_host_export的mojo。
本篇在博客园地址https://www.cnblogs.com/bbqzsl/p/18216717
上一篇逆向分析了mars这个网络模块,本篇逆向mojoIPC。如何从mojo core的MojoHandle找出binding层的Remote跟Receiver,并使用。
本篇内容结构:
0.mojo与orb架构
1.mojo网络协议栈
2.Invitation链路握手
3.MergePort,MessagePipe握手
3.1.Remote,Receiver传递。
4.Mojo对象
5.Trap, Arm, Proactor, Reactor
6.从Trap出发找Remote还有Receiver
7.MMMojoService
8.实战使用OCR
本篇的mojo专指chromium项目的mojo子项目,区别于AI领域的mojo语言。并且地,专指传统的MojoCore。2022年开始MojoCore逐步向IPCZ过渡,ipcz在github可查得始于chromium102。所以为了演示,WMPF使用85xx版本以保持使用MojoCore,而不是IPCZ。
我认为mojo是使用了orb架构,google了一下没有人这么说,那只能是我个人观点了,没有权威背书。或者换一种说法,可以orb架构来认识mojo,mojo中有许多orb架构的东西,但mojo只能用于本地机器的进程间,不是中间件。
对象请求代理的解释在https://en.wikipedia.org/wiki/Common_Object_Request_Broker_Architecture。
AI这样分析:
我的根据是从CORBA的熟知的架构图。
以network.mojom.NetworkContext接口为例,mojom分别生成骨架代码,NetworkContextProxy,NetworkContextStub。
当我看到remote, pending_remote,bind等, 我的第一关联印象就是ZeroIce的ObjectPrx,stringToProxy, unchecked_cast,checked_cast。
同理地,mojo core作为一种通信设备主管network,io线程池,dispatcher等事,等同于ZeroIce的Communicator。
mojo网络使用其专用通信线程。那么就借用我们熟悉的TCP/IP网络模型来认识mojo的网络模型。
我将Channel看作链路层,Port为IP层,MessagePipe类比UDP,DataPipe类比TCP。其中DataPipe实现以MessagePipe作为eventfd,SharedBuffer作为queue。MachPort早就有这个实现了,MachPort在IPC通信时可以传递VM内核对象。还是iphone4的时代。
我使用windows平台进行分析,所以在本篇里,Channel使用NamedPipe作为通信链路。每个mojo core看作一个mojo设备,在mojo网络代表一个节点node。同一进程内可以有多个mojo设备,每一个PE文件都可以编译进一个mojo设备。我说的mojo设备应该跟官方的mojo embbeder是同一个意思。以WeChat为例,WeChat.exe进程一共加载了2个mojo embbeder,分别来自mmmojo.dll,wmpf_host_export.dll。
每个mojo设备只有一个名字,所以在mojo网络中是唯一的。node之间有且只有一条有效链路,在当下。两个node之间不能够同时有两条或以上链路。所以,在上层的port,都使用同一条链路跟同一个节点node进行通信。
同样是IPC,zmq的SOCKET是真实对应一个底层socket,独立一条链路。那么Port是什么东东在mojo。IPC离不开一个关键字,就是MQ。zmq的SOCKET就是一种类型的MQ。每个SOCKET在上层都有接收跟发送的总计两个MQ。本方SOCKET的发送MQ通过链路socket将数据发送到对方SOCKET的接收MQ。我现在将前面的句子迭代名称。在mojo环境,本方Port的发送MQ通过两个Node之间链路NamedPipe将数据发送到对方Port的接收MQ。这样,Port的本质就是一个MQ。mojo::ports::Port的定义里,对象的主要资源就是一个用来接收MessageQueue。message_pipe就是两个互成Peers的Ports。对于有人喜欢用一个百万ports来衬托mojo比别的IPC利害,虽然不假,但也没有多少实际意义。只要有内存,56位的地址空间随你耗。但所有的一切跟一切都离不开一条链路。
这么一来,Node-Port,Node关联的是底层链路Channel,Port关联的是上层MQ。Node-Port就是如何通过Channel将数据放到对方的MQ。NodeController就可以看作是一个mojo设备的驱动,完成这些工作。
在这个mojo网络之上,运行着ORB。说人话就是,mojom跟c++bindings。
于是我们就可以得到自下而上的mojo网络协议栈。
找到一篇用协议栈来分析mojo的文章https://blog.lazym.io/2020/06/22/Mojo-More-of-a-Protocol/
接下来,我们来看握手。
这里存在两个层面的握手。链路层的握手,ports层的握手。
先来看链路层的握手。
invitation就是node之间在底层链路channel进行的连接握手。invitation跟一个关键字sync相关,sync不难让人想到TCP的sync包,也就是发起握手连接。这套握手礼仪就是invitation。
发起方扮演Inviter,接受方扮演Invitee。他们你来我往寒暄几轮。
下图是抽象后的简化图,最终的目标是让双方都AddPeer。如果有一方没有AddPeer,他就是没记住你,他不认识你。
再来看ports的握手。这是一个MergePort的过程。 或者说MessagePipe是如何在两个node之间建立“连接”。因为Port层不具有真正意义上的连接。
remote或receiver的传递或者返回,是通过MergePort来实现。下图是CreateURLLoaderFactory方法如何返回一个pending_receiver<URLLoaderFactory>的。
mojo通过UserMessage携带PortDescriptor,要求跟对方新绑定成一对Ports,并告知对方使用指定的PortName。灰色的Port只做引线人,短暂地当了一下子的Proxy。这个过程称作MergePort。
不过在完成UpdataPreviousPeer前,发起方仍然使用ProxyPort进行通信。这样能够让MergePort跟消息通信顺滑地同时进行,毕竟一整套MergePort流程跑下来,需要来往几轮。
下图是Browser向GPU绑定VizMain接口,并调用VizMain.CreateGpuService等方法,尽管Browser跟GPU努力地去促进ObserverProxying的工作,但是在Browser完成UpdataPreviousPeer前,Browser仍旧使用着ProxyPort去调用VizMain接口的方法。
MergePort流程抽象成下图
mojo有两种途径可以进行MergePort。一种是通过Invitation,另一种MessagePipe。我们并不能直接使用ports层的控制协议进行MergePort。我们必须依赖Mojo对象去自动完成MergePort的动作。
Invitation使用MojoAttachMessagePipeToInvitation跟MojoExtractMessagePipeFromInvitation,附加在Invitation握手流程。MessagePipe使用MojoAppendMessageData跟MojoGetMessageData,附加在一次Message传递。
下面演示,mmmojo跟wmpf_host,建立Invitation并MergePort建立新的MessagePipe连接。
下面演示,mmmojo跟wmpf_host,通过MessagePipe进行MergePort建立新的MessagePipe连接。
需要注意的是,MergePort发生在ports层,对于系统外部的使用者是透明不可见的,我将其归纳在ports层的控制协议。MergePort结束后的peer port,如果没有一个MessagePipe认领,使用者也是没有办法使用的。毕竟对于系统边界外部,只有MessagePipe是可知的,而不是Port。如果将MessagePipe等一类Mojo对象,归纳成一层。那么这一层是最接近系统边缘的。
接下来认识Mojo对象。《windows核心编程- 第三章内核对象》 已经阐明了句柄与内核对象的关系。(随手找了篇别人的第三章笔记)。内核对象由操作系统内核管理,内核通过内核对象句柄表,将句柄显露给用户空间,句柄就是内核对象句柄表的索引。这里要声明,在mojo代码里,platform对应操作系统平台,system对应的是mojo设备(或者mojo embedder, mojo core)。所以PlatformHandle就是OS的内核对象句柄。相应地MojoHandle就是mojo内核对象句柄。同理地,每个mojo设备的内核,维护着各自的Mojo内核对象句柄表。Mojo内核对象都是Dispatcher对象。它们分别有MessagePipeDispatcher, DataPipeConsumerDispatcher, DataPipeProducerDispatcher, InvitationDispatcher, SharedBufferDispatcher, WatcherDispatcher等。特别地,在接口层Watcher对应另一个名称Trap。
Trap是一种特殊的对象资源,本人认为类似于epoll。为MessagePipe,DataPipe内核对象提供异步IO事件的支持。
epoll让与确立了事件关联的fd,将触发事件添加到自己的ready list,用户藉由epoll_wait将事件取出。
类似地,Trap让与确立事件关联的MessagePipe或DataPipe,将触发的事件添加到它的ready_watches_,用户藉由MojoArmTrap将事件取出。
如果你能够很好地适应英文语境,若不能请不要理会Arm的字面意思,尝试一下换个角度去理解。坦诚地我适应不了Arm这个单词,换成epoll_wait去理解,一下子就通了。
WatcherDispatcher有一个成员armed_来标识是否正在进行Arm模式。Arm模式是一种Proactor模式。相反地,当armed_=false时,ArmTrap对应着Reactor模式。下面分析Arm模式是如何利用edge-trigger事件,来实现Proactor模式。c++binding层中,SimpleWatcher对应的是Proactor模式,WaitSet对应的是Reactor模式。
WatcherDispatcher的ready_watches_相当于level-trigger事件。
每个Watch的成员last_known_result_,用来判断是否产生了一个edge-trigger事件。
MessagePipeDispatcher,以下面的路径,WatcherSet,WatcherDispatcher,Watch,通过NotifyState函数,产生level-trigger事件,添加进WatcherDispatcher.ready_watches_。过程中Watch通过last_known_result_判断是否为edge-trigger事件,如果是edge-trigger事件,并且正进行Arm模式,就会在当前线程的RequestContext上将Trap的Callback安排一次后续的回调,从而实现了Proactor模式。这时armed_就会设置成false关闭Arm模式进入Reactor模式。现在ArmTrap退变成epoll_wait用来轮询level-trigger事件。SimpleWatcher在回调函数用Reactor模式将所有level-trigger事件处理。ArmTrap函数本身的基本作用首先是一个事件轮询(Reactor),然后有一个高阶的功能Arm(Proactor)。
或者换个角度,Arm模式是将ready_watches_是否为空,看作一个事件,不关心个别Watch。并且只对ready_watches_从空至有的edge-trigger事件发起回调。我们熟悉对edge-trigger的处理,是必须对事件源的数据读完。现在在这次回调中,事件源不是单个Watch,而是一整个ready_watches_,我们务必要将ready_watches_里面所有的Watch,将每个Watch对应的事件源的数据读完。将ready_watches_清空后,Arm才能重新开启,否则ArmTrap只能用于轮询ready_watches_里面的独立level-trigger事件。这大概就是只有CreateTrap可以设定一个唯一的回调函数,而AddTrigger并不给独立的事件设定回调函数。因为一整个Trap看作一个事件,这个事件就是ready_watches_是否为空。
ArmTrap是一个非阻塞函数,为弥补,在C++binding层的WaitSet提供了阻塞的版本。WaitSet通过一个Trap来对应一个默认的系统事件,WaitSet可以阻塞等待一个或多个系统事件,但至少包括它自身默认的事件。当Trap的ready_watches_不为空时,WaitSet的默认事件处理ON状态,Wait操作不会阻塞。但是Trap的ready_watches_空时,WaitSet的默认事件处理OFF状态,Wait操作就阻塞起来。直到Arm模式因为ready_watches_由空转有,回调函数将WaitSet的默认事件ON,从而唤醒阻塞的Wait操作。当Trap因为ready_watches_不再空关闭了Arm模式,在Wait操作前都需要ArmTrap轮询是否有事件,在有事件情况下预先将WaitSet的默认事件ON,从而使得后面的Wait操作不阻塞立即返回。如果要中止Wait操作,可以搭配另外指定的事件,让Wait同时阻塞等待这个事件,通过这个事件就可以唤醒中止Wait操作。这样就是一个epoll_wait的阻塞版本的实现。
然后顺带一提RequestContext,这个好像参照了linux的软件中断-后半处理。简单地说,就是内核在系统调用期间,硬中断事件保存到软中断队列,在系统调用线束时,控制权由内核空间转回用户空间前,顺便将软中断队列的事件给处理完。Mojo系统调用,当前线程在堆栈构建一个局部的RequestContext,并在TLS保存指针,给所有调用帧使用。当这次调用结束,局部的RequestContext析构时,将队列的回调通通执行。Arm模式就是利用RequestContext,将回调安排在RequestContext的回调队列中。延后到Mojo系统调用再执行。这样就可以充分契合多核环境的多线程。
下图演示,使用Arm-Proactor模式。Trap回调函数,进行Reactor穷尽所有level-trigger事件。
下图演示,使用Arm-Reactor模式。Trap回调函数只通知事件,唤醒我在控制台线程手动进行Reactor处理所有level-trigger事件。
where are we? 现在小结一下, 我们认识了链路Channel,链路连接Invitation,Port的连接MergePort,Mojo对象Trap。那么如何跟C++Binding的Service联系上。直接操作NamedPipe,只是在直接操作链路。Service使用MessagePipe进行通信,我们至少也要知道一个Service与哪个Port是对应的。但是我们仍然不知道Service对应的对象。我们熟知LongLongAgoFarFarAway有一个套路,底层向上层传递消息都用通知,而可以对一个MessagePipe监视的就只有Trap。Trap就是找到上层Service的一个关键。
只要拆解Trap,就可以上观天文下知地理。向上可以追踪Service,向下可以溯得MessagePipe。如我上面已经提到,Trap在Binding层主要有两个高阶的类,分别是SimpleWatcher同WaitSet。SimpleWatcher正是专门为MessagePipe提供Proactor-Read的。换句话是通向上层Receiver的。
下图演示通过Core的句柄表可以过滤出所有Trap,并找出哪些是SimpleWatcher。
然后通过SimpleWatcher找出Remote或者Receiver。下图演示
从底层Trap到上层ServiceStub的线索如下图。分别是Trap,SimpleWatcher::Context, SimpleWatcher, Connector,InterffaceEndpointClient,ServiceStub。
从Connector开始,往上所有参与的类皆是MessageReceiver。MessageReceiver使用了责任链模式,用虚函数MessageReceiver::Accept传递责任并处理Message。Connector对应着一个MessagePipe以及它的SimpleWatcher。它既代表一个底层通信MessagePipe的连接,同是也是底层跟Binding上层承接的连接点。Remote跟Receiver都是一个InterfaceEndpointClient。当一个InterfaceEndpointClient没有incoming_receiver_时,它角色则是Remote,是ServiceProxy的receiver_,这时InterfaceEndpointClient::Accept负责发送,注意的是Accept的责任来自上层的Proxy。相反地,当有incoming_receiver_时,它角色则是Receiver,并且incoming_receiver_就是ServiceStub。这时InterfaceEndpointClient::Accept直接将责任传递给ServiceStub,Accept责任来自底层的SimpleWatcher通知。
从上层开始的outgoing:
ServiceProxy, to InterfaceEndpointClient::Accept, to Connector::Acceptr, to MessagePipe
从底层开始的incoming:
MessagePipe, to SimpleWatcher, to Connector::ReadMessage, to incoming_receiver_->Accept, to ..., to InterfaceEndpointClient::Accept, to ServiceStub。
这样的话,只要构建一个Message,调用InterfaceEndpointClient::Accept,就可以使用服务了。如果InterfaceEndpointClient是Remote的话就会发送请求,如果是Receiver的话就会直接交给ServiceStub处理。采用这种方法需要注意一点,core部分的代码变更的情况比较少,但是binding部分的代码变更还是非常频繁的,这个Message并非abi创建的Message,而是binding层的Message,必须紧贴mojo embedder的代码版本。
回到mmmojo.dll。WeChat与子进程服务通过mmmojo.dll进行Ipc,与Wmpf通过wmpf_host_export.dll进行Ipc。mmmojo.dll定义了一个唯一的服务MMMojoService。实际上它是一个类似IPC.mojom.Channel的单向通路。所有服务需要一组双向通路,也就是两边都互为对方的MMMojoService。事实也是这样。逆向分析后得到MMMojoServiceImpl,既包含Remote到对方的MMMojoService,同时包含Receiver接受对方的调用本方的MMMojoService。
mmmojo设计了一个MMMojoEnvironment,并以MMMojoDelegate作为MMMojoServiceImpl的Delegation。让外界能够实现MMMojoDelegate抽象类,就可以通过MMMojoEnvironment绑定成MMMojoService进行使用。一个MMMojoEnvironment对应了一个默认的MMMojoServiceImpl,MMMojoServiceProxy,MMMojoServiceStub,Remote,Receiver。MMMojoEnvironment另外还有两个MMMojoServiceImpl携带了独立线程,分别用于“RW"跟“RW sync"。外部使用只要的实现了MMMojoDelegate的具体类,就可以直接通过MMMojoEnvironment使用MMMojoService进行IPC。mmmojo.dll的Read系列导出函数,对应使用MMMojoServiceImpl跟Receiver,Write系列导出函数则对应使用MMMojoServiceProxy跟Remote。ReadInfo同WriteInfo,对应MMMojoService的方法的参数进行了封装。ReadInfoRequest同WrtireInfoRequest对应RawPayload。换句话,mmmojo使用MMMojoService将mojo封装到IPCChannel服务,以c函数导出接口。使用者无须关心mojom,MMMojoService。
MMMojoDelegate有8个接口方法,前三个是给MMMojoService使用的,后面5个可能是MMMojoEnvironment使用的。
MMMojoService接口方法到MMMojoDelegate虚拟函数的关系如下:
比较简单常用的是MMMojoService::0, MMMojoService::1两个方法,分别对应MMMojoDelegate::OnReadPush,MMMojoDelegate::OnReadPull。这里说明一下,Service的每个method都有一个hashname,salt不同编译出来的hashname也不会相同。这里归约成hashname表的序号。MMMojoService::0就是第一个method。
Push的向MMMojoService服务端推消息,不要求返回结果。MMMojoService::0的调用,是没有返回结果的。
Pull则是向MMMojoService服务端拉消息,要求返回结果。MMMojoService::1的调用,是返回结果的。
WeChat跟子进程服务就用mmmojo.dll进行protobuf IPC。
旧版的WeChat应该是将OCR功能做在WeChatUtility.exe。WeChat会将图片的灰度图发给WeChatUtility.exe。使用MMMojoService::1。
新版,我也不知道是从哪一版,使用了WeChatOCR.exe。直接保存PNG,通知WeChatOCR.exe读取本地的PNG图片并返回结果。使用MMMojoService::0。
下图演示,通过OCRManager取得Remote,生成Message,使用Remote调用Accept向WeChatOCR.exe发送请求,并得到结果。
我们可以通过mmmojo.dll的c导出函数启动其它子服务进程,实现MMMojoDelegate就可以接收结果。
下面两图,发现OCR对有些情况解释有参差。
本篇到这里,下一篇再见。
逆向通达信 x 逆向微信 x 逆向Qt (趣味逆向,你未曾见过的signal-slot用法)
逆向WeChat(三, EventCenter, 所有功能模块的事件中心)
逆向WeChat (二, WeUIEngine, UI引擎)
我还有逆向通达信系列。
我还有一个K线技术工具项目KTL,可以用C++14进行公式,QT,数据分析等开发。
逆向WeChat (五)的更多相关文章
- iOS逆向(五)-ipa包重签名
为什么要重签名? 1.在没有源代码的情况下,你已经对某个应用进行了资源修改(比如修改了启动图或图标等).修改完成以后,如果想要让APP可以正常使用,该APP一定要重新签名然后压缩成IPA文件. 2.如 ...
- NSCTF2015 逆向第五题分析
这道题目我没有写出Exploit,因为编码时候里面几个细节处理出错.但对程序的逆向分析已完成,这里就学习一下别人写Exploit的思路.主要参考:绿盟科技网络攻防赛资料下载 0x01 题目要求 题目要 ...
- Python逆向(五)—— Python字节码解读
一.前言 前些章节我们对python编译.反汇编的原理及相关模块已经做了解读.读者应该初步掌握了通过反汇编获取python程序可读字节码的能力.python逆向或者反汇编的目的就是在没有源码的基础上, ...
- Python逆向(一)—— 前言及Python运行原理
一.前言 最近在学习Python逆向相关,涉及到python字节码的阅读,编译及反汇编一些问题.经过长时间的学习有了一些眉目,为了方便大家交流,特地将学习过程整理,形成了这篇专题.专题对python逆 ...
- 【二进制】【WP】MOCTF逆向题解
moctf 逆向第一题:SOEASY 这个是个 64 位的软件,OD 打不开,只能用 IDA64 打开,直接搜字符串(shift+F12)就可以看到 moctf 逆向第二题:跳跳跳 这个题当初给了初学 ...
- JS常用类
JS常用类 一.Number 1.常用数字 整数:10 小数:3.14 科学计数法:1e5 | 1e-5 正负无穷:Infinity | -Infinity 2.常用进制 二进制:0b1010 八进制 ...
- day53
JS常用类 一.Number 1.常用数字 整数:10 小数:3.14 科学计数法:1e5 | 1e-5 正负无穷:Infinity | -Infinity 2.常用进制 二进制:0b1010 八进制 ...
- JavaScript常用类
JS常用类 一.Number 1.常用数字 整数:10 小数:3.14 科学计数法:1e5 | 1e-5 正负无穷:Infinity | -Infinity 2.常用进制 二进制:0b1010 八进制 ...
- OD 实验(十五) - 对一个程序的逆向
程序: 打开程序 出现一个 NAG 窗口 这是主界面 点击 Exit 程序出现 NAG 窗口,然后退出 用 PEiD 看一下 是用 VC++ 6.0 写的程序 逆向: 用 OD 载入程序 跑一下程序 ...
- 工控安全入门(五)—— plc逆向初探
之前我们学习了包括modbus.S7comm.DNP3等等工控领域的常用协议,从这篇开始,我们一步步开始,学习如何逆向真实的plc固件. 用到的固件为https://github.com/ameng9 ...
随机推荐
- FEDORA 显卡驱动安装
FEDORA 显卡驱动安装 在fedora中akmod-nvidia包可以自动的处理开源驱动屏蔽等各种问题, 强烈推荐用这个安显卡驱动. -1. 在 BIOS 中关闭安全启动 0. 切换桌面环境至 X ...
- Fedora升级33->34
Fedora升级33->34 1. dnf --refresh upgrade 2. dnf install dnf-plugin-system-upgrade --best 3. ...
- AT24C02、04、08、16 操作说明
我们这里介绍一下常见的EEPROM,ATMEL的AT24x系列中的AT24C02,学会了这个芯片,其他系列的芯片也是类似的. AT24C02的存储容量为2K bit,内容分成32页,每页8Byte ( ...
- 关于docker-compose up -d 出现超时情况处理
由于要搭建一个ctf平台,用docker一键搭建是出现超时情况 用了很多办法,换源,等之类的一样没办法,似乎它就是只能用官方那个一样很怪. 只能用一种笨办法来处理了,一个个pull. 打个比如: 打开 ...
- 中台框架模块开发实践-用 Admin.Core 代码生成器生成通用代码生成器的模块代码
前言 之前分享中台 Admin.Core 的模块代码生成器,陆续也结合群友们的反馈,完善了一些功能和模板上的优化,而本篇将基于此代码生成器生成一个通用代码生成器模块的基本代码 后续再在此代码的基础上进 ...
- day01小程序快速入门
这几天正式开始微信小程序的修炼了,就目前而言来看简直就是vue和react的结合体,所以在学小程序前,先把框架熟悉还是挺有用的. 一.简介 1.1与普通网页区别 二.第一个小程序 需要注册小程序开发账 ...
- pymsql往数据库插入表情报错
修改数据库 需要数据库支持utf8mb4 修改/etc/my.conf [client] default-character-set = utf8mb4 [mysql] default-charact ...
- 搜索Python编程获取相关图书信息
1.获取相关图书信息 #搜索"Python编程"获取相关图书信息 from selenium import webdriver from selenium.webdriver.su ...
- DASCTF 2023六月挑战赛|二进制专项 PWN (下)
DASCTF 2023六月挑战赛|二进制专项 PWN (下) 1.can_you_find_me 检查保护 意料之中 64位ida逆向 只有add,和del功能不能show 先看add吧 最多申请10 ...
- 【Java】比较业务实体信息变化的工具类
一.业务需求 需要将业务表每次更新操作的前后记录进行保存,写入更新历史表中 方便用户查阅该业务记录发生的历史变化 二.代码实现 import lombok.AllArgsConstructor; im ...