逆向WeChat(六)

上篇回顾,逆向分析mojo,mmmojo.dll, wmpf_host_export.dll,还有如何通过mojoCore获取c++binding的remote或receiver,并调用它们的功能接口。   

本篇介绍如何对小程序mojoIPC进行嗅探Sniff,如何通过mojoIPC.sniffer抓包小程序https,如何打开小程序devtool,谁掌管了vconsole配置。

本篇在博客园地址https://www.cnblogs.com/bbqzsl/p/18370679

如何sniff mojoIPC

借用Chrome.IPC.Sniffer。改造成WeChat.IPC.Sniffer。虽然Chrome.IPC.Sniffer是C#项目,但主要也是在使用Win32API,主逻辑改造成c++也不难。程序主要使用tdevmonc.sys进行sniff。程序扫描chrome.dll,找出mojom相关服务名称,以及它们的method hash name。然后跟代码仓的mojom文件进行映射生成映射表。Chrome.IPC.Sniffer提供了wireshark的lua分析脚本。知道这些就可以按需求进行重构。扫描mmmojo.dll, wmpf_host_export.dll, WeChatAppEx.exe,释出它家自身的服务接口。找出WeChatAppEx.exe对应的chromium版本,同步更新lua脚本。在lua脚本添加自己的分析。

如何trace mojoIPC,拦截IOCP。

除了sniff mojoIPC外,还可以通过对每一个mojo进程进行io拦截,并跟踪。

现在先来讲通过拦截io跟踪mojoIPC,后面再继续讲sniff mojoIPC。

windows平台的mojo使用IOCP进行overlapped操作。所以理论上trace IOCP基本上可以trace到channel的所有数据。对于mojo设定的NamedPipe,WriteFile一定会block pending,在IOCP得到完成事件。但是ReadFile不然,却既可能立即成功,也可能产生pending。这是我在实际trace中发现到的。换言之,只trace IOCP的话,只能得到部分的PendingRead,以及所有的Write。所以必须trace Read跟IOCP。CompletionKey就是ChannelWin,保存着临时的Pending Buffer。Overlapped分别有ReadContext或WriteContext,这样就可以区分完成的操作是读还是写。ReadContext使用CircularBuffer,取出有效Buffer则可。WriteContext使用Buffer Pointer List,取队列首Buffer。保存起来就可以录像,数据库可以使用一些类mongodb的轻量数据库。或者通过一条NamedPipe,将Buffer写成WireShark Frame,结合Chrome.IPC.Sniffer,输出到WireShark并保存。WireShark方便又有可视化工具,缺点是携带额外信息有限,并且不能支持DataPipe,因为没有办法sniff共享内存。而trace IOCP的话,可以使用类mongodb的文档记录,则可灵活携带各种信息。同时还可以自行读取出DataPipe的SharedBuffer内容。需要注意的一点是,一次读写操作,可能打包多于一个Message。所以必须拆解。这种方法,没有现成的代码或工具,所有一切都要自己去完成,工作量不少。另外tdevmonc.sys有一个不足,捕捉的操作其实是不能准确分辨数据读还是写操作。自己用tdevmonc.sys实现过主逻辑就清楚。Chrome.IPC.Sniffer只是巧,只将tdevmonc.sys标记为Read的数据包记录。但实际上,同一个数据包必须应该被捕捉两次操作,一次写入管道,一次读出管道,然而有时是会漏掉其中一个的,所以只记录Read或Write时,必然会漏掉一些数据包。说它分辨不清Read跟Write,是因为Read跟Write两次操作的源进程id往往是一样的。如果pidA_write, pidB_Read。这样是明确的。但是,pidA_write,pidA_Read,你说这数据是从A到B,还是从B到A。怎么也不可能是从A到A,因为这个pipe由A跟B连接的。这是一个很至命的问题,并且不能解决掉。

另外,通过拦截Io来跟踪mojoIPC,能够获得实时的mojoIPC信息,主要是PlatformHandle,例如SharedBuffer,DataPipe所包括的共享内存。拦截跟踪串行在所有的mojoIPC操作中了。毕竟sniff只捕捉到瞬时残象。PlatformHandle在搬运过程不断地DuplicateHandle并紧接着关闭。当到你分析sniff数据时,所有PlatformHandle都可能已经变换重新它用了。如果需要准确地捕捉所有SharedBuffer或DataPipe通信的内容,似乎除了拦截Io,并无它选。后面在讲解sniff mojoIPC捕捉https时,我会用实例分析sniff可以做到哪种程度。

不得不吐一下,python因为GIL,多线程变得有够不(鸡)给(肋)力。原本为了方便,用ctypes代替JIT-C来使用。但是用在patch代码,尤其是多线程时,真是一个大坑。性能不说,死锁才气死人。cpython中GIL无处不在。python适合多进程大于多线程。如果只是执行python脚本,问题可能不大,因为仍然在系统闭环内。一但用ctypes进行跨越语言边界调用,并且多线程并发并行使用ctypes的时候,问题就会大条起身。就算只用两个线程并发用ctypes调用DebugLogStr打印固定内容,都会死锁。我用在patch密集的IO线程池代码时,即刻摔(仆)跤(街)。我又不得不将代码迁移回c/c++代码。毕竟python有太多轮子可以用,要不是问题大条到死锁不能用,我还是会忍受着运行效率。幸好我还开发了JIT-C相类似的功能。

下图演示,通过我的JustInTimeC生成执行代码,对Iocp进行patch,跟踪mojo数据。

下图演示,将跟踪到的数据进行分析。

下图演示,将跟踪到的数据,写到WireShark,让WireShark的lua脚本去分析。

 

如何通过sniff mojoIPC,抓包HTTPS。

截获https请求,网上有分享《基于微信PC端小程序抓包方法》通过搭建代理服务器,由代理服务器Burp做手脚,中间人攻击(MITM)或SSL拆分等的其它同类工具。

现在回到我的本篇,并不使用http(s)代理工具,chrominum存在另一溪径。在https通信之上,还有一层mojom服务。这些服务将用户的URL请求,转化成网络请求,并将网络响应数据,通过服务通知用户。所以我就可以绕开网络,直接sniff到明文请求,明文数据。对吧。

如果只是sniff请求的话,通过mojo也可以。只要理解了chrome的架构,就可以知道network也是一个子服务,要有一个宿主去运行。就像COM要有一个dllhost进程。只要sniff到network服务的mojo,不就可以截获https请求了吗,这是可以确定的。所有URL请求都通过URLLoaderFactory.CreateLoaderAndStart()方法调用network服务。URL响应由URLLoaderClient.OnReceiveResponse进行通知。响应一般都会采用DataPipe进行数据传递,所以sniff mojoIPC无能为力获得响应内容。这就跟CEF的处理流程是一致的。在CEF中,CefClient的CefRequestHandler,当Renderer处理脚本时需要加载href或url请求时,就会触发Browser的CefRequesthandler。同理地,chrominum中,Renderer向Browser请求处理URL请求,如果scheme需要网络请求,Browser就会转给network服务处理,否则自己处理。Browser再将响应结果传送给Renderer。

囗讲无凭,Now I show you how。

"network.mojom.URLRequest": {
    "definition": "struct URLRequest { string method; url.mojom.Url url; SiteForCookies site_for_cookies; bool update_first_party_url_on_redirect; url.mojom.Origin? request_initiator; array<url.mojom.Url> navigation_redirect_chain; url.mojom.Origin? isolated_world_origin; url.mojom.Url referrer; URLRequestReferrerPolicy referrer_policy; HttpRequestHeaders headers; HttpRequestHeaders cors_exempt_headers; int32 load_flags; int32 resource_type; RequestPriority priority; CorsPreflightPolicy cors_preflight_policy; bool originated_from_service_worker; bool skip_service_worker; bool corb_detachable = false; RequestMode mode; CredentialsMode credentials_mode; RedirectMode redirect_mode; string fetch_integrity; RequestDestination destination; URLRequestBody? request_body; bool keepalive; bool has_user_gesture; bool enable_load_timing; bool enable_upload_progress; bool do_not_prompt_for_login; bool is_outermost_main_frame; int32 transition_type; int32 previews_state; bool upgrade_if_insecure; bool is_revalidating; mojo_base.mojom.UnguessableToken? throttling_profile_id; mojo_base.mojom.UnguessableToken? fetch_window_id; string? devtools_request_id; string? devtools_stack_id; bool is_fetch_like_api; bool is_favicon; RequestDestination original_destination; TrustedUrlRequestParams? trusted_params; mojo_base.mojom.UnguessableToken? recursive_prefetch_token; TrustTokenParams? trust_token_params; WebBundleTokenParams? web_bundle_token_params; array<SourceType>? devtools_accepted_stream_types; NetLogSource? net_log_create_info; NetLogSource? net_log_reference_info; IPAddressSpace target_ip_address_space;};",
"network.mojom.URLLoaderFactory.CreateLoaderAndStart": {
    "definition": "CreateLoaderAndStart(pending_receiver<URLLoader> loader, int32 request_id, uint32 options, URLRequest request, pending_remote<URLLoaderClient> client, MutableNetworkTrafficAnnotationTag traffic_annotation);",
"network.mojom.URLLoaderClient.OnReceiveResponse": {
    "definition": "OnReceiveResponse(URLResponseHead head, handle<data_pipe_consumer>? body, mojo_base.mojom.BigBuffer? cached_metadata);",

URLLoaderFactory.CreateLoaderAndStart(),接受参数URLRequest跟remote<URLLoaderClient>。URLRequest可以分析,URLLoaderClient用于异步回调传送响应数据。URLLoaderClient.OnReceiveResponse将响应数据的body用DataPipeConsumer作为载体。

下图是URLLoaderFactory.CreateLoaderAndStart()抓包,参数分析。 xweb_xhr, URL,header-fields,URL-argurments,Cookie。

现在我再将Chrome.IPC.Sniffer,升级成支持DataPipe.Sniffer。抓包URLLoaderClient.OnReceiveResponse()。抓包js,json数据,PNG文件,视频流等。

 

从wireshark不难看出,有大于一半的DataPipe没有能够成功sniff出来。我在上面过滤出来的OnReceiveResponse(...)[+1 native handle],就意味着有一个DataPipe。能够成功sniff的,我都将其内容生成一个新包RawBuffer。而sniff失败的原因,是因为DataPipeDispatcher的序列化同时包含一个sharedMemory的句柄的序列化,我必须要将这个句柄DuplicateHandle出来备用,才能在每次监测到写控制时读得内容。但是mojo也在DuplicateHandle,并且同时关闭源句柄。这就是在跟它比速度啊。但是这个DuplicateHandle,mojo是十分频繁地在做的。如果一秒内只有几个还好,实际上却是几十,100地频繁做。

下图展示WeChatAppEx--type=Browser进程的DuplicateHandle次数统计。

 在开发过程发现,MergePort可以将自身以外的两个node之间建立MessagePipe连接。DataPipeConsumer还可以由中间人转发给其它node。试想一下,有这么一条数据流,A->me->B。如果me只是将数据转发,并且两两之间是用DataPipe进行数据传送,那么me将DataPipeConsumer传递给B使用,那就高效得多了。

下图展示,Utility--type=network原本将网络数据发送给Browser,由Browser转发送到Render-4的。变成Browser为Utility跟Render-4牵手,把DataPipeConsumer交给Render-4,之后Utility跟Render-4直接联谊。

 

 

如何打开小程序的devtools。

github有开源项目WeChatOpenDevTools。原理是,拦截WeChatWin.dll调用CreateProcessW,追加命令行选项--enable-xweb-inspect。拦截WeChatAppEx启动小程序的函数入口,修改Json选项,添加enable_vconsole:true。还是修改WeChatAppEx一处菜单项相关的字符串为DevTools。要部署新的东西太麻烦,用WinDbg代行所有流程后,发现是可行的。然后在逆向分析,我又发现这些都由WeChat进程配置控制的。enable-xweb-inspect由wmpf_host_export.dll一个全局json控制,vconsole由AppletPkgDownLoadMgr的属性控制。只要修改这两处,就可以顺利开启vconcole,并且不需要修改菜单项字符串以及其它奇奇怪怪的东西。Mini-Program,这个名字有点chinglish。在它们的代码命名也好像不用Mini-Program。Applet才是它的本名。凡是Game跟Applet相关的类,服务,函数等,基本都是跟小程序相关的。就连公众号也是在使用WMPF,不过就没有vconsole可用。不管怎么,vconsole这是一个它家做出来让人用的功能,我们做的事情只是调它出来用,应该不违反什么。

Wmpf是一个完整的Chrome。WeChat.exe将Browser进程实现成它的一个子服务进程,XPlugin WMPF就是通过wmpf_host_export.dll的MojoEmbedder启动Wmpf的Browser进程,并建立Ipc关系。WeChat作为WmpfHost角色。Browser如何打开小程序,明白了交互的过程,就清楚谁控制了vconsole。当Browser的小程序图标被点击时,onclick事件在Render处理js脚本,需要打开小程序,就会调用Browser的服务,Browser调用WeChat的身为WmpfHost的服务。这就十分明显,是由WeChat控制了vconsole的权限。

下图是85xx版的WMPF-Applet,有vconsole,没有devtool

下图是112xx版的WMPF-Applet,不但有vconsole,还有devtool。只能看到请求的url,看不到返回数据,以及请求相关的头部字段。但是Applet的DevTool还是被阉割限制了不少功能。

下图是WMPF的主页,它的DevTool是最完整的,但只允许调试主页。

 

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

 

逆向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-09-09 23:04  bbqz007  阅读(16)  评论(0编辑  收藏  举报