{搬运}某兔儿童手表App去强制升级手动修改教程
本文搬运自吾爱破解用户@raincrack 如需下架请联系[email protected]
申明:本文使用了deepseek chat润色,且破解过程和思路参考了deepseek chat给出的建议!
一、目标:我们要解决什么问题
现象: 这个 App 老是弹"您必须更新 / 发现新版本",点"暂不升级"也没用,过一阵又弹,甚至强制升级。
为什么要去升级(本次修改的原因): App 的新版本取消了很多常用功能,例如「守护听」(实时守护聆听儿童手表周围声音)、「直接播放/拨打儿童手表电话号码」等。这些功能在日常使用中很重要,一旦升到新版就没有了。为了继续使用这些功能,决定停留在当前 3.5.0 版本,并把新版本的升级提示和升级入口彻底关掉,防止被新版覆盖。
目标: 把 App 的版本升级提示和升级功能彻底关掉,但不影响手表固件 OTA(设置里的"系统升级")。
手段: 反编译 APK → 找到负责升级的代码 → 把关键方法"改没" → 重新打包 → 用自己证书签名。
贯穿全文的核心思想: 遇到一个"功能",先搞清楚它是"谁触发的、数据从哪来、结果往哪去",找到这条链上最上游、最集中的那个"开关",把它关掉。改"开关本身"(方法体)比到处删调用点更省事、更安全。
二、分析篇 · 我是怎么一步步找到修改点的
2.1 先看 APK 是什么(解压侦察)
APK 本质是个 zip 压缩包,第一步先解压看结构:
复制代码 隐藏代码
unzip 米兔App3.5.0.apk -d apk_extract
解压后看到:
复制代码 隐藏代码
classes.dex ← 第 1 个 dex(Android 字节码)
classes2.dex ... classes8.dex ← 一共 8 个 dex
res/ ← 资源文件(图片、布局、混淆文件名)
resources.arsc ← 资源索引表
assets/ ← 内置文件
lib/ ← .so 原生库(arm64-v8a / armeabi-v7a / x86)
AndroidManifest.xml ← 组件清单(二进制格式)
META-INF/ ← 签名信息
从这些特征能看出什么:
8 个 dex → 很大的 App,类很多,用了 multidex 分包;
assets/flutter_assets、mediapipe、HMSCore-.properties、firebase、play-services* → 技术栈很杂(Flutter + 华为HMS + Firebase + MLKit + 地图SDK 等);
assets/Download_Agent/6260、6261 → 这些是手表固件的刷机包,跟 App 自身升级是两回事。
这一节的思考: 现在还完全不知道升级代码在哪。但"技术栈复杂、有 8 个 dex"说明直接用文本搜索整包几乎没用,必须先反编译成可读代码再分析。同时先把"手表固件升级"和"App 版本升级"在心里分清楚,避免找错对象。
2.2 反编译成 Java(为什么用 jadx)
dex 里是二进制字节码(smali),人眼没法直接读。两个选择:
工具 产出 适合
jadx 反编译成 Java 源码(接近原始代码) 分析理解逻辑(选这个)
apktool 反编译成 smali(汇编风格字节码) 直接改代码重打包
策略: 先用 jadx 把整个包翻成 Java,快速读懂逻辑、找到要改的方法;改的时候再回到 smali 层面动手(因为最终要改的是 dex 里的字节码)。先读得懂,才改得对。
反编译命令(--no-res 只翻代码,快很多):
复制代码 隐藏代码
java -jar jadx.jar -d src --no-res 米兔App3.5.0.apk
几分钟后得到约 2.8 万个 .java 文件,大多是第三方库(androidx、com/google…)和混淆包(a/、b/、k/…)。
2.3 从一堆类里认出主包(找"自己人")
2.8 万个文件绝大多数是第三方库,怎么找出 App 自己的代码?思路:找只有这个 App 才有的字符串。 在 dex 原始字节里搜品牌相关字符串(dex 里字符串是明文存储的):
复制代码 隐藏代码
grep -aoh "mitu[a-z0-9._]*" *.dex | sort -u
搜到:mitu[.]xunkids[.]com、mituapi[.]xunkids[.]com、mitumob…
说明 App 的服务器域名是 *.xunkids.com,开发方是"小寻"(XiaoXun)。顺着包名一查:主包 com.xiaoxun.xun,应用包名 com.imibaby.client,Application 类叫 ImibabyApp。
这一节的意义: App 里可能嵌了别的 SDK,它们也会触发升级(比如华为升级 SDK)。只有先锁定"哪个包是 App 自己的",分析才不会被带偏。
2.4 定位升级功能(三条思路,哪条走通)
思路 A:直接搜中文文案(✗ 走不通)
想在 dex/源码里直接搜"立即升级""强制升级",搜不到。
为什么搜不到: App 的界面文字不放在代码里,而是放在资源表 resources.arsc 里,代码里只有资源 ID(一串数字)。但这也给了我一个反向入口。
思路 B:从"资源 ID"反查(✓ 走通了)
反编译出的 R$string.java 保存了所有字符串资源名和 ID,先在里面找名字带 update/version/升级的:
复制代码 隐藏代码
grep -n "update|version|upgrade" com/xiaoxun/xun/R$string.java
命中关键的一批:app_update_force(多半是"强制升级"弹窗标题)、app_update_wait、newversion_download、check_new_version_desc、have_new_version…
于是反过来搜"谁引用了 app_update_force":
复制代码 隐藏代码
grep -rn "app_update_force" --include="*.java" .
命中 com/haidii/wear/pe.java —— 升级弹窗代码的所在之处。
这是整个分析的关键转折点: 中文在资源表里,但资源名(app_update_force)是编程常量,反编译后保留在代码里。用"资源名 → 谁引用它"就能精准钓出弹窗类。
思路 C:搜英文关键词(辅助确认)
同时用 forceUpdate、checkUpdate、upgradeApp 搜,命中 MainActivity.java 等,进一步确认升级检查的触发入口。三条路互相印证,结论一致。
2.5 顺着调用链摸清升级全流程
读 pe.java 的核心方法 i(),梳理出完整逻辑:
复制代码 隐藏代码
MainActivity.onCreate()
└─> Z1() # "startCheckUpdate" 日志
└─> pe.i(app, this, true) # 判断要不要弹窗(返回 0/1/2)
├─ 读本地 app_update.info(服务器上次下发的升级JSON)
├─ 若 versionCode>186 且 force==1 → 弹"您必须更新"(强制,无冷却)
├─ 否则非强制 → 弹"推荐升级"(带 24 小时冷却)
└─ 若 pe.i() 返回 0 → 再调 pe.d() → pe.c() → re.b()
└─ POST /upgradeApp/v2 拉最新升级信息 → 存回 app_update.info
(点"立即升级"确认后)
└─ ne.g() # 小米/红米调起小米应用商店,vivo 调 vivo 商店
ne.b() # 查询应用商店当前版本号
四个关键类的职责:
类 一句话职责 对应现象
pe 判断版本、决定弹不弹窗 "您必须更新"弹窗
re 向服务器拉升级信息 每次启动的检查请求
ne 查商店版本 / 调起商店升级 点"立即升级"后的跳转
SignChecker 校验 APK 签名,失败 3 秒后退出 重打包后启动闪退
另外,在 ImibabyApp(Application 类)的 onCreate 里看到 checkSign() → SignChecker.checkSignFromLocal(),这是签名自校验:SHA1 必须等于官方值 59:B0:36:…,否则弹"签名校验错误"然后 System.exit(0)。重打包后签名必然变化,所以这道校验也必须处理。
为什么"想关掉升级"的人会额外关心签名校验? 因为重打包必然改变签名。不处理的话,改完的包一启动就因签名校验失败被杀。所以"改升级"和"绕签名校验"是打包解决的两件事。
2.6 关键:为什么是 classes7.dex?怎么定位的
答案:jadx 反编译时会在每个 .java 文件头部留一句来源标注。
打开 pe.java、re.java、ne.java、SignChecker.java 的头部,都有一行:
复制代码 隐藏代码
/* JADX INFO: loaded from: classes7.dex */
这行字是 jadx 反编译时自动加的,意思就是"这个类的原始字节在 classes7.dex 这个文件里"。四个类全是同一句。
进一步验证: 用 apktool 反编译后,每个 dex 单独一个目录(smali/、smali_classes2/…smali_classes8/),四个文件确实都在 decompile/smali_classes7/ 下:
复制代码 隐藏代码
decompile/smali_classes7/com/xiaoxun/xun/utils/SignChecker.smali
decompile/smali_classes7/com/haidii/wear/pe.smali
decompile/smali_classes7/com/haidii/wear/re.smali
decompile/smali_classes7/com/haidii/wear/ne.smali
为什么它们会"恰好"都在 classes7.dex?
Android 构建工具做 multidex 分包时,会把互相引用的类尽量分到同一个 dex,减少跨 dex 引用、保证冷启动加载顺序正确;
pe 调 re、ne,re/ne 又都引用 ImibabyApp,而 SignChecker 被 ImibabyApp 直接调用——它们是一张紧密的调用网,被构建工具分进了同一个 dex;
这个 App 的升级模块代码集中在 com.haidii.wear 包(相对独立的功能模块),和调用方 SignChecker 一起落在了 classes7.dex。
定位 dex 有什么用? 非常关键——意味着这次改动只需要重打 classes7.dex 这一个文件,其他 7 个 dex、全部资源、assets、lib 都可以保持原始字节不动。改动面最小、兼容性最好,也方便排查问题。
2.7 定修改方案(为什么这样改)
最终只动 4 个类、7 处方法:
类 方法 改成什么 为什么改这里
SignChecker checkSignFromLocal 恒传 true(校验必过) 不绕这个,重打包后启动即被签名校验杀掉
pe i() 恒返回 0(=无升级) 所有调用点问它"有没有升级"都答"没有",任何弹窗都不弹
pe c() / d() 空实现 禁止"去服务器拉升级信息/发起检查"
re b() 空实现 从根上掐断 /upgradeApp/v2 请求
ne b() / g() 空实现 禁止查商店版本 / 调起商店升级(双保险)
为什么改成"方法体极简"而不是删掉调用点? 改方法本体,那么所有调用它的地方(MainActivity、SystemUpdateActivity、SettingFragment…)自动全部失效,不用逐个去改、也不会漏改。这是"改开关"而非"拆线路"。
为什么不把 MainActivity 里的调用也删掉? 没必要。调用还在,但它问的都是空方法/恒 0,行为上等于没调用。改动越少,越不容易引入新的编译错误。
✅ 为什么不用担心影响手表 OTA? 手表固件升级走的是另一套方法(ImibabyApp.checkUpdateWatch、startSystemUpdateActivity(watchUpDateInfo,…)),与上面 4 个类无关,所以"去 App 升级"完全不影响"手表升级"。
三、动手篇 · 一步一步操作
3.1 准备工具
工具 用途 获取
Java 环境(JRE/JDK 17+) 运行下面所有 jar 官网,或直接用 jadx-gui 自带 jre\bin\java.exe
apktool.jar 反编译 / 重新打包(内置 smali 工具) github[.]com/iBotPeaches/Apktool/releases
uber-apk-signer.jar 签名 + zipalign(一次 v1/v2/v3) github[.]com/patrickfav/uber-apk-signer/releases
keytool 生成签名钥匙(Java 自带) java目录\bin\keytool.exe
Python 3 跑补资源脚本 python[.]org
文本编辑器 改 smali VSCode / Notepad++
工作目录假设为 D:\apkmod\,工具 jar 放 D:\apkmod\tools\,官方包放 D:\apkmod\米兔App3.5.0.apk。
3.2 反编译 APK
复制代码 隐藏代码
java -jar apktool.jar d -r -f -o decompile 米兔App3.5.0.apk
-r:保留原始资源(不解码 XML/图片),让打包后资源尽量和原包字节一致;
-f:覆盖已有输出;-o decompile:输出目录。
完成后 decompile\ 下出现 smali/、smali_classes2/…smali_classes8/、AndroidManifest.xml、resources.arsc、assets/、lib/ 等。
为什么用 apktool 而不是 jadx 来改? jadx 只能看、不能"改回" dex(它的反编译不可逆)。要真正改代码并重新打包,标准工具就是 apktool:把 dex 反汇编成可编辑的 smali,改完再汇编回去。分析用 jadx,修改用 apktool,各司其职。
3.3 修改 smali(7 处,每处带定位方法)
用编辑器打开文件,搜索方法签名行(形如 .method public static xxx(…)),把整个方法体(从 .method 到 .end method)替换成下面的极简写法。方法签名那一行必须一字不改地保留(参数和返回类型),只换中间内容。
① SignChecker.smali —— 绕过签名自校验
怎么找到它: 分析时从 ImibabyApp.onCreate() 里的 checkSign() 顺藤摸瓜。
搜索 .method public static checkSignFromLocal,替换为(不管签名是什么都传 true):
复制代码 隐藏代码
.method public static checkSignFromLocal(Landroid/content/Context;Lcom/haidii/wear/k33;)V
.locals 1
const/4 v0, 0x1
invoke-interface {p1, v0}, Lcom/haidii/wear/k33;->a(Z)V
return-void
.end method
这三行 smali 是什么意思?
const/4 v0, 0x1:把寄存器 v0 设为 1(即 true);
invoke-interface {p1, v0}, …k33;->a(Z)V:调用回调对象的 a(true);
return-void:方法结束。原来那些 SHA1 比对全部不再执行。
② pe.smali —— 让"是否有升级"恒为无
怎么找到它: 分析时靠 app_update_force 资源名反查到的类;方法 i() 是判断核心。
搜索 .method public static i(,替换为(返回 0,即"没有升级"):
复制代码 隐藏代码
.method public static i(Lcom/xiaoxun/xun/ImibabyApp;Landroid/app/Activity;Z)I
.locals 1
const/4 v0, 0x0
return v0
.end method
再搜索 .method public static c( 和 .method public static d(,替换为空方法(禁止拉取/检查):
复制代码 隐藏代码
.method public static c(Lcom/xiaoxun/xun/ImibabyApp;Z)V
.locals 0
return-void
.end method
.method public static d(Lcom/xiaoxun/xun/ImibabyApp;Z)V
.locals 1
return-void
.end method
③ re.smali —— 掐断服务器拉取
怎么找到它: 顺着 pe.i() 里的调用看到它调 re.b() 拉 /upgradeApp/v2。
搜索 .method public static b(,替换为空方法:
复制代码 隐藏代码
.method public static b(Lcom/xiaoxun/xun/ImibabyApp;Z)V
.locals 0
return-void
.end method
④ ne.smali —— 禁止调起应用商店
怎么找到它: pe.c() 里调 ne.b() 查商店版本;升级确认回调里调 ne.g() 调起商店。
搜索 .method public static b( 和 .method public static g(,替换为空方法:
复制代码 隐藏代码
.method public static b(Landroid/content/Context;Lcom/haidii/wear/oe;)V
.locals 0
return-void
.end method
.method public static g(Landroid/content/Context;)V
.locals 0
return-void
.end method
✅ 检查清单(改完必看): ① 签名行没动过;② 每处替换后都有配套的 .end method;③ 返回类型对得上(I 用 return v0,V 用 return-void)。
换别的版本/App 怎么办: 类名、方法名可能不同。通用找法:在 smali 里全局搜 upgradeApp、forceUpdate、checkUpdate、app_update、newversion,顺着调用链找到"弹窗判断/拉取/调起商店"三个环节,按同样思路改。
3.4 重新打包
复制代码 隐藏代码
java -jar apktool.jar b decompile -o unsigned.apk
成功标志:输出 Built apk into: …\unsigned.apk。接下来必须先做 3.5 的资源检查,再签名。
3.5 修复资源缺失(关键坑)
⚠️ 真实踩过的坑: apktool(尤其 v3 在 -r 模式下)反编译时会丢掉部分 res/ 文件,最常见是混淆过的短文件名(res/-8H.xml、res/0QS.png 这类)。当时丢了 135 个,导致 App 启动画面结束后、进入主界面加载布局/图片时抛 Resources.NotFoundException 直接闪退。
怎么发现丢没丢:对比文件清单。 存成 check_res.py,改好两个路径后 python check_res.py:
复制代码 隐藏代码
import zipfile
ORIG = 'D:/apkmod/米兔App3.5.0.apk' # 原始 APK
MOD = 'D:/apkmod/unsigned.apk' # 刚打包的
def is_sig(f):
if not f.startswith('META-INF/'): return False
r = f[len('META-INF/'):]
return r == 'MANIFEST.MF' or r.endswith('.RSA') or r.endswith('.SF') or r.endswith('.DSA')
on = set(zipfile.ZipFile(ORIG).namelist())
mn = set(zipfile.ZipFile(MOD).namelist())
miss = [f for f in sorted(on - mn) if not is_sig(f)]
print('丢失的非签名文件数:', len(miss))
for f in miss: print(' ', f)
为什么这个对比能发现问题: 我们只改了 dex,理论上其他文件都应原样。所以"原始有、改后没有"的文件就是异常,几乎一定是资源丢失。输出 0 则没丢,可直接签名。
丢了就重建:以原始包为准补全。 存成 fix_apk.py,改三个路径后运行:
复制代码 隐藏代码
import zipfile
ORIG = 'D:/apkmod/米兔App3.5.0.apk' # 原始 APK
MOD = 'D:/apkmod/unsigned.apk' # 改好但缺资源的包
OUT = 'D:/apkmod/fixed.apk' # 输出:补全后的包
def is_sig(f):
if not f.startswith('META-INF/'): return False
r = f[len('META-INF/'):]
return r == 'MANIFEST.MF' or r.endswith('.RSA') or r.endswith('.SF') or r.endswith('.DSA')
z_orig = zipfile.ZipFile(ORIG)
z_mod = zipfile.ZipFile(MOD)
mod_dex = {n for n in z_mod.namelist() if n.startswith('classes') and n.endswith('.dex')}
out = zipfile.ZipFile(OUT, 'w', zipfile.ZIP_DEFLATED)
n = 0
for info in z_orig.infolist():
name = info.filename
if is_sig(name):
continue # 剔除旧签名文件
data = z_mod.read(name) if name in mod_dex else z_orig.read(name)
out.writestr(info, data) # 保留原压缩方式
n += 1
out.close()
print('重建条目:', n)
z = zipfile.ZipFile(OUT)
on = set(z_orig.namelist())
left = [f for f in (on - set(z.namelist())) if not is_sig(f)]
print('复查仍缺失(非签名):', len(left))
print('res 文件数:', len([f for f in z.namelist() if f.startswith('res/')]))
print('完成。输出:', OUT)
脚本逻辑一句话: 把原始 APK 完整重写一遍(所有文件内容取自原始包),只有 8 个 classes*.dex 换用你改好的 unsigned.apk 里的版本,最后删掉旧签名文件。这样 = "原汁原味的资源 + 你的 dex 修改"。
3.6 生成签名钥匙并签名
① 生成自己的 keystore(只做一次,以后一直用)
为什么必须自己生成、且信息要规范: 用 Android 调试证书或 CN=Unknown 的证书,正是杀软报 a.gray.* 误报的常见原因。信息填完整、填真实的,能大幅减少误报。
复制代码 隐藏代码
keytool -genkeypair -v
-keystore D:/apkmod/myapp.keystore
-alias myapp
-keyalg RSA -keysize 2048 -validity 10000
-storepass 你的密码 -keypass 你的密码
-dname "CN=你的公司或团队名, OU=你的团队, O=你的公司, L=Beijing, ST=Beijing, C=CN"
记住: keystore 和密码单独备份。以后每次签名都用同一把,才能覆盖安装、最不容易误报。
② 签名
复制代码 隐藏代码
java -jar uber-apk-signer.jar
-a D:/apkmod/fixed.apk
--ks D:/apkmod/myapp.keystore
--ksAlias myapp
--ksPass 你的密码 --ksKeyPass 你的密码
-o D:/apkmod/signed
看到 signature verified [v2, v3] 和 Successfully processed 1 APKs and 0 errors 即成功,成品在 D:\apkmod\signed\。
3.7 安装与验证
先卸载手机上已装的旧版(签名不同,无法覆盖);
开启"允许未知来源",安装;
逐项核对下表。
验证项 预期
App 能正常启动进入主界面 不再闪退
"签名校验错误"弹窗 不出现
"您必须更新/新版本"弹窗 完全不出现
设置 → 系统升级(手表 OTA) 正常可用
安装时安全提示 正常不再报 a.gray 类风险
若闪退,可在开发者模式下抓崩溃日志:adb logcat -s AndroidRuntime:E,看 FATAL EXCEPTION 定位是哪个类/资源。
四、常见问题
Q1:装的时候提示"包含病毒 a.gray.xxx"?
绝大多数是误报。触发源通常是:用了 Android 调试证书 / 证书信息不规范 / 重打包本身被保守对待。解决:用 3.6 的自建规范证书重签。仍报说明该引擎对重打包保守,不影响自用;可传 VirusTotal 看是否只有一两家国内引擎报。
Q2:启动画面结束就闪退?
99% 是资源缺失,回到 3.5 用对比脚本检查 res/,再用重建脚本补回。
Q3:apktool build 报错?
多半是 smali 方法签名被改错。检查:.method 行的参数与返回类型必须与原方法一致(尤其结尾的 I/V),.end method 别丢。
Q4:别的版本找不到这些类/方法?
在 smali 里全局搜 upgradeApp、forceUpdate、checkUpdate、app_update、newversion,顺着调用链找"弹窗判断 / 拉取 / 调起商店"三环,按同样思路改。
Q5:以后官方出新版怎么办?
这个改包不再接收官方升级(这正是目的)。想要新版:拿新版官方 APK 重走一遍本教程,用同一把 keystore 签名,可直接覆盖安装。
五、重要提醒
合规: 本教程仅限自用、用于自己拥有的 App。请勿用于分发、上架、破解他人商业产品。
keystore 务必保存好,丢了以后无法再给后续版本签名覆盖安装。
修改后官方补丁(含安全修复)也收不到了,自行权衡。
本机若有 jadx-gui,可用它自带的 jre\bin\java.exe 跑所有 jar,不必另装 Java。