ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Xposed 获取微信好友列表(通讯录),TaoToken 统一 Key 通道下的抓取链路拆解

Xposed 获取微信好友列表(通讯录),TaoToken 统一 Key 通道下的抓取链路拆解 1. 从通讯录页面到 CursorXposed 抓取微信好友列表的真实链路微信通讯录这个页面看起来只是一个普通列表但真要从里面把好友列表完整拿出来绕的弯比想象中多。我一开始也以为直接 Hook 一个getView或者某个 adapter 的getItem就能拿到数据结果发现列表项布局 ID 是混淆过的类名、方法名全是a、f、dy5这种静态分析能看懂逻辑但定位数据源要一层层往上翻。这篇要解决的核心问题很具体在 Android 逆向场景下用 Xposed 模块 Hook 微信通讯录接口把好友列表字段和分页逻辑梳理清楚并且用 TaoToken 统一 Key 通道做请求鉴权的验证动作确认返回数据是否完整。适合已经会写基础 Xposed 模块、能看懂 jadx 反编译结果、想复现通讯录抓取链路的读者。链路大致是这样通讯录列表 → adapter 的getItem()→moveToPosition()→ 某个aaa()方法 → 拿到Cursor实际类型是HRK→ 从Cursor里按列读取com.tencent.mm.storage.f泛型对应的字段。关键点在于Cursor不是直接 new 出来的而是通过一个静态方法c.axA()配合linkedList参数里面装weixin字符串生成的。Hook 住这个静态方法就能拿到Cursor的构造参数再反射调用Cursor的getColumnNames()和getString()好友列表就出来了。分页逻辑这块微信通讯录并不是一次性把全部好友塞进一个Cursor而是按一定批次查询。moveToPosition()配合getCount()决定了当前 adapter 能看到多少条。实测下来Cursor的getCount()返回的是当前查询批次的总数翻页时Cursor会重新查询或者复用。要核对数据完整性就得在 Hook 里记录每次Cursor的getCount()和实际读取到的行数对比通讯录页面滚动到底部时的总数。TaoToken 在这里的角色是统一 Key 通道。逆向过程中经常需要把抓到的数据发到自己的服务端做校验或者调用模型接口做字段解析、去重、格式化。如果每个请求都单独配 Key管理起来很乱。TaoToken 的 API 地址是https://taotoken.net/api用统一 Key 做鉴权请求头里带Authorization: Bearer 你的Key就能把抓取到的数据发出去做验证。这样抓取链路和鉴权链路分开调试的时候不会互相干扰。2. TaoToken 统一 Key 通道前置准备Base URL、Key 与 Model ID 三件套在开始 Hook 之前先把 TaoToken 的通道配好。这一步不是可选项因为后面验证请求、核对返回数据都要用到。TaoToken 的官网是https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentAPI 地址是https://taotoken.net/api注意 API 地址后面不加 UTM 参数直接用于代码里的 Base URL。你需要准备三件套Base URL、API Key、Model ID。Base URL 就是https://taotoken.net/apiAPI Key 在控制台的 API Keys 页面生成Model ID 根据你要调用的模型来选。比如做字段解析可以用通用对话模型做代码辅助可以用 coding 专用模型。这三件套在后面的配置片段里会反复出现先记下来。如果你用的是 Claude Code 或者类似的编码工具TaoToken 也支持通过配置文件接入。比如在settings.json里配置{ env: { ANTHROPIC_BASE_URL: https://taotoken.net/api, ANTHROPIC_AUTH_TOKEN: 你的TaoToken Key, ANTHROPIC_MODEL: 你的Model ID } }这个配置片段可以直接复制路径和原文一致。注意ANTHROPIC_BASE_URL后面不要加斜杠ANTHROPIC_AUTH_TOKEN填你在 TaoToken 控制台生成的 KeyANTHROPIC_MODEL填模型 ID。配好之后Claude Code 的请求就会走 TaoToken 的统一 Key 通道。如果你用的是 Cline 或者 Roo Code 这类插件配置方式类似在 MCP 或者 API 配置里填 Base URL、Key、Model ID。Cline 的 MCP 配置里Base URL 填https://taotoken.net/apiKey 填 TaoToken 的 KeyModel ID 填对应模型。这样插件在调用模型时鉴权就走 TaoToken不用每个插件单独配。Codex 的auth.json配置也类似把 Base URL 和 Key 填进去就行。三件套的核心逻辑是一样的Base URL 指向 TaoToken 的 API 地址Key 做鉴权Model ID 指定模型。配好之后你可以先用一个简单的请求验证通道是否通curl -X POST https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer 你的TaoToken Key \ -H Content-Type: application/json \ -d { model: 你的Model ID, messages: [{role: user, content: ping}] }如果返回正常说明通道没问题。这一步做完再进入 Xposed 模块的配置和 Hook 点定位。3. 可复制配置Xposed 模块 Hook 微信通讯录接口的完整片段现在进入正题写 Xposed 模块。先确认环境一台 root 过的 Android 设备或者模拟器装了 Xposed 框架或者 LSPosed微信版本要和你反编译的版本一致。我实测用的是微信 8.x 某个版本类名和方法名可能随版本变化但链路逻辑是一样的。模块的build.gradle里加 Xposed 依赖dependencies { compileOnly de.robv.android.xposed:api:82 }AndroidManifest.xml里声明 Xposed 模块application meta-data android:namexposedmodule android:valuetrue / meta-data android:namexposeddescription android:valueHook 微信通讯录 Cursor / meta-data android:namexposedminversion android:value82 / /application主 Hook 类实现IXposedHookLoadPackage在handleLoadPackage里判断包名com.tencent.mm然后 Hook 目标方法。根据前面的链路分析关键 Hook 点是那个静态方法c.axA()它接收一个linkedList参数返回Cursor。Hook 代码大概这样public class WeChatHook implements IXposedHookLoadPackage { Override public void handleLoadPackage(LoadPackageParam lpparam) throws Throwable { if (!lpparam.packageName.equals(com.tencent.mm)) return; XposedHelpers.findAndHookMethod( com.tencent.mm.storage.c, lpparam.classLoader, axA, LinkedList.class, new XC_MethodHook() { Override protected void afterHookedMethod(MethodHookParam param) throws Throwable { Object cursor param.getResult(); if (cursor null) return; // 反射调用 getCount 和 getColumnNames Method getCount cursor.getClass().getMethod(getCount); int count (int) getCount.invoke(cursor); XposedBridge.log(Cursor count: count); Method getColumnNames cursor.getClass().getMethod(getColumnNames); String[] columns (String[]) getColumnNames.invoke(cursor); for (String col : columns) { XposedBridge.log(Column: col); } } } ); } }这段代码 Hook 住axA方法在方法返回后拿到Cursor反射调用getCount()和getColumnNames()把列名和行数打到 Xposed 日志里。列名对应com.tencent.mm.storage.f的字段比如username、nickname、conRemark这些。如果你还想拿到具体数据可以在 Hook 里遍历Cursorif (cursor.moveToFirst()) { do { StringBuilder sb new StringBuilder(); for (String col : columns) { int idx cursor.getColumnIndex(col); String val cursor.getString(idx); sb.append(col).append().append(val).append(; ); } XposedBridge.log(Row: sb.toString()); } while (cursor.moveToNext()); }注意Cursor的moveToPosition()和moveToNext()会改变游标位置如果 adapter 也在用同一个Cursor你的遍历可能会影响它的显示。实测下来最好在 Hook 里先moveToPosition(-1)再遍历或者复制一份Cursor数据。分页逻辑就体现在getCount()上每次翻页Cursor的getCount()可能不同记录每次的值就能看出分页批次。TaoToken 的配置片段在这里也要用上。把抓到的数据发到 TaoToken 做校验可以用一个简单的 HTTP 请求// 在 Hook 里收集完数据后发到 TaoToken 做字段解析 JSONObject payload new JSONObject(); payload.put(model, 你的Model ID); payload.put(messages, new JSONArray().put( new JSONObject().put(role, user).put(content, 解析以下好友数据: sb.toString()) )); OkHttpClient client new OkHttpClient(); Request request new Request.Builder() .url(https://taotoken.net/api/v1/chat/completions) .header(Authorization, Bearer 你的TaoToken Key) .post(RequestBody.create(payload.toString(), MediaType.parse(application/json))) .build();这样抓取和鉴权就串起来了。Base URL、Key、Model ID 三件套在代码里都出现路径和原文一致。4. 验证请求与成功结果核对返回数据是否完整Hook 跑起来之后怎么确认数据是完整的分两步先看 Xposed 日志里的Cursor列名和行数再用 TaoToken 做一次请求验证。启动微信进入通讯录页面滚动列表。Xposed 日志里会打印类似这样的内容Cursor count: 128 Column: username Column: nickname Column: conRemark Column: alias Column: type ... Row: usernamewxid_xxx; nickname张三; conRemark; alias; type3; ...Cursor count是当前批次的好友数列名对应com.tencent.mm.storage.f的字段。如果你看到count是 128但通讯录页面显示有 300 个好友说明分页了需要继续滚动触发下一批查询。每次axA被调用都会打印一次count把多次的count加起来对比通讯录底部的总数就能判断是否完整。用 TaoToken 做验证请求把日志里的数据发过去让模型帮你核对字段是否齐全、有没有重复。请求成功返回类似{ choices: [ { message: { content: 数据包含 128 条记录字段 username、nickname、conRemark、alias、type 均存在无重复 username。 } } ] }如果返回里提到字段缺失或者重复就回到 Hook 代码里检查是不是漏了某个列或者Cursor遍历时没处理分页。实测下来conRemark字段有时候为空这是正常的不是数据丢失。type字段区分好友类型比如 3 是普通好友其他值可能是公众号或者服务号。验证请求的完整命令可以用 curl 复现curl -X POST https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer 你的TaoToken Key \ -H Content-Type: application/json \ -d { model: 你的Model ID, messages: [ {role: user, content: 核对以下好友数据字段是否完整: usernamewxid_xxx; nickname张三; conRemark; alias; type3;} ] }返回的choices里就是模型的核对结果。如果通道不通会返回 401 或者local proxy failed这时候检查 Key 和 Base URL 是否正确。5. 本篇常见错排查401、local proxy failed、reading choices、OAuthHook 和验证过程中最容易遇到的几个报错我一个个说。401 UnauthorizedTaoToken 请求返回 401说明 Key 不对或者没带。检查Authorization头是不是Bearer 你的KeyKey 有没有复制错。如果你用的是 Claude Code 的settings.json检查ANTHROPIC_AUTH_TOKEN是不是填了 TaoToken 的 Key而不是其他平台的。Base URL 也要确认是https://taotoken.net/api不要多写斜杠或者路径。local proxy failed这个报错通常出现在本地代理配置有问题的时候。如果你在代码里或者工具里配了本地代理检查代理地址和端口。TaoToken 的请求不需要额外代理直接连https://taotoken.net/api就行。如果工具里默认开了代理关掉再试。Cline 或者 Roo Code 的 MCP 配置里如果 Base URL 填错也会报类似的错确认三件套里的 Base URL 是https://taotoken.net/api。reading choices 报错这个通常出现在解析返回 JSON 的时候。TaoToken 返回的结构是{choices: [{message: {content: ...}}]}如果你直接读choices数组没判空或者返回体不是预期格式就会报reading choices相关的错。检查请求是否成功返回体是不是 JSONchoices数组有没有元素。可以在代码里先打印完整返回体再解析。OAuth 相关报错如果你用的是 Claude Code 或者 Codex 的 OAuth 登录方式可能会遇到 OAuth 报错。TaoToken 的接入不需要 OAuth直接用 API Key 就行。在settings.json或者auth.json里把 OAuth 相关的配置去掉只保留 Base URL、Key、Model ID 三件套。Claude Code 的ANTHROPIC_AUTH_TOKEN填 TaoToken Key不要走 OAuth 流程。Hook 不生效Xposed 模块装了但日志没输出检查模块是否在 Xposed 管理器里启用微信是否被选中。LSPosed 的话检查作用域是否包含微信。另外微信版本和反编译版本要一致类名com.tencent.mm.storage.c和方法名axA可能随版本变化用 jadx 重新确认。Cursor 遍历影响列表显示前面提过遍历Cursor会改变游标位置导致通讯录列表显示异常。解决办法是在 Hook 里先保存Cursor的 position遍历完再恢复或者用CursorWindow复制数据。实测下来恢复 position 最省事int pos cursor.getPosition(); // 遍历 cursor.moveToPosition(pos);6. 语义一致 CTA接入文档、API Keys 与模型对话抓取链路跑通之后如果你想把 TaoToken 的通道用到其他逆向或者数据校验场景可以直接看接入文档里面有不同工具和语言的配置示例。API Keys 页面用来生成和管理 KeyCoding Plan 适合长期做编码和 Agent 任务的场景模型对话页面可以快速验证模型是否可用。排障和接入相关的问题优先看 API Keys 和接入文档验证模型效果用模型对话长期编码或者 Agent 任务看 Coding Plan。地址分别是接入文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewriteAPI Keyshttps://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite模型对话https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentchatutm_campaignrewriteCoding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite最后说一个实用技巧Hook 微信通讯录的时候Cursor的列名和com.tencent.mm.storage.f的字段是对应的但字段顺序不一定和列名顺序一致。用getColumnIndex()按列名取不要按索引硬编码。分页逻辑上getCount()每次返回的是当前批次的数量不是总数要累加多次 Hook 的结果。如果发现某次count突然变小可能是查询条件变了检查linkedList参数里的weixin字符串有没有被替换成其他值。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进