看一看新的Fuchsia操作系统和一些错误

2020-06-09 22:15:56

FUCHSIA是Google开发的一款新操作系统,面向AArch64和x86_64架构。虽然人们对这款操作系统的用途以及将在哪里使用知之甚少,但它的目标似乎是在智能手机上取代Android,在笔记本电脑上取代Chrome OS。

为了获取关于未来可能在数百万设备上运行的操作系统的知识,我们决定快速浏览一下Fuchsia,了解它的内部设计、安全属性、优缺点,并找到攻击它的方法。

当今最常见的内核设计形式是单片内核。例如,Linux和BSD内核都是单片的,而基于Linux的Android和Chrome OS内核也是单片的。

一个单片内核通常非常大,嵌入了所有设备驱动程序和网络堆栈,有数百个系统调用,简而言之,它提供了所有的系统功能。

单片内核的内部设计因内核而异,但总的来说,以下内部结构往往是通用的:

单一内核的一个明显的安全问题是,内部系统组件中的任何漏洞都会影响到整个内核。在上面的模式中,假设USB驱动程序存在可利用的内存损坏:由于驱动程序在内核空间内运行,利用此漏洞的攻击者将获得对所有内核的控制权。

正如其名称所示,微内核是一种被设计成非常小的内核,仅实现有限数量的核心功能,如分类调度、异常处理、内存管理、几个设备驱动程序(如果需要)和几个系统调用。其余的组件被移到用户区域,并且不是内核的一部分。

在这里,VFS层、套接字层、网络堆栈、文件系统和设备驱动程序被移到专用用户进程中的用户空间,这些进程之间通过IPC进行通信。

例如,FTP客户端可能只通过与其他用户进程通信来从网络获取数据并将其存储在USB Key中,而不需要内核的任何干预。内核只保证进程的特权分离和隔离。

这种微内核设计具有有趣的安全属性。再次假设USB驱动程序中存在漏洞;在这种情况下,攻击者将能够控制在用户区域运行的USB驱动程序进程(系统进程6),但随后将绑定到此进程,无法立即使用更广泛的权限运行,无论这些权限是内核权限还是其他进程(例如FTP客户端)的权限。

因此,攻击者必须利用额外的漏洞才能横向移动,与单一内核相比,这是一个强大的安全改进。

紫红色的微核被称为锆石。它是用C++编写的。我们在这里描述了该内核的一些相关内部结构。

该系统被组织成在用户区域中运行的组件。例如,网络堆栈是在用户领域运行的组件。USB驱动程序也是在用户区域运行的组件。

这些组件通过IPC彼此交互,我们在这里不讨论它们的接口。

对组件没有严格的编程语言要求:它们可以用C++、Rust、Go或其他语言编写,并通过IPC进行交互,没有问题。例如,USB驱动程序是用C++编写的,而网络堆栈是用Rust编写的。

当涉及到设备驱动程序时,它们在称为devhost的进程中合并在一起。devhost是一个包含多层驱动程序堆栈的进程。例如:

这里有三个魔鬼。例如,Devhost进程3包含AHCI驱动程序、SATA驱动程序以及MinFS和BlobFS文件系统,所有这些组件都位于同一进程中。

这会削弱分段模型,因为现在几个组件实际上是同一进程的一部分,因此一个组件中的漏洞也会影响进程的其他组件。但是,似乎devhosts的组织方式是一个进程中只能有一个设备堆栈,这通常意味着在同一devhost中不可能同时包含USB驱动程序和SATA驱动程序。因此,细分模式的好处仍然存在。

锆石通过使用CPU的MMU(内存管理单元),以现代操作系统的典型方式保护它的内存和进程的内存:每个进程都有一个地址空间,这个地址空间是由锆石上下文切换的。

然而,与其他操作系统相反,IOMMU(输入输出MMU)在Zircon上扮演着重要的角色:它是由内核编程的,因此每个设备主机进程只能在自己的地址空间上执行DMA操作,而不能在外部执行DMA操作。

因此,对于确保进程隔离而言,IOMMU与MMU一样重要,因为如果没有IOMMU,devhost进程只需对内核页面执行DMA操作并覆盖其内容即可。

此外,在x86上,TSS I/O位图(任务状态段)用于限制对I/O端口的访问,其方式与本文的讨论无关。

zircon通过句柄管理对组件的访问,句柄在Unix上可以被视为文件描述符,或通用访问令牌。

不涉及枯燥的细节,名称空间中的对象基本上是由句柄支持的,并且名称空间中的路径实际上对应于一个句柄。同样,内核不知道任何关于名称空间和irobject的信息,它只知道句柄。名称空间驻留在用户空间中,可以看作是围绕句柄的用户友好的大包装器。

把手有一种,也有一种权利。绝大多数锆石系统依赖句柄来管理访问权限。要访问某些类的syscall,句柄必须是正确类型的,要使用syscall执行特定操作,句柄也必须具有正确的权限。

需要注意的是,与Unix系统相反,内核不理解用户的概念。

一切都归结到句柄的概念上,从安全的角度来看,这是我们最感兴趣的:攻击者通常会试图获得比他们拥有的更好的句柄。

虽然不总是最新的,但官方文件很清楚,没有什么特别需要强调的。它显示需要哪些句柄来执行哪些类的syscall。

在缓解方面,Fuchsia使用ASLR(userland必选)、DEP、SafeStack、ShadowCallStack、AutoVarInit。默认情况下,Zircon内核使用所有这些进行编译。

当谈到安全实践时,可以在Fuchsia代码中注意到很多(全部?)。组件具有关联的单元测试和模糊器。模糊是通过libfuzzer来模糊组件内的内部结构,并通过syzkler来模糊用户暴露的syscall来完成的。也有对ASSAN和UBSAN消毒剂的支持,但是似乎没有MSAN或TSAN支持。

最后,当谈到编程语言时,如前所述,组件可以用C++、Go、Rust编写。可以说,这里使用的最容易出现编程错误的语言是C++。对于C++代码,组件通常会覆盖几个运算符来执行健全性检查;例如,[]运算符(在访问数组时使用)经常会超载,以确保索引在数组范围内,并且不会溢出或下溢。因此,即使在容易出错的语言上,也会主动采取一些安全措施。

紫红色使用微内核,其攻击面受自然限制:入口点少,逻辑不太复杂。

该系统被组织成在用户区域中运行的组件。这带来了良好的分段属性:仅影响组件进程的漏洞。

这些组件实际上可以用诸如Rust这样的安全语言编写,在这些语言中根本不存在几类漏洞。

这些组件有自己的虚拟文件系统,可以将其放入沙箱中,并且完全位于用户端。内核对此一无所知。

对组件和syscall的访问是基于句柄的,句柄充当内核知道的唯一令牌。它们被抽象为名称空间中的对象。

那么,关于富奇西亚的安全问题,我们能说些什么呢?总体而言,它的内核设计本质上比Linux更安全,围绕它的缓解措施和安全实践也比Linux目前采用的更好。

当涉及到设备驱动程序时,devhost在一个进程中组合多个组件的事实稍微削弱了分段模型。

与所有其他主流操作系统相反,直接瞄准Zircon内核似乎相当困难。在系统中面向世界的部分(USB、蓝牙、网络堆栈等)上成功的RCE(远程代码执行)只会让您控制目标组件,但它们在独立的用户进程中运行,而不是在内核中运行。然后,您需要使用您拥有的句柄可以访问的有限数量的syscall将权限提升到内核。总体而言,将其他组件作为目标而不是内核,并将重点放在可以通过IPC与之对话并且您知道其具有有趣句柄的组件上似乎更容易。

为了好玩,我们决定在系统的某些部分做一些漏洞研究,看看我们能在有限的时间内走多远,看看Fuchsia总体上良好的安全属性是否真的兑现了他们的承诺。

将USB设备连接到机器时,作为USB枚举过程的一部分,Fuchsia将从设备获取描述符表。这是由USB Devhost中的一个组件完成的。该组件在处理配置描述符表时实际上存在错误:

//读取配置描述符头以确定大小usb_configuration_description_t config_desc_Header;size_t Actual;status=GetDescriptor(usb_dt_config,config,0,&;config_desc_header,sizeof(Config_Desc_Header),&;Actual);if(status==ZX_OK&;&;Actual!=sizeof(Config_Desc_Header)){status=ZX。}if(status!=ZX_OK){zxlogf(error,";%s:GetDescriptor(USB_DT_CONFIG)FAILED\n";,__func__);return status;}uint16_t config_desc_size=letoh16(config_desc_header。wTotalLength);auto*config_desc=new(&;ac)uint8_t[config_desc_size];如果(!交流。check()){return ZX_ERR_NO_MEMORY;}CONFIG_DESCS_[CONFIG]。Reset(config_desc,config_desc_size);//读取完整配置描述符status=GetDescriptor(USB_DT_CONFIG,config,0,config_desc,config_desc_size,&;Actual);IF(status=ZX_OK&;&;Actual!=config_desc_size){status=ZX_ERR_IO;}if(status!=ZX_OK){zxlogf(。%s:GetDescriptor(USB_DT_CONFIG)失败\n";,__func__);返回状态;}。

让我们看看这里发生了什么事。首先,该组件获取具有固定大小的config_desc_headerstructure;然后,它读取该结构的wTotalLength字段,分配此大小的缓冲区,然后这一次重新获取表,检索全部数据。

在USB堆栈的后面,wTotalLength值被信任为结构的总大小,这在这里是有意义的。

问题是,在第一次获取和第二次获取之间,USB设备可能已经修改了wTotalLength值。事实上,在第二次获取之后,wTotalLength可能大于初始值;在这种情况下,USB堆栈的其余部分仍将信任它,并将执行越界访问。

提醒一下,USB堆栈在用户端运行,而不是在内核中运行,所以它不是一个内核缺陷。

在浏览USB代码时,我们注意到一个函数存在明显的堆栈溢出:

zx_status_t HidDevice::HidDeviceGetReport(hid_report_type_t rpt_type,uint8_t rpt_id,uint8_t*out_report_data,size_t report_count,size_t*out_report_tual){input_report_size_t Need=GetReportSizeById(rpt_id,static_cast<;ReportType>;(Rpt_Type));report_count){return ZX_ERR_BUFFER_TOO_Small;}uint8_t报告[HID_MAX_REPORT_LEN];size_t Actual=0;ZX_STATUS_t STATUS=HIDBUS_。GetReport(rpt_type,rpt_id,report,Required,&;Actual);/*.*/}

简而言之,GetReportSizeById()函数返回以前从USB设备获得的16位值。HID_MAX_REPORT_LEN的值为8192。在这里,对GetReport()的调用可以用USB控制的数据溢出报告数组。

这项功能似乎没有相关的用户可以让它成为USB触发器,所以它是一个有点乏味的漏洞。另请注意,使用SafeStack缓解时,报告数组实际上位于不安全堆栈中,这意味着溢出报告数组将不允许攻击者覆盖返回指令指针。

ResponseT RSP(状态);IF(状态==状态::k拒绝){IF(!RSP。ParseReject(Rsp_Payload)){bt_log(trace,";L2CAP";,";cmd:忽略格式错误的命令拒绝,大小%zu";,rsp_payload。size());return ResponseHandlerAction::kCompleteOutbound Transaction;}return InvokeResponseCallback(&;RSP_CB,std::Move(RSP));}。

使用rsp_payload调用ParseReject()方法,rsp_payload包含任意大小的接收数据包。该方法实现如下:

Bool CommandHandler::Response::ParseReject(const ByteBuffer&;rej_payload_buf){auto&;rej_payload=rej_payload_buf。AS<;CommandRejectPayload>;();Reject_Reason_=static_cast<;RejectReason>;(letoh16(rej_payload.。原因));/*.*/}。

在这里,有效负载突然被视为CommandRejectPayload结构,没有明显的长度检查。这可能是越界访问,但实际上,.as;>;指令会自动执行长度检查:

//使用边界检查将基础缓冲区转换为给定类型。允许缓冲区大于//T。用户负责检查第一个sizeof(T)字节//是否表示T的有效实例。模板<;typeName T>;const T&;as()const{//std::is_trivial_v将更有力地保证缓冲区包含有效的T对象,//但不允许强制转换为具有有用构造函数的类型,这可能会导致//数据编码的//未初始化字段错误/。static_assert(std::is_trivially_copy_v<;T>;,";无法重新解释字节";);ZX_Assert(size()>;=sizeof(T));return*reInterprete_cast<;const T*>;(data());}。

因此,这只是蓝牙组件的拒绝服务(DoS),幸运的是,从利用的角度来看,这不是一个有趣的漏洞。

Status ServiceSearchResponse::Parse(Const ByteBuffer&;buf){/*.*/if(buf.。size()<;(2*sizeof(Uint16_T){bt_log(spew,";sdp";,";包太小,无法解析";);返回状态(HostError::kPacketMalform);}/*.*/size_t read_size=sizeof(Uint16_T);/*.*/uint16_t记录计数=betoh16(buf。视图(READ_SIZE)。AS<;uint16_t>;();read_size+=sizeof(Uint16_T);if((buf.。size()-read_size-sizeof(Uint8_T))<;(sizeof(ServiceHandle)*Record_Count){bt_log(spew,";sdp";,";数据包对于%d个记录";,Records_count);返回状态(HostError::kPacketMalform);}(uint16_t i=0;i<;Records_count;i+。view(read_size+i*sizeof(ServiceHandle));SERVICE_RECORD_HANDLE_LIST_。emplace_back(betoh32(查看。as<;uint32_t>;());}/*.*/}。

这里的bug非常清楚:buf.size()-read_size可以等于零,在这种情况下,整个无符号表达式(buf.size()-read_size-sizeof(Uint8_T))包装并变为正数,这意味着长度检查成功。

然后,代码迭代并执行越界访问.。除了这一次,还使用了一些结构:

const BufferView ByteBuffer::view(size_t pos,size_t size)const{ZX_ASSERT_MSG(pos<;=this->;size(),";无效偏移量(pos=%zu)";,pos);return BufferView(data()+pos,std::min(size,this->;size()-pos));}。

view()方法捕获越界访问。再说一遍,这只是蓝牙组件的ados,一点也不有趣!:';(。

FUHSIA附带了适用于AArch64和x86_64的嵌入式虚拟机管理程序。目前还不完全清楚为什么会出现这个虚拟机管理程序;可能是为了促进迁移到Fuchsia,让来宾Android或Chrome OS系统在VM上运行并执行Android或Chrome OS应用程序。

管理程序在VMcall上实现PVCLOCK服务。有了这项服务,客户内核可以通过以客户物理地址(GPA)为参数执行vmcall指令来询问虚拟机管理程序的时间。虚拟机管理程序将时间结构写入内存中给定的GPA。

但是,vmcall指令在来宾用户中实际上是合法的,并且系统管理程序不会验证该vmcall是否来自来宾内核。因此,来宾用户仅能使用任何GPA执行vmcall,并覆盖来宾内核内存。

这可用于从来宾用户到来宾内核的权限提升。一旦在来宾内核中,攻击者拥有更多可用的虚拟机管理程序接口,就可以从那里研究和利用VM逃逸漏洞。

ZX_THREAD_WRITE_STATE()系统调用允许设置挂起线程的寄存器。只需线程上的一个句柄就可以访问此syscall,并且默认情况下,任何userland程序都允许创建线程,因此我们可以调用此syscall。

其中,此系统调用允许修改编码FPUstate的寄存器,特别是x86上的MXCSR寄存器。这基本上是一个32位寄存器,其保留位应该保持为零。问题是,锆石不允许修改这些保留位。

通过使用ZX_THREAD_WRITE_STATE(),我们可以将MXCSR设置为0xFFFFFFFF,并且当挂起的线程恢复时会引发致命的#GP异常,从而导致内核死机:

当然,这只是一场无法利用的恐慌,但我们正在取得进展:至少我们设法击中了内核。

在x86上,为了从中断或异常返回,需要使用iretq指令。如果试图返回到非规范地址,也就是说,如果返回地址在0x0000800000000000-0xFFFF7FFFFFFFFFF范围内,则此指令将出错(#GP)。

此故障是特殊的:它是通过已加载到gs.base寄存器中的用户域线程本地存储(TLS)接收的,但带有内核的CPL(当前特权级别)。gs.base寄存器基本上是保存指向TLS的SA指针的64位寄存器。

当从中断或异常处理程序返回时,锆石不会验证返回地址是否规范。

zircon不能正确处理iretq生成的错误,并且不会将内核TLS重新存储在#GP处理程序中。

这两者的结合意味着有可能在内核中制造iretq故障,并使用userland TLS执行故障处理程序!

Zircon上的内核TLS是一个x86_ercppu结构,它包含代码执行的有用字段,比如gpf_return_target(如下所示)。

使用ZX_THREAD_WRITE_STATE()更改挂起线程的%RIP寄存器,并在其中放置一个非规范值,如0x00FFFFFFFFFFFFFF。还要将gs.base的值更改为我们将称为FakeTlsAddr的特定值。

当线程恢复时,内核将返回t。

..