逆向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分别生成骨架代码,NetworkContextProxyNetworkContextStub

当我看到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上层承接的连接点。RemoteReceiver都是一个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就可以接收结果。

多年前,我曾在一个项目里,需要找出android手机摄像头的显示缓存,满足需求的某些功能。所以很容易直觉发现WeChat发送了一帧图像数据给WeChatUtility。

相关写了从surfaceflinger历史变更谈截屏 ,某些机root也不能访问dma-buf ,记一次YUV图像分析(二) , YUV亮度扫描小工具,如何确定尺寸以及错误尺寸下图像发生什么变化 。

还写了一个opencv工具,zhelper-cvtool,用FilterPipeline模式,调试视觉识别流程中,图像处理流中各个滤镜环节的参数,识别方法如match,cascade,blob,feature等封装成输出滤镜。例如命令”cvtool Mario/%04d.png pyrDown,pyrUp,morphology,channel,canny,contours", 将图片Mario/%04d.png集合作为输入流,contours识别算法为输出滤镜,中间经过多个图像滤镜处理,以产生适合识别算法的图像。最后识别滤镜将识别结果与输入源原图进行混合。每个滤镜都有一个单独窗口,可以调整参数,同时同步实时生成每个滤镜的处理完的图像结果。

 

 

下面两图,发现OCR对有些情况解释有参差。

 

本篇到这里,下一篇再见。

 

逆向WeChat(七,查找sqlcipher的DBKey,查看protobuf文件)

逆向WeChat(六,通过嗅探mojo抓包小程序https,打开小程序devtool)

逆向WeChat(五,mmmojo, wmpfmojo)

逆向通达信 x 逆向微信 x 逆向Qt (趣味逆向,你未曾见过的signal-slot用法)

逆向WeChat(四,mars, 网络模块)

逆向WeChat(三, EventCenter, 所有功能模块的事件中心)

逆向WeChat (二, WeUIEngine, UI引擎)

逆向wechat(一, 计划热身)

我还有逆向通达信系列

我还有一个K线技术工具项目KTL可以用C++14进行公式,QT,数据分析等开发。

posted on 2024-08-22 20:12  bbqz007  阅读(498)  评论(0编辑  收藏  举报