本文编写于121 天前,最后修改于87 天前,其中某些信息可能已经过时。

折腾自己手里的Nintendo Switch

Nintendo Switch是使用Nvidia Tegra芯片制作的一款掌机, 其性能当然是比不上“高速路由器”之称的Plash Speed5主机的~
不过呢, 因为Switch拥有独一无二的Dock模式、掌机模式双切换, 以及可以拆卸的蓝牙手柄, 这里还是要给个好评的ww
可是Nvidia Tegra毕竟是给智能电视和安卓平板设计的SOC呀, 如果只是拿来玩游戏是不是也太浪费了? 如果能在享受正版游戏在线服务体验的同时, 跑个Ubuntu当便携电脑使用, 或者跑个安卓当电视盒子看番, 那么它不香么?
再或者是把塞尔达传说中的林克变身成女林克也好呀ww (不是x
……
反正啦, 综上所述, 作为一个搞安全专业的Switch玩家, 还是有必要破解一下自己手里的Switch, 为其增加更多的新玩法(ww

Notice: 我是正版玩家, 无论是Switch卡带还是Steam库存, 从头到尾都是倾向于支持正版.
搞这些技术性研究只是为了增添乐趣和分享思路, 因此也请大家不要问我破解游戏如何安装、如何在游戏中使用金手指(作弊脚本)、在哪下载破解游戏等等. 此类问题超过我的研究范围, 我也无法回答, 谢谢ww


硬破Switch

因为我们想把Switch拿做其他用途, 所以务必是要在Switch上执行我们自己的任意代码.
然而Switch对于自己的代码段可谓是重兵把守, 开机时Nvidia的bootrom会使用RSA校验代码完整性, 而后续的可执行游戏或软件也是直接使用SSL从任天堂服务器下载, 运行之前还要进行多次完整性校验和授权许可检查, 这使得常规手段是根本无懈可击的.
因此我们第一件事当然是破解Switch的校验机制, 让Switch能跑我们自己的代码.

首先, 要说明的是, 我手里这台Switch是Erista机型, 而并非新出的Mariko(续航增强版)机型.
早期的Erista机型实际上在bootrom的usb recovery模式(俗称RCM)中存在着一个缓冲区溢出漏洞, 利用这个漏洞可以直接破坏堆栈执行任意代码. 由于这个漏洞甚至可以控制trusted zone, 权限非常非常大且无法软件修复, 导致任天堂一度头痛.
但是后来这个漏洞被Nvidia的新芯片给修掉了, 因此这个办法也就不能用了, 这里也就不再多详细解释啦.
直到后来Team Xecuter那边又出了个Modchip:


这个Modchip号称可以破解所有版本的Switch, 甚至号称可以破解国行和Lite. 而我手里因为只有自己常玩的Erista, 没有别的机型, 所以也没办法去测试.
至于原理嘛, 其实也很简单, 我们稍后会提到.


安装SX Core Modchip

本着 “不要自己造轮子” 的原则, 完全没有任何前置经验积累的情况下, 咱们还是直接安装SX Core的Modchip比较合适.
安装的过程也不再详细描述, B站上面其实已经有了足够多的up主发类似视频了, 因此这里只放图, 不细讲啦~

不过有两点要说的是, 我发现大家普遍反映焊接SOC上面的0402电容很难, 实际上根本不难, 甚至两分钟就可以搞好……
这里的秘诀就是千万不要用那种50块钱以下的地摊烙铁, 否则你会非常非常头痛(有些质量非常差的甚至不接地, 严重情况下静电可能会将SOC击穿, 非常危险), 更不要作死使用无铅焊锡(熔点非常高, 难以控制).
这里我自己的装备是DIY的调温T12焊台和细尖头T12烙铁芯, 因为头非常尖而且温度可控, 焊接这种细小东西, 甚至是手机主板都非常好用. 前者的价格大概是90-150左右, 后者的价格大概是10-20左右(买焊台的时候店家也可能直接赠送).
大家去某电商平台搜索“DIY T12焊台”、“T12细尖头”就可以找到.


另一个问题就是撬屏蔽罩的问题, 找准卡扣直接撬开, 就可以完整的拿下来了(这里感谢老婆大人ww). 建议使用刀片或者一字头小螺丝刀, 操作时小心不要割伤自己的爪子qwq

我的这个Switch是我自己改过壳子喷漆的, 因此是这种颜色ww~ 不用担心, 它确确实实是一台货真价实的Erista~



这里我们卸掉Switch的散热, 并清理一下SOC上面的硅脂(感谢老婆大人的帮忙ww~~同时桌子上的卡带证明一下我不是白嫖怪(x

这里准备焊接Modchip的这几个0402电容

焊接之后, 相信我, 大多数焊不好的都是因为烙铁质量太次, 只要设备还算过关, 2分钟就可以轻松搞定的ww



这里我直接用手账胶带固定了……因为我突然找不到绝缘胶带了……不过不会有什么其他问题, 小场面ww

嘛, 至此我们的Switch应该就已经被破解了.

这个Modchip我后面其实有研究, 原理非常简单, Team Xecuter不知道从哪里py到了Switch的引导密钥和签名密钥, 直接自己签了一个BCT(Boot Control Table)和一段可执行代码, 并解密了emmc, 通过这个小Modchip在每次Switch启动的时候把这些东西写到了BOOT0分区的正确位置.
这样一来, Tegra芯片上电的时候执行的就不再是任天堂的官方Bootloader代码, 而是这个Modchip刷进去的那些可执行代码, 直接夺取了控制权.
这个方法和之前的USB RCM有本质的不同点, 因为他们得到了Switch的引导密钥和签名密钥(据说Mariko的密钥也是被泄露了), 这下直接大摇大摆的过掉了Nvidia的签名校验, 让SecureBoot变摆设, 因此也通杀了Mariko, 破坏力极强, 且同样是无法软件修复.


加载Hekate

因为我手里这个机器是Erista机型, 虽然被打过了USB RCM的补丁, 但是SOC内部的寄存器结构大同小异, 几乎是没有任何修改的.
这样一来的话, 前人所留下来的经验, 我们都可以直接拿过来通用, 基本没有什么大问题在里面啦ww
按照之前的办法, SX的东西都会加载一个叫boot.dat的东西作为bootloader使用, 而这个东西的文件结构已经被别人逆出来了, 我们要不要自己编译一个Hekate然后生成一个这样的格式, 丢到SD卡里面试试呢?
结果我犯了大错特错, SX表示“你这是啥玩意呀, 快拿开快拿开, 我不要的~”


经过了一番研究发现, SXOS 3.x开始就非常不厚道, 在这个文件里面新增了一段RSA签名, 验证这个文件是否是SX官方的, 如果签名校验失败, 就不加载……
这……还好这个SXOS有个加载第三方可执行代码(payload)的选项, 因此还是能继续折腾下的www~
(后来发现这个功能似乎有巨坑, 但是似乎没有对我造成什么影响, 文章最后会说明)

然后我们就进了hekate~ 在把Switch给折腾成开发板之前, 当然要记得备份一下原来的emmc咯~


加载大气层系统NX-Atmosphere

在这里我其实撞了一大堆天坑, 浪费了接近一个星期的时间才折腾好, 多半是Atmosphere和这个Modchip有些小的兼容性问题, 而另一半纯粹是Atmosphere自己的bug.
本来想试试Atmosphere 0.13.0能不能正常加载, 结果撞上了很难处理的超级大坑, 后来发现NX-Atmosphere的官方Github被提了很多关于0.13.0的issue, 之后干脆换了0.12.0然后修改必要代码, 成功解决了问题……

按照常规逻辑, 官方下载的Atmosphere文件主要分为三部分: fusee-primary(第一步的逻辑, 主要用于Hello world和初始化外设), fusee-secondary(第二步的逻辑, 负责加载并初始化Atmosphere的各个模块部分), sept(用于获取Terga的TSEC Key).
而我们如果不想太麻烦的话, 只需要继续使用Hekate的Payload页面, 加载fusee-primary文件就好了, 剩下的东西大气层理应会帮我们一连串搞定.
可是这么一搞不要紧, 加载fusee-primary之后, 机器直接黑屏挂了…… Sept的logo界面都没出……
……
这时候有热心观众说直接加载fusee-secondary试一下, 结果还是直接挂了……
于是热心观众表示干脆使用hekate的fss0选项(解包fusee-secondary的各个模块并由hekate进行装载, 绕过大气层自带的装载逻辑)引导大气层, 结果还是直接挂……了……
……
热心群众也无话可说了……


发生了什么?

本着见一个bug杀一个bug的原则, 我们一定要把这个问题一探究竟呀对不对?
于是我们git clone一下大气层的源码, 研究一下呗~ 这一研究不要紧, 结果发现了fusee模块的源码实际上有一处debug开关, 默认是关闭的:


这一处代码的意思大概是默认为SCREEN_LOG_LEVEL_NONE, 也就是不输出任何信息, 之后会通过一个叫bct0.ini的文件加载一个是否是debug模式的选项. 而bct0.ini官方全程没有任何说明, google搜也搜不到什么资料. 虽然最后还是在源码里面找到了怎么样打开这个开关, 但是我们不敢保证代码到底执行到哪了, 所以还是干脆把SCREEN_LOG_LEVEL_NONE改成SCREEN_LOG_LEVEL_INFO然后重新编译比较好ww

嗯, 一旦有了思路去调试, 剩下的事情就好说了, 走你~
然后发现卡在了[NXBOOT] Reading boot0... (这里的图片找不到了)……
没关系, 我们继续在这个函数里面加详细输出:


然后很惊讶的发现程序最后卡在了reading pk1/pk1l这一行, 没事没事, 我们继续加更细粒度的输出:


到这里我们就已经成功定位到问题了, 原来keyblob读取失败了, 代码直接跑卡死了……
这是这个函数的完整代码:

int package1_read_and_parse_boot0(void **package1loader, size_t *package1loader_size, nx_keyblob_t *keyblobs, uint32_t *revision, FILE *boot0) {
    nvboot_config_table *bct; /* Normal firmware BCT, primary. TODO: check? */
    nv_bootloader_info *pk1l_info; /* TODO: check? */
    size_t fpos, pk1l_offset;
    union {
        nx_keyblob_t keyblob;
        uint8_t sector[0x200];
    } d;

    if (package1loader == NULL || package1loader_size == NULL || keyblobs == NULL || revision == NULL || boot0 == NULL) {
        errno = EINVAL;
        return -1;
    }

    bct = malloc(sizeof(nvboot_config_table));
    if (bct == NULL) {
        errno = ENOMEM;
        return -1;
    }
    pk1l_info = &bct->bootloader[0];

    fpos = ftell(boot0);

    /* Read the BCT. */
    if (fread(bct, sizeof(nvboot_config_table), 1, boot0) == 0) {
        free(bct);
        return -1;
    }
    if (bct->bootloader_used < 1 || pk1l_info->version < 1) {
        free(bct);
        errno = EILSEQ;
        return -1;
    }

    *revision = pk1l_info->version - 1;
    *package1loader_size = pk1l_info->length;

    pk1l_offset = 0x4000 * pk1l_info->start_blk + 0x200 * pk1l_info->start_page;
    free(bct);
    (*package1loader) = memalign(0x10000, *package1loader_size);

    if (*package1loader == NULL) {
        errno = ENOMEM;
        return -1;
    }

    /* Read the pk1/pk1l, and skip the backup too. */
    if (fseek(boot0, fpos + pk1l_offset, SEEK_SET) != 0) {
        return -1;
    }
    if (fread(*package1loader, *package1loader_size, 1, boot0) == 0) {
        return -1;
    }
    if (fseek(boot0, fpos + pk1l_offset + 2 * PACKAGE1LOADER_SIZE_MAX, SEEK_SET) != 0) {
        return -1;
    }

    /* Read the full keyblob area.*/
    for (size_t i = 0; i < 32; i++) {
        if (!fread(d.sector, 0x200, 1, boot0)) {
            return -1;
        }
        keyblobs[i] = d.keyblob;
    }

    return 0;
}

这段代码的功能是读取BOOT0分区, 根据BCT定位到几个必要的数据段在哪, 然后通过fseek函数跳过去读.
但是怎么这段代码就卡住了呢? 在Switch目前的启动阶段, 能让代码跑卡住的几乎只有两个可能, 要么是死循环了, 要么就是空指针或者无效指针了. 但是看目前的表现显然肯定不是死循环, 一定是无效指针导致程序跑炸了.
都是好好的大气层, 在别人机器上就没问题, 在我机器上怎么会这样呢……?
……
……
这时候突然想起来罪大恶极的Modchip, 难不成它对BOOT0做了什么手脚? 赶紧用Hex Editor打开我们备份的EMMC, 看看BOOT0到底发生了什么:


那么到这里问题就水落石出了:

Modchip为了执行自己的代码, 它修改了原来的BCT, 让BCT中的bootloader指针指向自己写入的那些代码(0x3F0000), 而不是原来的pk1l (0x100000).
这样做并没有破坏原来的pk1/pk1l, 但是直接导致了Atmosphere的引导根据BCT中的指针去定位pk1/pk1l会完全错误, 后续的代码也直接越界访问无效内存而崩溃.

这样一来那就好说了, 既然BCT已经被篡改掉了, 那我们放弃呗, 反正这个位置即使是硬编码也无妨嘛, 截止到目前, 每台Switch的pk1l offset都固定是0x100000.
于是, 修改后的代码变成了这样:

int package1_read_and_parse_boot0(void **package1loader, size_t *package1loader_size, nx_keyblob_t *keyblobs, uint32_t *revision, FILE *boot0) {
    // do not use BCT whiling SX_Modchip flashed their garbage BCT
    /*
    nvboot_config_table *bct; 
    nv_bootloader_info *pk1l_info;
    */
    size_t fpos, pk1l_offset;
    union {
        nx_keyblob_t keyblob;
        uint8_t sector[0x200];
    } d;
    print(SCREEN_LOG_LEVEL_INFO, "[NXBOOT-Angelic47] package1_read_and_parse_boot0: started\n");

    if (package1loader == NULL || package1loader_size == NULL || keyblobs == NULL || revision == NULL || boot0 == NULL) {
        errno = EINVAL;
        return -1;
    }

    /*
    bct = malloc(sizeof(nvboot_config_table));
    if (bct == NULL) {
        errno = ENOMEM;
        return -1;
    }
    pk1l_info = &bct->bootloader[0];

    fpos = ftell(boot0);
    
    print(SCREEN_LOG_LEVEL_INFO, "[NXBOOT-Angelic47] package1_read_and_parse_boot0: reading BCT\n");
    */
    
    /* Read the BCT. */
    /*
    if (fread(bct, sizeof(nvboot_config_table), 1, boot0) == 0) {
        free(bct);
        return -1;
    }
    if (bct->bootloader_used < 1 || pk1l_info->version < 1) {
        free(bct);
        errno = EILSEQ;
        return -1;
    }
    */
    
    // print(SCREEN_LOG_LEVEL_INFO, "[NXBOOT-Angelic47] package1_read_and_parse_boot0: Skipping BCT reading whiling SX_Modchip flashed their BCT\n");
    
    fpos = ftell(boot0);
    
    pk1l_offset = 0x100000;
    // print(SCREEN_LOG_LEVEL_INFO, "[NXBOOT-Angelic47] package1_read_and_parse_boot0: Hardcoded pk1l_offset = %p\n", pk1l_offset);
    
    *revision = 8;
    *package1loader_size = PACKAGE1LOADER_SIZE_MAX;
    
    (*package1loader) = memalign(0x10000, *package1loader_size);

    if (*package1loader == NULL) {
        errno = ENOMEM;
        return -1;
    }
    // print(SCREEN_LOG_LEVEL_INFO, "[NXBOOT-Angelic47] package1_read_and_parse_boot0: reading pk1/pk1l\n");

    /* Read the pk1/pk1l, and skip the backup too. */
    // print(SCREEN_LOG_LEVEL_INFO, "[NXBOOT-Angelic47] package1_read_and_parse_boot0: fseek(boot0, fpos + pk1l_offset, SEEK_SET)\n");
    if (fseek(boot0, fpos + pk1l_offset, SEEK_SET) != 0) {
        print(SCREEN_LOG_LEVEL_INFO, "[NXBOOT-Angelic47] package1_read_and_parse_boot0: reading pk1/pk1l fail #1\n");
        return -1;
    }
    // print(SCREEN_LOG_LEVEL_INFO, "[NXBOOT-Angelic47] package1_read_and_parse_boot0: fread(*package1loader, *package1loader_size, 1, boot0)\n");
    if (fread(*package1loader, *package1loader_size, 1, boot0) == 0) {
        print(SCREEN_LOG_LEVEL_INFO, "[NXBOOT-Angelic47] package1_read_and_parse_boot0: reading pk1/pk1l fail #2\n");
        return -1;
    }
    // print(SCREEN_LOG_LEVEL_INFO, "[NXBOOT-Angelic47] package1_read_and_parse_boot0: fseek(boot0, fpos + pk1l_offset + 2 * PACKAGE1LOADER_SIZE_MAX, SEEK_SET)\n");
    if (fseek(boot0, fpos + pk1l_offset + 2 * PACKAGE1LOADER_SIZE_MAX, SEEK_SET) != 0) {
        print(SCREEN_LOG_LEVEL_INFO, "[NXBOOT-Angelic47] package1_read_and_parse_boot0: reading pk1/pk1l fail #3\n");
        return -1;
    }

    // print(SCREEN_LOG_LEVEL_INFO, "[NXBOOT-Angelic47] Read the full keyblob area\n");
    /* Read the full keyblob area.*/
    for (size_t i = 0; i < 32; i++) {
        // print(SCREEN_LOG_LEVEL_INFO, "[NXBOOT-Angelic47] Read keyblob: %d\n", i);
        if (!fread(d.sector, 0x200, 1, boot0)) {
            print(SCREEN_LOG_LEVEL_INFO, "[NXBOOT-Angelic47] Read the full keyblob area fail\n");
            return -1;
        }
        keyblobs[i] = d.keyblob;
    }
    // print(SCREEN_LOG_LEVEL_INFO, "[NXBOOT-Angelic47] Read keyblobs OK!\n");

    // print(SCREEN_LOG_LEVEL_INFO, "[NXBOOT-Angelic47] package1_read_and_parse_boot0: end\n");
    return 0;
}

那么我们继续编译, 走你~!
结果果不其然的又黑屏了, 但是黑屏之前很快的闪过了一些输出……因为肉眼看不清楚, 所以我使用了手机录像录屏, 一帧一帧的慢慢放~


而这里的代码是这样的:

我们可以看到, 我们距离display_splash_screen_bmp几乎只有一步之遥, 也就是说这个位置我们理应马上就要看到Atmosphere的Logo页面了才对.
但是为啥我们连Logo页面都没看到就直接黑屏了呢? 这篇文章太长啦, 一天写不完~ 我们下次继续更新后续吧~
欲知后事如何, 倾听下回分解~ 大家可以先猜猜原因ww

传送门: Switch 硬破折腾记之 NX-Atmosphere 大气层的坑和调试 (2)