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

大类
逆向
iOS
Windows
技术标签
开发-HookPatch-Windows
开发-HookPatch-iOS
逆向-iOS
逆向-Windows
优先级
Low
开始日期
Feb 12, 2023
状态
Maintaining
Public
Public
最后更新
Feb 24, 2023

微信小程序的格式

WxApkg格式

格式很简单,header + file信息区域 + file内容区域
从这里可以找到010 editor的bt模板:https://www.52pojie.cn/thread-718025-1-1.html
// Define structures to parse program of weixin local int warnings = 0; local string temp_warning; // A hack to get warning messages to both "Warn" (show in status) and output to the "output" window void PrintWarning(string message) { Warning(temp_warning); // Ensure new-line, "Warning" statuses should not have them SPrintf(temp_warning, "%s\n", message); Printf(temp_warning); // Hack to trigger a more generic "look at warnings in output" warnings++; } //struct of dataIndex typedef struct{ int fileNameLen <comment="length of filename">; char fileName[fileNameLen] <comment="file name">; int fileOffset <comment="file offset">; int fileLen <comment="file size">; } dataIndex_table <read=DataIndexName, optimize=false>; string DataIndexName(dataIndex_table &dataIndex){ return dataIndex.fileName; } struct { BigEndian(); struct { byte magic1 <comment="magic number 1, should be -66">; int unknow <comment="unknow">; int offsetLen <comment="offsetLen">; int bodyDataLen <comment="bodyDataLen">; byte magic2 <comment="magic number 2, should be -19">; int fileCount <comment="file count">; if(magic1 != -66 || magic2 != -19){ PrintWarning("Invalid wxapkg file"); return -1; } } header; local quad dataIndex_offset = 1 + 4 + 4 + 4 + 1 + 4; if(file.header.fileCount > 0){ FSeek(dataIndex_offset); struct{ dataIndex_table dataIndex_table_element[file.header.fileCount]; }data_header_table; } }file; // It's not really useful to see just the last warning, so inform us how many warnings we should see in output if(warnings > 1) { Warning("%d warnings have occured and logged to the output box!", warnings); } // This will make the template show "Template executed successfully." if(warnings != 0) { SPrintf(temp_warning, "%d warnings found, template may not have run successfully!", warnings); return temp_warning; ,}
 

PC微信上的WxApkg

 
PC微信上WxApkg位于WeChat Files\Applet\[wxid]\…,被加密,开头是V1MMWX,随后使用PBKDF2生成密钥并加密,涉及到saltiest等字符串:
可以用这个作为特征快速定位PC WxApkg逻辑
stdstr::init1((int **)&buffer, "V1MMWX"); v15 = HIBYTE(v36); if ( v36 < 0 ) { v15 = v35; encPart1Content = encPart1; } stdstr::append((unsigned __int8 *)&buffer, encPart1Content, v15);

PC微信的小程序patch

为何不能直接替换wxapkg

问题1:需不需要重新加密回去(不是问题)

根据IDA的结果,不需要。PC微信会自动检测header是不是V1MMWX,不是则无需解密
if ( fileutil_ReadFile(a2, v4, 7u) ) isEncrypt = isEncryptedWxapkg(v4, 7); else isEncrypt = 0;
bool __cdecl isEncryptedWxapkg(int a1, int a2) { return a2 >= 7 && (*(_DWORD *)a1 ^ 'MM1V' | *(unsigned __int16 *)(a1 + 4) ^ 'XW') == 0; }

问题2:替换回去之后会怎么样

微信报错小程序资源损坏,需要重新下载
 
⇒ 推测,微信内部肯定会检测wxapkg的checksum
搜索wxapkg字符串时,会时不时出现wxapkg_md5,故大概率是通过MD5进行校验。
 

逆向分析小程序 - 静态 (版本6500)

  • 信息
    • 根据上面说到的字符串V1MMWX和saltiest可以轻松定位到加密逻辑
    • 根据日志可以轻松判断出当前函数名称(尤其是file_util_win下的各个文件)
  • 定位加密函数
    • 搜索V1MMWX可以到达加密函数,仔细理解流程可以发现是将V1MMWX和一个1024字节的str拼接,随后写入到文件中
    • stdstr::init1((int **)&buffer, "V1MMWX"); v15 = HIBYTE(v36); if ( v36 < 0 ) { v15 = v35; encPart1Content = encPart1; } stdstr::append((unsigned __int8 *)&buffer, encPart1Content, v15); if ( a4 < 0x400 ) { doWriteFile: v23 = HIBYTE(v33); if ( v33 < 0 ) { v23 = len; p_buffer = buffer; } fileutil_WriteFile((int)wxapkgFilePath, p_buffer, v23);
    • 需要认识到的是,这是正向加密函数,但我们显然需要找逆向解密函数
      • 通过saltiest字符串寻找:仅2个xref,一个为加密函数,另一个函数则仅被一个函数调用,肯定为解密流程的一部分
      • 仔细阅读新发现的上层函数,可显然看出这个函数为解密函数
        • __int64 *__cdecl DecryptFileToBuf(__int64 a1, int a2, wchar_t *a3, char *a4) { // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND] v4 = HIBYTE(a2); if ( a2 < 0 ) v4 = HIDWORD(a1); v5 = 0; if ( v4 && (unsigned __int8)fileutil_PathExists((char *)a3) ) { v32 = 0i64; v33 = 0; if ( !(unsigned __int8)fileutil_ReadFileToString(a3, (int)&v32) ) goto LABEL_57;
      • 该函数被AppletPluginManager::loadPlugin、AppletPackage::loadModule、AppletPackage::loadPublicLibWxPkg等函数调用
    • 未能找到相应的MD5校验逻辑(其实就在下面的parsePkgFileContent函数中)
      • if ( (unsigned __int8)DecryptFileToBuf(v27, v28, (wchar_t *)v36, (unsigned __int8 *)v34) ) { stdstr::init1(v37, (char *)&PrefixString); parsePkgFileContent(v34, &a3, v36, v37);

逆向分析小程序 - 动态调试环境

背景:小程序的运行环境

小程序基于WeChatAppEx.exe运作,可以很明显的看出来这是个Chromium浏览器内核,那自然没有--type的进程就是主进程
notion image
类似于Chrome的标签页:
  • 似乎每个小程序仅仅是浏览器中的一个tab,所以打开/关闭小程序的时候,只有renderer进程会启动和退出
  • 小程序的JSAPI逻辑(wx.XXXX)、加载逻辑自然是实现在主进程里面
 

弯路:尝试映像劫持调试

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\WeChatAppEx.exe下新建Debugger键值
  • 尝试1:直接填写x32dbg
    • 失败:x32dbg直接加载都加载不起来,原因不明
  • 尝试2:通过Python3包一层来进行调用
    • 失败:x32dbg有特殊的命令行参数
      • x32dbg [exe path] [cmdline]
      • cmd /c "whoami && whoami"x32dbg C:\Windows\System32\cmd.exe "cmd.exe /c \"whoami && whoami\""
  • 尝试3:通过Python3重构命令行
    • 最开始尝试通过shlex.join,然而发现让只会输出单引号wrap的参数,而windows不支持单引号
      • 经过查询找到了 subprocess.list2cmdline,工作良好
      python3 -c "import sys; import subprocess; args = [r'E:\ScoopSpace\apps\x64dbg\current\release\x32\x32dbg.exe' , sys.argv[1], subprocess.list2cmdline(sys.argv[2:])]; subprocess.call(args)"
 
  • 结果:x32dbg能够正常使用命令行启动WeChatAppEx,但WeChatAppEx无法正常工作,持续闪退
    • 可能由于WeChatAppEx内部通过pid、handle进行通信,而x32dbg启动后PID并不是源程序而是调试器,导致inter process通讯失败。
 

解决:思考小程序的工作流

仔细回想小程序的运行环境,发现其实并不需要从WeChatAppEx一开始就开始debug,而是可以在小程序加载之前attach到主进程上进行调试。
 

逆向分析小程序 - 动态分析 获取日志

解锁日志输出

在上面静态分析的函数中有很多日志字符串,用类似cout的方式流式拼起来,然后传到了一个日志函数里
if ( CheckLogLevel(2) ) { sub_2531D20((char *)v13, "../../applet/applet/browser/mini_game_runtime_host.cc", 23, 2); AddLogString((int)v14, (int)"MiniGameRuntimeHost::MiniGameRuntimeHost this = ", 48); sub_57C580(this); DoLog(v13); }
我们x32dbg修改下CheckLogLevel的汇编让他一直返回true即可

输出日志

DoLog函数的具体实现如下:
int __thiscall DoLog(_DWORD *logstr) { // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND] *logstr = off_5E2303C; v1 = logstr + 2; v2 = *(_DWORD *)(logstr[2] + 4); v3 = -1; v30 = logstr + 2; if... v27 = v3; sub_24F8182(NumberOfBytesWritten); v4 = std::locale::use_facet((std::locale *)NumberOfBytesWritten, (struct std::locale::id *)&unk_6F1CAC8); v5 = (*(int (__thiscall **)(const struct std::locale::facet *, int))(*(_DWORD *)v4 + 28))(v4, 10); sub_24FA570(NumberOfBytesWritten); sub_57C9C0(v5); sub_57A6D0(v1); lpBuffer = (LPCVOID)-1; nNumberOfBytesToWrite = -1; v34 = -1; v6 = logstr + 3; lpOutputString = (const CHAR *)&lpBuffer; =>sub_57D17A(&lpBuffer); if ( (byte_6EAED88 & 0x19) != 0 ) { NumberOfBytesWritten[0] = (DWORD)logstr; NumberOfBytesWritten[1] = (DWORD)&lpBuffer; sub_25329E0(0, NumberOfBytesWritten); } if ( !dword_6F1D270 || !(unsigned __int8)dword_6F1D270(logstr[1], logstr[37], logstr[38], logstr[36], &lpBuffer) ) { v7 = dword_6F1D268; if ( (dword_6F1D268 & 2) != 0 ) { if ( v34 < 0 ) lpOutputString = (const CHAR *)lpBuffer; OutputDebugStringA(lpOutputString); v7 = dword_6F1D268; }
观察下面的OutputDebugStringA自然知道lpBuffer就是我们的格式化好的日志字符串
我们在sub_57D17A的返回处下断点,让x64dbg输出{s:[eax]}即可
💡
这种log point最好不要用软件断点,因为x64dbg需要每次断下后修改EIP和指令字节来恢复执行,并发数高的时候容易出现race condition导致EIP差1,程序直接就飞了 可以使用硬件断点来避免这个问题
 

逆向分析小程序 - 动态分析 找到精确wxapkg加载逻辑

通过call stack找

  • 前面的分析中Decrypt函数被4个地方调用,我们需要具体精确一下是哪个调用的。
  • wxapkg是从file_util系列函数处加载的,而file_util的读取文件走的是_wfsopen。通过比对wfsopen的参数就能知道加载的啥
    • 当参数为wxapkg时查看Call Stack即可
  • 找到具体的加载函数为AppletPackage::loadModule
 

通过日志分析找

WeChatAppEx原始日志
  • 有了日志就可以快速的分析了,查看日志搜索md5.wxapkgchecksum等字样可以以迅速发现校验的位置
notion image
  • 可以看到md5的检查发生在parsePkgFileContent中
    • correctMd5_content_1 = HIBYTE(v121); if ( v121 < 0 ) correctMd5_content_1 = HIDWORD(correctMd5); if ( correctMd5_content_1 && !*(_DWORD *)(sub_169B180((_DWORD *)*this) + 332) ) { if ( CheckLogLevel(2) ) { sub_2531D20((char *)&v122, "../../applet/applet/runtime/applet_package.cc", 191, 2); AddLogString((int)&v123, (int)"Need check md5", 14); DoLog(&v122); } v36 = HIBYTE(v121); if... v38 = HIBYTE(v119); if ( v119 < 0 ) { v18 = (int *)v117; v38 = v118; } if ( !(unsigned __int8)CompareStrNoCase(v18, v38, correctMd5_content, v36) ) { if ( CheckLogLevel(4) ) { sub_2531D20((char *)&v122, "../../applet/applet/runtime/applet_package.cc", 193, 4); v85 = AddLogString((int)&v123, (int)"parsePkgFileContent", 19); AddLogString(v85, (int)" check md5 failed,kill self ! ", 30); DoLog(&v122); }
 

逆向分析小程序 - 优雅的Patch检查

  • 分析这段逻辑,想要绕过有两个思路
    • 思路1:patch md5的比对,让其失效
      • 简单直接,绝对有效
      • 当版本更新的时候需要重新分析一遍WeChatAppEx
        • 问题是WeChatAppEx太大了,分析起来很慢
    • 思路2:让correctMd5为空
      • 如果能找到不依赖二进制的pattern,那么就非常舒服
  • correctMd5的来源
    • 在当前函数中看,correctMd5来源于this[135]查表,这种完全没法定位
    • 从日志中看,correctMd5对应的内容最早出现在AppletManager::Init
      • "[D][02-12 07:47:54.772][19152,31120][applet_manager.cc(139)] AppletManager::Init app_id_ = wx7a59aadcb728eca4; moduleListInfo = [{\"independent\":false,\"md5\":\"e0275c5cd469811468a54b6133a36c50\",\"name\":\"/playableDemo/\"},{\"independent\":false,\"md5\":\"b1f95a71311d11f09dcfe1a4cd88a56a\",\"name\":\"__APP__\"}]\n\n" "[D][02-12 07:47:54.773][19152,31120][applet_subpkg_mgr.cc(32)] AppletSubPkgMgr::init subInfos = [{\"independent\":false,\"md5\":\"e0275c5cd469811468a54b6133a36c50\",\"name\":\"/playableDemo/\"},{\"independent\":false,\"md5\":\"b1f95a71311d11f09dcfe1a4cd88a56a\",\"name\":\"__APP__\"}]\n\n" "[I][02-12 07:47:54.774][19152,31120][applet_plugin_manager.cc(37)] AppletPluginManager::Init pluginInfos = [ {\r\n \"md5\": \"644acbce66bd1f1c387b50e01bd74730\",\r\n \"name\": \"wx70d8aa25ec591f7a\",\r\n \"prefix_path\": \"__plugin__/wx70d8aa25ec591f7a\",\r\n \"version\": 24\r\n} ]\r\n; pluginDir = E:\\WXFileRecv\\WeChat Files\\Applet\\; 2AB30380\n"
    • 显然,md5是从json里面取出来的,所以肯定有"md5"这样一个字符串
      • IDA中搜索00 "md5" 00可以找到单独的md5字符串,找到了两个
      • 第一个有很多引用,第二个则仅仅用于TLS库中(忽略)
      • 在第一个的引用中,有几个就来自于 AppletSubPkgMgr::init 系列函数
        • stdstr::init1((int **)&v18, (char *)&PrefixString); sub_2CD3B11((int)&v34, v22, "name", v18, v19); if ( v28[11] < 0 ) sub_5D85270(*(int *)v28); v24 = &v20; *(_DWORD *)&v28[8] = DWORD2(v34); *(_QWORD *)v28 = v34; v19 = DWORD2(v34); stdstr::init1((int **)&v18, (char *)&PrefixString); v11 = v22; sub_2CD3B11((int)&v34, v22, "md5", v18, v19); if ( v29 < 0 ) sub_5D85270(*(int *)&v28[12]); v29 = DWORD2(v34); *(_QWORD *)&v28[12] = v34; LOBYTE(v31) = sub_2CD3957(v11, "independent", 0); v12 = v21[5];
        • 可以观察到 name md5 independent 等key出现
      • 其他的引用均是各种密码学算法列表中作为element,考虑到MD5已被淘汰,这些引用并不重要
    • 进一步探究moduleListInfo的来源,可以找到WeChatWin.dll中的单独md5字符串
      • md5、wxapkg_md5、name、independent等字样均一个函数中,且伴随着Json库函数调用,显然就是他负责和WeChatAppEx通讯
  • Patch的方法:在WeChatAppEx中直接修改md5为md6即可解决
    • 在WeChatWin.dll中修改这个字符串同样能实现效果,但考虑到微信中的逻辑较多,直接修改md5的后果难以预计,故还是在WeChatAppEx中修改更为稳妥
    • (测试通过~)

iOS微信的小程序Patch

万事第一步:解锁日志

  • 考虑到上面的思路,我们首先解锁日志输出,随后根据日志输出来寻找入手点
  • 通过直接搜索wxapkg字符串,我们可以找到大量拼接wxapkg路径和获取wxapkgMD5的字符串
  • 随便点进去一个逻辑(例如找 wxapkg ready这个字符串的引用)
    • 引用位于MBPackageLogic类中,通过+[iConsole logWithLevel:module:errorCode:file:line:func:format:]函数输出日志
  • 考虑到vararg不好hook,我们直接hook内部的函数
    • v17 = objc_retain(format); if ( v17 && (unsigned int)+[AMLogHelper shouldLog:](&OBJC_CLASS___iConsole, "shouldLog:", v15) ) { v18 = objc_alloc((Class)&OBJC_CLASS___NSString); v19 = objc_msgSend(v18, "initWithFormat:arguments:", v17, &args); -[__objc2_class logWithLevel:module:errorCode:file:line:func:message:]( a1, "logWithLevel:module:errorCode:file:line:func:message:", v15, a4, v13, a6, v11, a8, v19); objc_release(v19); } objc_release(v17); }
    • 只要让shouldLog返回YES,然后hook下面的Log Message函数即可
 

环境:Theos hook + 轻松签

  • Theos配置generator为internal避免引入MobileSubstrate
    • %config(generator=internal)
      • 不够,只是让Tweak.x → Tweak.m的过程使用objc-message.h,链接还是带着substrate
    • 阅读Theos makefile源码可以看出需要让LOGOS_DEFAULT_GENERATOR不等于substrate或libhooker
    • 故添加:wxapkgpatch_LOGOS_DEFAULT_GENERATOR = internal
  • 轻松签直接注入即可
 

通过Hook分析:寻找wxapkg加载位置

  • 尝试hook wxapkg ready\n触发时的逻辑
    • 成功时会执行 - [MBPackageLogic setWxapkgPathUrl:]
    • 失败,没有触发
  • 分析日志:
    • iOS微信小程序日志
    • 搜索md5发现结果太乱,无果
    • 搜索.wxapkg尝试寻找路径
      • 发现了这样两个函数涉及到wxapkg的路径
        • +[WALocalCacheFilePathUtil getWeAppLocalCacheFilePathWithAppid:version:isDebugMode:packageType:moduleName:encryptType:versionDesc:] -[WAPackageInfoCacheLogic unpackPkgFromPath:appid:version:isDebugMode:packageType:extParams:]
      • 第一个是获取路径,有很多个函数用
      • 第二个显然是解压
 

Patch:尝试优雅的Hook

优雅方式:仅Hook unpackPkgFromPath使用的路径,绕过checksum

思路:在.wxapkg文件旁边放一个.wxapkg_patch文件,让文件checksum用wxapkg,实际使用的时候用wxapkg_patch
  • unpackPkg最后的函数是 - (BOOL)unpackPkgWithFilePath:(NSString *)filePath unpackLib:(void *)lib,之后便会转入C代码继续执行
  • 只要hook这个函数,不管body里面放什么东西,微信都会崩溃
    • 考虑到其他函数可以正常hook,因此不像是反调试,问题大概率出现在logos的hook方法上
    • 上下追踪lib参数,发现并没有任何的objc引用 ⇒ lib参数为纯c变量
    • 猜测lib参数被ARC自动retain导致内存访问异常,将void *改为uintptr_t后,微信正常运行
 
  • 结果:hook成功了,小程序能正常打开,但是小程序的patch没有生效
    • 微信内部还有另外一层
 

简单粗暴:去除校验和

  • 修改wxapkg文件来强制触发校验失败,分析日志找出不同点
  • 搜索上面提到的两个函数(unpackPkgWithFilePathgetWeAppLocalCacheFilePathWithAppid)可以轻松找到报checksum错误的点:
    • Feb 12 23:39:01 WeChat(wxapkgpatch.dylib)[18182] <Notice>: iConsole log: level4 module-WeAppError errCode-0 (WALocalCacheMgr.mm:405):-[WALocalCacheMgr verifyLocalCacheChecksum:] verifyLocalCacheChecksum error! localFileCheckSum:0d31e0517f00984517e84ea139c333e1, checkSum:5c5bfeb1db97e7cd9ad59852177d8e38 Feb 12 23:39:01 WeChat(wxapkgpatch.dylib)[18182] <Notice>: iConsole log: level2 module-WeApp errCode-0 (WADatabaseMgr.mm:1227):-[WADatabaseMgr deletePluginVersionInfoByAppId:version:] delete pluginVersionInfo success! appId = wxee969de81bba9a45, version = 38
  • 直接hook verifyLocalCacheChecksum返回对即可
  • 考虑到微信的小程序每个版本会存放在单独的文件夹,因此直接将小程序对应目录设置为只读,让报错更直观
 

意外收获

  • WCPureTweak 微信净化的某个版本会导致iConsole输出不出来东西,找个别的版本或者禁用就可以了

最终Tweak.x

%config(generator=internal) #import <Foundation/Foundation.h> #import <CoreFoundation/CoreFoundation.h> NSString *getPatchWxapkg(NSString *path) { if (![path hasSuffix:@".wxapkg"]) { return path; } NSString *patchPath = [NSString stringWithFormat:@"%@%@", path, @"_patch"]; NSLog(@"wxapkg_patch: Checking patch wxapkg path: %@", patchPath); if ([[NSFileManager defaultManager] fileExistsAtPath:patchPath]) { NSLog(@"wxapkg_patch: Patch exists, overriding wxapkg path from %@ to %@", path, patchPath); return patchPath; } return path; } %hook MBPackageLogic - (void) setWxapkgPathUrl: (NSURL *)url { NSString *newPath = getPatchWxapkg([url path]); %orig([NSURL URLWithString:newPath]); } %end %hook WAPackageInfoCacheLogic - (BOOL)unpackPkgWithFilePath:(NSString *)filePath unpackLib:(uintptr_t)lib { NSLog(@"wxapkg_patch: Entered unpackPkgWithFilePath %@ %lx!", filePath, lib); NSString *newPath = getPatchWxapkg(filePath); NSLog(@"wxapkg_patch: Final Path %@", newPath); return %orig(newPath, lib); } %end %hook WALocalCacheMgr - (BOOL) verifyLocalCacheChecksum:(id)a1 { return YES; } %end %hook AMLogHelper + (BOOL) shouldLog:(int)level { return YES; } %end %hook iConsole + (void)logWithLevel:(int)level module:(const char *)module errorCode:(unsigned int)errorCode file:(const char *)file line:(int)line func:(const char *)func message:(NSString *)arg7 { NSLog(@"iConsole log: level%d module-%s errCode-%d (%s:%d):%s %@", level, module, errorCode, file, line, func, arg7); } %end %ctor { NSLog(@"wxapkg_patch: Initializing!"); %init; }

iOS上的小程序Debug

当前日志状况

  • 经过上面的patch后,小程序的日志状态是这样的
    • 小程序的Console不会输出到NSLog那边
    • 小程序的错误(SyntaxError等)会通过-[WAEJBindingGlobalUtils _func_log:argc:argv:exception:]-[WAGameViewController log:func:line:]传出
      • -[WAGameViewController log:func:line:] GameConsole: -[WAEJBindingGlobalUtils _func_log:argc:argv:exception:],81,{"level":3,"logs":["MiniProgramError\134n{\134

尝试从OC层log函数下手

  • 尝试寻找可能跟log相关的函数
    • -[WAEJBindingGlobalUtils _func_log:argc:argv:exception:]: Ejecta库的反射函数,最后会字EJClassLoader里面把所有WAEJBindingBase的继承类里面的_ptr_to_func_XXX绑定给JS,比如这个函数就是绑定到GlobalUtils.log
      • 这个函数内部调用了log:func:line:, 最终由Delegate WAGameController、WAOpenGLView等处理
      • WAGameController调用jsLog:方法处理日志
      • Ejecta是个iOS的WebGL库,所以普通的小程序肯定是不用这些的,所以这里应该不是主要的日志点
    • -[WAGameViewController jsLog:]: 当vConsole初始化了的时候调用console._log,否则存到logs数组里面
      • jsLog被 -[WAJSGameService printConsoleLog:level:]调用
    • -[WAJSGameService printConsoleLog:level:]:由WAJSCoreService调用,看起来负责输出console log
  • 尝试hook:
    • hook上jsLog和printConsoleLog,没反应(无日志输出)
 

尝试从JS层下手

  • 注意到输出的日志里面有JS层传过来的MiniProgramError
    • OC内没有MiniProgramError字符串,说明Error被Wrap了一层变成了MiniProgramError才传到OC
    • 微信程序包内搜索MiniProgramError,发现在很多PublicRes里面有,如WAGame.js
  • 深入逆向WAGame.js
    • 发现会对globalThis.console进行monkeypatch
    • 联想到之前在OC层中的WAGameController中大量出现的vConsole,查询资料得知小程序的日志是走vConsole而不是走OC层的

逆向vConsole开启条件

  • 标准方式:vConsole需要在微信小程序处于调试模式下,点击菜单弹出选择开启调试按钮
  • 直接在方法列表中搜索vConsole
    • 注意到-[WAVConsoleJSLogicImpl injectVConsole],一路向上到-[WAVConsoleJSLogicImpl injectWeixinJSBridge]发现有很多调用
    • 注意到[WAJSCoreService isDebugAndVConsoleOpen]
      • 找他的setter方法[WAJSCoreService setIsDebugAndVConsoleOpen:]一路向上找发现只有来自WAAppTask的引用,在这些引用中他把isDebugAndVConsoleOpen属性复制给JSCoreService,但微信方法太多,全局交叉引用需要时间过久,放弃
      • 找看谁用到了这个属性,失败,只有VConsole相关的逻辑再用它,看起来是非常靠后的属性
    • 注意到-[WAGameViewController onSwitchVConsole]
      • -[WAGameViewController gameActionSheet:clickedButtonTitle:]调用,可以在里面看到“开启调试”按钮的标题为WCAppBrand_MenuItem_OpenDebugMode
      • 通过标题交叉引用可找到-[WAGameViewController menuDebugButtonTitleArray]
        • 内部通过-[WAConfigMgr pageIsUseVConsoleForAppID:]判断当前是否开启调试
        • 通过-[WAWebViewController debugMode]判断是否允许调试
        • 注意到挨着-[WAConfigMgr pageIsUseVConsoleForAppID:][WAConfigMgr openUseVConsoleWithAppID:],都是在维护dicUseVConsoleApp,openUseVConsoleWithAppIDonSwitchVConsole引用
    • -[WAWebViewController debugMode]通过判断m_extraInfo"weAppIsDebugMode"键值来判断是否开启debug
    • -[WAAppTask generateExtraInfoWithAppID:Contact:]中对weAppIsDebugMode进行初始化,用的是contact的m_uiDebugModeType值
    • -[WAContact m_uiDebugModeType]是实际的值提供者,有100多个引用(太多了),可以到此为止了

Hook开启vConsole

  • 从最底层hook,patch m_uiDebugModeType
    • 会提示 开发版小程序过期,重新扫码
    • //%hook WAContact //- (int) m_uiDebugModeType { // return 1; //} //%end //%hook CContact //- (int) m_uiDebugModeType { // return 1; //} //%end
  • 从上一层hook改weAppIsDebugMode
    • generateExtraInfoWithAppID:(id)appid Contact:(id)contact
    • 能用!开启调试按钮出现了,vConsole按钮也可以出现
  • 查看Log,发现Log为空
    • 想起之前hook了jsLog和printConsoleLog,取消这些hook之后,log就出来了
    • 说明之前这些函数其实hook对了,只是因为js层monkeypatch了console.log导致日志没有过到OC层