写在前面 本文完全由DeepSeekV4Flash撰写,包括产出物,逆向操作,有了AI之后逆向的成本非常低了,天然反混淆的能力不是人能比的,但是云端服务还是不能作为破解的能力。这次失败的逆向话花了1.1亿Toekn,价值14.14元。

搜狗录音笔 App「本地解锁」逆向实战 —— 思路、步骤与踩坑全记录

目标:厂商停服后,把 sougou.apk(搜狗录音助手 v3.9.6, com.sogou.translatorpen)改造成"能绕过登录、能录音、能本地试听/导出"的可用版本,并尝试解锁其私有音频格式。

一篇把反编译思路 + 每一步具体命令 + 踩过的坑讲清楚的可复制指南。


0. 先说结论(避坑总览)

  • 反编译 / 改 smali / 重打包 / 签名 / 真机安装:完全可行,已有成例。
  • 绕过登录 + 修复崩溃 + 恢复录音功能:✅ 已做到。
  • 本地解码 .avc(搜狗私有音频):❌ 确定性不可行——它不是标准 CELT/Opus/AMR,libairoha-celtapi.so 等 5 个解码器全部解不出,只有搜狗云端才能解;厂商停服即无解。

最关键的认知:很多"厂商专有"的录音笔,其音频编码设计上就只有云端能解(用于在线转文字),本地播放依赖云端返回转好的文件。所以"本地解锁试听/导出"可能从根上就做不到。动手前先判格式,别在解码上空转。


1. 环境与工具(Mac / Windows 通用思路)

工具 作用 备注
jadx (1.5.x) dex → Java 源码 需要 JRE 11+;java -cp jadx-all.jar jadx.cli.JadxCLI -d out classes.dex
apktool (2.9.x) APK ↔ smali;重打包 -p <可写路径> 指定 framework 缓存
zipalign + apksigner 对齐 + 签名 在 Android build-tools/<ver>/
adb + root 装包 / 读私有数据 / 日志 root(Magisk)才能读 /data/data/.../storage/.../Android/data
ffmpeg / ffprobe 验证音频可解性 判格式、尽力恢复
Java keytool 生成签名 keystore -storetype JKS(apksigner 兼容)
# 反编译 Java 源码
java -cp jadx-all.jar jadx.cli.JadxCLI -d ./src classes.dex classes2.dex classes3.dex

# 解包成 smali
java -jar apktool.jar d -p /tmp/fw -o ./out app.apk

# 重打包
java -jar apktool.jar b -p /tmp/fw -o unsigned.apk ./out

# 对齐 + 签名(JKS keystore)
zipalign -f 4 unsigned.apk aligned.apk
apksigner sign --ks release.jks --ks-key-alias <alias> --ks-pass pass:<PASS> \
--key-pass pass:<PASS> --out signed.apk aligned.apk

坑①:jadx 1.5+ 首次运行要在用户目录创建配置目录;macOS 沙箱若禁止写入 ~/Library/Application Support/io.github.skylot.jadx 会拒启,先手动建目录或换 JADX_HOME
坑②:apktool 的 framework 缓存默认写 ~/Library/apktool/framework,被拦时用 -p /tmp/fw
坑③:keytool JDK9+ 默认产出 PKCS12,apksigner(内置 Java8) 解析不了新 PKCS12,-storetype JKS 重新生成。


2. 逆向思路(可复用方法论)

按这个顺序推进,能最快定位"核心功能被什么卡住":

  1. 读清单 → 找主入口、应用类、所需权限。
  2. 找"业务核心包" → 通常是被混淆但仍保留真实命名的大包(本项目是 com.sogou.teemo.translatepen)。
  3. 顺数据流 → 从入口一步步追:登录门控 → 主页 → 详情 → 播放/导出。
  4. 定格式 → 音频/文件是什么编码(用 ffprobe / 解码器暴力测试)。
  5. 判云端 vs 本地 → 关键能力是本地实现还是必须回云端接口。
  6. 再决定"改哪一层" → 是改门控、改数据、还是改解码。

2.1 第一步:看到底是什么 app

aapt dump badging sougou.apk          # 包名/版本/权限/入口
aapt dump xmltree sougou.apk AndroidManifest.xml | grep -i launch
  • 包名 com.sogou.translatorpen,应用名"搜狗录音助手",入口 com.sogou.appcontainer.business.view.EntranceActivity
  • 权限含 BLE / 录音 / 存储 → 是本地蓝牙 + 本地文件型 app。

2.2 第二步:定位真正的业务代码

主包下只有 wxapi(混淆占位)。真正的核心在 com.sogou.teemo.translatepen(1022 文件),因为它没有被过度混淆(方法/字段留着有意义的 Kotlin 名)。这是复用原厂逻辑的关键。

2.3 第三步:顺"登录门控"找"解锁点"

EntranceActivity 是最佳入手点。jadx 反编译后看 e()(登录判断):

if (TextUtils.isEmpty(UserManager.f8579b.a().r())) { // token 空 → 未登录
startActivity(LoginActivity...); finish(); return;
}
aa.a(new d()); // 已登录 → 去主页

→ 这就是"强制登录"的精确位置。在 smali 里把它短路(无条件跳已登录分支)即可。

同样,用户协议/隐私弹窗、云迁移判定,都会是独立的 if (XX==XX) return/弹窗,逐一定位。

2.4 第四步:处理"未登录导致的连带空白"

绕过登录后会遇到连锁问题:

  • 很多代码依赖 App.getApp().userId / userDao 里的用户 → 不注入用户就会 NPE 或空白
  • 做法:新写一个辅助类(如 OksBootstrap.java/.smali),在入口"构造一个本地假用户并写入 Room UserDao"。
  • 本项目注入用户后,网络请求都带 sgid=local-sgid,主页能正常加载本地录音列表。

2.5 第五步:修"绕过后才暴露的运行时崩溃"

典型:kotlin.UninitializedPropertyAccessException: lateinit property realtimeCore has not been initialized

  • 原因:某个初始化方法(本项目 App.l())开头有 if(状态==0) return; 提前返回,导致核心组件(TeemoService.realtimeCore)从未初始化。
  • 解法:同登录一样,把该 if(...) return 短路成"永远执行初始化"。

原则:把所有"因未登录/未初始化而提前 return"的 if 分支改成无条件放行,比逐个补齐状态更稳。


3. 音频格式判定(决定成与败的那步)

这一步最值得做扎实,能救你大量时间。

在手机上实时录一段,产生文件后:

# 1) 看文件命名/位置
ls -la .../files/local-user/
# 1787817494.mp3 ← 从笔下载(演示):完好
# 1787817530.avc ← 实时录音:私有
# 1787817530_1.avc / _1.mp3 ← 分片

# 2) 看字节头,判是否有标准容器魔数
xxd rec.avc | head # 0x18 0xff 0xfe ...
# OggS/OpusHead/#!AMR/RIFF/ID3 都没有 → 非标准容器

# 3) 帧结构
# 每 80B 一帧、20ms、几乎每帧高熵 → 固定帧长的私有编码

# 4) 暴力试 app 内置所有解码器(写个探测类,见后)
# celtapi / celtapi2 / avo / avm / opus 全部 pcm=0
# 底层 AirohaCeltApi(init 1/2 × 剥帧头) 也全 pcm=0

如果所有标准/内置解码器都解不出 → 基本可判定是"仅云端可解"的私有编码,本地这条路可以放弃了,别继续烧时间。


4. 如何"用 app 自己探解码"(可复用技巧)

与其反复手测,不如在 app 里注入一个解码探测类,一个版本测完所有可能:

public class OksAvcTest {
public static void verify(File f) {
// 对每种 JNIWapperType(celtapi/celtapi2/avo/avm/opus):
// 逐 80B/20ms 帧 → AvcJniWapper.a(frame) → 累加返回的 PCM
// 日志打印 frames / pcmBytes
// 直接调 AirohaCeltApi.celtDecodeInit(1|2) × skip(0|3) 再试
}
}

ShorthandDetailViewModel.ak()(进详情页必调的构造播放列表方法)开头注入 scanAll(),进详情即触发,用 adb logcat 看结果。一次真机操作,测完所有解码组合。


5. 重打包 / 安装 / 真机验证的注意点

adb install -r signed.apk          # 覆盖安装(改签名会清数据)
adb logcat -c && am start -n <pkg>/<EntryActivity>
adb shell "dumpsys activity | grep topResumedActivity" # 确认进到主页
adb shell "pidof <pkg>" # 进程存活 = 没崩
adb shell "su -c 'sqlite3 <db> \"SELECT ... from Session\"'" # 查 Room 数据
adb exec-out "su -c 'cat <file>'" > ./local # root 导出私有文件

常见坑

  • 小米设备会被第三方锁屏(flip.clock)挡住:adb shell "cmd dreams stop-dreaming"
  • USB 连接不稳、反复掉线:把"清日志+启动+抓取+导出"合并到一条命令里,减少断线窗口。
  • 改签名后旧数据被隔离(新签名 = 新 uid 数据区),录音笔内的录音不受影响。

6. 真机数据真相(本项目实证)

  • 蓝牙走 BLE,系统配对列表里没有录音笔(app 主动连,非同传统配对)。
  • 离线笔录后不打开 app,app 再开不会把笔内录音下载到手机 → 该笔是"必须 app 实时连着"的产品设计。
  • 实时录音音频完整地存在 .avc(每帧高熵、结构完整),但 .avc 仅云端能解
  • 本地 LAME 转的 _1.mp3 几乎是空的(有效 MP3 仅 ~1.5KB),所以"修本地 mp3 绕云"也走不通。

7. 最终可交付成果清单

sougou_rec-override.apk      修改版 APK(绕过登录/崩溃修复/可录音)
sougou_rec-avctest.apk 探测版 APK(内含解码测试,仅供调试)
avc_dump/ 已导出的 .avc / _1.mp3 / *_recovered.wav
工程全记录.md / 可行性分析报告.md / 路径A最小改动清单.md
真机验证记录.md / 恢复进度与约束.md

确定性结论:登录、崩溃、录音均可恢复;但私有音频 .avc 仅云端可解,厂商停服后本地无法试听/导出——这是产品设计层面的限制,不是逆向能绕过的。


8. 给后来者的建议

  1. 先判音频/数据格式,再决定投入——私有 + 仅云端 = 基本没戏,别空转。
  2. 绕过登录/崩溃比解码简单 100 倍——这是"改造 app"里投入产出比最高的部分。
  3. **善用"注入探测类"**在真机一次测全,而不是反复改参数打包。
  4. 多 dex 注册与签名隔离小米锁屏USB 掉线这些环境坑,尽早规避。
  5. 保存好每轮 smali 修改的 diff 与每段 logcat,回溯时省大力气。

开源地址

sougou-record