省流
没成。
引言
垃圾广电,宽带是租的,用户侧 BGP,出口随机,2024 年仍无 IPv6,更无公网 IP,电视机顶盒禁止所有方式的第三方 APK 安装。
现象
无论使用 U 盘、ADB、Shell 命令,安装第三方 APK,均报错 INCONSISTENT_CERTIFICATES
,即证书错误,可知是通过证书限制的方法禁止第三方 App。
机顶盒型号为 GDT-JL1100
,查询得知其芯片为晨星MSO9385
,标识型号为MStar Android TV
。
MSTAR 晨星 公司介绍: MStar 晨星半导体成立于 2002.05,总部位于福建旁边新竹科技园,核心技术团队来自美国 TI 公司; 员工(全球)超过 2000 名,其中芯片研发人员约 1200 人,25% 员工具有硕士以上学历; 全球共设有 17 个分支机构,在新竹、台北、美国、俄罗斯、法国、英国、中国大路均有 芯片设计团队,另外还有韩国、土耳其、日本、新加坡等办事处。
目前晨星 MSO9385 方案有: CM311-3/3s;烽火 HG680-MC/MY;海信 IP202H/IP203H;Z86-ZN 兆能;M411M/M401M-YS…等
广电机顶盒型号多样,通过对比手上的另一型号 GDT-JL1000,虽芯片方案不一样,但其系统结构基本一致,均为安卓 4.4.2 的定制系统。
分析
出于国情,国内市面上正式发售的盒子,都没有内置谷歌套件和市场,所以不会用谷歌框架实现对安装文件的合法性验证。
安卓应用的安装最终落实到对.apk 安装包的操作,其基本流程如下:
PackageInstaller 或 pm install
-> PackageManager -> PackageManagerService
当前国内研发人员实现限制安装的常见方法如下:
1、数字签名白名单
这种方法多见于安卓手机或车机等设备,盒子较少使用,
白名单数字证书一般位于/system/etc/security/
;
2、修改版 PackageInstaller
如果一个盒子可以在终端中用pm install
命令安装.apk,而在盒子的文件管理器中点击.apk 却安装失败,则多半可能是 PackageInstaller 这个安卓应用层面的安装器被“加强”了;
3、内置文件管理器过滤
原厂固件内置的文件管理器已将 .apk
文件列入过滤列表,不会响应用户的操作;**
4、修改版系统框架
pms、ams 这些安卓系统服务都位于 framework 中,把安卓源代码改得面目全非是每个资深研发人员的“最高”追求,
这也是最常用的限制实现途径。
结合现象分析,广电机顶盒应该是通过修改系统框架,实现了对数字签名的验证。
理论分析完毕,开始实操。
备份
在任何操作之前,需要先备份好原系统,以免变砖后无法复原。
通过 ADB 连接到机顶盒,进入机顶盒设置,输入广电客服电话“96956”,即可打开 ADB 调试。
连接到机顶盒
adb connect 192.168.1.20
adb ls /system/app 查看app文件夹
adb ls /dev/block/platform/mstar_mci.0/by-name/ 查看各分区
adb shell df 查看整个分区
编写了一个备份脚本,可以将系统打包备份到 U 盘中。
@echo off
color 0a
ECHO.
rem 清理旧备份
rd /s /q backup > nul 2>&1
adb ls dev/block/platform/mstar_mci.0/by-name
adb shell "rm -rf /mnt/sdcard/backup"
adb shell "mkdir /mnt/sdcard/backup"
call :backup_partition MBOOT
call :backup_partition MPOOL
call :backup_partition boot
call :backup_partition customer
call :backup_partition dtb
call :backup_partition dtbo
call :backup_partition misc
call :backup_partition factoryinfo
call :backup_partition product
call :backup_partition recovery
call :backup_partition param
call :backup_partition tvcertificate
call :backup_partition tvconfig
call :backup_partition tvservice
call :backup_partition vbmeta
rem 提示用户备份 vendor 分区可能需要一些时间
ECHO 正在备份晨星 vendor 分区,可能需要较长时间......
adb shell "dd if=/dev/block/platform/mstar_mci.0/by-name/vendor of=/mnt/sdcard/backup/vendor.img"
rem 提示用户备份 system 分区可能需要一些时间
ECHO 正在备份晨星 system 分区,可能需要较长时间......
adb shell "dd if=/dev/block/platform/mstar_mci.0/by-name/system of=/mnt/sdcard/backup/system.img"
call :upload_and_cleanup
ECHO 备份完成,固件存放位置为工具根目录 backup 文件!!!
pause > NUL
goto :eof
:backup_partition
set PARTITION_NAME=%1
ECHO 正在备份晨星 %PARTITION_NAME% 分区......
adb shell "dd if=/dev/block/platform/mstar_mci.0/by-name/%PARTITION_NAME% of=/mnt/sdcard/backup/%PARTITION_NAME%.img"
goto :eof
:upload_and_cleanup
ECHO 正在上传备份的固件,可能需要较长时间......
@REM adb pull -p /mnt/sdcard/backup backup
@REM adb shell "rm -rf /mnt/sdcard/backup"
adb shell "cp -r /mnt/sdcard/backup /mnt/usb/sda1"
adb shell "rm -rf /mnt/sdcard/backup"
goto :eof
逆向
将 /system/framework/services.jar
拉下来,在电脑上使用 jadx 打开。
在左侧结构树中,找到 pm 包,进而找到 PackageManagerService 类。
通过搜索错误代码-104
,找到 12 处代码,逐一查看,找到关键判断代码:
/* JADX INFO: Access modifiers changed from: private */
public void installPackageLI(InstallArgs args, boolean newInstall, PackageInstalledInfo res) {
Signature testProjectSignature;
int pFlags = args.flags;
String installerPackageName = args.installerPackageName;
File tmpPackageFile = new File(args.getCodePath());
boolean forwardLocked = (pFlags & 1) != 0;
boolean onSd = (pFlags & 8) != 0;
boolean replace = false;
int scanMode = (onSd ? 0 : 1) | 4 | 8 | (newInstall ? 16 : 0);
res.returnCode = 1;
int parseFlags = this.mDefParseFlags | 2 | (forwardLocked ? 16 : 0) | (onSd ? 32 : 0);
PackageParser pp = new PackageParser(tmpPackageFile.getPath());
pp.setSeparateProcesses(this.mSeparateProcesses);
PackageParser.Package pkg = pp.parsePackage(tmpPackageFile, (String) null, this.mMetrics, parseFlags);
if (pkg == null) {
res.returnCode = pp.getParseError();
return;
}
String pkgName = pkg.packageName;
res.name = pkgName;
if ((pkg.applicationInfo.flags & 256) != 0 && (pFlags & 4) == 0) {
res.returnCode = -15;
return;
}
if (!pp.collectCertificates(pkg, parseFlags)) {
res.returnCode = pp.getParseError();
return;
}
if (args.manifestDigest != null && !args.manifestDigest.equals(pkg.manifestDigest)) {
res.returnCode = -23;
return;
}
if (pkg.mSharedUserId != null && pkg.mSharedUserId.equals("android.uid.system")) {
testProjectSignature = sGuangdongSystemAppSignature;
} else {
testProjectSignature = sGuangdongThirdAppSignature;
}
if (testProjectSignature == null) {
res.returnCode = -104;
return;
}
if (pkg.mSignatures == null) {
res.returnCode = -104;
Slog.d(TAG, "installPackageLI package=" + tmpPackageFile + " no certificates");
return;
}
boolean found = false;
Signature[] arr$ = pkg.mSignatures;
int len$ = arr$.length;
int i$ = 0;
while (true) {
if (i$ < len$) {
Signature s = arr$[i$];
if (s == null || !s.equals(testProjectSignature)) {
i$++;
} else {
found = true;
break;
}
} else {
break;
}
}
if (!found) {
res.returnCode = -104;
Slog.d(TAG, "installPackageLI package=" + tmpPackageFile + " no project certificates");
return;
}
解析代码
将这段代码喂给 AI,得到:
这段代码的主要功能是安装一个应用程序包(APK)。代码的流程包括解析包、验证签名、处理现有应用包以及完成安装。下面是详细的逻辑解析:
方法声明和参数:
/* JADX INFO: Access modifiers changed from: private */ public void installPackageLI(InstallArgs args, boolean newInstall, PackageInstalledInfo res)
InstallArgs args
: 包含安装所需的各种参数,如代码路径、标志、安装程序包名等。boolean newInstall
: 标记是否是新的安装。PackageInstalledInfo res
: 存储安装结果的信息。
初始化变量:
Signature testProjectSignature; int pFlags = args.flags; String installerPackageName = args.installerPackageName; File tmpPackageFile = new File(args.getCodePath()); boolean forwardLocked = (pFlags & 1) != 0; boolean onSd = (pFlags & 8) != 0; boolean replace = false; int scanMode = (onSd ? 0 : 1) | 4 | 8 | (newInstall ? 16 : 0); res.returnCode = 1; int parseFlags = this.mDefParseFlags | 2 | (forwardLocked ? 16 : 0) | (onSd ? 32 : 0);
- 设定了一些变量,包括文件路径、标志位等。
解析包:
PackageParser pp = new PackageParser(tmpPackageFile.getPath()); pp.setSeparateProcesses(this.mSeparateProcesses); PackageParser.Package pkg = pp.parsePackage(tmpPackageFile, (String) null, this.mMetrics, parseFlags); if (pkg == null) { res.returnCode = pp.getParseError(); return; }
- 使用
PackageParser
解析 APK 文件。如果解析失败,返回错误代码并退出。
- 使用
检查应用包:
String pkgName = pkg.packageName; res.name = pkgName; if ((pkg.applicationInfo.flags & 256) != 0 && (pFlags & 4) == 0) { res.returnCode = -15; return; } if (!pp.collectCertificates(pkg, parseFlags)) { res.returnCode = pp.getParseError(); return; } if (args.manifestDigest != null && !args.manifestDigest.equals(pkg.manifestDigest)) { res.returnCode = -23; return; }
- 检查应用的标志、证书和 manifest 摘要。如果任何检查失败,设置错误代码并退出。
签名验证:
if (pkg.mSharedUserId != null && pkg.mSharedUserId.equals("android.uid.system")) { testProjectSignature = sGuangdongSystemAppSignature; } else { testProjectSignature = sGuangdongThirdAppSignature; } if (testProjectSignature == null) { res.returnCode = -104; return; } if (pkg.mSignatures == null) { res.returnCode = -104; Slog.d(TAG, "installPackageLI package=" + tmpPackageFile + " no certificates"); return; } boolean found = false; Signature[] arr$ = pkg.mSignatures; int len$ = arr$.length; int i$ = 0; while (true) { if (i$ < len$) { Signature s = arr$[i$]; if (s == null || !s.equals(testProjectSignature)) { i$++; } else { found = true; break; } } else { break; } } if (!found) { res.returnCode = -104; Slog.d(TAG, "installPackageLI package=" + tmpPackageFile + " no project certificates"); return; }
- 根据应用的共享用户 ID 选择正确的签名。检查应用包的签名是否与预期的签名匹配。如果没有找到匹配的签名,则设置错误代码并退出。
处理现有应用包:
String oldCodePath = null; boolean systemApp = false; synchronized (this.mPackages) { if ((pFlags & 2) != 0) { String oldName = this.mSettings.mRenamedPackages.get(pkgName); if (pkg.mOriginalPackages != null && pkg.mOriginalPackages.contains(oldName) && this.mPackages.containsKey(oldName)) { pkg.setPackageName(oldName); pkgName = pkg.packageName; replace = true; } else if (this.mPackages.containsKey(pkgName)) { replace = true; } } PackageSetting ps = this.mSettings.mPackages.get(pkgName); if (ps != null) { oldCodePath = this.mSettings.mPackages.get(pkgName).codePathString; if (ps.pkg != null && ps.pkg.applicationInfo != null) { systemApp = (ps.pkg.applicationInfo.flags & 1) != 0; } res.origUsers = ps.queryInstalledUsers(sUserManager.getUserIds(), true); } }
- 检查包是否已存在。如果是系统应用且安装在 SD 卡上,设置错误代码并退出。如果是新安装,则处理包的路径和用户信息。
执行安装:
if (systemApp && onSd) { Slog.w(TAG, "Cannot install updates to system apps on sdcard"); res.returnCode = -19; return; } if (!args.doRename(res.returnCode, pkgName, oldCodePath)) { res.returnCode = -4; return; } setApplicationInfoPaths(pkg, args.getCodePath(), args.getResourcePath()); pkg.applicationInfo.nativeLibraryDir = args.getNativeLibraryPath(); if (replace) { replacePackageLI(pkg, parseFlags, scanMode, args.user, installerPackageName, res); } else { installNewPackageLI(pkg, parseFlags, scanMode, args.user, installerPackageName, res); } synchronized (this.mPackages) { PackageSetting ps2 = this.mSettings.mPackages.get(pkgName); if (ps2 != null) { res.newUsers = ps2.queryInstalledUsers(sUserManager.getUserIds(), true); } }
- 如果所有检查都通过,执行实际的包安装(替换或新安装)。设置应用的路径信息,并更新用户信息。
证书验证逻辑
继续询问 AI 关键的签名验证部分,得到:
签名验证部分是确保安装的应用程序包(APK)具有预期的签名,以防止安装恶意或未经授权的应用。下面是签名验证部分的详细讲解:
if (pkg.mSharedUserId != null && pkg.mSharedUserId.equals("android.uid.system")) {
testProjectSignature = sGuangdongSystemAppSignature;
} else {
testProjectSignature = sGuangdongThirdAppSignature;
}
- 根据应用包的共享用户 ID (
pkg.mSharedUserId
) 来确定使用哪个测试项目签名。- 如果共享用户 ID 是
android.uid.system
,则使用sGuangdongSystemAppSignature
。 - 否则,使用
sGuangdongThirdAppSignature
。
- 如果共享用户 ID 是
if (testProjectSignature == null) {
res.returnCode = -104;
return;
}
- 如果没有找到合适的
testProjectSignature
,则设置返回码为-104
并退出方法。
if (pkg.mSignatures == null) {
res.returnCode = -104;
Slog.d(TAG, "installPackageLI package=" + tmpPackageFile + " no certificates");
return;
}
- 如果解析出来的包没有签名(
pkg.mSignatures
为空),则设置返回码为-104
,记录日志并退出方法。
boolean found = false;
Signature[] arr$ = pkg.mSignatures;
int len$ = arr$.length;
int i$ = 0;
while (true) {
if (i$ < len$) {
Signature s = arr$[i$];
if (s == null || !s.equals(testProjectSignature)) {
i$++;
} else {
found = true;
break;
}
} else {
break;
}
}
- 遍历
pkg.mSignatures
中的签名,检查是否有一个签名与testProjectSignature
匹配。- 如果找到匹配的签名,则将
found
设置为true
并跳出循环。 - 如果没有找到匹配的签名,则继续循环直到检查完所有签名。
- 如果找到匹配的签名,则将
if (!found) {
res.returnCode = -104;
Slog.d(TAG, "installPackageLI package=" + tmpPackageFile + " no project certificates");
return;
}
- 如果没有找到匹配的签名(
found
仍为false
),则设置返回码为-104
,记录日志并退出方法。
修改
有一个简单粗暴的方法,使得 boolean found
始终= true
即可。
理论上将修改后的services.jar
打包放回系统即可,但若失败需要拆机使用 TLL 编程器进行救砖,我手上的机顶盒还在保修期中,就不进行尝试了。
救砖方法:https://www.right.com.cn/forum/thread-8302557-1-1.html
有趣的事情
解包内置的机顶盒设置
,位于 system/app/SUMA_GCABLE_SystemSetting_sys_signed.apk
,可以找到 ADB 的后门代码。
private int[] mAdbBackDoorKeys = {16, 13, 16, 12, 13};
private int[] mVersionBackDoorKeys = {19, 19, 20, 20, 21, 22, 21, 22};
@Override // android.app.Activity, android.view.KeyEvent.Callback
public boolean onKeyDown(int i, KeyEvent keyEvent) {
Log.i(TAG, "keyCode: " + i);
int i2 = this.mCurrentAdbBackDoorIndex;
int[] iArr = this.mAdbBackDoorKeys;
if (i2 <= iArr.length && i == iArr[i2]) {
this.mCurrentAdbBackDoorIndex = i2 + 1;
Log.i(TAG, "mCurrentAdbBackDoorIndex: " + this.mCurrentAdbBackDoorIndex);
if (this.mCurrentAdbBackDoorIndex >= this.mAdbBackDoorKeys.length) {
Log.i(TAG, " SystemProperties.set(\"ctl.start\",\"adbd\");");
Reflects.SystemProperties.set("ctl.start", "adbd");
Reflects.SystemProperties.set("ctl.start", "console");
Toast.makeText(this, "adb 已经打开", 0).show();
this.mCurrentAdbBackDoorIndex = 0;
}
} else {
this.mCurrentAdbBackDoorIndex = 0;
}
return super.onKeyDown(i, keyEvent);
}
参考文献
https://www.bilibili.com/video/BV1bm421E76a
https://www.znds.com/tv-1210588-1-1.html