译者:鸢尾
自从我之前写的那篇有关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来显示网络浏览器的代码执行。
进程相关
我尝试考虑其他进程之间的交互,这样我们的选择就变得更多了,但是还没想到。
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的模块来说其中有些也是能够使用的。
线程
有趣的是我们创建的一个线程在后台可以继续运行,同时其他应用也在活跃着。为了证明这点,我们可以创建一个在任意超时后启动网络浏览器的线程
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);
尝试挂载或读/写保存数据时会收到一个错误信息。