微信小程序逆向实战:定位Hook地址获取网络请求参数

📅 2026/7/10 10:34:03
微信小程序逆向实战:定位Hook地址获取网络请求参数
1. 项目概述与核心价值最近在分析一个微信小程序时遇到了一个典型的需求需要获取其网络请求中的关键参数比如用户令牌或者加密签名。这个参数通常被封装在小程序的底层逻辑里直接抓包看到的只是一堆加密后的乱码。常规的抓包工具如Charles或Fiddler在这里就束手无策了因为它们只能看到“是什么”却无法知道“怎么来的”。这时候逆向工程就成了打开这扇门的钥匙。而逆向的核心往往在于找到那个关键的“钩子点”——也就是我们常说的Hook地址。它就像程序逻辑中的一个岔路口我们在这里设置一个观察哨就能截获并修改流经此处的数据。这次实战的目标非常明确针对微信小程序以11275版本为例使用Python脚本配合逆向神器IDA Pro精准定位并获取其网络请求模块中的关键Hook地址。这不仅仅是获取一个内存地址那么简单而是一套完整的、可复现的分析方法论。掌握了它你就能理解小程序如何构建请求、如何加密数据进而实现自动化数据采集、安全审计或功能扩展。整个过程会涉及静态分析、动态调试、脚本编写和逻辑推理是移动端逆向中非常经典且实用的一个案例。适合阅读这篇笔记的是那些对移动安全、逆向分析感兴趣有一定Python基础并且不满足于表面抓包的开发者或安全研究人员。即使你对IDA Pro还不熟悉也没关系我会把关键步骤拆解得足够细致。我们最终得到的不仅是一个针对特定版本的地址更是一套应对未来版本变化、独立分析问题的能力。2. 逆向环境搭建与工具链解析工欲善其事必先利其器。在开始逆向之前一个稳定、高效的工具环境是成功的基石。这个环境主要分为两大块动态调试环境和静态分析/脚本环境。2.1 安卓动态调试环境配置微信小程序运行在微信客户端内而微信客户端本质上是一个安卓应用。因此我们的首要任务是在一个可控的安卓环境中运行特定版本的微信。1. 模拟器与微信版本选择我强烈推荐使用雷电模拟器9它基于Android 9内核对ARM应用的支持比较友好且Root权限获取简单。关键在于微信版本我们必须使用指定的11275版本。这是因为不同版本的小程序框架、底层so库如libwechatcommon.so的函数偏移地址完全不同。直接去官网下载历史版本APK并不容易通常需要在一些第三方APK存档网站如APKMirror耐心寻找。下载后直接拖入模拟器安装即可。2. 核心工具IDA Pro 与调试服务器IDA Pro是我们的主战场。你需要准备IDA Pro 7.7或更高版本。安装完成后找到IDA安装目录下的dbgsrv文件夹里面有针对不同平台的调试服务器。我们需要的是android_server64因为现在微信是64位应用。将这个文件通过ADBAndroid Debug Bridge推送到模拟器中adb push android_server64 /data/local/tmp/ adb shell cd /data/local/tmp chmod 755 android_server64然后在模拟器的shell中运行./android_server64它会监听23946端口。接着在主机上使用ADB进行端口转发adb forward tcp:23946 tcp:23946。这样IDA Pro就能通过本地端口连接到模拟器中的调试服务器了。注意有些模拟器自带ADB与系统ADB冲突导致adb devices列表为空。解决方法是关闭模拟器的ADB并确保电脑只运行一个ADB服务。用adb kill-server和adb start-server命令重置一下通常能解决问题。2.2 Python与Frida环境搭建虽然本次实战以IDA静态分析和Python脚本为主但Frida作为动态插桩的王者在辅助验证和快速探测时无可替代。1. Python环境建议使用Python 3.8-3.10版本太新的版本可能遇到一些第三方库兼容性问题。使用pip安装核心库pip install frida-tools pip install requests # 用于后续的脚本通信2. Frida环境配置在模拟器或真机上我们需要安装Frida-server。首先用adb shell getprop ro.product.cpu.abi命令查看设备架构模拟器通常是x86_64或arm64-v8a然后去Frida的GitHub Release页面下载对应架构的frida-server文件。和IDA的调试服务器一样推送到设备赋予执行权限并运行。adb push frida-server-xx.x.x-android-xx /data/local/tmp/fs adb shell chmod 755 /data/local/tmp/fs adb shell /data/local/tmp/fs 在电脑上运行frida-ps -U如果能看到设备上的进程列表说明Frida环境搭建成功。3. 工具链协同工作流理解工具的分工很重要。IDA Pro擅长静态反汇编和深度调试我们可以设断点、单步跟踪、分析复杂的控制流。而Frida擅长快速、非侵入式的动态Hook写几行JavaScript就能批量打印函数调用和参数。在本项目中我们的思路是先用Frida进行大范围的“侦察”快速定位可疑的模块和函数名然后利用IDA Pro对目标so库进行精细的静态分析结合动态调试验证最终用Python脚本从IDA中提取出稳定的Hook地址。这个“动静结合”的思路能极大提升分析效率。3. 目标分析与Hook点定位策略逆向分析最忌无头苍蝇似的乱撞。在开始调试之前我们必须先明确目标并制定清晰的定位策略。3.1 微信小程序网络请求链路分析微信小程序的网络请求并非由小程序JavaScript直接发出。其典型链路是小程序的JS逻辑调用wx.request等API - 微信客户端JS引擎如JSCore或V8 - 通过桥接层JNI调用安卓Java代码 - Java层最终通过原生网络库可能是OkHttp但微信有高度定制发起请求。在这个过程中关键参数如header里的X-WX-TOKEN、X-WX-SIGN的生成和添加很可能发生在Java层到Native层C/C的转换过程中。为什么是Native层因为核心的加密算法、签名逻辑为了安全和性能通常用C/C实现并编译成so库。我们的目标就是找到这个负责“组装最终请求”或“计算签名”的Native函数。这个函数在调用时其参数中必然包含了请求的URL、方法、头部和待加密的原始数据。3.2 静态分析从入口点到核心逻辑首先我们需要获取微信的安装包APK并进行解包。使用apktool工具可以反编译出资源文件和classes.dexJava代码。但我们的重点在so库。将APK后缀改为.zip并解压在lib目录通常是lib/arm64-v8a下找到微信的核心so文件名字可能包含wechat、common、network等关键词。1. 使用IDA Pro加载so库用IDA Pro打开目标so库例如libwechatcommon.so。首次加载会花费一些时间进行分析。分析完成后我们面对的是成千上万的汇编函数。直接搜索字符串是一个很好的起点。在IDA的Strings窗口快捷键ShiftF12我们可以搜索与网络请求相关的关键词如“X-WX-SIGN”、“POST”、“User-Agent”或者一些可能出现的错误信息字符串。找到这些字符串后点击它IDA会跳转到该字符串在数据段.rodata的位置然后通过交叉引用Xrefs to就能找到哪些函数使用了这个字符串。2. 识别关键函数特征找到引用字符串的函数后我们需要判断它是否是我们的目标。一个典型的请求构造函数可能会有以下特征函数开头有大量的参数准备和校验。调用了已知的加密函数如MD5_Init、SHA1_Update、或一些密码学库函数可通过函数名或导入表识别。函数内部有明显的逻辑分支可能根据不同的请求类型GET/POST或接口路径进行不同处理。其函数参数可能包含指向请求URL、Header列表、Body数据的指针。3.3 动态验证使用Frida进行快速侦察静态分析提供了线索但需要动态运行来验证。这时Frida就派上用场了。我们可以写一个Frida脚本去Hook那些在静态分析中发现的疑似函数。一个基础的Frida侦察脚本框架如下Java.perform(function() { // 如果怀疑是Java函数可以Hook Java层 // var URLClass Java.use(com.tencent.mm.network.xxx); // URLClass.yourMethod.implementation function(...){...}; // 更可能的是Hook Native函数 Interceptor.attach(Module.findExportByName(libwechatcommon.so, 可疑函数名), { onEnter: function(args) { console.log([*] 函数被调用); // 打印参数args[0], args[1]... // 可以尝试将指针内容读成字符串看看 var arg0 args[0]; if (arg0 ! 0) { console.log(arg0: Memory.readUtf8String(arg0)); } }, onLeave: function(retval) { console.log([*] 函数返回: retval); } }); });通过运行这个脚本并操作小程序触发网络请求观察控制台输出。如果目标函数被Hook到并且打印出了我们熟悉的URL或请求头信息那么恭喜你找到了一个非常关键的切入点。这个函数的地址就是我们要获取的Hook地址之一。但Frida给出的地址是模块基址偏移量这个偏移量在每次so库加载时是固定的而基址每次运行都会变。因此我们真正需要记录的是函数在so库中的偏移量RVA, Relative Virtual Address。4. 使用IDA Pro进行深度分析与地址提取通过Frida的侦察我们可能锁定了一到多个候选函数。接下来就需要回到IDA Pro进行深度分析以确定最合适的Hook点并提取出稳定的偏移地址。4.1 定位与反编译目标函数假设我们通过Frida脚本发现函数native_generate_signature非常可疑。在IDA Pro中我们可以通过Jump-Jump to function...快捷键CtrlF直接输入函数名跳转。如果函数名没有被剥离strip你会直接看到该函数的反汇编汇编代码或反编译伪C代码视图。我强烈建议使用IDA的反编译插件如Hex-Rays Decompiler它能将复杂的汇编逻辑转换成更易读的C伪代码极大提升分析效率。在反编译视图中仔细阅读函数逻辑。关注以下几点参数类型和数量反编译代码开头会显示函数签名如__int64 __fastcall native_generate_signature(__int64 a1, __int64 a2, __int64 a3)。这能帮助我们理解需要Hook多少个参数。关键调用函数内部是否调用了其他重要的子函数比如加密函数、字符串拼接函数如sprintf、或网络发送函数。数据流观察传入的参数a1, a2, a3是如何被使用的。它们可能是指向结构体的指针里面包含了请求的全部信息。4.2 计算稳定的Hook偏移量找到目标函数后我们需要记录它的偏移量。在IDA的汇编视图或反编译视图中函数起始行显示的地址是当前加载的基址Image Base加上偏移量。例如地址显示为0x7A6C4B2D10。获取模块基址在IDA的View-Open subviews-Segments快捷键ShiftF7中可以看到所有段的信息。.text段的起始地址Start通常就是该so库在本次调试会话中的加载基址。假设libwechatcommon.so的.text段起始地址是0x7A6C000000。计算偏移量RVA函数文件偏移 (RVA) 函数内存地址 - 模块加载基址 RVA 0x7A6C4B2D10 - 0x7A6C000000 0x4B2D10这个0x4B2D10就是函数在libwechatcommon.so文件中的相对虚拟地址。无论so库在内存中加载到哪个地址这个偏移量是固定的。验证偏移量你可以重启微信或重新附加调试再次查看该函数的内存地址。用新的加载基址加上0x4B2D10计算出的地址应该就是该函数的新位置。4.3 编写Python脚本与IDA交互获取地址手动计算和记录偏移量效率低下尤其是当需要追踪多个函数时。我们可以利用IDA Pro强大的脚本接口IDAPython来自动化这个过程。思路是让Python脚本在IDA中运行搜索特征如字符串引用、特定指令模式自动计算出目标函数的RVA并输出报告。以下是一个示例IDAPython脚本框架用于寻找与特定字符串相关的函数并计算其偏移import idautils import idc import idaapi def find_functions_by_string(search_str): 通过字符串查找引用它的函数 target_funcs [] # 遍历所有字符串 for s in idautils.Strings(): if search_str in str(s): str_ea s.ea # 字符串的地址 # 获取所有引用此字符串的代码地址 for ref in idautils.DataRefsTo(str_ea): # 获取包含该代码地址的函数起始地址 func_ea idaapi.get_func(ref).start_ea if func_ea ! idaapi.BADADDR: func_name idc.get_func_name(func_ea) # 计算该函数相对于模块的偏移量 # 首先获取当前模块的基址通常是.text段的起始地址 for seg in idautils.Segments(): seg_name idc.get_segm_name(seg) if seg_name .text: # 假设目标在.text段 seg_start idc.get_segm_start(seg) break rva func_ea - seg_start target_funcs.append((hex(func_ea), func_name, hex(rva))) break # 假设只找一个字符串 return target_funcs if __name__ __main__: # 搜索包含“X-WX-SIGN”字符串的函数 results find_functions_by_string(X-WX-SIGN) for mem_addr, name, rva in results: print(f内存地址: {mem_addr}, 函数名: {name}, RVA偏移: {rva})将这个脚本保存为.py文件在IDA中通过File-Script file...加载运行结果会输出在IDA的输出窗口。这样我们就得到了一个不依赖于本次调试会话的、稳定的Hook点偏移地址。5. 实战构建自动化Hook脚本与数据捕获获取到稳定的Hook地址RVA后我们的目标就变成了编写一个独立的Python脚本它能在小程序运行时自动附加到微信进程根据RVA计算出实际内存地址并进行Hook捕获我们感兴趣的数据。5.1 基于Frida的自动化Hook脚本我们选择Frida作为动态Hook的执行引擎因为它跨平台、脚本友好。下面的Python脚本完成了整个自动化流程import frida import sys import time # 目标函数在 libwechatcommon.so 中的偏移量 (RVA) TARGET_FUNCTION_RVA 0x4B2D10 def on_message(message, data): 处理从Frida脚本传回的消息 if message[type] send: payload message[payload] print(f[*] 收到数据: {payload}) # 这里可以将捕获到的URL、Headers等数据保存到文件或数据库 if url in payload: print(f 请求URL: {payload[url]}) if headers in payload: print(f 请求头: {payload[headers]}) elif message[type] error: print(f[!] 错误: {message}) def main(): # 1. 连接到设备 try: device frida.get_usb_device(timeout10) except Exception as e: print(f连接设备失败: {e}) print(请确保: 1. 设备已通过USB连接并开启调试。 2. frida-server已在设备上运行。) sys.exit(1) # 2. 附加到微信进程 (进程名可能因版本而异如 com.tencent.mm) session None for app in device.enumerate_applications(): if 微信 in app.name or com.tencent.mm in app.identifier: print(f找到目标应用: {app.name} (PID: {app.pid})) try: session device.attach(app.pid) break except frida.ProcessNotFoundError: print(微信进程未找到请确保微信已在前台运行。) sys.exit(1) if not session: print(未找到微信进程。) sys.exit(1) # 3. 创建Frida JavaScript脚本 js_code Interceptor.attach(Module.findBaseAddress(libwechatcommon.so).add(%d), { onEnter: function(args) { // args[0] 可能是一个结构体指针包含了请求信息 // 这里需要根据逆向分析的结果来解析 var requestInfoPtr args[0]; if (requestInfoPtr.isNull()) return; // 假设我们通过分析知道在偏移0x20处是URL字符串指针 var urlPtr requestInfoPtr.add(0x20).readPointer(); var url urlPtr.isNull() ? null : urlPtr.readUtf8String(); // 假设在偏移0x30处是请求头链表指针 var headersPtr requestInfoPtr.add(0x30).readPointer(); var headers {}; if (!headersPtr.isNull()) { // 这里需要根据实际的链表结构进行遍历解析此处为示例 // 可能是键值对数组或Map结构 } // 发送回Python端 send({ type: network_request, url: url, headers: headers, timestamp: Date.now() }); // 可以在这里修改参数或返回值以实现拦截或篡改 // this.context.pc ... ; // 修改PC寄存器可以改变执行流 }, onLeave: function(retval) { // 函数执行完成后处理 // console.log(函数返回: retval); } }); % TARGET_FUNCTION_RVA # 4. 创建脚本并加载 script session.create_script(js_code) script.on(message, on_message) print([*] 正在加载Hook脚本...) script.load() print([*] Hook脚本加载成功正在监听...) # 5. 保持脚本运行 try: sys.stdin.read() except KeyboardInterrupt: print(\n[*] 用户中断停止Hook。) finally: if session: session.detach() if __name__ __main__: main()5.2 脚本关键环节解析与定制这个脚本是一个框架其中最关键的部分是JavaScript代码块中的onEnter函数。里面的解析逻辑如requestInfoPtr.add(0x20).readPointer()必须根据你在IDA Pro中的实际分析结果来编写。结构体解析args[0]很可能是一个指向复杂结构体的指针。你需要通过IDA的静态分析弄清楚这个结构体的内存布局。常用的方法是在目标函数开头下断点动态调试时观察args[0]指向的内存区域。在IDA中查看传递给目标函数的上级调用者看它是如何准备这个结构体的。通过反复调试和猜测结合常见的网络请求结构包含URL、方法、头、体等字段逐步确定各个字段的偏移量。数据类型处理Frida的Memory.readPointer()、readUtf8String()、readByteArray()等API用于读取进程内存。对于嵌套指针或复杂数据结构需要逐层解析。稳定性处理脚本中加入了大量的空指针检查.isNull()这是因为在Hook过程中可能会遇到参数无效的情况。不加检查会导致脚本崩溃。实操心得在编写Hook脚本时不要追求一次性完美解析所有数据。应该采用“增量开发”的方式。首先只Hook并打印args[0]的地址和几个关键偏移量的原始十六进制值。然后手动触发一次网络请求观察输出。再根据输出在IDA或动态调试中查看对应内存验证你的猜想逐步完善解析逻辑。这样能有效避免因错误解析导致的脚本失效或崩溃。6. 版本适配与通用化思路我们以11275版本为例进行了分析但微信更新频繁so库一变函数偏移量就全变了。如何让我们的方法具备一定的持续有效性6.1 基于特征码的地址定位最可靠的方法是不依赖固定的偏移量而是搜索函数内部一段独特的字节序列特征码。只要函数逻辑主体不变即使编译器优化导致指令地址变化其核心字节码序列也相对稳定。在IDA中提取特征码在目标函数的汇编视图中选择一段长度适中通常20-40字节、不包含直接地址引用如调用其他函数的BL指令的代码。在Hex View中复制其十六进制表示例如“7F 45 4C 46 02 01 01 00 00 00 00 00 00 00 00 00”。编写特征码扫描脚本修改Frida脚本在libwechatcommon.so的内存空间中扫描这段特征码。Frida提供了Memory.scan()API来实现这个功能。var moduleBase Module.findBaseAddress(libwechatcommon.so); var signature 7F454C46020101000000000000000000.split( ).map(h parseInt(h, 16)); Memory.scan(moduleBase, Module.findSizeOf(libwechatcommon.so), { onMatch: function(address, size){ console.log([] 发现特征码在: address); // 这里可以进一步验证比如检查地址附近是否具有函数特征 // 然后Hook这个地址 Interceptor.attach(address, { ... }); }, onComplete: function(){ console.log(扫描完成。); } });这种方法比固定偏移量健壮得多即使函数位置移动只要代码特征不变就能找到它。6.2 逻辑定位与模糊匹配如果特征码也因代码更新而失效就需要退回到更上层的逻辑定位。例如我们的目标是Hook“添加签名头的函数”。那么我们可以Hook底层系统函数比如Hooklibc的send或sendto函数然后回溯调用栈寻找属于libwechatcommon.so且函数名/代码逻辑符合特征的调用者。Hook Java-Native接口JNI如果签名函数是通过JNI被Java层调用的我们可以先Hook对应的JNI方法函数名通常以Java_开头再从那里向下跟踪。这些方法更复杂但适应性最强。它们要求分析者对程序运行时的调用关系有清晰的理解。7. 常见问题、排查技巧与安全边界在实际操作中你一定会遇到各种各样的问题。下面是我踩过坑后总结的一些典型问题与解决方法。7.1 调试与Hook过程中的典型问题问题现象可能原因排查与解决思路IDA无法附加到进程1. 调试服务器未运行或崩溃。2. 进程已反调试。1. 检查android_server64是否在设备上运行端口转发是否正确。2. 尝试在android_server64启动时加-D参数守护进程模式。3. 微信可能检测了调试器尝试在非调试模式下启动微信后再附加或使用一些反反调试技巧如修改TracerPid。Frida脚本注入后无任何输出1. 脚本语法错误。2. Hook的地址不正确。3. 目标函数未被调用。1. 检查Frida脚本的JavaScript语法用frida -l your_script.js -U com.tencent.mm直接测试。2. 在脚本开头加console.log(“脚本已加载”)验证注入成功。3. 确认Hook的RVA或特征码是否正确先用Frida枚举模块内所有导出函数看看目标是否存在。4. 确保你的操作能触发小程序的网络请求有时需要特定页面或操作。Hook后应用闪退1. Hook函数时破坏了寄存器或栈平衡。2. 在onEnter/onLeave中执行了耗时操作。3. 访问了非法内存地址。1. 在Frida的onEnter中尽量只读取内存避免修改上下文this.context除非你知道后果。2. 将复杂的处理逻辑如网络上传放到onLeave中或异步执行。3. 加强空指针和边界检查确保readPointer()等操作的目标地址有效。捕获到的数据乱码或不全1. 结构体偏移量分析错误。2. 字符串编码不是UTF-8。3. 数据被压缩或加密。1. 回到IDA结合动态调试重新分析参数结构。可以尝试Hook函数入口和出口对比参数变化。2. 尝试用Memory.readCString()或readUtf16String()等其他编码读取。3. 如果数据在传递过程中已被加密可能需要继续向上游追踪找到加密前的函数。7.2 安全、合规与伦理边界在进行任何逆向工程时都必须清醒地认识到法律和伦理的边界。目的正当性逆向分析应仅用于学习、研究、安全评估或兼容性开发等合法目的。绝不能用于破解软件、窃取用户数据、制作外挂或进行任何形式的非法牟利。尊重知识产权分析过程中接触到的代码、算法是开发者的知识产权。公开分享的笔记应停留在方法论和思路层面避免披露具体的、未公开的算法细节、密钥或大量原始代码。用户隐私通过Hook获取的网络请求数据可能包含用户隐私信息。在测试环境中应使用自己的测试账号。任何捕获的数据都不得泄露或用于侵犯他人隐私。平台规则微信等平台有明确的使用条款禁止逆向、篡改客户端。你的分析行为可能违反这些条款导致账号被封禁。因此务必在独立的测试设备或模拟器中进行所有操作切勿在主力机或生产环境上进行。逆向工程是一把双刃剑它赋予我们深层次理解系统运作的能力但同时也要求使用者具备更高的责任感和自律性。将技术用于提升自身能力、改善工具效率或增强系统安全才是其价值的正确体现。