A9VG电玩部落论坛

 找回密码
 注册
搜索
查看: 21115|回复: 53

【原创翻译】CTurt:对 PS4 的安全和破解进展的分析

[复制链接]

精华
0
帖子
533
威望
0 点
积分
585 点
种子
17 点
注册时间
2010-9-9
最后登录
2021-6-18
 楼主| 发表于 2015-12-8 17:09  ·  美国 | 显示全部楼层 |阅读模式
本帖最后由 boomer 于 2015-12-8 17:10 编辑

https://cturt.github.io/ps4.html

译者按:2015 年 12 月 6 日,本文作者 CTurt 宣布发现了第一个完全可用的 PS4 内核漏洞。这篇文章对这个漏洞的发现过程和原理的理解能有很好的帮助。

对 PS4 的安全和破解进展的分析

因为在很长时间里都没有关于 PS4 破解的重大消息,我想解释一下 PS4 的破解进度,以及阻碍进一步发展的因素。

我将介绍一些对于所有现代系统大都适用的安全概念,以及我在 PS4 上运行 ROP 测试的发现。

如果你对破解还不太熟悉,可以先阅读我的文章《通过存档文件中的栈瓦解(stack smash)漏洞破解 DS 游戏》

你可以在这里下载我的全套文件用来自己运行这些测试;它们目前只能在 1.76 固件上使用。

PS4 的背景信息

你可能已经知道,PS4 配备了一个定制的 AMD x86_64 CPU(8 核),而且已经有许多对于这个 CPU 架构的研究了,即使这个特定的版本可能与已知的标准有细微的差异。例如,PFLA(Page Fault Liberation Army;译注:一个研究小组)在 29C3 大会(译注:一个计算机安全会议)上发表了概念性的证明,仅用分页错误和 x86 MMU 实现完备的图灵机。可以在 YouTube 上看到他们的精彩视频。此外,如果你试着在虚拟机内部运行代码并且想在宿主 CPU 上执行指令的话,这也很有趣。
- EurAsia 新闻文章 3251

除了具备一个有详细文档的 CPU 架构外,PS4 当中用到的许多软件开源的。

最重要的是,PS4 的 Orbis 操作系统是基于 FreeBSD 的,就像 PS3 的操作系统一样(也有一些 NetBSD 的组件);但是除了 FreeBSD 9.0 外,其他值得注意的软件还包括 Mono VMWebKit

WebKit 入口点

WebKit 是 iOS、Wii U、3DS、PS Vita 以及 PS4 当中的浏览器用来渲染网页的开源排版引擎。

虽然广泛使用而且成熟,WebKit 的确有它的弱点;你可以在阅读 Pwn2Own 的总结文章时了解其中的大部分。

尤其是,PS4 1.76 固件中的浏览器使用的 WebKit 版本包含 CVE-2012-3748 所描述的缺陷,一个在 JSArray::sort(...) 方法中的基于堆的缓冲区溢出漏洞。

在 2014 年,nas 和 Proxima 宣布他们成功地将这个漏洞移植到了 PS4 的浏览器上,并且公布了概念性的代码,作为第一个 PS4 破解的入口点。

这使得我们可以任意读写 WebKit 进程所能读写的一切,用以转储模块,并且改写栈上的返回地址,使我们能够控制程序计数器(以进行 ROP)。

从这以后,WebKit 当中许多其他的漏洞也被发现,使得在 PS4 的更高版本的固件上也可能转储模块和 ROP。不过在本文写作的时候,还没有人公开地将任一漏洞移植到 PS4 上。

什么是 ROP?

不像 DS 这样原始的设备,PS4 的内核可以控制不同内存区域的属性。被标记为可执行的内存页面不能被改写,而被标记为可写入的内存页面不能被执行;这被称为数据执行保护(DEP)

这意味着我们不能简单地将一份载荷复制到内存当中并且执行。但是,我们可以执行已经加载到内存并被标记为可执行的代码。

如果我们不能把自己的代码写入到一个地址上的话,只是跳转到那个地址是没有效果的,所以我们使用 ROP。

返回导向编程(Return-Oriented Programming,ROP)只是对传统的栈瓦解的拓展,不过我们并非仅仅改写程序计数器将跳转到的值,而是将许多不同的地址连接到一起,称为指令序列(gadget)。

一个指令序列通常只是单个需要的指令,紧接着一个 ret 指令。

在 x86_64 汇编语言中,当执行到一个 ret 指令时,一个 64 位值被退栈,并且程序计数器跳转到这个值;因为我们可以控制栈,我们就可以使每一个 ret 指令都跳转到下一个需要的指令序列。

例如,从 0x80000 开始,可能有如下指令:

mov rax, 0
ret

并且,从 0x90000 开始,可能有如下指令:

mov rbx, 0
ret

如果我们改写栈上的一个返回地址,使得它包含 0x80000,接着是 0x90000,那么只要运行到第一个 ret 指令,执行就会跳转到 mov rax, 0,紧接着,下一个 ret 指令会将 0x90000 退栈,并且跳转到 mov rbx, 0。

这串指令可以有效地将 rax 和 rbx 二者置零,就像我们把代码写入到单个位置并且执行一样。

ROP 链不仅限于一个地址列表;假设从 0xa0000 开始有如下指令:

pop rax
ret

我们可以设置链中的第一个项目为 0xa0000,然后下一个项目为需要写入 rax 的任意值。

指令序列也不一定要以 ret 指令结束;我们可以使用以 jmp 结束的指令序列:

add rax, 8
jmp rcx

让 rcx 指向一个 ret 指令,ROP 链将会正常继续:

chain.add("pop rcx", "ret");
chain.add("add rax, 8; jmp rcx");

有的时候,你并不能找到完全符合需要的指令序列,而是在其后有其他的指令。例如,如果你想设置 r8 为某些值,但是只有这个指令序列,你只能把 r9 设置为某些无意义的值:

pop r8
pop r9
ret

虽然你在编写 ROP 链时可能需要有创造性,不过一般认为,在足够大的内存转储当中,可以找到足够的指令序列用来实现图灵完备性;这使得 ROP 成为一个有效地绕过 DEP 的方法。

寻找指令序列

将 ROP 视为写一本书的新的一章,但是只允许使用前面的章节当中的句子末尾的单词。

非常明显,我们不太可能在任何句子的末尾找到“and”或者“but”这样的单词,但是如果我们要写出任何有意义的东西,都必须有这些连接词。

但是,很有可能的是,某个句子是以“sand”结尾的。虽然作者的意图绝对是从“s”开始读这个单词,如果我们从“a”开始读的话,它就会恰好变成一个完成不同的单词,“and”。

这些规则同样适用于 ROP。

因为几乎所有的函数的结构都遵循这样的规则:

; 保护现场
push    rbp
mov     rbp, rsp
push    r15
push    r14
push    r13
push    r12
push    rbx
sub     rsp, 18h

; 函数体

; 恢复现场
add     rsp, 18h
pop     rbx
pop     r12
pop     r13
pop     r14
pop     r15
pop     rbp
ret

你只能预期找到 pop 指令序列,或者更加稀有的一些东西,例如 xor rax, rax 用以在返回之前将返回值置零。

进行比较,例如:

cmp [rax], r12
ret

没有任何意义,因为比较的结果不会被函数使用。但是,我们也是可能找到这样的指令序列的。

x86_64 指令与单词的相似之处在于长度是可变的,而且,取决于解码开始处,意义可能完全不同。

x86_64 架构是一个可变长度的 CISC 指令集。在 x86_64 上的返回导向编程利用了指令集非常“密集”的事实,即任何随机的字节序列都可能被解释为某些有效的 x86_64 指令**。
- 维基百科

为了展示这一点,可以阅读 WebKit 模块的这一个函数的末尾:

000000000052BE0D                 mov     eax, [rdx+8]
000000000052BE10                 mov     [rsi+10h], eax
000000000052BE13                 or      byte ptr [rsi+39h], 20h
000000000052BE17                 ret

现在,如果我们从 0x52be14 开始解码,再看这段代码:

000000000052BE14                 cmp     [rax], r12
000000000052BE17                 ret

即使这段代码绝对没有被执行的意图,它位于一个被标记为可执行的内存区域,所以可以完美地用作指令序列。

当然,人工查阅每一个 ret 指令前的每一种可能的代码的解释方法,需要的时间是不可想象的;这就是为什么有工具来帮助你。我用来搜索 ROP 指令序列的是 rp++;要生成一个满是指令序列的文本文件,只要执行:

rp-win-x64 -f mod14.bin --raw=x64 --rop=1 --unique > mod14.txt

段错误

如果我们试图执行一个不可执行的内存页面,或者试图写入一个不可写入的内存页面,就会发生段错误

例如,试图在被映射为只能读写的栈上执行代码:

setU8to(chain.data + 0, 0xeb);
setU8to(chain.data + 1, 0xfe);

chain.add(chain.data);

并且试图写入被映射为只能读取和执行的代码:

setU8to(moduleBases[webkit], 0);

如果发生了段错误,就会出现“可用系统内存不足”的消息,而网页加载会失败:



也有其他的导致这个消息出现的原因,例如执行无效指令或者未实现的系统调用,但是最常见的是段错误。

ASLR

地址空间布局随机化(Address Space Layout Randomization,ASLR)是使你每次启动 PS4 时模块的基地址都不相同的安全技术。

我收到报告说,非常旧的固件(1.05)没有启用 ASLR,但是在 1.70 固件前的某个时间被启用了。值得注意的是,内核 ASLR 没有被启用(至少在 1.76 或者更低版本的固件当中),这一点将在下文中证明。

对绝大多数漏洞而言,ASLR 都是一个问题,因为如果你不知道内存中的指令序列的地址的话,就无法确定写入栈的值。

幸运的是,我们并不局限于编写静态 ROP 链。我们可以使用 JavaScript 来读取模块表,这将告诉我们所有已经装载的模块的基地址。使用这些基地址,我们就可以在触发 ROP 执行之前计算所有的指令序列的地址,绕过 ASLR。

这份模块表同时包含了模块的文件名:
  • WebProcess.self
  • libkernel.sprx
  • libSceLibcInternal.sprx
  • libSceSysmodule.sprx
  • libSceNet.sprx
  • libSceNetCtl.sprx
  • libSceIpmi.sprx
  • libSceMbus.sprx
  • libSceRegMgr.sprx
  • libSceRtc.sprx
  • libScePad.sprx
  • libSceVideoOut.sprx
  • libScePigletv2VSH.sprx
  • libSceOrbisCompat.sprx
  • libSceWebKit2.sprx
  • libSceSysCore.sprx
  • libSceSsl.sprx
  • libSceVideoCoreServerInterface.sprx
  • libSceSystemService.sprx
  • libSceCompositeExt.sprx

虽然 PS4 明显使用了 [签名的] PPU 可重载执行(Relocatable Executable)([S]PRX)格式模块,在 libSceSysmodule.sprx 的转储中的一些字符串提及了 [签名的] 执行与链接格式(Executable and Linking Format)([S]ELF)对象文件,例如 bdj.elf、web_core.elf 以及 orbis-jsc-compiler.self。这个模块和对象的组合与 PSP 和 PS3 是相似的。

你可以在 libSceSysmodule.sprx 当中看到一个完整的可使用的模块列表(不仅限于浏览器加载的模块)。我们可以通过一些索尼自定义的系统调用装载和转储当中的一部分,在下文会解释。

JuSt-ROP

使用 JavaScript 来写入和执行动态 ROP 链,与标准的缓冲区溢出攻击相比,我们获得了巨大的优势。

在绕过 ASLR 的同时,我们还能读取浏览器的用户代理,并且为不同的浏览器版本提供不同的 ROP 链,使得我们的破解具有尽可能的广泛的兼容性。

我们甚至可以用 JavaScript 读取指令序列所在的内存地址以检查它们是否正确,让我们获得几乎完美的可靠性。

编写动态的 ROP 链,比起事先用脚本生成它们,要聪明得多。

我创造了一个用于编写 ROP 链的 JavaScript 框架,JuSt-ROP,专门为此服务。

关于 JavaScript 的说明

JavaScript 用 IEEE-754 双精度浮点数(64 位)格式表示数字。这为我们提供了 53 位精确值,意味着不可能表示每一个 64 位值,某些必须使用估算。

如果你只要需要将一个 64 位值转换为低精度,例如 256,那么 setU64to 就很好。

但是,在一些情况下,你必须写入一个缓冲区或者结构体,如果用 64 位块来写入的话,那么特定的字节就有可能不会被正确写入。

因此,你应该用 32 位块来写入数据(牢记 PS4 是小端序),以确保每一个字节都是准确的。

系统调用

有趣的是,PS4 使用的是与 Linux 和 MS-DOS 相同的系统调用命名约定,将参数保存在寄存器中,而不是传统的 UNIX 方式(也是 FreeBSD 默认使用的方式),将参数保存在栈中:
  • rax - 系统调用号
  • rdi - 参数 1
  • rsi - 参数 2
  • rdx - 参数 3
  • r10 - 参数 4
  • r8 - 参数 5
  • r9 - 参数 6

我们可以使用以下 JuSt-ROP 方法试图进行任意系统调用:

this.syscall = function(name, systemCallNumber, arg1, arg2, arg3, arg4, arg5, arg6) {
        console.log("syscall " + name);
       
        this.add("pop rax", systemCallNumber);
        if(typeof(arg1) !== "undefined") this.add("pop rdi", arg1);
        if(typeof(arg2) !== "undefined") this.add("pop rsi", arg2);
        if(typeof(arg3) !== "undefined") this.add("pop rdx", arg3);
        if(typeof(arg4) !== "undefined") this.add("pop rcx", arg4);
        if(typeof(arg5) !== "undefined") this.add("pop r8", arg5);
        if(typeof(arg6) !== "undefined") this.add("pop r9", arg6);
        this.add("mov r10, rcx; syscall");
}

只要确保事先将栈底设置为一些未分配内存即可:

this.add("pop rbp", stackBase + returnAddress + 0x1400);

使用系统调用可以让我们知道关于 PS4 内核的海量信息。不仅如此,系统调用很可能也是我们与内核互动的惟一方式,并且可能触发一个内核漏洞。(译注:是的,已经找到一个了)

如果你在对模块逆向工程以确定一些索尼自定义的系统调用的话,你可能遇到一种不同的命名约定:

有些时候,索尼通过常规的系统调用 0(一般在 FreeBSD 中不会有任何效果)来执行系统调用,使用第一个参数(rdi)来确定将被执行的系统调用:
  • rax - 0
  • rdi - 系统调用号
  • rsi - 参数 1
  • rdx - 参数 2
  • r10 - 参数 3
  • r8 - 参数 4
  • r9 - 参数 5

很有可能的是,索尼这么做是为了简单地实现与函数调用的命名约定的兼容性。例如:

.global syscall
syscall:
        xor     rax, rax
        mov     r10, rcx
        syscall
        ret

这样一来,他们就可以在 C 当中使用函数调用命名约定来执行系统调用:

int syscall();

int getpid(void) {
        return syscall(20);
}

当编写 ROP 链时,我们可以使用任意一种命名约定:

// 两者都可以获得当前进程 ID:
chain.syscall("getpid", 20);
chain.syscall("getpid", 0, 20);

了解这一点是有好处的,因为我们可以在可用的指令序列中使用相对方便的那种。

getpid

只需要使用系统调用 20,getpid(void),我们可以了解许多关于内核的信息。

这个系统调用能够被我们利用的直接原因是索尼没有混淆系统调用号来作为一种安全手段(在 BSD 许可证下,他们可以实现这一点而不公布新的系统调用号)。

所以,我们自动地获得了一份 PS4 内核中的系统调用的清单,用以尝试。

其次,调用 getpid(),重新启动浏览器,再调用一次,我们获得了一个比之前的返回值高 2 的返回值。

这告诉我们,互联网浏览器应用实际上包含了两个不同的进程:WebKit 核心(我们已经接管了),用来处理 HTML 和 CSS 解析、图像解码以及例如执行 JavaScript 等等,另一个负责处理其他一切:显示图像、接收控制器输入及管理历史记录和书签等等。

而且,虽然 FreeBSD 4.0 开始已经支持了 PID 随机化,但顺次分配 PID 仍是默认的方式。

使用默认的 PID 分配方式表明索尼似乎没有加入任何额外的安全增强措施,例如被 HardenedBSD 等项目建议的那些。

有多少自定义的系统调用呢?

FreeBSD 9 的最后一个标准系统调用是 wait6,编号 532;一切比这个更高的肯定是索尼自定义的系统调用。

在没有正确的参数的情况下,触发大多数的索尼自定义的系统调用,都会返回错误 0x16,“无效参数”;但是,有关兼容性的或者没有实现的系统调用会报告“可用系统内存不足”错误。

经过试错,我发现系统调用号 617 是最后一个索尼的系统调用,任何更高的都没有被实现。

所以,我们可以确定,在 PS4 内核当中一共有 85 个索尼自定义的系统调用(617 - 532)。

这明显要比 PS3 的将近 1000 个系统调用要少得多。这表明我们可能掌握的攻击向量更少,但是要把所有的系统调用整理成文档可能更容易。

更进一步,85 个系统调用中的 9 个总是返回 0x4e,ENOSYS,说明它们可能只能在开发机上被调用,我们剩下可用的只有 76 个。

在这 76 个当中,只有 45 个被 libkernel.sprx 引用(所有非核心应用程序使用系统调用的途径),所以开发者只有 45 个自定义系统调用可以使用。

有趣的是,虽然设计上只有 45 个系统调用可用(因为 libkernel.sprx 提供了它们的包装),其他 31 个当中仍然有可以从互联网浏览器进程调用的。这些本来不应使用的系统调用存在漏洞的可能性更高,因为它们可能只经过了最少的测试。

libkernel.sprx

要了解 libkernel 是如何使用自定义系统调用的,你必须先记住,它只是一个对标准 FreeBSD 9.0 库的修改。

这是 thr_init.c 当中的 _libpthread_init 片断:

/*
* Check for the special case of this process running as
* or in place of init as pid = 1:
*/
if ((_thr_pid = getpid()) == 1) {
        /*
         * Setup a new session for this process which is
         * assumed to be running as root.
         */
        if (setsid() == -1)
                PANIC("Can't set session ID");
        if (revoke(_PATH_CONSOLE) != 0)
                PANIC("Can't revoke console");
        if ((fd = __sys_open(_PATH_CONSOLE, O_RDWR)) < 0)
                PANIC("Can't open console");
        if (setlogin("root") == -1)
                PANIC("Can't set login to root");
        if (_ioctl(fd, TIOCSCTTY, (char *) NULL) == -1)
                PANIC("Can't set controlling terminal");
}

同样的函数可以在 libkernel.sprx 的偏移量 0x215F0 处找到。这是上面的片断在 libkernel 转储中的样子:

call    getpid
mov     cs:dword_5B638, eax
cmp     eax, 1
jnz     short loc_2169F

call    setsid
cmp     eax, 0FFFFFFFFh
jz      loc_21A0C

lea     rdi, aDevConsole ; "/dev/console"
call    revoke
test    eax, eax
jnz     loc_21A24

lea     rdi, aDevConsole ; "/dev/console"
mov     esi, 2
xor     al, al
call    open

mov     r14d, eax
test    r14d, r14d
js      loc_21A3C
lea     rdi, aRoot       ; "root"
call    setlogin
cmp     eax, 0FFFFFFFFh
jz      loc_21A54

mov     edi, r14d
mov     esi, 20007461h
xor     edx, edx
xor     al, al
call    ioctl
cmp     eax, 0FFFFFFFFh
jz      loc_21A6C

对模块转储进行逆向工程来分析系统调用

libkernel 并不是完全开源的;有很多自定义代码可以帮助揭示一些索尼的系统调用。

虽然这个过程会根据你所查找的系统调用的具体情况而不同;对于某些来说,简单理解被传递的参数的含义是比较容易的。

系统调用的包装会在 libkernel.sprx 中的某处被声明,并且总是符合这个模板:

000000000000DB70 syscall_601     proc near
000000000000DB70                 mov     rax, 259h
000000000000DB77                 mov     r10, rcx
000000000000DB7A                 syscall
000000000000DB7C                 jb      short error
000000000000DB7E                 retn
000000000000DB7F
000000000000DB7F error:
000000000000DB7F                 lea     rcx, sub_DF60
000000000000DB86                 jmp     rcx
000000000000DB86 syscall_601     endp

注意,mov r10, rcx 指令并不意味着系统调用至少需要 4 个参数;所有的系统调用包装都是这样,即使是无参数的,例如 getpid。

当你找到包装之后,你可以查找对它的交叉引用:

0000000000011D50                 mov     edi, 10h
0000000000011D55                 xor     esi, esi
0000000000011D57                 mov     edx, 1
0000000000011D5C                 call    syscall_601
0000000000011D61                 test    eax, eax
0000000000011D63                 jz      short loc_11D6A

最好是多查找几个,以确保寄存器没有被无关的代码修改:

0000000000011A28                 mov     edi, 9
0000000000011A2D                 xor     esi, esi
0000000000011A2F                 xor     edx, edx
0000000000011A31                 call    syscall_601
0000000000011A36                 test    eax, eax
0000000000011A38                 jz      short loc_11A3F

一致的是,系统调用的命名约定的前 3 个寄存器(rdi、rsi 和 rdx)在触发调用前被修改,所以我们有理由相信它需要 3 个参数。

明确地说,这是我们如何在 JuSt-ROP 当中复现这些调用的方法:

chain.syscall("unknown", 601, 0x10, 0, 1);
chain.syscall("unknown", 601, 9, 0, 0);

对大多数系统调用来说,在成功时的返回值为 0,这一点由 test 指令之后的 jz 指令所表明。

查找一切比参数数量更进一步的信息,都需要对之前和之后的代码进行深入得多的分析以理解上下文,但是这应该可以帮助你上手了。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?注册

x

精华
0
帖子
533
威望
0 点
积分
585 点
种子
17 点
注册时间
2010-9-9
最后登录
2021-6-18
 楼主| 发表于 2015-12-8 17:09  ·  美国 | 显示全部楼层
***枚举系统调用

虽然对模块转储进行逆向工程是鉴别系统调用最可靠的方式,但一些系统调用完全没有在转储中被引用,所以我们只能进行盲分析。

如果我们猜测某个系统调用可能使用特定的参数**,那就可以用我们选定的参数,***枚举所有的能返回特定值(0 代表成功)的系统调用,并且忽略一切返回错误结果的。

我们也可以向所有的参数传递 0,然后***枚举全部系统调用,并确定返回了有价值的错误信息的那些,例如 0xe“错误地址”表示它们至少需要一个指针。

首先,我们需要在页面加载后立即执行 ROP 链。我们可以通过把自己的函数附加到 body 元素的 onload 来实现:
  1. <body onload="exploit()">
复制代码
接下来,我们需要执行一个由 HTTP GET 值所确定的特定的系统调用。虽然这可以用 JavaScript 实现,为了简明,我将演示如何用 PHP 实现:

var Sony = 533;
chain.syscall("Sony system call", Sony + <?php print($_GET["b"]); ?>, 0, 0, 0, 0, 0, 0);
chain.write_rax_ToVariable(0);

当系统调用被执行之后,我们可以检查返回值,如果返回值没有意义,就把页面重定向到下一个系统调用:

if(chain.getVariable(0) == 0x16) window.location.assign("index.php?b=" + (<?php print($_GET["b"]); ?> + 1).toString());

在末尾附加 ?b=0 并运行页面,就可以从第一个索尼的系统调用来进行***枚举。

这个方法需要大量的实验,通过传递不同的值到被***枚举所发现的系统调用,再分析新的返回值同。你应该可以部分鉴别某些系统调用。

系统调用 538

作为例子,我将在不依赖任何模块转储的情况下,分析系统调用 538。

这些返回值是取决于第一个参数所传递的值的:
  • 0 - 0x16,“无效参数”
  • 1 - 0xe,“错误地址”
  • 指向一串 0 的指针 - 初始值 0x64,每次页面刷新后加 1

其他可供尝试的参数包括 PID、线程 ID 以及文件描述符。

虽然绝大多数的系统调用在成功时返回 0,从每次调用后的返回值都增加这个特征来看,似乎它在分配一个资源编号,例如文件描述符。

下一步是察看系统调用之前和之后的数据,确定它是否被写入。

因为数据没有发生变化,我们现在可以推测它是一个输入。

接下来,我试着传递了一个长字符串作为第一个参数。对每一个找到的输入,你都应当这么做,因为有可能发现缓冲区溢出。

writeString(chain.data, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
chain.syscall("unknown", 538, chain.data, 0, 0, 0, 0, 0);

这次的返回值是 0x3f,ENAMETOOLONG。不幸的是,看来这个系统调用正确地限制了名称的长度(包括 NULL 结束符一共 32 字节),但是这确实告诉我们,它需要的是一个字符串,而不是一个结构体。

关于这个系统调用的功能,现在我们知道了少量的几种可能,最明显的是关于文件系统的某些方面(例如自定义的 mkdir 或者 open),但是这似乎并不像是某种在我们向指针写入任何数据之前就被分配的资源。

要测试第一个参数是不是路径,我们可以将其以多个 / 字符来分割,看看是否能允许更长的字符串:

writeString(chain.data, "aaaaaaaaaa/aaaaaaaaaa/aaaaaaaaaa");
chain.syscall("unknown", 538, chain.data, 0, 0, 0, 0, 0);

因为这同样返回 0x3f,我们可以认为第一个参数不是路径;它是某种被顺次分配的东西的名称。

在分析更多的系统调用之后,我发现以下这些都具有完全相同的行为:
  • 533
  • 538
  • 557
  • 574
  • 580

从以上信息可知,要精确地知道这些系统调用的功能几乎是不可能的,但是随着你运行更多的测试,更多信息也会被慢慢地发现。

为了你能节约一些时间,系统调用 538 用于分配一个事件标志(而且不只是接收一个名称)。

运用关于内核运行的一般知识,你可以猜测并验证这些系统调用在分配什么(信号量、互斥锁等等)。

转储额外的模块

我们可以使用以下步骤转储额外的模块:
  • 装载模块
  • 获取模块的基地址
  • 转储模块

我通过非常乏味的尝试,在互联网浏览器内转储了每一个模块,并且将结果发表在了 psdevwiki。所有的模块,只要后面有 Yes 字样,就可以通过这个方法转储。

要装载一个模块,我们需要使用 libSceSysmodule.sprx + 0x1850 处的 sceSysmoduleLoadModule 函数。第一个参数是需要装载的模块编号,其他三个只需要为 0。

以下的 JuSt-ROP 方法可以用来执行一次函数调用:

this.call = function(name, module, address, arg1, arg2, arg3, arg4, arg5, arg6) {
        console.log("call " + name);
       
        if(typeof(arg1) !== "undefined") this.add("pop rdi", arg1);
        if(typeof(arg2) !== "undefined") this.add("pop rsi", arg2);
        if(typeof(arg3) !== "undefined") this.add("pop rdx", arg3);
        if(typeof(arg4) !== "undefined") this.add("pop rcx", arg4);
        if(typeof(arg5) !== "undefined") this.add("pop r8", arg5);
        if(typeof(arg6) !== "undefined") this.add("pop r9", arg6);
        this.add(module_bases[module] + address);
}

所以,要装载 libSceAvSetting.sprx(0xb):

chain.call("sceSysmoduleLoadModule", libSysmodule, 0x1850, 0xb, 0, 0, 0);

就像大多数系统调用那样,它在成功时应该返回 0。要查看被分配的已装载模块编号,我们可以使用一个索尼自定义的系统调用,编号 592,来得到一个当前已装载的模块列表:

var countAddress = chain.data;
var modulesAddress = chain.data + 8;

// 系统调用 592,getLoadedModules(int *destinationModuleIDs, int max, int *count);
chain.syscall("getLoadedModules", 592, modulesAddress, 256, countAddress);

chain.execute(function() {
        var count = getU64from(countAddress);
        for(var index = 0; index < count; index++) {
                logAdd("Module: 0x" + getU32from(modulesAddress + index * 4).toString(16));
        }
});

在不装载额外的模块的情况下运行,会得到如下列表:

0x0, 0x1, 0x2, 0xc, 0xe, 0xf, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1e, 0x37, 0x59

但是,如果我们在装载模块 0xb 之后运行,就会看到一个额外的条目,0x65。注意,模块的编号与已装载的模块编号不同。

我们现在可以使用另外一个索尼的系统调用,编号 593,接收的是一个已装载的模块编号和一个缓冲区,然后将已装载的模块信息写入缓冲区当中,包括它的基地址。由于已装载的模块编号始终是 0x65,我们可以将其硬编码到 ROP 链当中,而不是必须从模块列表中存储结果。

缓冲区必须以应返回的结构体的长度开始,否则将返回 0x16 错误,“无效参数”:

setU64to(moduleInfoAddress, 0x160);
chain.syscall("getModuleInfo", 593, 0x65, moduleInfoAddress);

chain.execute(function() {
        logAdd(hexDump(moduleInfoAddress, 0x160));
});

在成功时它返回 0,并且将缓冲区写入一个可以这样读取的结构体:

var name = readString(moduleInfoAddress + 0x8);
var codeBase = getU64from(moduleInfoAddress + 0x108);
var codeSize = getU32from(moduleInfoAddress + 0x110);
var dataBase = getU64from(moduleInfoAddress + 0x118);
var dataSize = getU32from(moduleInfoAddress + 0x120);

我们现在具备了转储模块所需的一切条件!

dump(codeBase, codeSize + dataSize);

此外还有一个索尼的系统调用,编号 608,与 593 的工作方式相似,但是提供的是略微不同的关于已装载的模块的信息:

setU64to(moduleInfoAddress, 0x1a8);
chain.syscall("getDifferentModuleInfo", 608, 0x65, 0, moduleInfoAddress);
logAdd(hexDump(moduleInfoAddress, 0x1a8));

并不清楚这些信息的含义。

浏览文件系统

PS4 使用标准的 FreeBSD 9.0 系统调用来读取文件和目录。

对某些目录,例如 /dev/,使用 read 是可行的。对其他的目录,例如 /,会失败。

我不清楚原因是什么,但是如果我们对目录使用 gendents 而非 read,它将更可靠地工作:

writeString(chain.data, "/dev/");
chain.syscall("open", 5, chain.data, 0, 0);
chain.write_rax_ToVariable(0);

chain.read_rdi_FromVariable(0);
chain.syscall("getdents", 272, undefined, chain.data + 0x10, 1028);

这是内存中的结果:

0000010: 0700 0000 1000 0205 6469 7073 7700 0000  ........dipsw...
0000020: 0800 0000 1000 0204 6e75 6c6c 0000 0000  ........null....
0000030: 0900 0000 1000 0204 7a65 726f 0000 0000  ........zero....
0000040: 0301 0000 0c00 0402 6664 0000 0b00 0000  ........fd......
0000050: 1000 0a05 7374 6469 6e00 0000 0d00 0000  ....stdin.......
0000060: 1000 0a06 7374 646f 7574 0000 0f00 0000  ....stdout......
0000070: 1000 0a06 7374 6465 7272 0000 1000 0000  ....stderr......
0000080: 1000 0205 646d 656d 3000 0000 1100 0000  ....dmem0.......
0000090: 1000 0205 646d 656d 3100 0000 1300 0000  ....dmem1.......
00000a0: 1000 0206 7261 6e64 6f6d 0000 1400 0000  ....random......
00000b0: 1000 0a07 7572 616e 646f 6d00 1600 0000  ....urandom.....
00000c0: 1400 020b 6465 6369 5f73 7464 6f75 7400  ....deci_stdout.
00000d0: 1700 0000 1400 020b 6465 6369 5f73 7464  ........deci_std
00000e0: 6572 7200 1800 0000 1400 0209 6465 6369  err.........deci
00000f0: 5f74 7479 3200 0000 1900 0000 1400 0209  _tty2...........
0000100: 6465 6369 5f74 7479 3300 0000 1a00 0000  deci_tty3.......
0000110: 1400 0209 6465 6369 5f74 7479 3400 0000  ....deci_tty4...
0000120: 1b00 0000 1400 0209 6465 6369 5f74 7479  ........deci_tty
0000130: 3500 0000 1c00 0000 1400 0209 6465 6369  5...........deci
0000140: 5f74 7479 3600 0000 1d00 0000 1400 0209  _tty6...........
0000150: 6465 6369 5f74 7479 3700 0000 1e00 0000  deci_tty7.......
0000160: 1400 020a 6465 6369 5f74 7479 6130 0000  ....deci_ttya0..
0000170: 1f00 0000 1400 020a 6465 6369 5f74 7479  ........deci_tty
0000180: 6230 0000 2000 0000 1400 020a 6465 6369  b0.. .......deci
0000190: 5f74 7479 6330 0000 2200 0000 1400 020a  _ttyc0..".......
00001a0: 6465 6369 5f73 7464 696e 0000 2300 0000  deci_stdin..#...
00001b0: 0c00 0203 6270 6600 2400 0000 1000 0a04  ....bpf.$.......
00001c0: 6270 6630 0000 0000 2900 0000 0c00 0203  bpf0....).......
00001d0: 6869 6400 2c00 0000 1400 0208 7363 655f  hid.,.......sce_
00001e0: 7a6c 6962 0000 0000 2e00 0000 1000 0204  zlib............
00001f0: 6374 7479 0000 0000 3400 0000 0c00 0202  ctty....4.......
0000200: 6763 0000 3900 0000 0c00 0203 6463 6500  gc..9.......dce.
0000210: 3a00 0000 1000 0205 6462 6767 6300 0000  :.......dbggc...
0000220: 3e00 0000 0c00 0203 616a 6d00 4100 0000  >.......ajm.A...
0000230: 0c00 0203 7576 6400 4200 0000 0c00 0203  ....uvd.B.......
0000240: 7663 6500 4500 0000 1800 020d 6e6f 7469  vce.E.......noti
0000250: 6669 6361 7469 6f6e 3000 0000 4600 0000  fication0...F...
0000260: 1800 020d 6e6f 7469 6669 6361 7469 6f6e  ....notification
0000270: 3100 0000 5000 0000 1000 0206 7573 6263  1...P.......usbc
0000280: 746c 0000 5600 0000 1000 0206 6361 6d65  tl..V......***e
0000290: 7261 0000 8500 0000 0c00 0203 726e 6700  ra..........rng.
00002a0: 0701 0000 0c00 0403 7573 6200 c900 0000  ........usb.....
00002b0: 1000 0a07 7567 656e 302e 3400 0000 0000  ....ugen0.4.....
00002c0: 0000 0000 0000 0000 0000 0000 0000 0000  ................

你可以读取其中的一些设备,例如:读取 /dev/urandom 将向内存填充随机数据。

解析这段内存并且创建一个干净的条目列表也是有可能的;例如完整的文件浏览器的源中的 browser.html:



不幸的是,由于沙盒的限制,我们没有对文件系统的完整访问权限。试图读取确实存在但是被限制的文件和目录会导致错误 2,ENOENT,“没有这个文件或目录”。

我们的确可以访问很多有意思的东西,包括加密后的存档、奖杯和账号信息。我将在下一篇文章中讨论更多关于文件系统的内容。

沙盒

除了与文件相关的系统调用会在特定的路径失败,系统调用失败还有其他原因。

最常见的是,被禁止的系统调用会返回 1,EPERM,“操作被禁止”;例如试图使用 ptrace,但是其他系统调用可能因为不同原因失败:

兼容性系统调用被禁止。例如,当试图调用 mmap 时,你必须使用系统调用 477,而不是 71 或者 197;否则就会触发段错误。

其他系统调用,例如 exit,也会触发段错误:

chain.syscall("exit", 1, 0);

试图创建一个 SCTP 套接字将返回错误 0x2b,EPROTONOSUPPORT,说明 SCTP 套接字被 PS4 内核禁用:

//int socket(int domain, int type, int protocol);
//socket(AF_INET, SOCK_STREAM, IPPROTO_SCTP);
chain.syscall("socket", 97, 2, 1, 132);

此外,虽然使用参数 PROT_READ | PROT_WRITE | PROT_EXEC 调用 mmap 会返回一个有效的指针,但 PROT_EXEC 标志会被忽略。读取它的保护将返回 3(读写),而任何尝试执行该内存的操作将触发段错误:

chain.syscall("mmap", 477, 0, 4096, 1 | 2 | 4, 4096, -1, 0);
chain.write_rax_ToVariable(0);
chain.read_rdi_FromVariable(0);
chain.add("pop rax", 0xfeeb);
chain.add("mov [rdi], rax");
chain.add("mov rax, rdi");
chain.add("jmp rax");

PS4 使用的开源软件清单中,没有任何例如 Capsicum 这样的沙盒软件,所以 PS4 只可能使用纯粹的 FreeBSD jail,或者某些自定义的,专用的,沙盒系统(不太可能)。

Jail

我们可以证明 FreeBSD jail 被 PS4 内核频繁使用,因为 auditon 系统调用不能在 jail 环境中执行:

chain.syscall("auditon", 446, 0, 0, 0);

auditon 系统调用会首先在这里检查 jail 的存在,如果结果为真,则返回 ENOSYS:

if (jailed(td->td_ucred))

return (ENOSYS);

否则,这个系统调用极有可能从 mac_system_check_auditon 的这个位置返回 EPERM:

error = mac_system_check_auditon(td->td_ucred, uap->cmd);
if (error)

return (error);

或者从 priv_check 的这个位置

error = priv_check(td, PRIV_AUDIT_CONTROL);
if (error)

return (error);

这个系统调用能到达的最远处一定是紧接在 priv_check 之后,返回 EINVAL 之前,因为长度参数为 0,如下所示

if ((uap->length <= 0) || (uap->length > sizeof(union auditon_udata)))

return (EINVAL);

因为 mac_system_check_auditon 和 priv_check 不可能返回 ENOSYS,所以对 jail 的检查结果为真是惟一可能返回 ENOSYS 的情况。

执行这个 ROP 链时,返回值是 ENOSYS(0x48)。

这就告诉我们,无论 PS4 使用的什么沙盒系统,至少它都是基于 jail 的,因为对 jail 的检查能够通过。

FreeBSD 9.0 内核漏洞

现在还试图在 FreeBSD 9.0 内核源代码中寻找新的漏洞几乎没有意义,因为自从它在 2012 年发布之后,已经有数个内核漏洞被发现,而 PS4 也有可能受其影响。

出于明显的原因,我们可以立即忽略其中的一些:

FreeBSD 9.0-9.1 mmap/ptrace - 权限提升漏洞 - 没有用处,之前已经提到,我们无法访问 ptrace 系统调用。
FreeBSD 9.0 - Intel SYSRET 内核权限提升漏洞 - 没有用处,因为 PS4 使用 AMD 处理器。
FreeBSD 内核 - 多个漏洞 - 也许是第一个有实际作用的漏洞,但是其他两个依赖于 SCTP 套接字,而这被 PS4 内核禁用(之前已经提到)。

但是,还有一些相似的漏洞,可能有实际作用:

getlogin

一个看上去非常容易尝试的缺陷是使用 getlogin 系统调用来泄漏少量内核内存

getlogin 系统调用的功能是将当前会话的登录名复制到用户内存中,但是,因为一个 bug,整个缓冲区总是被复制下来,而不仅是登录名字符串的长度。这意味着我们可以从内核中读取一些未初始化的数据,可能派上用场。

这个系统调用(49 号)实际上是 int getlogin_r(char *name, int len); 而并非 char *getlogin(void);。

所以,我们可以试着复制一些内核内存到用户内存的一个未被使用的部分。

chain.syscall("getlogin", 49, chain.data, 17);

不幸的是,我们最多只能获得 17 字节的数据,因为:

登录名被限制为 MAXLOGNAME (定义于 <sys/param.h>)个字符,目前包括 null 在内是 17。
- FreeBSD Man 页面

执行 ROP 链之后,返回值是 0,证明这个系统调用的确有效!很好的开始。现在,我们来看看所指向的内存:

执行 ROP 链之前:

00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00

执行 ROP 链之后:

72 6f 6f 74 00 fe ff ff 08 62 61 82 ff ff ff ff
00

将前 4 个字节作为 ASCII 解码:

root

所以浏览器是以 root 身份执行的!这完全是意料之外的。

更有意思的是,泄漏的内存看上去像是指向内核某处的指针,而且每次运行 ROP 链的时候都是相同的;这就是支持Yifan Lu 的 PS4 没有内核 ASLR 论点的证据!

总结

从已知的信息来看,PS4 的内核与标准的 FreeBSD 9.0 内核非常相似。

重要的是,这些差别看来只是改变了标准的内核配置(比如禁用 SCTP 套接字),而不是修改了代码。索尼还在内核中加入了一些他们自定义的系统调用,但是除此以外,内核的其他部分看来几乎没有被修改。

从这个角度来看,我倾向于 PS4 同样具有绝大多数的 FreeBSD 9.0 内核的好用的漏洞!

不幸的是,由于我们目前被沙盒所限制(就像标准的 FreeBSD jail 一样),大多数内核漏洞不能从 WebKit 入口点被触发。

并且,由于 FreeBSD 10 的推出,不太可能有人再对 FreeBSD 9 的私有漏洞保密,所以除非突然有新的漏洞公布,我们就只能使用已有的那些。

借助某些已知的内核内存泄漏来破解 PS4 内核并非不可能,但是绝对不容易。

从此可知,最好的办法看来是逆向工程全部的可以转储的模块,将尽可能多的索尼的自定义系统调用归档;我预感,比起标准的 FreeBSD 系统调用,我们在这里的运气会更好。

最近 Jaicrab 在 PS4 上找到了两个 UART 端口,说明有一些硬件黑客对 PS4 感兴趣。虽然传统上的硬件黑客的角色是取得系统的内存转储,例如 DSi,而我们已经可以通过 WebKit 漏洞做到这一点,但也有可能找到一个由硬件触发的内核漏洞,例如 geohot 原创的 PS3 hypervisor 破解。最有可能的还是通过系统调用在 PS4 上找到内核漏洞。

致谢
  • flatz
  • SKFU
  • droogie
  • Xerpi
  • bigboss
  • Hunger
  • Takezo
  • Proxima

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?注册

x

精华
0
帖子
533
威望
0 点
积分
585 点
种子
17 点
注册时间
2010-9-9
最后登录
2021-6-18
 楼主| 发表于 2015-12-8 17:10  ·  美国 | 显示全部楼层
本帖最后由 boomer 于 2015-12-8 18:12 编辑

注意到这句话:
FreeBSD 9.0 - Intel SYSRET 内核权限提升漏洞 - 没有用处,因为 PS4 使用 AMD 处理器。

突然发现 AMD 立功了。。。

精华
0
帖子
3334
威望
0 点
积分
3546 点
种子
48 点
注册时间
2009-3-15
最后登录
2024-11-21
发表于 2015-12-8 17:21  ·  广东 | 显示全部楼层
看了眼花,就看个热闹,还是继续玩正。。。。。。。。。。

精华
0
帖子
103
威望
0 点
积分
130 点
种子
101 点
注册时间
2012-10-5
最后登录
2024-11-21
发表于 2015-12-8 17:34  ·  福建 | 显示全部楼层
翻译这么长的篇章也不容易,给LZ点个赞

征服者

穿越者

精华
0
帖子
6036
威望
0 点
积分
6682 点
种子
947 点
注册时间
2010-5-7
最后登录
2024-11-21
发表于 2015-12-8 17:36  ·  北京 | 显示全部楼层
研究这个还不如想想晚上吃什么

求败者

啊啊啊

精华
0
帖子
20113
威望
0 点
积分
20822 点
种子
885 点
注册时间
2011-9-18
最后登录
2024-11-21
发表于 2015-12-8 17:36  ·  北京 来自手机 | 显示全部楼层
太专业了…破解留给需要的人吧

精华
0
帖子
3075
威望
0 点
积分
3118 点
种子
521 点
注册时间
2013-2-21
最后登录
2024-11-21
发表于 2015-12-8 17:44  ·  广东 | 显示全部楼层
第三方app的webkit漏洞还没修复

精华
0
帖子
7101
威望
0 点
积分
7327 点
种子
5 点
注册时间
2010-11-26
最后登录
2019-3-8
发表于 2015-12-8 17:48  ·  上海 | 显示全部楼层
想起了之前修改BOIS达到OEM激活win7的效果的事
现在的我肯定不高兴搞这么麻烦的事

精华
0
帖子
533
威望
0 点
积分
585 点
种子
17 点
注册时间
2010-9-9
最后登录
2021-6-18
 楼主| 发表于 2015-12-8 17:53  ·  美国 | 显示全部楼层
jocover 发表于 2015-12-8 17:44
第三方app的webkit漏洞还没修复

你得看最后…用 root 权限运行浏览器,索尼写软件的绝对是人才

事实上,翻译完整篇文章,我的感想就是,PS4 被破解那是客观规律,不以索尼的意志为转移的
您需要登录后才可以回帖 登录 | 注册

本版积分规则

Archiver|手机版|A9VG电玩部落 川公网安备 51019002005286号

GMT+8, 2024-11-22 00:39 , Processed in 0.216998 second(s), 16 queries , Redis On.

Powered by Discuz! X3.4

Copyright © 2001-2020, Tencent Cloud.

返回顶部