将MUSL移植到PS4的历险

2020-05-25 14:29:32

在过去一年左右的时间里,我一直在与OpenOrbis团队合作开发一个工具链,通过使用官方SDK材料在不违反版权法的情况下为PS4构建自制软件。这并不是一项容易的任务,构建在没有官方工具的情况下在系统上运行的自制软件带来了许多挑战。这些挑战中的关键是:

Sony使用的libc库不遵守其他libc库遵循的标准。

虽然处理自定义ELF格式是一项有趣的任务,我将来可能会参与其中,但移植MUSL的过程也提供了有趣的障碍和教训。

许多开发人员将libc视为一个黑匣子,他们并不关心libc如何工作,他们只关心它是如何工作的。如果目标平台已经有成熟的libc在使用,这是非常好的。但是,在移植到新平台时,您需要深入了解细节,因为libc实质上是用户和内核之间的粘合剂。例如,您很少看到userland应用程序直接发出系统调用,但这在libc世界中很常见。那么,如果我们细分libc,那么libc向应用程序提供的基本组件是什么呢?

抽象公共功能的层(即。文件I/O、网络、数据类型、内存管理),远离平台的低级接口(系统调用、sysctls/ioctls等)。

对于那些不熟悉PS4内部结构的人来说,它是一个基于FreeBSD的系统,基于FreeBSD9.0。索尼增加了自己的系统调用,并修改了一些现有的内核代码,同时也运行了自定义的用户区域,但大多数标准的系统调用仍然非常相似。考虑到它在基础上是一个BSD系统,有人可能会问;为什么不使用BSD libc而不是MUSL-毕竟,它应该与系统暴露的内容更紧密地匹配?知道了这一点,我决定试一试。以下是我对BSD libc和MUSL的一些观察。

假设BSD libc开箱即用,只需对CRT存根稍作更改即可。它构建时没有问题,但在运行时,情况并非如此。即使是像sprintf()这样简单的操作在EINVAL中也失败了,考虑到手册页中甚至没有将';列为可能的返回值,这是相当令人难以置信的。真扫兴。

它需要一个BSD系统来构建它,您可以通过构建BSD世界来构建它,这个世界非常庞大,并且不仅仅包括libc。导航代码库要困难得多,代码也不太容易操作。

由于BSD是一个庞然大物,所以构建时间非常长。

如果没有一些重大的改变,MUSL肯定不会开箱即用,因为它是一个基于Linux的libc,而我们正在处理BSD。甚至让它构建所需的更改。

与BSD世界不同,MUSL只是一个libc(至少是一个最小的libc)。修复东西相对容易,即使您不熟悉代码库,代码也很容易阅读和理解。

与BSD libc相比,MUSL构建时间快得惊人。构建BSD libc需要大约20分钟,而构建MUSL需要将近2分钟。

尽管从理论上讲,移植MUSL比构建修改后的BSD libc需要更多的更改,但当我们考虑到使用MUSL要容易得多,构建起来要快得多时,MUSL是更好的选择。

在大多数情况下,在C程序中,main()不是真正的入口点。虽然对于应用程序作者而言,它是入口点,但除非编译器标志或预处理器指令更改,否则实际端点通常是_start()。这是因为C运行时希望在运行任何用户代码之前初始化它的环境。这由CRT存根处理。C运行时引导代码的大部分包含在crt1.o对象文件中。通常,在此文件中有入口点start(),它调用__libc_start_main(),传递用户定义的main()的地址,__libc_start-main()将在完成环境相关内容的初始化后执行。

PS4在这方面略有不同,因为PS4没有提供给它的常规环境或参数集。事实上,PS4甚至不希望应用程序返回,因为试图从main()返回会使游戏崩溃。这是有道理的,因为游戏不需要或不使用参数,除非玩家退出游戏或应用程序,否则游戏不应该返回。考虑到这一点,我忽略了__libc_start_main(),并定义了我自己的_start(),它与BSD libc生成的更加匹配。

__ASM__(";.intel_语法noprefix\n";";.global";start";\n";start";:\n";subRSP,0x28\n&34;";mov RDI,R8\n";";在exit\n";";xor edX,edX\n&。";mov rsi,r10\n";";调用main\n";";mov r11d,eax\n&34;";mov edi,r11d\n";";调用退出\n";);

另一个必须做出的重大改变是PS4有一个定制的ELF加载器。动态链接是自定义的,并且还有额外的非标准/索尼定义的部分链接到PS4应用程序中。虽然这些其他段中的大多数都超出了本文的范围,但其中一个段是.sce_process_param。此段定义元数据信息,并且需要链接到每个应用程序。正因为如此,CRT存根是放置这个段的一个很好的位置,因为它总是与使用工具链构建的每个应用程序相链接。

其中一些元数据信息包括ORBIS&34;应用程序的魔术版本、其他专门对象的条目、用于构建应用程序的SDK版本,以及其他各种信息。以下是此自定义部分的片段;为了简洁起见,我没有粘贴全部内容,但是如果您感兴趣,可以转到端口存储库并查看/arch/ps4/crt_arch.h来查看此内容。

__ASM__(";.intel_语法noprefix\n";";.ign 0x8\n";.Section\";.data.sce_process_param\";\n";";_sceProcessParam:\n";//size";.quad 0X50\n";//magic";orbi&。.long 0x3\n";//.);

由于MUSL是一个基于Linux的libc,它将调用Linux syscalls来弥合用户和内核之间的鸿沟。Linux和FreeBSD有不同的syscall集。虽然有很多相似之处(例如open()、read()、write()、close()),但是当您遇到不太常见的系统调用时,差别就开始显现出来。幸运的是,编写MUSL时就考虑到了这一点,它提供了定义自定义架构(更像是目标)的便利功能,允许您指定(以及其他各种内容)一组输入syscall标识符。

Linux和FreeBSD之间的syscall差异因子系统而异。对于某些(如文件I/O),差别很小甚至没有。对于其他人来说,不同之处仅仅是不同的名字或几个互换的争论。不过,在某些情况下,直接在BSD中不存在syscall,而在Linux中则不存在。快速用户空间互斥锁(或简称futex)就是这种情况。在移植到PS4时,我遇到了所有这些类型的差异。

对于不同的名称,我只是为MUSL期望的名称定义了别名,并将它们定义给执行相同操作的BSD SysCall。用于信令的一组syscall就是一个很好的例子。

在更难解决的情况下,比如系统调用不存在,它总是处于奇异的功能中,所以我会调用我们没有的syscall。将来,可以添加任何需要实现的功能。执行ifdef&39;ing是为了防止破坏非PS4架构的MUSL。FreeBSD上不存在的fan otify_init()syscall说明了这一点。

这些都是构建MUSL所必需的更改,但是在运行时会遇到错误。这是因为Linux的内核ABI和BSD之间的差异。

应用程序二进制接口,或ABI";,基本上是一个规范,它概述了要遵循的调用约定、二进制格式、动态链接和其他内容。调用约定是最重要的,因为我们需要知道哪些寄存器被保留,哪些寄存器被丢弃,哪些寄存器是易失性的(又名)。";Scratch";)寄存器,以及哪些寄存器用于参数和返回值。userland和kernel的调用约定略有不同。

Linux和BSD都使用";system V&34;或";SYSV";ABI规范。出于本文的目的,我们将重点介绍调用约定。在SYSV下,以下规格适用:

寄存器RBX、RSP、RBP、R12、R13、R14和R15被保留,这意味着它们的值由被调用方函数保存。

寄存器RAX、RDI、RSI、RDX、RCX、R8、R9、R10和R11是易失性/暂存寄存器,这意味着它们的值不被被调用方函数保存。

对于传递参数和返回值,以下内容适用:按照第1个参数到第6个参数的顺序,分别使用寄存器RDI、RSI、RDX、RCX、R8和R9。除此之外,参数在堆栈上传递。

对于传递参数,适用以下条件:RAX寄存器用于指定从系统调用表调用哪个系统调用索引。

按照第1 Arg到第6 Arg的顺序,分别使用寄存器RDI、RSI、RDX、R10、R8和R9。

寄存器RCX被内核syscall异常处理程序丢弃,以存储syscall之前的原始指令指针值。

通常,虽然参数寄存器在用户环境中是易失性的,但是它们的值在syscall之后由syscall异常处理程序本身保存和恢复。

请注意,在上一节中,我说过通常在syscall之后保存和恢复参数寄存器值,重点是";,通常是";。在Linux中是这样,在FreeBSD上过去也是这样。在PS4和2019年1月以后更新的FreeBSD版本上,这一假设被打破了。

从系统设计的角度来看,在将特权级别切换到内核或从内核切换特权级别时,必须小心使用寄存器。您需要确保将寄存器恢复到它们的原始值,或者在返回到用户空间之前更改它们,理想情况下是将寄存器恢复到它们的原始值。首先,如果可以的话,您不想让寄存器值从您的userland调用方下面诱骗和切换出来。有时,参数寄存器可能用于存储在syscall返回之后重新使用的数据,特别是考虑到参数寄存器是临时寄存器,编译器认为这是公平的游戏。

从安全的角度来看,第二个也可能是更重要的原因是您不想将内核寄存器值泄露给userland。像这样的信息泄露可能是强大的攻击,它可以单枪匹马地摧毁内核ASLR。这似乎是一个令人担忧的问题,但他们并没有将寄存器恢复到syscall之前的用户地值,而是选择在返回用户地之前通过与自己进行异或来清除寄存器值。这个银河系大脑的想法会引起问题!我只在R10寄存器中发现了这个问题,但我认为它可能不仅仅发生在R10上,所以我检查了PS4内核转储中系统调用的Xfast_syscall异常处理程序。

mov rdi,qword[rsp]mov rsi,qword[rsp+0x8]mov RDX,qword[rsp+0x10]mov rax,qword[rsp+0x30]mov r11,qword[rsp+0xb8]mov RCX,qword[rsp+0xa8]mov RSP,qword[rsp+0xc0]xor r8,r8{0x0}xor。

它不仅清除R10,还清除R8和R9。更有趣的是,此代码是由Sony添加的,因为FreeBSD 9中没有此代码。但此代码确实存在于FreeBSD 12中。如果我们查看包含异常处理程序实现的BSD/sys/amd64/amd64/exception.S的git故障,我们将注意到以下提交:

从系统调用快速返回时,%r8、%r10和非KPTI配置%r9上的%r9未恢复。

在FreeBSD 11.2-STRISE(R343782)、11.2-Release-p9、12.0-STRISE(R343781)和12.0-Release-p3之前的版本中,内核被调用者保存寄存器在从系统调用返回之前未正确消毒,可能会导致系统调用中使用的某些内核数据暴露。

正如怀疑的那样。存在通过临时寄存器泄露内核信息的情况。但是,他们并没有将它们正确地恢复为用户土地节约的值,而只是将它们清零。当构建BSD的libc时,我不知道这怎么能不打破假设,他们一定有一些我找不到的特殊代码。要么就是这个问题,要么就是这个问题产生了尚未发现/披露的错误。

这里奇怪的是我查看的PS4转储是5.05固件,它是在2018年1月发布的。然而,FreeBSD主线中的这一承诺是在2019年2月推动的。索尼比FreeBSD早一年多就知道这个问题。要么索尼报告了这个问题,并花了一段时间才在FreeBSD中登陆,要么FreeBSD的维护人员必须稍后独立发现该问题。由于修复程序非常相似,我猜测很可能是索尼插手了FreeBSD主线补丁,因为修复程序基本上是相同的。

这并不是最佳的修复方法,因为常规的咔哒声不会出现这种行为。编译器喜欢高效,因此它会在可能的情况下尝试重用寄存器,以避免重新加载寄存器的性能成本。随着这一假设的打破,引入了逻辑错误。每当编译器在调用syscall之后重新使用r8、r9或r10时,都会触发未定义的行为。在指针的情况下,这会导致从源代码级别看起来完全有效的代码中的空指针取消引用。由于这些空指针取消引用而发现了此问题。

例如,MUSL中的printf()调用名为__stdout_write的函数,该函数发出TIOCGWINSZ ioctl syscall来获取窗口大小。以下是拆卸过程:

在您考虑syscall拦截器r10之前,这段代码看起来没有问题。BOOM,空指针取消引用移动双字[R10+0x90],-1指令。为了解决这个问题,我修改了syscall包装器来备份和恢复这些寄存器。

static__inline long__syscall6(long n,long a1,long a2,long a3,long a4,long a5,long a6){//.__asm_Volatile__(";.intel_语法\n\t";";Push R8\n\t";";Push R9\n\t";";Push R10\n\t"。//.";POP R10\n\t";";POP R9\n\t";";POP R8\n\t";:";=a";(Ret):";a";(N),";D";(A1),";S";(A2),";d";(A3),"。r";(R10),";r";(R8),";r";(R9):";rcx";,";r11";,";内存";);return ret;}。

这个错误更多的是我的错误,因为我没有意识到SYSV并没有扩展到仅仅定义与错误处理相关的寄存器。它没有定义syscall应该如何指示发生了错误,以及是否返回正的或负的errno值。在Linux中,成功时系统调用返回0或它们应该返回的任何内容的正值(通常是描述符或计数)。在失败的情况下,返回否定的errno。预料到这一点,MUSL有一个处理程序来检查返回是否为-4095<;=rax<;=-1。如果它在此范围内,则通过否定将errno反转回正来适当地设置errno,并返回-1。

long__syscall_ret(无符号长r){if(r&>;-4096UL){errno=-r;return-1;}return r;}

不过,FreeBSD有点不同。FreeBSD系统调用返回一个正的errno,而不是一个负的errno。有人可能想知道这是如何工作的;您怎么知道syscall失败了,返回值应该被解释为错误,而不是解释为有效数据?碰巧FreeBSD在出错时将进位标志或标志的";cf";设置为1,在成功时设置为0。

当使用标准FreeBSD调用约定时,进位标志在成功时被清除,在失败时设置。

我花了一点时间才弄清楚如何用一种感觉不像黑客的方式来做这件事。我最后根据进位标志执行条件跳转,如果设置了标志,则否定Rax。这些更改再次添加到syscall包装器中。

static__inline long__syscall6(long n,long a1,long a2,long a3,long a4,long a5,long a6){//._ASM_Volatile__(";.intel_语法\n\t";//.";syscall\n\t";";JNC syscallexit%=\n\t";";neg rax\。syscallexit%=:\n\t";//.:";=a";(Ret):";=a";(Ret):";a";(N),";D";(A1),";S";(A2),";d";(A3),";r";(R10),";r。(R8),";r";(R9):";rcx";,";r11";,";内存";);

在移植像libc这样的东西时,有很多低级因素需要考虑,而且很容易受挫,成为细微差异的牺牲品。为了测试MUSL端口,我编写了一些读者可能会感兴趣的一套单元测试,并使用OpenOrbis PS4工具链针对新构建的MUSL libc静态库进行了编译。您可以在下面找到指向MUSL PS4端口[1]的链接、上述测试集[2]、直接来自PS4[3]的测试结果以及其他参考资料。