思路:打开WeChatAppEx的调试日志
技术选型
- 免维护:我使用的是8391版本的WeChatAppEx。考虑到我们以后估计也会经常调试小程序,我设计的时候希望能尽可能通用。
- 方便挂载调试器:我们希望能在一开始就attach上debugger。而WeChatAppEx的初始化速度很快,如果十几秒内没有响应,微信就会重启一个新的WeChatAppEx,如果重启5次之后都不成功,那么就必须重启微信来恢复小程序功能。所以我们既不能sleep太久,也不能不sleep。
在Windows上面跨二进制版本的hook一般就frida或者dll注入,考虑到我们希望在程序一开始运行的时候,所以我选了dll注入。
让微信不删除第三方dll
WeChatAppEx用了dxgi.dll,这个不在KnownDll里面,可以用来注入。
但是我写了dxgi.dll到同目录下,发现文件总是被删除。初步看了一下,file_resources.xml里面规定了文件的filter规则。我尝试简单增加对应的entry并改上md5,但没有效果。随后尝试用python打开文件来锁定,避免被删除,WeChatAppEx则直接拒绝启动。
想了一下,估计是得patch。用Process Monitor看了一下
可以看出删除的行为发生在WeChatWin.dll里面。简单逆了一下,是XPlugins的初始化逻辑里面删除了文件,我懒得逆了,直接用detours hook了kernelbase.DeleteFileW,判断路径里面如果有XPlugins就直接返回。
寻找关键patch逻辑
WeChatAppEx log的逻辑是这样的,如果g_logCallback不存在或者返回空,那么就会按照g_debugFlag的标志位,相应输出到OutputDebugString、日志文件、stderr里面
自动化寻找patch位置(patchfinder) – 寻找函数
由于这个函数比较大,pattern比较复杂,所以我们交叉引用一下g_debugFlag可以找到另一个比较简单的引用位置
这个函数的末尾会使用debug.log这个字符串
patchfinding的时候,我们呢直接找到debug.log这个字符串,寻找到他的交叉引用。
引用字符串的时候这个程序里面全是用的lea,lea的offset和call类似,也是末尾四个字节 + 下一条指令地址 == 引用位置。找了一圈没有找到好用的轮子,所以自己搓了一个。
流程是这样的:
- 找到”debug.log”的位置,转为VA
- 然后对着每个位置用上面的条件判断
- 判断对了之后,向上找CC CC CC CC 这样的int3序列标识的函数对齐字节,这样就能找到这个函数的开头
自动化寻找patch位置(patchfinder) – 寻找函数内引用的全局变量
到这里还没结束,由于我们要找的是个全局变量,我们还得解析执行来找到他具体的引用位置来算出来地址。
很明显我们只要找到test byte ptr XXX的指令就可以找到g_debugFlag了,那么我们用Capstone反汇编一下就可以了。
提取出来op_str里面的偏移加上地址就可以拿到最终的地址了
完成DLL注入
上面提到的4个控制log状态的flag其实是连在一起的。考虑到除非改源码里面的声明顺序,否则全局变量一般顺序不会变,所以我们就只用做g_debugFlag这一个的偏移,然后剩下的靠加加减减算出来就可以了
考虑到这些flag和callback在启动过程中会有变化,所以我直接写了个循环来跑,持续把g_debugFlag置位、g_logLevel降低、g_logCallback清零
编译好之后,mklink我们自己的dll到dxgi.dll,原始的C:\Windows\System32\dxgi.dll到dxgi_.dll就可以了
实现等待debugger
WeChatAppEx实际上就是个chromium,我们可不希望渲染进程也等着debugger,所以判断一下,只在父进程等debugger
思路:控制小程序的输入参数来允许patch wxapkg、允许devtool
跳过PC端小程序的wxapkg加密
wxapkg的加密机制很有标志性,直接搜索加密的header V1MMWX即可找到相关逻辑。经过检查每一个的decrypt的位置都会判断这个header,如果没有这个header他自动便会跳过解密。
跳过wxapkg的MD5校验
虽然加密可以跳过,但是换上去还是会报错资源损坏。
但是,我们有了日志,所以wxapkg的校验位置就很好找了。可以看出日志中的correctMd5相关的输出告诉我们check failed。
根据字符串找到函数,这个函数比较长,仔细看逻辑可以发现当且仅当correctMd5为空或不存在的时候才会跳过MD5检查。
这里面的md5都是存在两层结构体里面的,不好patch,所以我们需要向上找
寻找wxapkg配置的下发逻辑
我们在debugger里面断下来,配合x64dbg的stack trace和ida的xref一起看可以发现他md5是这么一路传进来的(从下到上):
- AppletPackage::loadModule
- AppletRuntime::InitAppletPkg
- AppletRuntime::ParseInitConfig(从类结构体里面取出来进一步使用)
- RealLaunchPreloadRuntime(将参数复制到类结构体里面)
- AppletRuntimeManager::LaunchApplet(this, appid, init_config [这就是我们的参数] )
所以,我们可以从AppletRuntimeManager::LaunchApplet或RealLaunchPreloadRuntime入手。我选择了AppletRuntimeManager::LaunchApplet,这样可以保证配置不会被复制到别处,导致我们的修改不生效。
理解传入配置
AppletRuntimeManager::LaunchApplet有两个xref,其中一个导向了一个类似于factory的结构
这个显然无法帮助我们理解,两边共同出现的函数就是CloneAppletInitConf,负责clone一份config出来。这个可以帮助我们还原大部分的结构体成员。
尝试使用debugger inspect config内容
我第一个考虑的是使用debugger来看每个string的内容都是啥。这个WeChatAppEx有146M,所以用IDA自带的Debugger来调试几乎肯定会卡死,而且会导致数据库被rebase。考虑到这个问题,我不得不使用x64dbg。
但是这个struct里面有几十个std_string,而这个版本的std string是需要判断是否是stack string的,所以直接看memory view很费劲。
我初步查了一下x64dbg是支持struct的。我照葫芦画瓢,测试了一下,发现支持std_string所使用的union。所以我直接把这个结构体丢进去了:
ParseTypes XXX.h EnumTypes VisitType AppletInitConfig
这么操作下来可以很明显的看见结构体确实是进去了, 但是窗口却死活出不来。我在菜单栏里面翻了许久,完全找不到。最后发现是在这里(电子竞技果然不需要视力)
这么一波操作下来之后,没有达到我预想的效果(类似于010 editor字符串全都自动显示出来那种)
不过右键自己跳到memory view然后再跳一层也总比啥都没有强。
最后看下来,60%的字符串都是空的字符串,推断不出来什么意义。
意外收获:找到了config结构体的deserializer
查看AppletRuntimeManager::LaunchApplet的另一个xref:
点进去发现是取出来每个property
这样就能一路还原完完整的struct
struct __declspec(align(8)) AppletInitConfig { std_string appId; std_string brandName; std_string iconUrl; _DWORD debugType; _BYTE gap_4C[4]; std_string orientation; std_string pkgDirPath; std_string publicPkgDirPath; _DWORD publicVer; _BYTE gap_9C[4]; std_string moduleListInfo; std_string dataPath; std_string tmpPath_or_appIconPath; std_string username; std_string nickName; std_string signature; std_string logPath; std_string clientJsExtInfo; std_string operationInfo; std_string shareName; std_string shareKey; std_string remote_debug_endpoint; _DWORD appVersion; int versionState; _WORD width; __int16 height; int sysBtn; _DWORD launchScene; _BYTE gap_1D4[4]; std_string appId2; std_string extraData; std_string privateExtraData; std_string messageExtraData; std_string url; std_string agentId; _DWORD sourceType; _BYTE gap_26C[4]; std_string openapiInvokeData; std_string transitiveData; std_string enterPath; _DWORD originalFlag; _BYTE gap_2BC[4]; std_string originalRedirectUrl; _BYTE isNativeView; _DWORD version_type; std_string field_2E0; std_string uin; std_string deviceType; _DWORD clientVersion; _BYTE isTest; char isPreload; char field_32E; char field_32F; std_string wxIconUrl; std_string wxNickName; _DWORD productId; _DWORD gap_364; std_string commonJsInfo; _BYTE isMiniGame; _BYTE gap_381[7]; std_string ozone_platform; std_string pluginDir; std_string hostAppId; };
自动化patchfinder
类似于上面找g_debugFlag的逻辑,我们直接拿”LaunchApplet init_config.productId”去找就行
找完之后我们发现,这个函数开头没有CC的align(因为这个函数恰好就是对齐的),所以我们改成了同时用C3(retn)和CC来判断函数开头
使用Frida来改写传入配置
用frida hook上之后,套一层try catch对着每个8字节对齐地址去读stdstring,能读出来这些字符串
LaunchApplet! wx6eefcfd533448458 got str: wx6eefcfd533448458 got str: http://mmbiz.qpic.cn/mmbiz_png/JicUuN5XqU396yST0kgCs14GiaP3S8FMh5YjP8FazplXgawibjOep5D47zE3vhgwiaKchBmPLztpSjCHumkWMBAFBg/640?wx_fmt=png&wxfrom=200 got str: E:\WXFileRecv\WeChat Files\Applet\wx6eefcfd533448458\22\ got str: E:\WXFileRecv\WeChat Files\Applet\publicLib\318\clientPublicLib.wxapkg got str: [{"independent":false,"md5":"150dc46169dbec55f62d7992474b3279","name":"/subpackages/main3/"},{"independent":false,"md5":"8f1dc195d6b94bda201816d4fc9fd7be","name":"/subpackages/main2/"},{"independent":false,"md5":"4e3bd83ff8900f875b17eeebbd934c2f","name":"/subpackages/main1/"},{"independent":false,"md5":"15f849542615f20ef6c29692c014da93","name":"/subpackages/main/"},{"independent":false,"md5":"a5933d359e3d7985253283cdeccf6599","name":"/subpackages/wx/"},{"independent":false,"md5":"648658f39959782a1d4a0b0f27c9266c","name":"__APP__"}] got str: E:\WXFileRecv\WeChat Files\wxid_wpxxxxxxxxxxxx\Applet\ got str: E:\WXFileRecv\WeChat Files\wxid_wpxxxxxxxxxxxx\FileStorage\Cache\2023-08\8e70c81429b77a1df6f13d646ebc628e.jpg got str: gh_9965d5174ba6@app got str: 火源战纪 got str: 我本是一个小小火柴人,为打败那个背带裤猎人加入王者骑士团,每天冒险是砍砍咸鱼挂挂机,今天遇上他,握紧手中武器,或许这次我能赢 got str: C:\Users\Misty\AppData\Roaming\Tencent\WeChat\log\ got str: {"call_plugin_info":[{"alias":"MiniGameCommon","contexts":[],"inner_version":23,"md5_list":[],"plugin_id":"wxaed5ace05d92b218","plugin_user_name":"\u5c0f\u6e38\u620f\u5b98\u65b9\u63d2\u4ef6","plugin_version":1001011,"semver":""}],"user_version":"1.26437.26800"} got str: {"apiAvailable":{"authorize":0,"bluetoothContactAndCanlanderNeedAuth":0,"gameSceneFromMyApp":0,"getUserInfo":0,"h5PayDisableForward":0,"navigateToMiniProgram":1,"navigateToMiniProgramConfig":0,"openData":0,"openSetting":0,"screenCanvasReadPixelsFreely":0,"share":0,"shareCustomImageUrl":1},"bgKeepAlive":{"music":0},"jumpWeAppFromLongPressCodeBanInfo":{"banJumpApp":0,"banJumpGame":0},"misc_ban_info":{"minigame_freeze_status":0},"privacy":{"banGetWifiListIfEmptyDesc":1,"banLocationIfEmptyDesc":1},"wxaAppSwitch":{"userProfile":1},"navigate_ban_info":{"navigate_ban_rule_list":[],"do_report":0},"op_info":{"grow_protect":1},"warning_info":{"jsapi_alter":[]}} got str: filehelper got str: 357394753 got str: Windows%2011%20x64 got str: https://wx.qlogo.cn/mmhead/ver_1/.............. got str: Misty got str: {"SubContextImgDomain":["https://wx.qlogo.cn/","https://thirdwx.qlogo.cn/"],"enable_vconsole":false,"frameset":false,"isAttrSync":false,"isStar":false,"is_game_debug":false,"landscape":false,"mini_game_config_switchs":"show_fps_info=0 ","mini_game_debug_port_number":11223,"mini_game_debug_proxy_url":"","mini_game_launch_from_file":false,"needCloseAni":true,"pluginDir":"E:\\WXFileRecv\\WeChat Files\\Applet\\","pluginList":"[{\"md5\":\"e3fc805ffe6ad46fe26fa6c89c8cc841\",\"name\":\"wxaed5ace05d92b218\",\"prefix_path\":\"__plugin__/wxaed5ace05d92b218\",\"version\":23}]\n","preScene":1,"preScene_note":"wxid_wpxxxxxxxxxxxx","proxy":"","resizable":false,"scene_note":"filehelper:0_wx6eefcfd533448458_9453b50c40d97628e23db9246fd72a2d_1692909890_0","sessionId":"SessionId@357394753_6#1692991585814","show_image_debug_info":false,"startClickTime":1692991585815.0,"usedstate":2,"versionType":0}
懒得去写std string那套resize、alloc、free逻辑了,直接要求修改的字符串长度小于等于原来的长度,省事++(send是Fermion的console.log),offset从上面的patchfinder拿
var base = Process.enumerateModules()[0].base function readStdString(s) { var flag = s.add(23).readU8() if (flag == 0x80) { // heap var size = s.add(8).readUInt() return s.readPointer().readUtf8String(size) } else { // stack return s.readUtf8String(flag) } } function writeStdString(s, content) { var flag = s.add(23).readU8() if (flag == 0x80) { // heap var orisize = s.add(8).readUInt() if (content.length > orisize) { throw "must below orisize!" } s.readPointer().writeUtf8String(content) s.add(8).writeUInt(content.length) } else { // stack if (content.length > 22) { throw "max 23 for stack str" } s.writeUtf8String(content) s.add(23).writeU8(content.length) } } var pvLaunchAppletBegin = base.add(0x301663c) Interceptor.attach(pvLaunchAppletBegin, { onEnter(args) { send("LaunchApplet! " + readStdString(args[1])) for (var i = 0; i < 0x400; i+=8) { try { var s = readStdString(args[2].add(i)) if (s) { send("got str: " + s) } var s1 = s.replaceAll("md5", "md6").replaceAll('"enable_vconsole":false', '"enable_vconsole": true') if (s === s1) { } else { send("changing to str: " + s1) writeStdString(args[2].add(i), s1) } } catch (a) { } } } }) send("loaded!")
恢复为满血 Devtool
默认小程序给的devtool是受限版的,没有办法打断点调试,但是Search和Quick source可以看里面的内容。
简单配合日志逆了一下,发现他是直接把整个devtool frontend存在了程序里面,用的url是https://applet-debug.com/devtools/wechat_app.htm
本来打算把整个devtool frontend换掉的, 但是找的过程中发现WeChatAppEx里面不止有wechat_app.html,还有wechat_web.html
抱着试一试又不亏的想法,直接在x64dbg里面patch了一下,发现真的可以,这下大家都方便了哈哈。