夺回平板所有权:讯飞 iClass C6 逆向记

设备:iFLYTEK iClass C6(
CB-C6-STU),展锐 UD710,Android 9(SDK 28) 全程只读 + 本机操作,没有攻击任何服务器或者进行任何非法操作。
0. 先导言
自3年前我在高二开发出平板看视频的功能后,我始终没有把破解大业向前推动一步,在高考毕业后,也是如愿收到官方所谓的原生安卓推送,在我拿到后始终感觉这个原生安卓并不原生,依旧是ifly的魔改版。
今天(已经大二了)正好借助Claude Code对这个系统进行一次改造,如果还有同辈手里有这台闲置设备又想让权限更上一层楼,或者为了隐私安全,可以一看。
1. 先把「所有权」拆成四层
这是我一开始就想清楚的事,也是整篇文章的骨架。笼统地问「能不能刷成一个干净的系统」没有意义,因为不同层的答案完全不同:
| 层 | 内容 | 卡在哪 |
|---|---|---|
| 应用层 | 禁应用、装 APK、断网 | 需要 ADB |
| 设置层 | 开发者选项、USB 调试 | 需要一个入口 |
| 系统层 | 换原生 Settings、GMS 进 /system | 需要 root + 写 /system |
| 引导层 | bootloader、自定义 ROM | 需要解锁,教育机基本不给 |
后来证明这个划分是对的:前两层可以全部拿下,后两层受限于个人能力和胆量一步没动。如果你像在其他安卓设备上一样,尝试多次点击版本号的话,会弹出一个需要输入暗码的窗口,这个后面会涉及。
先给结论,免得你抱错期望:
- 展锐(Unisoc)教育机型的 bootloader 基本锁死,没有 TWRP、没有机型适配 ROM
ro.build.type=user+release-keys意味着adb root直接拒绝,没有任何提权空间- 网上流传的「C6 刷机包」来源不明,踩到投毒固件的概率比原厂后门还高
所以这场改造的目标不是「换系统」,是「在现有系统上把被拿走的操作权限要回来」。
2. 第一关:设置里什么都没有
现象很典型:
- 设置不是原生 Android 设置,是深度定制的
- 没有「开发者选项」
- 没有 USB 调试开关
- 拨号盘被删,所以
*#*#83781#*#*这类工程码也敲不了 - 有一个关不掉的「系统更新」通知
但有个有利条件:APK 可以随便装。
这就够了。第一步不是找漏洞,是找入口。
2.1 用 Activity Launcher 绕过菜单
Android 的设置界面本质是一堆 Activity。厂商的做法通常是删掉菜单入口,而不是删除 Activity 本身。
只要 Activity 还在,就能绕过菜单直接启动它。
工具:Activity Launcher(F-Droid 开源)。
进去搜 com.android.settings,展开活动列表 —— 279 项。
279 这个数字本身就是情报:说明 AOSP 的整个 Settings 包还在,只是被挖了几个洞。如果只剩几十项,那才是真删干净了。
3. 三种报错,三种含义(这张表很值钱)
在 279 项里乱点的时候,会遇到三种结果。学会分辨它们,能省掉大量瞎试:
| 报错 | 含义 | 还有救吗 |
|---|---|---|
ActivityNotFoundException | 这个类在 dex 里被删了 | 彻底没戏 |
SecurityException: not exported | 类还在,但没对外开放 | 可能有别的绕法 |
| 正常打开 | 能用 | —— |
我第一次点 Settings$DevelopmentSettingsDashboardActivity,报的是 ActivityNotFoundException。
如果按这张表,结论应该是「类被删了」。但这个结论后来被推翻了 —— 原因在下面。
4. 意外之门:EngineerMode
拨号被砍,工程码敲不了。那就换个思路:工程码最终只是跳转到一个 Activity,和拨号盘没有本质关系。
在 Activity Launcher 的包列表里翻,展锐平台通常有这个包:
com.sprd.engineermode这是产线测试用的工具,厂商一般不敢删。展开它的 Activity,进去翻,找到了 USB Debug 开关。
4.1 为什么这个开关能用
设置里那个「USB 调试」和工程模式里这个,底层是同一个东西:
Settings.Global.ADB_ENABLED厂商只是把设置里的入口删了,没有(也无法轻易)删掉这个底层开关。工程模式是另一个入口。打开它、插线、平板上点「允许 USB 调试」:
adb devices# Q102112224614763 device product:AIPad model:CB_C6_STU第一层直接轻松拿下。
这一步是整个逆向的转折点。没有 ADB,后面所有分析几乎都做不了。
5. ADB 到手后,先读指纹
连上第一件事不是急着改东西,是摸清底细。构建属性决定了一切可能性的边界:
for p in ro.build.type ro.build.tags ro.debuggable ro.secure \ ro.board.platform ro.build.version.release ro.build.fingerprint; do printf "%-28s %s\n" "$p" "$(adb shell getprop $p)"done输出:
ro.build.type userro.build.tags release-keysro.debuggable 0ro.secure 1ro.board.platform ud710ro.build.version.release 9ro.build.fingerprint SPRD/ud710_2h10_native_noorca/ud710_2h10:9/...:user/release-keys翻译一下:
user+release-keys→ 正式发布构建。adb root会被直接拒绝(实测:adbd cannot run as root in production builds)ro.debuggable=0→ 没有调试后门ud710→ 展锐 UD710 平台
结论很干脆:root 无望,不过 ADB 已经够用了。
顺手确认一下有没有 MDM 锁:
adb shell dumpsys device_policy结果是空的 —— 没有启用的 Device Admin,没有 Device Owner。这意味着 pm disable-user 可以自由使用。这是一个好消息。
6. 逆向第一步:把 Settings.apk 拉下来
有了 ADB,就能读 /system(shell 用户对它可读,不需要 root):
adb shell pm path com.android.settingsadb pull /system/priv-app/Settings/Settings.apk .# 54 MB现在目标很明确:在这个 APK 里找到那个「暗码」。
因为现象里有一条关键线索:连点设置内的版本号好几次后,ifly弹窗要求输入暗码。这说明:
- 存在一个硬编码或可推导的校验值
- 它就在这个 APK 里(或者它请求的某个服务里)
先赌在 APK 里。
7. 在 54MB 里找一个中文词
7.1 先试试用 grep
APK 就是 zip。先解开,用最原始的办法:
unzip -o Settings.apk -d settings_xgrep -al "暗码" settings_x/*.dex settings_x/resources.arsc结果很有意思:
settings_x/resources.arsc ← 命中(dex 里没有)这一条信息量很大:「暗码」只存在于资源文件里,不存在于代码里。
在 Android 里这意味着:
resources.arsc存的是界面文案(strings.xml编译后的形态)classes.dex存的是逻辑(常量、比较、跳转)
「暗码」是 UI 文案(多半就是那个输入框的提示),真正的校验逻辑在 dex 里,只是它没有把密码本身写成明文字符串 —— 否则 dex 里也会命中。
7.2 用 Python 挖上下文
grep 只能告诉你「有」,看不出「是什么」。用几行 Python 把前后的字节挖出来:
import glob, osfor f in glob.glob('settings_x/**/*', recursive=True): if not os.path.isfile(f): continue data = open(f, 'rb').read() b = '暗码'.encode('utf-8') i = data.find(b) if i != -1: print(f, data[max(0,i-160):i+240].decode('utf-8', 'replace'))(resources.arsc 里的字符串是 UTF-8 池,注意还要试 utf-16-le。)
输出里除了「请输入暗码」,还有一条同样扎眼的:
启用开发者选项这说明开发者选项的界面资源还在,只是入口被关了。这就推翻了第三节那个 ActivityNotFoundException 的结论。
8. 可疑的wind_ 前缀
strings.xml 里找到资源名:
<string name="wind_delelopment_password_hint">请输入暗码</string><string name="wind_delelopment_title">开启开发者模式</string>wind_ 是讯飞的定制前缀(顺带一提,delelopment 是他们的拼写错误,这种瑕疵反而好认)。
于是问题变成:谁用了这个字符串?
这时候装反编译器:
brew install jadxjadx -d settings_java Settings.apk然后搜:
grep -rn "wind_delelopment" settings_java/sources/直接指到一个文件:
com/android/settings/WindDeviceInfo.java:1199. 柳暗花明:WindDeviceInfo.java
打开一看,密码就明明白白写在第 111 行,不能更扎眼了:
if (developDilaog.getPassword() != null && developDilaog.getPassword().equals("IFlyCBaistudy5121")) { toDevelopmentMode();}暗码 = IFlyCBaistudy5121。
而且同一个方法里还藏着完整的触发条件:
if (CLICK_COUNT >= 6 && CLICK_COUNT > 0) { // 从第 6 次开始显示倒数 Toast.makeText(..., plurals.show_dev_countdown, 12 - CLICK_COUNT, ...);}if (CLICK_COUNT >= 12) { // 第 12 次才弹暗码框 developDilaog.showDialog(...);}所以这里不是原生 Android 的连点 7 次,而是连点 12 次。
再看 toDevelopmentMode():
DevelopmentSettingsEnabler.setDevelopmentSettingsEnabled(mContext, true);ComponentName componentName = new ComponentName( "com.android.settings", "com.android.settings.Settings$DevelopmentSettingsDashboardActivity");setTileEnabled(componentName, true); // ← 关键mContext.startActivity(intent);10. 解开 enabled="false" 的谜
回到第三节那个矛盾:Activity Launcher 说 ActivityNotFoundException,但代码里明明在跳转它。
答案在 AndroidManifest.xml 里:
<activity android:name="Settings$DevelopmentSettingsDashboardActivity" android:enabled="false" ← 就是这里 ...>组件被声明成禁用状态。
在 Android 里,启动一个 enabled=false 的组件,抛出的正是 ActivityNotFoundException —— 和「类不存在」的报错一模一样。这就是为什么那张分诊表也会骗人:这个报错有两种成因,但大不相同。
厂商的设计是:先用 DevelopmentSettingsEnabler 打开全局开关,再由 Settings 自己的 uid 调 setComponentEnabledSetting 把组件启用。两步都对,才能进去。
10.1 用 ADB 直接绕
我们有 shell,能不能直接启用组件?
adb shell pm enable 'com.android.settings/...Settings$DevelopmentSettingsDashboardActivity'被拒了:
SecurityException: Shell cannot change component state for ... to 1Android 9 限制了 shell 修改非调试应用的组件状态。这条路封死。
好在 am start 能行:
adb shell "am start -n 'com.android.settings/com.android.settings.Settings\$DevelopmentSettingsDashboardActivity'"为什么?shell 用户持有 START_ANY_ACTIVITY 一类的特权,Activity 启动和组件启用是两套不同的权限检查。禁用组件拦的是普通应用,却拦不住 shell。
注意这里有个坑:
$会被远程 shell 吃掉,组件名会被截成.Settings。一定要用单引号包住组件名,或者在$前加转义。容易踩坑,报错信息里的组件名少了半截,非常容易看岔。
结果:
mResumedActivity: ActivityRecord{... com.android.settings/.Settings$DevelopmentSettingsDashboardActivity}正式确认,开发者选项已经打开了。
10.2 让它永久生效
am start 只是这次启动成功,组件本身还是 enabled=false,重启后会丢。
正确的做法是走一遍官方流程:连点版本号 12 次 → 输入 IFlyCBaistudy5121。这样由 Settings 自己调 setTileEnabled,持久化写入。
逆向的终点,往往是回到那个原本就该有的按钮上。
11. 顺手解剖 OTA 包
开发者选项搞定后,还剩那个关不掉的更新通知。
先定位:
adb shell pm list packages -s | grep -iE "ota|update|fota"# com.iflytek.study.ota# com.sprd.systemupdate第一个是讯飞的教育机 OTA,第二个是展锐原生的更新器。两个要区别对待。
把讯飞那个拉下来:
adb shell pm path com.iflytek.study.otaadb pull /system/app/IFlyOTA/IFlyOTA.apk .11.1 从 dex 里榨 URL
不反编译也能拿到线索 —— dex 里的字符串常量是明文的:
LC_ALL=C grep -aoh 'https\?://[A-Za-z0-9._~:/?#@!$&()*+,;=%-]\{6,200\}' classes*.dex \ | LC_ALL=C sort -u(LC_ALL=C 是必须的,否则中文环境下的 sort 会报 Illegal byte sequence。)
结果比预想的热闹:
https://hsmp.changyan.com/ ← 自己家的https://byod.xunfeixxj.com/byod/https://k12-api.openspeech.cn/https://aiapi.changyan.com/coep-rest/http://coop.voicecloud.cn/activity/ms/uploadSnapshot ← 这个名字一言难尽https://android.bugly.qq.com/rqd/async ← 腾讯 Bugly 崩溃上报https://astat.bugly.qcloud.com/rqd/asynchttps://h.trace.qq.com/kv一个「系统更新」应用,捆了崩溃上报、行为统计、日志服务和一个叫 uploadSnapshot 的上传接口。
11.2 为什么是禁用而不是删除
adb shell pm disable-user --user 0 com.iflytek.study.ota选禁用不选卸载,理由有三个:
- 可逆。
pm enable就回来了 - 系统分区删不掉。
/system/app没 root 写不了,「卸载」对系统应用只能是--user 0,本质还是禁用 - 退路拆了容易玩儿完
最后这点很重要:com.sprd.systemupdate 我没动。 它很可能就是「从 SD 卡本地升级」的执行者 —— 那是这台机器唯一还能刷回原厂的能力。禁掉它,等于彻底把安全绳剪了。
禁用后通知立刻消失,剩余进程只剩输入法和相册。
12. 哪些做成了,哪些没做
| 目标 | 结果 | 原因 |
|---|---|---|
| 开发者选项 | ✅ | EngineerMode + 暗码 |
| ADB | ✅ | 同上,adb_enabled=1 持久化 |
| 停掉更新通知 | ✅ | disable-user |
| 停掉后台遥测 | ✅ | 随 OTA 包一起停 |
| 原生系统设置 | ❌ | 换 APK 要写 /system → 要 root |
关于在这台机器上装 GMS 补一句:Android 9 上把 GSF/GMS 当普通 APK 装是能装上的,但账号登录必然失败 —— GSF 需要 /system/priv-app 位置和签名权限。没有 root,这条路是死胡同。要 GMS 体验,换一台能解锁 bootloader 的机器(Pixel、部分小米/红米)比在这台上耗着划算得多。
除此之外,我还把默认浏览器换成了Via(原版CVE太多了),用小企鹅替换了原版输入法等等,剩下的都是应用问题了。
13. 方法论
回头看,真正起作用的是这几个思路:
- 先分层,再谈可行性。 「能不能刷干净」是伪问题,「哪一层能拿到、卡在哪」才是真问题。
- 厂商删入口不删实现。 菜单项删了,Activity 还在;设置项藏了,底层
Settings.Global还在。找另一个入口,比找漏洞容易得多。 - 报错信息要分级。
NotFound/SecurityException是两个完全不同的世界。而且要警惕同一个报错有多种成因 ——enabled=false也能伪装成「类不存在」。 - 资源文件和代码要分开搜。 中文文案在
resources.arsc,逻辑在dex。搜错了地方会得出「这里什么都没有」的错误结论。 - 壳可以伪装,常量不行。 一个应用可以声称自己是「系统更新」,但它的 dex 里躺着 Bugly 和
uploadSnapshot。URL 不会撒谎。 - 变化要可逆。 每一步都问一句「怎么退回去」。禁用而不是删除,留着
com.sprd.systemupdate,都是这个原则。
这次能成,一半靠技术,一半靠厂商的帮衬。 工程模式没删、暗码硬编码在客户端、组件只是禁用没有移除,不过这个平板本身就是学校发的学生机。一毕业,客户残值也就没了。
附录:完整命令清单
# ---------- 连接 ----------adb devices -l
# ---------- 摸清底细 ----------for p in ro.build.type ro.build.tags ro.debuggable ro.secure \ ro.board.platform ro.build.version.release ro.build.fingerprint; do printf "%-28s %s\n" "$p" "$(adb shell getprop $p)"doneadb shell dumpsys device_policy
# ---------- 拉包 ----------adb shell pm path com.android.settingsadb pull /system/priv-app/Settings/Settings.apk .adb shell pm path com.iflytek.study.otaadb pull /system/app/IFlyOTA/IFlyOTA.apk .
# ---------- 静态分析 ----------unzip -o Settings.apk -d settings_xgrep -al "暗码" settings_x/*.dex settings_x/resources.arscbrew install jadxjadx -d settings_java Settings.apkgrep -rn "wind_delelopment" settings_java/sources/
# ---------- 启动被禁用的组件 ----------adb shell "am start -n 'com.android.settings/com.android.settings.Settings\$DevelopmentSettingsDashboardActivity'"
# ---------- 停掉 OTA ----------adb shell pm disable-user --user 0 com.iflytek.study.otaadb shell pm list packages -d # 查看已禁用的包
# ---------- 榨 URL ----------LC_ALL=C grep -aoh 'https\?://[A-Za-z0-9._~:/?#@!$&()*+,;=%-]\{6,200\}' classes*.dex \ | LC_ALL=C sort -u这篇结束的时候,开发者选项已经能正常打开了。距离第一次在设置里翻不到任何东西的高中毕业季,过去了一年多。
支持与分享
如果这篇文章对你有帮助,欢迎分享给更多人或打赏支持!





