索尼PS4破解之用户层代码执行(Part2)

原文:Hacking the PS4, part 2

译者:鸢尾

来源:索尼PS4破解之用户层代码执行

自从我之前写的那篇有关PS4安全发布一来,我又有了一些新的发现。这得益于此我现在能够在WebKit进程中进行代码执行。虽然我还不想发布我的代码执行解决方案,但是我目前使用的PS4-SDK已经开源,如果有兴趣你可以去看看。

代码执行

ROP只是以一种聪明的方法执行现有的加载到内存中的代码,同时在技术上来讲ROP是一种图灵完全性(Turing complete),相对于一些基础测试它更加复杂且不太实用。

在flatz的帮助下,我可以利用ROP来设置内存,这样我便能够写入我自己的代码并执行它。简单来说,这就意味着我可以编译C代码,比如PS4-SDK examples,并将其作为本机的x86_64代码执行。我们仍然是在网络浏览器下进行运行,同样存在与之前文章中提到的限制(像沙盒等)。

注:基于最近发布的LLVM 3.7,如果我们将-target x86_64-scei-ps4指定为clang,那么我们就可以像索尼官方在PS4中使用的编译代码一样进行编译。

WebKit进程限制

在我之前的文章中曾提过,这个网络浏览器实际上是由两个独立的进程构成。其中我们进行劫持代码执行的那一个是核心WebKit进程(例如处理解析HTML和CSS,解码图片,执行JavaScript)

我们可以使用如下代码转储所有内存:

struct memoryRegionInfo info;
struct otherMemoryRegionInfo otherInfo;
void *m = NULL;
int i;
// Iterate over first 107 memory mappings
for(i = 0; i < 107; i++) {
    // Find base of next mapping
    getOtherMemoryInfo(m, 1, &otherInfo);
    // Get more info about this mapping
    getMemoryInfo(otherInfo.base, &info);
    // If readable, dump it
    if(info.flags & PROT_CPU_READ) {
        sceNetSend(sock, info.base, info.end - info.base, 0);
    }
    m = info.end;
}

在这个转储中,你可能无法通过其他进程找到诸如”Options”, “Close Window”, “Refresh”,或者”There is not enough free system memory”字符串。

图形

主要的影响非常明确:如果其他的进程处理显示图形,我们无法轻易的劫持活跃的libSceVideoOut

我与xerpi一直在尝试重新初始化libSceVideoOut,尽管所有的函数都返回了一个让我们满意的值,我们依旧无法从浏览器视图中获取屏幕改变。

为了确认我们的进程无法访问任何现有由其他进程创建的视频处理,我们尝试brute forcing所有正整数来验证是否都是有效的。

Brute forcing代码执行

Brute forcingROP框架非常不现实。我依靠对每个测试重定向之后,以及exploit不是100%有效,brute forcer可能会被卡住大概1分钟时间。

我们可以使用sockets追踪来自PC的远程进程,如果接收到一个无效的处理或者0,sceVideoOutWaitVblank会返回一个错误,如果其接收到一个正确的处理:

int i;  for(i = 0; i < 0x7FFFFFFF; i++) {  if(!sceVideoOutWaitVblank(i)) return i;  if(i % 0x10000 == 0) debug(sock, "At %08x\n", i);  } sceNetSocketClose(sock);  return 0;

运行之后它会返回0,确认我们的进程没有访问其他进程的视频处理。

画布

如果我们创建一个HTML5 canvas标签并填充单个颜色,我们可以找到在RAM中的framebuffer地址,并从本机代码中创建一个新线程进行渲染,舍弃原线程正常更新canvas。

在PS4-SDK我添加了一个例子,有心的朋友可以看看。

如果canvas的分辨率过高,是很难找到它的地址的,且通常刷新率很低。然而,我们可以将一张低分辨率图片延展全屏,这样可以完美解决:

var body = document.getElementsByTagName("body")[0];
// Create canvas
var canvas = document.createElement("canvas");
canvas.id = "canvas";
canvas.width = 160;
canvas.height = 144;
canvas.style.zIndex = 1;
canvas.style.position = "absolute";
canvas.style.border = "1px solid";
// Centered
//canvas.style.left = ((window.screen.width - canvas.width) / 2).toString() + "px";
//canvas.style.top = ((window.screen.height - canvas.height) / 2).toString() + "px";
// Fullscreen
canvas.style.left = "0px";
canvas.style.top = "0px";
canvas.style.width = "100%";
canvas.style.height = "100%";
body.appendChild(canvas);

在创建画布之前,你一定很想把其他元素都删除掉,来提高一丝丝的效率:

while(body.firstChild) { 
    body.removeChild(body.firstChild);  
}

最后你会想隐藏光标:

document.body.style.cursor = "none";

控制器

libScePad模块类似libSceVideoOut,但不是由我们的进程进行控制,所以我们无法让其工作。

除非你事先就调用了scePadInit,不然调用scePadOpen会返回一个错误。这里我们得知每个单独的进程模块都有属于他们自己的内部状态,并且我们的进程无法使用libScePad(因为其还没有初始化)

所以,和图形一样我们不能劫持任何已经打开的处理,也无法创建新的处理。或许我们也无法从控制器中读取内容,因为它已经在使用了。那么我们就尝试从第二个控制器中读取内容,但不幸的是,我只有一个控制器所以无法进行测试了。

对此有两个解决方案:使用USB库接收来自第三方控制器的输入,或仅仅使用WIFI兼容设备以及按钮通过一个UDP socket发送输出,我选择使用任天堂DS无线版本。

USB闪存

当你将USB插入PS4,新设备会显示在/dev/: ugen0.4显示第一个位置,ugen0.5显示第二个位置。

不幸的是,由于mount系统调用(类似于nmount变化)一直返回1,EPERM,所以我们无法挂载设备。然而,我们可以使用libSceUsbd.sprx模块访问USB闪存,这与libusb十分相似。

以下为libusb代码:

libusb_context *context; 
libusb_init(&context); 
libusb_exit(context);

会转换为libSceUsbd代码:

sceUsbdInit(); 
sceUsbdExit();

这是一个底层库向USB设备直接发送命令,对于我们而言这不太实用。在xerpi的帮助下,我们可以port libusb到PS4,并切能够读取USB闪存设备的原始镜像

同时在未来我们可以port一个完整的FAT实现基于USB的直接命令,现在我仅仅是使用Win32 Disk Imager(类似Linux的dd)写入我的数据作为USB闪存设备的原始镜像。

Cinoop

Cinoop是我之前写的一个GameBoy模拟器。虽然在这里它不是一个最好的GameBoy模拟器,但是我认为他足够有能力port到PS4来显示网络浏览器的代码执行。

src="http://v.qq.com/iframe/player.html?vid=a0176ngalw6&tiny=0&auto=0" allowfullscreen="">

进程相关

我尝试考虑其他进程之间的交互,这样我们的选择就变得更多了,但是还没想到。

fork(2)系统调用是被禁止的,所以我们不能创建新的进程。execve(59)系统调用开着的,在libSceSystemService.sprx中调用了sceSystemServiceLoadExec函数,但是因为文件系统只读限制,我们无法进行测试,并且不能安装USB闪存驱动器。同时还被告知,这里只接受加密的文件。

libc函数getprocname返回一个空字符串。

无法找到任何libprocstat函数。

chroot(61)系统调用同样被进制。

root的困惑

在之前的文章中我提到getlogin返回”root”,同时username可能为root,但我不相信这是我们口中经常谈论的那个root。例如在这个所谓的root用户下,getuid一直返回0。但是实际上正常的root用户是返回1的。

之前我还演示我们的进程运行在一个FreeBSD jail中,这里我不确定进程是否是在root权限下运行。对于FreeBSD用户我不太了解,以及jails是怎么回事。但是我知道索尼弄一个没有root权限的root用户,完全是在嘲讽我们!

加载模块

我们可以从libkernel中使用sceKernelLoadStartModule加载模块:

int libPad = sceKernelLoadStartModule("libScePad.sprx", 0, NULL, 0, 0, 0);

模块加载到内存中后,我们可以读取它的base,size并且像之前那样转储。这个加载模块的方法,事实上你可以在其中调用函数,并在转储中遵循其他类似libc和libkernel模块xrefs。该函数也让我们转储几个使用旧方法会导致分割错误的模块

从函数名寻找函数偏移量

FreeBSD使用系统调用337,kldsym,从其命名的一个动态库中定位函数地址。

在C中,可以这样使用:

struct kld_sym_lookup data; 
data.version = sizeof(struct kld_sym_lookup); 
data.symname = "getpid";  
if(kldsym(libKernel, KLDSYM_LOOKUP, &data) == 0) { 
    printf("%p\n", data.symvalue); 
    printf("%d\n", data.symsize);  
}

然而在PS4内核这个功能被禁用,并且总是返回0x4e, ENOSYS.这是因为索尼创建了自定义系统调用模块,为了替代kldsym我们可以用系统调用591(他们的工作都差不多,而且这个要简单些)这个系统调用是PS4-SDK的基础,一旦我们加载一个模块并获取其处理,我们可以调用已知名称和参数的任何函数。

以下ROP链将获取libkernel中的getpid偏移量:

var result = chain.data; 
var name= chain.data + 8; 
writeString(name, "getpid"); 
chain.syscall("getFunctionAddressByName", 591, LIBKERNEL, name, result); 
chain.execute(function() {
    logAdd(readString(name) + " libkernel offset = 0x" + (getU64from(result) - module_infos[LIBKERNEL].image_base).toString(16));  
});

对于固件版本1.76,它的返回值为0xbbb0,我们可以从libkernel转储中验证该偏移量(20为getpid系统调用数字)

000000000000BBB0getpid          proc near 
000000000000BBB0                 mov     rax, 20  
000000000000BBB7                 mov     r10,rcx 
000000000000BBBAsyscall 
000000000000BBBCjb      short loc_BBBF 
000000000000BBBEretn 
000000000000BBBF ; ---------------------------------------------------------------------------  
000000000000BBBF  
000000000000BBBF loc_BBBF:  
000000000000BBBF                 lea     rcx,sub_DF60 
000000000000BBC6jmp     rcx 
000000000000BBC6 getpid          endp

用其他函数名来试试,你需要使用反汇编程序的字符串视图来查看(或者在十六进制编辑器搜索sce)。你可以看到索尼在许多方法中都留下了一些有用的debug信息。

例如libkernel包含了”verify_header: sceKernelPread failed %x\n”字符串,既然我们已经确认了sceKernelPread函数,我们来猜猜sceKernelPwrite等是否也存在。

不幸的是,sceKernelPread和sceKernelPwrite没啥亮点,他们只是与系统调用相关的普通FreeBSD文件包装器。

在过去的几年索尼一直都在使用他的这种命名方式,你也可以尝试使用一些索尼PSP函数名,对于PS4的模块来说其中有些也是能够使用的。

线程

有趣的是我们创建的一个线程在后台可以继续运行,同时其他应用也在活跃着。为了证明这点,我们可以创建一个在任意超时后启动网络浏览器的线程

src="http://v.qq.com/iframe/player.html?vid=s017633wgds&tiny=0&auto=0" allowfullscreen="">

int (*sceSystemServiceLaunchWebBrowser)(const char *uri, void *);  
void *t(void *n) { 
    sceKernelSleep(10); 
    sceSystemServiceLaunchWebBrowser("http://google.com/", NULL);  
    return NULL;  
}  
int _main(void) { 
    initKernel(); 
    initLibc(); 
    initPthread();  
    int libSceSystemService; 
    loadModule("libSceSystemService.sprx", &libSceSystemService); 
    RESOLVE(libSceSystemService, sceSystemServiceLaunchWebBrowser);  
    ScePthread thread; 
    scePthreadCreate(&thread, NULL, t, NULL, "t");  
    return 0;  
}

读取内存保护

我们可以使用两个索尼自定义的系统调用547和572,来读取一个内存页面的内容(16KB),包括它的保护:

function getStackProtection() {
    var info = chain.data;
    chain.syscall("getMemoryInfo", 547, stack_base, info);
    chain.execute(function() {
        var base = getU64from(info + 0x0);
        var size = getU64from(info + 0x8) - base;
        var protection = getU32from(info + 0x10);
        logAdd("Stack base: 0x" + base.toString(16));
        logAdd("Stack size: 0x" + size.toString(16));
        logAdd("Stack protection: 0x" + protection.toString(16));
    });
}
function getStackName() {
    var info = chain.data;
    chain.syscall("getOtherMemoryInfo", 572, stack_base, 0, info, 0x40);
    chain.execute(function() {
        var base = getU64from(info + 0x0);
        var size = getU64from(info + 0x8) - base;
        var name = readString(info + 0x20);
        logAdd("Stack base: 0x" + base.toString(16));
        logAdd("Stack size: 0x" + size.toString(16));
        logAdd("Stack name: " + name);
    });
}

以上代码向我们展示了堆栈名为”main stack”以及它的保护是3(读和写)

列出所有内存页

由于ASLR(所有内容都是随机的),我们之前很难映射PS4的内存。

幸运的是,现在我们可以绕过它了:如果系统调用572的第二个参数被设置为1,我们指定一个地址(非映射),那么下一个内存页也就被使用了。这就是说,我们可以指定任意地址并找到一个有效的内存页面。例如指定0作为地址,就会告诉我们第一个映射内存页面的内容:

var info = chain.data;
chain.syscall("getOtherMemoryInfo", 572, 0, 1, info, 0x40);
chain.execute(function() {
    var base = getU64from(info + 0x0);
    var size = getU64from(info + 0x8) - base;
    var name = readString(info + 0x20);
    logAdd("First page base: 0x" + base.toString(16));
    logAdd("First page size: 0x" + size.toString(16));
    logAdd("First page name: " + name);
});

使用它我们可以从我们的进程提取一个内存页面访问完整列表:

Name                            Address         Size        Protection
executable                  0x65620000      0x4000      0x5
executable                  0x65624000      0x4000      0x3
anon:000819401c98           0x200578000     0x4000      0x3
anon:00081baf2243           0x20057c000     0x8000      0x3
anon:00081add693a           0x200584000     0x8000      0x3
anon:00081baf22d6           0x20058c000     0x8000      0x3
anon:00081add739e           0x200594000     0x100000    0x3
anon:00081add6ad2           0x200694000     0x8000      0x3
anon:00081add6ad2           0x20069c000     0x8000      0x3
anon:000815405218           0x2006a4000     0x4000      0x3
anon:00081ac4f19e           0x2006a8000     0x8000      0x3
anon:00081add739e           0x2006b0000     0x100000    0x3
anon:00081ba08107           0x2007b0000     0x4000      0x3
anon:00081ad834f7           0x2007b4000     0x4000      0x1
anon:00081add739e           0x2007b8000     0x300000    0x3
stack guard                 0x7ef788000     0x4000      0x0
JavaScriptCore::BlockFree       0x7ef78c000     0x10000     0x3
stack guard                     0x7ef79c000     0x4000      0x0
RscHdlMan:Worker                0x7ef7a0000     0x10000     0x3
stack guard                     0x7ef7b0000     0x4000      0x0
SceWebReceiveQueue              0x7ef7b4000     0x10000     0x3
stack guard                     0x7ef7c4000     0x4000      0x0
SceFastMalloc                   0x7ef7c8000     0x10000     0x3
stack guard                     0x7ef7d8000     0x4000      0x0
sceVideoCoreServerIFThread      0x7ef7dc000     0x10000     0x3
(NoName)WebProcess.self         0x7ef7ec000     0x4000      0x0
main stack                      0x7ef7f0000     0x200000    0x3
                                0x7ef9f0000     0x4000      0x5
libSceRtc.sprx                  0x802ccc000     0x4000      0x5
libSceRtc.sprx                  0x802cd0000     0x4000      0x3
libSceSystemService.sprx        0x803468000     0x14000     0x5
libSceSystemService.sprx        0x80347c000     0x4000      0x3
libSceSystemService.sprx        0x803480000     0x8000      0x3
libSceSysmodule.sprx            0x8049bc000     0x4000      0x5
libSceSysmodule.sprx            0x8049c0000     0x4000      0x3
libkernel.sprx                  0x808774000     0x34000     0x5
libkernel.sprx                  0x8087a8000     0x2c000     0x3
libSceRegMgr.sprx               0x80a520000     0x4000      0x5
libSceRegMgr.sprx               0x80a524000     0x4000      0x3
libSceSsl.sprx                  0x80d1c0000     0x48000     0x5
libSceSsl.sprx                  0x80d208000     0x8000      0x3
libSceOrbisCompat.sprx          0x80f648000     0x15c000    0x5
libSceOrbisCompat.sprx          0x80f7a4000     0x38000     0x3
libSceOrbisCompat.sprx          0x80f7dc000     0x4000      0x3
libSceLibcInternal.sprx         0x8130dc000     0xd0000     0x5
libSceLibcInternal.sprx         0x8131ac000     0x8000      0x3
libSceLibcInternal.sprx         0x8131b4000     0x18000     0x3
libScePigletv2VSH.sprx          0x815404000     0x74000     0x5
libScePigletv2VSH.sprx          0x815478000     0x2c000     0x3
libSceVideoCoreServerInterface. 0x819400000     0xc000      0x5
libSceVideoCoreServerInterface. 0x81940c000     0x4000      0x3
libSceWebKit2.sprx              0x81ac44000     0x2414000   0x5
libSceWebKit2.sprx              0x81d058000     0x148000    0x3
libSceWebKit2.sprx              0x81d1a0000     0xbc000     0x3
libSceIpmi.sprx                 0x81da60000     0x14000     0x5
libSceIpmi.sprx                 0x81da74000     0x14000     0x3
libSceMbus.sprx                 0x8288a0000     0x8000      0x5
libSceMbus.sprx                 0x8288a8000     0x4000      0x3
libSceCompositeExt.sprx         0x829970000     0x8000      0x5
libSceCompositeExt.sprx         0x829978000     0x44000     0x3
libSceNet.sprx                  0x82ccdc000     0x1c000     0x5
libSceNet.sprx                  0x82ccf8000     0x14000     0x3
libSceNetCtl.sprx               0x833f1c000     0x8000      0x5
libSceNetCtl.sprx               0x833f24000     0x4000      0x3
libScePad.sprx                  0x835958000     0x8000      0x5
libScePad.sprx                  0x835960000     0x8000      0x3
libSceVideoOut.sprx             0x83afe4000     0xc000      0x5
libSceVideoOut.sprx             0x83aff0000     0x4000      0x3
libSceSysCore.sprx              0x83cdf4000     0x8000      0x5
libSceSysCore.sprx              0x83cdfc000     0x4000      0x3
SceLibcInternalHeap             0x880984000     0x10000     0x3
SceKernelPrimaryTcbTls          0x880994000     0x4000      0x3
SceVideoCoreServerInterface     0x880998000     0x4000      0x3
SceLibcInternalHeap             0x88099c000     0xc0000     0x3
SceLibcInternalHeap             0x880a5c000     0x20000     0x3
SceLibcInternalHeap             0x880a7c000     0x490000    0x3
SceLibcInternalHeap             0x880f0c000     0x470000    0x3
anon:00080f64a807               0x912000000     0x100000    0x3
anon:00080f64a98d               0x912100000     0x10000000  0x3
anon:00080f64aaa5               0x922100000     0x4000000   0x5
CompositorClient                0x1100000000    0x200000    0x33
CompositorClient                0x1100200000    0x200000    0x33
CompositorClient                0x1100400000    0x200000    0x33
CompositorClient                0x1100600000    0x200000    0x33
CompositorClient                0x1180000000    0x200000    0x33
CompositorClient                0x1180200000    0x200000    0x33
CompositorClient                0x1180400000    0x200000    0x33
CompositorClient                0x1180600000    0x200000    0x33
CompositorClient                0x1180800000    0x200000    0x33
CompositorClient                0x1180a00000    0x200000    0x33
CompositorClient                0x1180c00000    0x200000    0x33
CompositorClient                0x1180e00000    0x200000    0x33
CompositorClient                0x1181000000    0x200000    0x33
CompositorClient                0x1181200000    0x200000    0x33
CompositorClient                0x1181400000    0x200000    0x33
CompositorClient                0x1181600000    0x200000    0x33
CompositorClient                0x1181800000    0x200000    0x33
CompositorClient                0x1181a00000    0x200000    0x33
CompositorClient                0x1181c00000    0x200000    0x33
CompositorClient                0x1181e00000    0x200000    0x33
CompositorClient                0x1182000000    0x200000    0x33
CompositorClient                0x1184000000    0x200000    0x33
CompositorClient                0x1186000000    0x200000    0x33
CompositorClient                0x1188000000    0x200000    0x33
CompositorClient                0x118a000000    0x200000    0x33
CompositorClient                0x118c000000    0x200000    0x33
CompositorClient                0x118e000000    0x200000    0x33

CompositorClient一直在0×1100000000,但其他地址每次都不同。

提取的这个列表跟我们的预期完全一致,但CompositorClient映射到0×33,这绝对不是FreeBSD标准内存保护

GPU

由于CPU和GPU共享一个的内存池,索尼在GPU控制器中添加自己的保护标志,使得GPU能够保持FreeBSD标准保护CPU,可以通过逆向libSceGnmDriver模块中找到:

CPU Read - 1
CPU Write - 2
CPU Execute - 4
GPU Execute - 8
GPU Read - 16
GPU Write - 32

CompositorClient被标记为0×33 (1 | 2 | 16 | 32),CPU RW和GPU RW

索尼对GPU的保护处理的非常不错:

// Give GPU read and write access to stack:
chain.syscall("mprotect", 74, stack_base, 16 * 1024 * 1024, 1 | 2 | 16 | 32);
// Give GPU read and execute access to WebKit2 module:
chain.syscall("mprotect", 74, module_infos[WEBKIT2].image_base, 16 * 1024 * 1024, 1 | 4 | 16 | 8);

尝试绕过DEP,失败:

// Give GPU read and execute access to stack:
chain.syscall("mprotect", 74, stack_base, 16 * 1024 * 1024, 1 | 2 | 16 | 8);
// Give GPU read and write access to WebKit2 module:
chain.syscall("mprotect", 74, module_infos[WEBKIT2].image_base, 16 * 1024 * 1024, 1 | 4 | 16 | 32);

注册表

这里有一个名为libSceRegMgr.sprx的模块,它表明索尼向PS4增加了某些类型的注册表系统。

模块中的所有函数包装为系统调用532,之前我们以为是一个wait6;其第一个参数为一条命令。事实上wait6已经覆盖了索尼的一个自定义系统调用,我开始坚信这个系统调用编号与FreeBSD 9.0标准肯定不同。虽然这个模块加载并在网络浏览器中使用,限制我们的进程;所有函数调用返回0×80020001,Sony等效于EPERM

转储文件

最近我给PS4-Playground增加了一个文件浏览器,使用代码执行,文件转储变得更加轻松。也可以单独使用ROP,但是挺麻烦的,需要更多步骤来完成这项任务。通过使用PS4文件浏览器,你可以找到并转储许多有趣的东西,我就转储了/sandboxDir/common/font/DFHEI5-SONY.ttf

如果你想要转储的文件路径是从10个随机字符串(沙盒目录)开始,那你需要注意每次重启PS4这个路径是会改变的。你可以使用ROP链去寻找:

setU64to(chain.data, 11);
chain.syscall("getSandboxDirectory", 602, 0, chain.data + 8, chain.data);
chain.write_rax_ToVariable(0);
chain.execute(function() {
    var name = readString(chain.data + 8);
    logAdd(name);
});

对我来说这是AaQj0xlzjX.对于非常小的文件,你只需读入chain.data。但对于大文件,你需要指定内存,我们可以通过mmap系统调用标准来完成,刷新页面,使用这个链:

chain.syscall("mmap", 477, 0, 0x1000000, 1 | 2, 4096, -1, 0);
chain.write_rax_ToVariable(0);
chain.execute(function() {
    chain.logVariable(0);
});

在本例中,返回的地址是0×200744000.

再次刷新页面,使用这个链读取文件并获取大小,使用你的沙盒目录替换AaQj0xlzjX:

writeString(chain.data, "/AaQj0xlzjX/common/font/DFHEI5-SONY.ttf");
chain.syscall("open", 5, chain.data, 0, 0);
chain.write_rax_ToVariable(0);
chain.read_rdi_FromVariable(0);
chain.syscall("read", 3, undefined, 0x200744000, 0x1000000);
chain.syscall("fstat", 189, undefined, chain.data);
chain.execute(function() {
    chain.logVariable(0);
logAdd("Size: " + getU32from(chain.data + 0x48).toString());
});

现在打开代理或你用来拦截流量的网络工具,我创建了一个名为TCP-Dump的简单C server

刷新页面,使用此链发送缓冲区;使用适当的值替换IP, port, address,以及文件大小

sendBuffer("192.168.0.4", 9023, 0x200744000, 8312744);
chain.execute(function() {
    logAdd("Dumped");
});

使用cookies你可以自动向后续阶段传递信息,但我不会这么做,您还应该注意文件系统是只读的。

保存

保存数据存储在以下位置

/user/home/[userID]/savedata/[titleID]/

例如:

/user/home/10000000/savedata/CUSA00455/FFXIVSYSTEM.bin

我们可以转储这些文件,但他们都是加密的。开发人员不太可能直接处理这个加密,我认为是由libSceSaveData模块来进行处理的。成功加载和初始化这个模块:

int libSave = sceKernelLoadStartModule("libSceSaveData.sprx", 0, NULL, 0, 0, 0);
int (*sceSaveDataInitialize)(void *);
RESOLVE(libSave, sceSaveDataInitialize);
sceSaveDataInitialize(NULL);

尝试挂载或读/写保存数据时会收到一个错误信息。

展开阅读全文
©️2020 CSDN 皮肤主题: 黑客帝国 设计师: 上身试试 返回首页
实付0元
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值