在电脑上直接调试微信WxApkg小程序

思路:打开WeChatAppEx的调试日志

技术选型

  1. 免维护:我使用的是8391版本的WeChatAppEx。考虑到我们以后估计也会经常调试小程序,我设计的时候希望能尽可能通用。
  1. 方便挂载调试器:我们希望能在一开始就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看了一下
在电脑上直接调试微信WxApkg小程序
可以看出删除的行为发生在WeChatWin.dll里面。简单逆了一下,是XPlugins的初始化逻辑里面删除了文件,我懒得逆了,直接用detours hook了kernelbase.DeleteFileW,判断路径里面如果有XPlugins就直接返回。
在电脑上直接调试微信WxApkg小程序

寻找关键patch逻辑

WeChatAppEx log的逻辑是这样的,如果g_logCallback不存在或者返回空,那么就会按照g_debugFlag的标志位,相应输出到OutputDebugString、日志文件、stderr里面
在电脑上直接调试微信WxApkg小程序

自动化寻找patch位置(patchfinder) – 寻找函数

由于这个函数比较大,pattern比较复杂,所以我们交叉引用一下g_debugFlag可以找到另一个比较简单的引用位置
在电脑上直接调试微信WxApkg小程序
这个函数的末尾会使用debug.log这个字符串
在电脑上直接调试微信WxApkg小程序
patchfinding的时候,我们呢直接找到debug.log这个字符串,寻找到他的交叉引用。
引用字符串的时候这个程序里面全是用的lea,lea的offset和call类似,也是末尾四个字节 + 下一条指令地址 == 引用位置。找了一圈没有找到好用的轮子,所以自己搓了一个。
在电脑上直接调试微信WxApkg小程序
流程是这样的:

  1. 找到”debug.log”的位置,转为VA
  1. 然后对着每个位置用上面的条件判断
  1. 判断对了之后,向上找CC CC CC CC 这样的int3序列标识的函数对齐字节,这样就能找到这个函数的开头

自动化寻找patch位置(patchfinder) – 寻找函数内引用的全局变量

到这里还没结束,由于我们要找的是个全局变量,我们还得解析执行来找到他具体的引用位置来算出来地址。
在电脑上直接调试微信WxApkg小程序
很明显我们只要找到test byte ptr XXX的指令就可以找到g_debugFlag了,那么我们用Capstone反汇编一下就可以了。
在电脑上直接调试微信WxApkg小程序
提取出来op_str里面的偏移加上地址就可以拿到最终的地址了

完成DLL注入

上面提到的4个控制log状态的flag其实是连在一起的。考虑到除非改源码里面的声明顺序,否则全局变量一般顺序不会变,所以我们就只用做g_debugFlag这一个的偏移,然后剩下的靠加加减减算出来就可以了在电脑上直接调试微信WxApkg小程序
考虑到这些flag和callback在启动过程中会有变化,所以我直接写了个循环来跑,持续把g_debugFlag置位、g_logLevel降低、g_logCallback清零
在电脑上直接调试微信WxApkg小程序
编译好之后,mklink我们自己的dll到dxgi.dll,原始的C:\Windows\System32\dxgi.dll到dxgi_.dll就可以了

实现等待debugger

WeChatAppEx实际上就是个chromium,我们可不希望渲染进程也等着debugger,所以判断一下,只在父进程等debugger
在电脑上直接调试微信WxApkg小程序

思路:控制小程序的输入参数来允许patch wxapkg、允许devtool

跳过PC端小程序的wxapkg加密

wxapkg的加密机制很有标志性,直接搜索加密的header V1MMWX即可找到相关逻辑。经过检查每一个的decrypt的位置都会判断这个header,如果没有这个header他自动便会跳过解密。

跳过wxapkg的MD5校验

虽然加密可以跳过,但是换上去还是会报错资源损坏。
但是,我们有了日志,所以wxapkg的校验位置就很好找了。可以看出日志中的correctMd5相关的输出告诉我们check failed。
根据字符串找到函数,这个函数比较长,仔细看逻辑可以发现当且仅当correctMd5为空或不存在的时候才会跳过MD5检查。
在电脑上直接调试微信WxApkg小程序
这里面的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的结构
在电脑上直接调试微信WxApkg小程序
这个显然无法帮助我们理解,两边共同出现的函数就是CloneAppletInitConf,负责clone一份config出来。这个可以帮助我们还原大部分的结构体成员。
在电脑上直接调试微信WxApkg小程序

尝试使用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

这么操作下来可以很明显的看见结构体确实是进去了, 但是窗口却死活出不来。我在菜单栏里面翻了许久,完全找不到。最后发现是在这里(电子竞技果然不需要视力)
在电脑上直接调试微信WxApkg小程序
这么一波操作下来之后,没有达到我预想的效果(类似于010 editor字符串全都自动显示出来那种)
在电脑上直接调试微信WxApkg小程序
不过右键自己跳到memory view然后再跳一层也总比啥都没有强。
最后看下来,60%的字符串都是空的字符串,推断不出来什么意义。

意外收获:找到了config结构体的deserializer

查看AppletRuntimeManager::LaunchApplet的另一个xref:
在电脑上直接调试微信WxApkg小程序
点进去发现是取出来每个property
在电脑上直接调试微信WxApkg小程序
这样就能一路还原完完整的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”去找就行
在电脑上直接调试微信WxApkg小程序
找完之后我们发现,这个函数开头没有CC的align(因为这个函数恰好就是对齐的),所以我们改成了同时用C3(retn)和CC来判断函数开头
在电脑上直接调试微信WxApkg小程序
在电脑上直接调试微信WxApkg小程序

使用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可以看里面的内容。
在电脑上直接调试微信WxApkg小程序
简单配合日志逆了一下,发现他是直接把整个devtool frontend存在了程序里面,用的url是https://applet-debug.com/devtools/wechat_app.htm
在电脑上直接调试微信WxApkg小程序
本来打算把整个devtool frontend换掉的, 但是找的过程中发现WeChatAppEx里面不止有wechat_app.html,还有wechat_web.html
在电脑上直接调试微信WxApkg小程序
抱着试一试又不亏的想法,直接在x64dbg里面patch了一下,发现真的可以,这下大家都方便了哈哈。
在电脑上直接调试微信WxApkg小程序

给TA打赏
共{{data.count}}人
人已打赏
技术文档

微信小程序改包调试patch方案

2023-12-22 20:22:18

技术文档

某解析wasm逆向

2024-1-15 16:08:48

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
有新私信 私信列表
搜索