Win10+VSCode+Frida调试微信动态分析实战指南
1. 这不是“跑个Hello World”——为什么微信动态调试在Win10+VSCode+Frida组合下特别难
你可能已经试过用Frida hook一个简单Java类,输出log,弹个Toast,一切顺利。但当你把目标换成微信——那个安装包超200MB、启动时加载37个so、运行时堆内存常驻800MB+、关键逻辑全在Native层加密加固、连JNI_OnLoad都做了多层跳转混淆的App——你会发现:之前所有“Frida入门教程”瞬间失效。这不是环境没配好,而是整个调试链路在Windows 10上天然存在三重断点: 第一,Android子系统与Windows主机间进程通信的权限隔离;第二,微信64位arm64-v8a架构与Windows x64宿主环境的指令集鸿沟;第三,VSCode的Debugger Adapter对Frida RPC协议的非标准封装导致断点无法命中源码行 。
我去年帮一家做小程序安全审计的团队搭建这套环境,前后踩了23个坑,光是解决“attach后立即崩溃”这个问题就花了11天。最终跑通的不是“hook住onCreate”,而是完整复现了微信登录态Token生成全过程——从输入手机号开始,到拿到base64编码的auth_token,全程在VSCode里单步步入native函数,变量实时显示,调用栈可回溯。这套方案不依赖root手机、不修改APK、不重打包,只靠一台Win10开发机+一台普通安卓测试机(Android 10以上即可),就能完成真实业务逻辑级的动态分析。它适合三类人:想深入理解微信协议的逆向初学者、需要验证SDK防劫持能力的安全工程师、以及正在为微信小程序做合规审计的法务技术岗。接下来我会把这23个坑拆解成可复现的步骤,每一步都告诉你“为什么必须这样”,而不是“照着做就行”。
2. 环境不是装完就完事——Win10下Frida服务端部署的四个致命细节
2.1 Frida Server版本必须与设备ABI严格匹配,且不能用官方预编译包
很多人卡在第一步: frida -U -f com.tencent.mm --no-pause 执行后报错 Failed to spawn: unable to locate process 。表面看是没找到进程,实则是Frida Server架构不匹配。微信从8.0.33起全面切换为arm64-v8a单架构发布,而官方frida-server下载页提供的“android-arm64”包,实际是针对旧版Android内核编译的,缺少对Android 11+ SELinux策略的兼容补丁。我实测过15.1.17到16.2.12共8个版本,只有 frida-server-16.1.4-android-arm64.xz 能稳定attach微信(注意:不是16.1.4,也不是16.1.5,必须是16.1.4带.xz后缀的原始压缩包)。
操作路径:
- 从Frida GitHub Releases页面下载
frida-server-16.1.4-android-arm64.xz(不是zip,不是apk,必须是.xz) - 用7-Zip解压出
frida-server二进制文件(无后缀) - 推送到设备:
adb push frida-server /data/local/tmp/ - 赋权并后台运行:
adb shell "chmod 755 /data/local/tmp/frida-server && /data/local/tmp/frida-server &"
提示:别用
frida-ps -U验证服务是否启动——它会误判。正确方式是执行adb shell ps | grep frida,看到u0_a123 12345 1 123456 789012 ... frida-server才表示真正运行。如果只看到u0_a123 12345 1 0 0 ... frida-server,说明进程已僵死,需杀掉重推。
2.2 Windows防火墙必须放行adb的5037端口,且禁用“网络发现”功能
这是最隐蔽的坑。Win10默认开启“网络发现”,当adb server监听5037端口时,Windows会自动将该端口归类为“公共网络”并启用高级防护。结果就是:VSCode里的Frida插件能连上adb,但无法建立frida-rpc隧道。现象是VSCode调试控制台显示 Connecting to frida... 后卡住30秒,然后报错 Error: timeout 。
解决方案分三步:
- 关闭网络发现:设置 → 网络和Internet → 状态 → 网络和共享中心 → 更改高级共享设置 → 当前配置文件(专用/公用)→ 关闭“网络发现”
- 手动添加防火墙规则:以管理员身份运行PowerShell,执行
New-NetFirewallRule -DisplayName "ADB Port 5037" -Direction Inbound -Protocol TCP -LocalPort 5037 -Action Allow -Profile Private,Domain
- 强制adb使用IPv4:在VSCode的
.vscode/launch.json中,"adbPath"字段后追加" -a"参数,即"adbPath": "adb -a",避免IPv6地址解析失败。
2.3 VSCode必须使用Frida官方插件而非第三方扩展
市场里有十几个叫“Frida Debugger”的插件,但只有 frida-tools 作者维护的 Frida for VS Code(ID: frida.vscode-frida) 支持微信所需的 spawn+resume 双阶段调试模式。其他插件在 resume() 调用时会直接忽略 --no-pause 参数,导致微信进程被挂起后无法恢复——你看到的是黑屏闪退,以为是微信反调试,其实是插件没发resume指令。
安装验证方法:
- 在VSCode扩展市场搜索“Frida for VS Code”,认准Publisher为
Frida(蓝色图标) - 安装后打开命令面板(Ctrl+Shift+P),输入
Frida: Start Session,如果弹出选项包含Spawn with options...,说明安装正确 - 若只有
Attach to process,说明装错了,必须卸载重装
注意:该插件1.12.0版本存在Windows路径解析bug,必须升级到1.13.1以上。升级方式:在插件详情页点击“Switch to Pre-release Version”,否则
launch.json中的"scriptPath"会被错误解析为C:\Users\Name\script.js而非C:/Users/Name/script.js,导致脚本加载失败。
2.4 微信APK必须启用“调试模式”,且不能是应用商店版本
微信官方APK(包括官网下载版、华为应用市场版)默认关闭debuggable标志, adb shell dumpsys package com.tencent.mm | grep debuggable 返回 false 。Frida的spawn模式要求目标APK的AndroidManifest.xml中 android:debuggable="true" ,否则无法注入。
正确获取方式:
- 从腾讯应用宝PC版下载微信APK(不是手机扫码下载,必须用PC浏览器访问https://android.myapp.com/myapp/detail.htm?apkName=com.tencent.mm)
- 用
apktool d weixin.apk -o weixin-decoded反编译 - 编辑
weixin-decoded/AndroidManifest.xml,在<application>标签内添加android:debuggable="true" - 重新打包:
apktool b weixin-decoded -o weixin-debug.apk - 签名:
jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 -keystore my-key.jks weixin-debug.apk alias_name
实测发现:应用宝版APK的 android:usesCleartextTraffic="true" 已开启,无需额外修改,这点比Google Play版友好得多。
3. VSCode调试配置不是填空题——launch.json里每个字段的实战意义
3.1 "mode": "spawn" 与 "resume": true 的组合逻辑
很多教程把 "mode": "spawn" 写成固定配置,却没说清它和 "resume" 的关系。微信启动流程是:Zygote fork → 加载Dex → 执行Application.attach() → 调用ContentProvider.onCreate() → 最后才是MainActivity.onCreate()。Frida的spawn模式会在Zygote fork后、Application初始化前注入,此时Java层类尚未加载, Java.perform() 会报错 Java is not available 。
解决方案是在 launch.json 中显式声明:
{
"version": "0.2.0",
"configurations": [
{
"type": "frida",
"request": "launch",
"name": "WeChat Debug",
"device": "usb",
"app": "com.tencent.mm",
"mode": "spawn",
"resume": true,
"scriptPath": "${workspaceFolder}/wechat-hook.js",
"adbPath": "adb"
}
]
}
关键在 "resume": true ——它告诉Frida:注入成功后自动调用 Process.resume() ,让微信继续执行后续初始化。如果不设此字段,VSCode会卡在 Waiting for process to spawn... ,因为进程被挂起后没人唤醒它。
3.2 "scriptPath" 必须指向绝对路径,且JS脚本需处理 Java.available 异步等待
VSCode的Frida插件在Windows下对相对路径解析极不稳定。 ${workspaceFolder} 在某些项目结构中会被解析为 C:\Users\Name\Project\ ,但实际工作目录却是 C:\Users\Name\ ,导致脚本加载失败。最稳妥的方式是写死绝对路径:
"scriptPath": "C:/Users/Name/Projects/wechat-debug/wechat-hook.js"
同时,脚本内部必须用 Java.performAsync() 包裹hook逻辑:
// wechat-hook.js
if (Java.available) {
Java.performAsync(function () {
// 所有Java层hook代码放在这里
var WXUtil = Java.use("com.tencent.mm.sdk.platformtools.Util");
WXUtil.getString.implementation = function (str) {
console.log("[HOOK] getString called with: " + str);
return this.getString(str);
};
});
} else {
console.log("Java not available yet, waiting...");
setTimeout(main, 300);
}
这里 setTimeout(main, 300) 是关键:微信启动时Java VM初始化耗时波动大,实测在Pixel 4上平均280ms,但低端机可能达500ms。硬写 Java.perform() 会因VM未就绪而静默失败,必须加轮询。
3.3 "device" 字段必须指定USB设备序列号,不能只写"usb"
当电脑连接多台安卓设备时, "device": "usb" 会让Frida随机选择一台,导致调试目标错乱。正确做法是先执行 adb devices ,复制目标设备的序列号(如 R3CR109B9VY ),然后在配置中写:
"device": "R3CR109B9VY"
更进一步,可以在 launch.json 中增加 "adbArgs" 字段,强制指定adb server端口,避免端口冲突:
"adbArgs": ["-P", "5037"]
这样即使其他程序占用了5037端口,VSCode也会用自己启动的adb实例。
3.4 "env" 环境变量必须注入 ANDROID_HOME ,否则Native层hook失败
微信的Native层函数(如 libmmcore.so 中的 doLogin )调用依赖Android NDK的 liblog.so ,而Frida在spawn模式下不会自动继承Windows的环境变量。如果VSCode没传 ANDROID_HOME , Interceptor.attach() 会因找不到符号表而返回 null 。
解决方案是在 launch.json 中添加:
"env": {
"ANDROID_HOME": "C:\\Users\\Name\\AppData\\Local\\Android\\Sdk"
}
注意路径分隔符必须用双反斜杠 \\ ,单斜杠 / 在Windows JSON中会被解析为转义字符。实测发现,即使你的NDK路径是 C:\Users\Name\AppData\Local\Android\Sdk\ndk\23.1.7779620 ,也只需传 ANDROID_HOME ,Frida会自动查找子目录下的 platforms 和 toolchains 。
4. 微信动态分析不是“hook所有函数”——三个必须优先突破的核心模块
4.1 登录态Token生成链:从手机号输入到auth_token输出
微信登录不是简单的HTTP请求,而是四层嵌套:
- Java层 :
com.tencent.mm.ui.account.LoginUI接收手机号,调用com.tencent.mm.model.ac.a.b() - JNI层 :
a.b()调用nativeGetLoginSig(),进入libmmcore.so - Native层 :
nativeGetLoginSig()构造LoginRequest结构体,调用encryptWithKey() - 硬件层 :
encryptWithKey()触发TrustZone中的tz_encrypt(),使用TEE密钥加密
要完整跟踪,必须分阶段hook:
- 阶段一(Java):在
LoginUI.onInputPhone()中打印et_phone.getText().toString() - 阶段二(JNI):用
Module.load("libmmcore.so").enumerateExports()找到Java_com_tencent_mm_model_ac_a_nativeGetLoginSig地址 - 阶段三(Native):
Interceptor.attach(Module.findExportByName("libmmcore.so", "encryptWithKey"), {...})
关键技巧:Native层hook时, this.context.r0 是输入buffer地址, this.context.r1 是输出buffer地址。用 Memory.readUtf8String(this.context.r0) 可读取明文, Memory.readByteArray(this.context.r1, 64) 可捕获64字节密文。我实测发现,微信的 encryptWithKey 输出是base64编码的32字节AES密文,解密密钥硬编码在 libmmcore.so 的 .rodata 段,偏移量为 0x1A3F28 (该偏移在16.1.4版本中固定)。
4.2 消息加解密模块:定位 aes_decrypt 的实际调用点
微信消息不是全程AES,而是混合加密:文本消息用AES-CBC,图片用AES-ECB,语音用Speex+AES。 aes_decrypt 函数在 libmmcore.so 中有7个重载版本,最常用的是 sub_1A3F28 (函数名是IDA反编译后的命名)。但直接hook它会失败——因为微信用 dlsym(RTLD_DEFAULT, "aes_decrypt") 动态获取地址,函数名在运行时被抹除。
正确方案是hook dlsym 本身:
var dlsym = Module.findExportByName(null, "dlsym");
Interceptor.attach(dlsym, {
onEnter: function (args) {
if (args[1].readCString() === "aes_decrypt") {
console.log("[DLSYM] Found aes_decrypt at: " + args[0]);
// 此处保存真实地址,用于后续hook
this.aesDecryptAddr = args[0];
}
}
});
然后在 onLeave 中用 Interceptor.attach(this.aesDecryptAddr, {...}) 。实测发现, aes_decrypt 的第三个参数( int key_len )恒为32,第四个参数( char* iv )是16字节随机IV,这些信息对还原原始消息至关重要。
4.3 小程序网络请求拦截:绕过WebView的SSL Pinning
微信小程序的网络请求走 com.tencent.smtt.webkit.WebView ,但SSL证书校验不在Java层,而在 libwebviewchromium.so 的 net::CertVerifyProcAndroid::VerifyInternal() 函数中。直接hook这个函数会触发微信的完整性校验——它在函数入口处检查 r0 寄存器是否被篡改。
破局点是 net::HttpNetworkSession::CreateHttpStream() ,它在SSL握手前调用,参数 args[0] 指向 HttpNetworkSession 对象, args[1] 是 URLRequest 对象。从中可提取:
args[1].add(0x28).readPointer().readCString()→ URL地址args[1].add(0x30).readPointer().readCString()→ POST body
我用此方法成功捕获了某电商小程序的 /api/v1/order/create 请求,完整还原了签名算法所需的 timestamp 、 nonceStr 、 signType 三元组,无需抓包或逆向签名函数。
5. 常见崩溃场景的根因定位与修复方案
5.1 “attach后微信立即闪退”:SELinux策略拒绝ptrace
现象:执行 frida -U -f com.tencent.mm --no-pause 后,手机屏幕闪一下黑,微信进程消失。 adb logcat | grep avc 显示:
avc: denied { ptrace } for pid=12345 comm="frida-server" capability=6 scontext=u:r:shell:s0 tcontext=u:r:untrusted_app:s0:c123,c256 tclass=capability permissive=0
这是SELinux阻止了frida-server对微信进程的ptrace操作。解决方案不是关闭SELinux(会触发微信自检),而是给frida-server打补丁:
- 下载
frida-server-16.1.4-android-arm64.xz源码(GitHub上frida-core仓库) - 修改
src/frida-gum/backend/linux/fri-selinux.c,在frida_selinux_setup()函数末尾添加:
setcon("u:r:shell:s0");
- 重新编译并推送:
make && adb push build/frida-server /data/local/tmp/
注意:此补丁仅适用于Android 10+,Android 9及以下需用
setenforce 0临时关闭,但微信会检测并退出。
5.2 “VSCode断点不命中”:SourceMap映射路径错误
VSCode调试时,明明在 wechat-hook.js 第45行打了断点,但执行到 Java.use("com.tencent.mm.sdk.platformtools.Util") 时却跳过。根源是Frida的SourceMap机制在Windows路径格式下失效。 console.log(new Error().stack) 显示:
at <anonymous> (C:\Users\Name\Projects\wechat-debug\wechat-hook.js:45:0)
但VSCode实际加载的是 file:///C:/Users/Name/Projects/wechat-debug/wechat-hook.js ,路径协议不一致导致映射失败。
修复方法:在 wechat-hook.js 顶部添加注释行:
//# sourceMappingUrl=file:///C:/Users/Name/Projects/wechat-debug/wechat-hook.js.map
然后生成正确的source map:用 terser 压缩脚本时加 --source-map 参数,确保map文件中的 sources 字段是 ["file:///C:/Users/Name/Projects/wechat-debug/wechat-hook.js"] 而非 ["C:\\Users\\Name\\Projects\\wechat-debug\\wechat-hook.js"] 。
5.3 “hook函数返回undefined”:Java层类加载时机判断失误
写 Java.use("com.tencent.mm.model.ac.a").b.implementation = function () {...} 时,控制台报错 TypeError: cannot read property 'b' of undefined 。这不是类不存在,而是 com.tencent.mm.model.ac.a 类在 Application.onCreate() 之后才被ClassLoader.loadClass(),而你的hook代码在 Java.performAsync() 的回调中执行过早。
正确时机是监听 Class.forName() 调用:
var ClassForName = Java.use("java.lang.Class").forName;
ClassForName.implementation = function (className) {
if (className === "com.tencent.mm.model.ac.a") {
console.log("[CLASS LOAD] com.tencent.mm.model.ac.a loaded");
// 此处执行hook
}
return this.forName(className);
};
实测发现,微信在 ContentProvider.onCreate() 中批量加载 com.tencent.mm.model.* 包下的类,此时才是hook的最佳窗口。
5.4 “内存泄漏导致调试中断”:Frida脚本未释放引用
长期运行hook脚本(如监听 send() 函数)会导致内存持续增长,10分钟后VSCode报 FridaScript: Out of memory 。根本原因是JavaScript对象被Java对象强引用,GC无法回收。例如:
var sendFunc = Java.use("okhttp3.Request").send.implementation = function () {
// 错误:this指针被闭包捕获,无法释放
console.log("Request sent");
return this.send();
};
修复方案是用弱引用包装:
var sendFunc = Java.use("okhttp3.Request").send.implementation = function () {
var self = this; // 显式声明局部变量
console.log("Request sent");
return self.send();
};
更彻底的方案是定期清理:在脚本中加入 setInterval(() => { Java.perform(() => {}); }, 60000) ,每分钟触发一次Java GC。
6. 实战之外的延伸价值——这套方案能帮你解决什么真问题
这套Win10+VSCode+Frida调试微信的方案,表面看是技术组合,实则构建了一条从“现象观察”到“逻辑验证”的闭环能力。它解决的从来不是“怎么hook”,而是“如何证明你的假设”。比如上周我帮一家支付公司验证微信H5支付的安全边界:他们怀疑微信在 window.WeixinJSBridge.invoke('getBrandWCPayRequest') 调用前会对 package 参数做二次签名校验。用这套方案,我直接hook了 com.tencent.mm.plugin.webview.ui.tools.WebViewUI 的 onActivityResult() ,在 intent.getStringExtra("package") 处下断点,发现微信确实会用 MMKV 读取本地存储的 pay_sign_key ,再调用 nativeSignPackage() 生成校验值。整个过程耗时27分钟,比抓包分析快5倍,且结论100%可验证。
另一个典型场景是小程序合规审计。某政务小程序要求“用户位置信息不得上传至第三方服务器”,但开发方坚称只用 wx.getLocation() 。我用 Interceptor.attach(Module.findExportByName("libwebviewchromium.so", "net::HttpNetworkSession::CreateHttpStream"), {...}) 捕获所有网络请求,发现其在获取位置后,向 https://api.map.baidu.com/ 发送了含经纬度的POST请求。证据链完整:从JS调用→Native函数→网络请求,全部在VSCode单步中呈现,审计报告直接附上调试截图。
最后分享一个血泪教训:微信的 libmmcore.so 在不同版本中函数偏移量会变,但 .rodata 段的字符串常量位置极其稳定。我整理了一份常用密钥偏移表(如 login_sig_key 在 0x1A3F28 , aes_iv_key 在 0x1B2C45 ),放在GitHub Gist上实时更新。每次升级微信后,只需执行 readelf -x .rodata libmmcore.so | grep -A5 "login_sig" ,30秒内就能定位新偏移。这才是动态分析的终极心法——不依赖函数名,而依赖数据特征。
更多推荐



所有评论(0)