吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 1408|回复: 38
收起左侧

[Android 原创] 某兔儿童手表App去强制升级手动修改教程

[复制链接]
raincrack 发表于 2026-8-8 18:06
本帖最后由 raincrack 于 2026-8-8 20:17 编辑

申明:本文使用了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.commituapi.xunkids.commitumob

说明 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_waitnewversion_downloadcheck_new_version_deschave_new_version

于是反过来搜"谁引用了 app_update_force":

grep -rn "app_update_force" --include="*.java" .

命中 com/haidii/wear/pe.java —— 升级弹窗代码的所在之处。

这是整个分析的关键转折点: 中文在资源表里,但资源名(app_update_force)是编程常量,反编译后保留在代码里。用"资源名 → 谁引用它"就能精准钓出弹窗类。

思路 C:搜英文关键词(辅助确认)

同时用 forceUpdatecheckUpdateupgradeApp 搜,命中 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.javare.javane.javaSignChecker.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 引用、保证冷启动加载顺序正确;
  • perenere/ne 又都引用 ImibabyApp,而 SignCheckerImibabyApp 直接调用——它们是一张紧密的调用网,被构建工具分进了同一个 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.checkUpdateWatchstartSystemUpdateActivity(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.xmlresources.arscassets/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;③ 返回类型对得上(Ireturn v0Vreturn-void)。

换别的版本/App 怎么办: 类名、方法名可能不同。通用找法:在 smali 里全局搜 upgradeAppforceUpdatecheckUpdateapp_updatenewversion,顺着调用链找到"弹窗判断/拉取/调起商店"三个环节,按同样思路改。

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.xmlres/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 安装与验证

  1. 先卸载手机上已装的旧版(签名不同,无法覆盖);
  2. 开启"允许未知来源",安装;
  3. 逐项核对下表。
验证项 预期
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 里全局搜 upgradeAppforceUpdatecheckUpdateapp_updatenewversion,顺着调用链找"弹窗判断 / 拉取 / 调起商店"三环,按同样思路改。

Q5:以后官方出新版怎么办?

这个改包不再接收官方升级(这正是目的)。想要新版:拿新版官方 APK 重走一遍本教程,用同一把 keystore 签名,可直接覆盖安装。


五、重要提醒

  • 合规: 本教程仅限自用、用于自己拥有的 App。请勿用于分发、上架、破解他人商业产品。
  • keystore 务必保存好,丢了以后无法再给后续版本签名覆盖安装。
  • 修改后官方补丁(含安全修复)也收不到了,自行权衡。
  • 本机若有 jadx-gui,可用它自带的 jre\bin\java.exe 跑所有 jar,不必另装 Java。

米兔App3.5.0_去升级版下载地址.txt

180 Bytes, 下载次数: 22, 下载积分: 吾爱币 -1 CB

免费评分

参与人数 1吾爱币 +1 热心值 +1 收起 理由
helian147 + 1 + 1 谢谢@Thanks!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

Hmily 发表于 2026-8-10 15:01
raincrack 发表于 2026-8-8 20:11
因为AI润色后,比自己写的更易读

相反,从我个人看,太AI我是不愿意看的。
qwertyuioplm7 发表于 2026-8-8 19:37
 楼主| raincrack 发表于 2026-8-8 20:11

因为AI润色后,比自己写的更易读

点评

相反,从我个人看,太AI我是不愿意的。  详情 回复 发表于 2026-8-10 15:01
lout 发表于 2026-8-10 15:33
确实AI写的帖子总感觉差点什么,但是不用AI写的又看重写作水平,纠结
aoidcoo 发表于 2026-8-10 15:43
我就有某兔儿童手表
xixi0123 发表于 2026-8-10 16:20
好文章,学习了
passionyh 发表于 2026-8-10 16:34
思路清晰,收藏学习
逆劫古修 发表于 2026-8-10 16:57
很不错的思路,学习了
Tsuki0402 发表于 2026-8-10 18:33
有股ai味
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-9-8 05:08

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表