省流

没成。

引言

垃圾广电,宽带是租的,用户侧 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)。代码的流程包括解析包、验证签名、处理现有应用包以及完成安装。下面是详细的逻辑解析:

  1. 方法声明和参数:

    /* JADX INFO: Access modifiers changed from: private */
    public void installPackageLI(InstallArgs args, boolean newInstall, PackageInstalledInfo res)
    
    • InstallArgs args: 包含安装所需的各种参数,如代码路径、标志、安装程序包名等。
    • boolean newInstall: 标记是否是新的安装。
    • PackageInstalledInfo res: 存储安装结果的信息。
  2. 初始化变量:

    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);
    
    • 设定了一些变量,包括文件路径、标志位等。
  3. 解析包:

    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 文件。如果解析失败,返回错误代码并退出。
  4. 检查应用包:

    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 摘要。如果任何检查失败,设置错误代码并退出。
  5. 签名验证:

    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 选择正确的签名。检查应用包的签名是否与预期的签名匹配。如果没有找到匹配的签名,则设置错误代码并退出。
  6. 处理现有应用包:

    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 卡上,设置错误代码并退出。如果是新安装,则处理包的路径和用户信息。
  7. 执行安装:

    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
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

https://www.52pojie.cn/thread-1136160-1-1.html

https://www.52pojie.cn/thread-710679-1-1.html