一、概述
本文主要讨论在Samsung Android内核中发现的内存损坏漏洞,由于测试设备的原因,我们仅针对Galaxy A50 A505FN的内核进行了研究,暂时没有分析过Samsung其他设备所使用的内核。我们将详细分析该漏洞,并尝试编写这个漏洞的漏洞利用(比较不稳定)。我们还将描述第二个漏洞,该漏洞已经在上游内核、上游稳定版本和Android通用内核中实现修复,但未在Samsung内核中修复,同时也将讨论这个漏洞的利用方式。
如果各位读者想要自行查看相应的源代码,可以从这里下载Samsung A505FN的内核源代码。其中的不同版本似乎已经过排序,最新的版本在列表最靠前的位置。在撰写这篇文章时,最新的内核版本是A505FNXXS3ASK9,对应已经安装了2019年11月的安全补丁。
二、厂商定制的内核修改
在Android上,厂商经常会将针对设备定制的代码添加到内核中,而这些代码也正是安全漏洞的常见来源。Android通过限制特定进程访问设备驱动程序(通常由厂商定制)来减少此类代码对安全性的影响。在如今的Android手机中,是通过专门的帮助进程来访问硬件设备,这些帮助进程形成了硬件抽象层(Hardware Abstraction Layer,HAL)。
在这里,补充说一句。Linux自3.6版本开始,支持安全、直接的硬件访问方式,通过虚拟函数I/O从用户空间访问PCI设备,通过/dev/bus/usb/从用户空间访问USB设备。如果能有更多的OEM使用这些机制来替代自定义驱动程序,将会提升设备的安全性,同时也有助于维护这些驱动程序,因为在这些机制中使用的是稳定的用户空间API,而不是没有保证的内核API。
但遗憾的是,通常情况下,要定位厂商在修改内核功能时新增的攻击面,对于安全研究人员来说往往更加困难。
例如,Samsung内核在凭据结构中添加了额外的“保护”:借助虚拟机管理程序代码(CONFIG_RKP_KDP,用于保护凭据结构)将cred结构设置为只读,并且如果要转换为UID 0,需要针对当前可执行文件的路径(CONFIG_SEC_RESTRICT_SETUID,除允许的进程外限制对于root特权的更改)进行特殊检查。但是,这些修改实际上都不能阻止对内核具有足够控制权的攻击者,通过直接读取或修改用户数据的方法来修改凭据结构。举例来说,攻击者可以:
1、为了直接访问攻击者无法访问的资源:
(1)修改文件系统内部数据结构,以使自己能够访问通常无法访问的inode(后文将详细描述);
(2)直接从内核内存中读取凭据。
2、为了获取对具有特定特权或访问特定数据的进程的控制权(例如:电子邮件应用程序、聊天应用程序、zygote或system_server),由于实际上所有用户数据都可以被至少一个用户空间上下文访问:
(1)通过直接映射(安全人员也称之为“physmap”)修改存在于页面缓存中的用户空间代码;
(2)修改内核中存储的其他用户空间进程的已保存寄存器状态;
(3)修改保存在内核中的用户空间指针,以用来将其写入到用户空间中;
(4)修改内存管理状态,使受害者进程拥有的页面可以被攻击者进程访问。
当然,上面并不是一个详尽的清单。换而言之,Samsung的保护机制将无法为试图入侵用户手机的恶意攻击者提供有意义的保护,它们只能阻止针对Samsung手机定制的直接root工具。我认为,这样的修改是意义不大的,因为:
(1)这些修改导致后续将难以继承上游内核的升级版本,而上游内核的升级是非常频繁的;
(2)这样的修改额外增加了攻击面。
三、Samsung的进程验证器(PROCA)分析
3.1 子系统
在A50版本的Samsung内核中,包含一个额外的安全子系统(称为“PROCA”,是“Process Authenticator”的缩写,其代码位于security/proca/),用于跟踪进程标识。通过将这个子系统中的几个逻辑问题(它们本身可能已经导致跟踪状态与实际进程状态之间不匹配)与一些存在问题的代码模式组合在一起,就有可能通过赢得竞争条件来引发安全漏洞。
PROCA似乎是基于附加到其可执行文件的ASN.1编码签名来跟踪有关进程标识的信息。这些信息可能会提供给管理程序,但这只是我们目前的推测。ASN.1编码的签名是从扩展属性user.pa加载,同时扩展属性security.five也起到一些作用。我认为,PROCA数据结构的偏移量是通过drivers/staging/samsung/sec_gaf_v5.c中的GAFINFO结构暴露给内核外部的,这可能是针对虚拟机管理程序提供的。但如果是这样,我们还没有确定虚拟机管理程序代码是否位于A50上,或者我们假设GAFINFO不包含有关锁创建位置的任何信息,那么还需要了解虚拟机管理程序是如何保护对PROCA数据结构的访问,以防止并发情况的。
在/system和/vendor文件系统中,只有少数文件具有user.pa属性:
/vendor/bin/hw/wpa_supplicant
/vendor/bin/[email protected]
/system/app/SecurityLogAgent/SecurityLogAgent.apk
/system/app/Bluetooth/oat/arm64/Bluetooth.odex
/system/app/Bluetooth/Bluetooth.apk
/system/priv-app/Fast/oat/arm64/Fast.odex
/system/priv-app/Fast/Fast.apk
/system/bin/apk_signer
/system/bin/dex2oat
/system/bin/patchoat
/system/bin/vold
/system/framework/oat/arm64/services.odex
/system/framework/services.jar
从厂商的映像挂载文件系统后,可以使用类似于“getfattr -e base64 -n user.pa system/bin/dex2oat | grep -F 'user.pa=' | sed 's|^user.pa=0s||' | openssl asn1parse”的命令,在Linux主机上对签名进行解码。我们可以在security/proca/proca_certificate.asn1中看到ASN.1的结构。
PROCA的主要数据结构,包括每个进程的proca_task_descr结构,其形式如下:
struct proca_task_descr { struct task_struct * task; /* 0 0x8 */ struct proca_identity { void * certificate; /* 0x8 0x8 */ long unsigned int certificate_size; /* 0x10 0x8 */ struct proca_certificate { char * app_name; /* 0x18 0x8 */ long unsigned int app_name_size; /* 0x20 0x8 */ char * five_signature_hash; /* 0x28 0x8 */ long unsigned int five_signature_hash_size; /* 0x30 0x8 */ } parsed_cert; /* 0x18 0x20 */ struct file * file; /* 0x38 0x8 */ } proca_identity; /* 0x8 0x38 */ struct hlist_node { struct hlist_node * next; /* 0x40 0x8 */ struct hlist_node * * pprev; /* 0x48 0x8 */ } pid_map_node; /* 0x40 0x10 */ struct hlist_node { struct hlist_node * next; /* 0x50 0x8 */ struct hlist_node * * pprev; /* 0x58 0x8 */ } app_name_map_node; /* 0x50 0x10 */ };
针对当前正在执行的每个进程,如果包含user.pa扩展属性,那么都存在proca_task_descr结构的一个实例。
proca_task_descr结构的实例通过g_proca_table进行寻址,而g_proca_table是proca_table结构的全局实例,是一个容器结构,其中包含两个带锁的哈希表:
struct proca_table { unsigned int hash_tables_shift; DECLARE_HASHTABLE(pid_map, PROCA_TASKS_TABLE_SHIFT); spinlock_t pid_map_lock; DECLARE_HASHTABLE(app_name_map, PROCA_TASKS_TABLE_SHIFT); spinlock_t app_name_map_lock; };
尽管内核同时要维护两个哈希表,但它仅在pid_map中执行查询操作,另一个app_name_map未被使用,或者仅用于从管理程序代码中查找。
3.2 两个逻辑漏洞
Pid_map使用数字PID作为查找的关键字(其中PID是内核中表示“每个任务”或“每个线程ID”,而并不是用户空间中表示“每个线程组ID”)。在下面的示例中,已经对映射进行修改:
1、当任务创建一个不共享父级虚拟内存映射(未设置CLONE_VM)的子任务时,five_hook_task_forked()将TASK_FORKED工作条目发送到g_hook_workqueue。在异步处理这个工作条目时,如果存在用于父级PID的proca_task_descr结构,就会为子级PID创建一个副本。内核的哈希表实现允许具有相同键的多个条目,因此,如果表中已经存在子级PID的条目,就会创建另一个条目。
2、当任务经过execve()(更准确地说,是在search_binary_handler()中)时,five_hook_file_processed()收集有关正在执行的二进制文件的一些信息,然后将FILE_PROCESSED工作条目发送到g_hook_workqueue。异步处理这个工作条目时,可以根据pre-execve()状态和新的可执行文件,将其插入到pid_map或从pid_map删除。
3、在释放task_struct结构时,proca_task_free_hook()会同步删除其PID的表条目。
这也就意味着,PROCA子系统的状态很容易与实际的系统状态不同步。这将导致PROCA认为已经执行的节点(search_binary_handler()中的安全挂钩)是在execve()路径中的“不返回节点”之前。在后面的节点中,执行可能会中止,但此时原始可执行文件在运行,但PROCA会认为是新的可执行文件在运行。如果PROCA被用于身份验证判断,那么攻击者可能会通过这种方式来运行自定义代码,从而得到一个具有特权的可执行文件。
除此之外,还有一个我们更加关注的逻辑漏洞。PID在触发proca_task_free_hook()的很早之前就可以重用。在Linux上,一旦任务从Zombie转换为Dead,就可以重新分配任务的PID。但是,只有在对task_struct的所有引用计数都消失之后,才会进行释放(触发挂钩)。尽管大多数行为良好的内核代码只会对其进行短暂的引用,但binder驱动程序会对其进行长时间的引用,这发生在上游代码中的某个位置,出现在Android通用内核树和Samsung内核当前版本中的两处位置。这是PROCA存在的一个缺陷,这表明在经过PROCA身份认证的任务结束后,可能会错误地认为重新使用PID的新进程已经经过身份验证。最危险的是,如果与下面的代码结合利用,将会导致内存安全漏洞风险。
3.3 内核安全漏洞
在proca_table_remove_by_pid()中(位于security/proca/proca_table.c的最下方),有一些可疑的锁定(在操作过程删除并重新获得了一个锁)。经过分析,我们发现这是proca_task_free_hook()用于查找的帮助程序,如果成功查找,将会从pid_map中删除一个条目:
void proca_table_remove_task_descr(struct proca_table *table, struct proca_task_descr *descr) { [...] spin_lock_irqsave(&table->pid_map_lock, irqsave_flags); hash_del(&descr->pid_map_node); spin_unlock_irqrestore(&table->pid_map_lock, irqsave_flags); [... same thing for app_name_map ...] } struct proca_task_descr *proca_table_get_by_pid( struct proca_table *table, pid_t pid) { struct proca_task_descr *descr; struct proca_task_descr *target_task_descr = NULL; unsigned long hash_key; unsigned long irqsave_flags; hash_key = calculate_pid_hash(table, pid); spin_lock_irqsave(&table->pid_map_lock, irqsave_flags); hlist_for_each_entry(descr, &table->pid_map[hash_key], pid_map_node) { if (pid == descr->task->pid) { target_task_descr = descr; break; } } spin_unlock_irqrestore(&table->pid_map_lock, irqsave_flags); return target_task_descr; } struct proca_task_descr *proca_table_remove_by_pid( struct proca_table *table, pid_t pid) { struct proca_task_descr *target_task_descr = NULL; target_task_descr = proca_table_get_by_pid(table, pid); proca_table_remove_task_descr(table, target_task_descr); return target_task_descr; }
如我们所见,proca_table_remove_by_pid()首先在保持pid_map_lock的同时在表中进行查找,寻找具有匹配PID的条目。然后,它获取指向该条目的原始指针(没有增加引用计数器),解锁,然后再次锁定,然后从哈希表中删除元素。只有在保证同一PID不能同时对proca_table_remove_by_pid()进行两次调用时,这个模式才是安全的。但是,只有在删除任务的最后一个引用时,才会调用这个函数,而这一过程可以延迟到已经重新使用这个PID之后。因此,可以针对某个PID并发调用此函数,这时,该漏洞可能导致proca_task_descr的再次释放。在这种情况下,将会对已经释放的proca_task_descr进行以下操作:
1、proca_table_remove_task_descr()中的两个hash_del()调用;
2、在destroy_proca_task_descr()中:
(1)deinit_proca_identity():deinit_proca_certificate()在两个成员上调用kfree(),如果文件不为空则fput(identity->file),kfree(identity->certificate);
(2)在proca_task_descr上执行kfree()。
要尝试对上述漏洞进行利用,我们有几种方案。我们决定将其用于Use-After-Free列表的取消链接(通过hash_del()),而忽略双重释放这一点。
3.4 绕过身份验证
第一步,我们要让PROCA追踪一个攻击者控制的进程。目前看来,这似乎是难以实现的,因为PROCA仅追踪运行包含特定属性的特定二进制文件的进程,大概是基于运行可信代码的逻辑。但是,我们可以利用逻辑漏洞,让PROCA跟踪我们控制的进程。
一种方法是在search_binary_handler()中的安全挂钩后,产生一个执行中止(例如:在查找解释器过程中出现问题),但是在实践中,我们没能找到简单的触发方式。因此,我们决定使用之前发现的第二个逻辑漏洞,在追踪的过程中重用PID:
1、攻击者控制的进程P1创建子进程P2,共享P1的文件描述表;
2、P2打开/dev/binder,导致文件的binder_proc结构保存对P2的引用;
3、P2执行/system/bin/dex2oat --blah,dex2oat是一个文件,允许从带有user.pa扩展属性的APP上下文(以及adb shell上下文)中执行,这将导致PROCA对P2进行追踪;
4、由于dex2oat使用无效参数被调用,因此P2会打印出一些用法信息并退出,进入到Zombie状态;
5、P1通过waitpid()获得P2,P2进入Dead状态。但是,/dev/binder文件(从P1共享的文件描述符表中引用,在P2退出时未关闭)仍然引用P2,因此不会释放P2的task_struct,并且该关联条目没有从pid_map中删除。
6、P1继续生成子进程,直至其中的一个子进程P3具有和P2相同的PID。此时,PROCA会追踪P3,因为它的PID在pid_map中包含一个条目。(如果PROCA设计到虚拟机管理程序代码,考虑到{proca_task_descr}->task仍然指向P2,我们不清楚会发生什么,但是我们无需关注内核代码。)
3.5 基本竞争条件场景
为了引起两个并发的proca_table_remove_by_pid()调用之间的竞争,我们需要让两个具有相同PID的任务同时释放。通常,对任务的最后一次引用来自于结构PID,其生命周期受RCU语义的异常映像。由于我们不希望每次内核决定运行RCU回调时都发生释放,因此我们必须为任务创建不同的引用计数。
说明:PID结构的引用计数不具有RCU语义,但来自正在执行任务的引用则具有RCU语义。这意味着,如果在RCU读端临界区(Read-Side Critical Section),并且没有额外锁定、不增加任何引用计数的情况下,只要我们从同一个RCU读端临界区中的task_struct获得指向它的指针,就可以无条件地增加任何PID的引用计数。这种模式在Linux内核中并不常见。
在binder的binder_thread结构中,存在一个指向任务的引用计数指针。在创建binder_thread时,它会调用引用任务;在通过ioctl(..., BINDER_THREAD_EXIT, ...)销毁它时,则会同步删除任务的引用。一开始,我以为只能在一侧进行竞争:BINDER_THREAD_EXIT仅允许我们释放其PID与调用者的PID匹配的binder_thread实例,因此对于同一PID似乎无法并行调用两次。出于这个原因,在我最初的代码中,我通过关闭binder文件描述符来触发内部的竞争,触发文件的->release()处理程序,该处理程序将工作条目binder_deferred_work发送到全局工作队列中。在处理这个项目后,将删除binder_proc的任务引用。
但实际上,有一种方法可以在竞争的两端都使用BINDER_THREAD_EXIT。如果可以更改任务的->pid指向靠后的某个位置,我们就可以创建其->pid与关联任务(->task->pid)的PID不匹配的binder_thread。这一过程可以通过从非引导线程调用execve()来实现,换而言之就不局限于必须要从进程通过execve()或fork()创建的线程中调用。在这种情况下,调用线程将获得引导线程的->pid。本质上,在Linux中,当不是主线程的线程调用execve()时,该线程首先停止所有同级别线程,然后使用主线程的身份。这一过程是在fs/exec.c的de_thread()中实现的。
因此,要触发竞争,我们需要:
1、让任务A(PID P1)调用BINDER_THREAD_EXIT,以开始释放PID P1的任务。
2、在删除pid_map_lock的同时,以某种方式让线程A的执行停止在竞争窗口的中间。
3、让任务B(PID P2)调用BINDER_THREAD_EXIT,以使用->pid与->task->pid不匹配的binder_thread释放PID P1的任务。在完成这个步骤后,任务A当前正在执行的proca_task_descr就已经释放。
4、将proca_task_descr的内存重新分配为其他受控制的数据。
5、让任务A继续删除已经释放的对象(UAF,然后是双重释放)。
3.6 扩大竞争窗口
为了在任务A位于两个锁定区域之间时创建足够大的竞争窗口,我们可以滥用先占权(Preemption),类似于之前所讨论的mremap()问题。但是,与mremap()问题不同,我们在到达要抢占任务的地方之前,一直持有一个自旋锁(Spinlock)。此时,我们想到一个信息,在CONFIG_PREEMPT系统上,如果调度程序要在该任务持有自旋锁的情况下抢占该任务,则会在任务上设置一个标志,让其在完成自旋锁的区域后立即移出CPU。
由于未锁定的区域非常短,因此我们希望在自旋锁锁定的区域之前,使其尽可能的大,然后尝试在其过程中抢占任务A。在持有自旋锁的过程中,任务A正在进行一个简单的哈希表查找:
spin_lock_irqsave(&table->pid_map_lock, irqsave_flags); hlist_for_each_entry(descr, &table->pid_map[hash_key], pid_map_node) { if (pid == descr->task->pid) { target_task_descr = descr; break; } } spin_unlock_irqrestore(&table->pid_map_lock, irqsave_flags);
在正常情况下,哈希表应该具有一定的恒定时间,但是在最不理想的情况下,它们会成为链表,并具有O(n)的查找时间,而不是近似于O(1)。(这是在28C3使用的hashDoS攻击,重点在于拒绝服务攻击,但也可以用于其他各种形式,包括泄露在查询键中编码的地址,或者扩大竞争窗口。)由于calculate_pid_hash()中没有任何秘密参数,我们可以轻松确定哪些PID将于我们用于竞争的PID属于同一列表桶,并且可以使用大量proca_task_descr实例填充哈希表存储桶,这些实例中截断后的PID哈希会发生冲突,而PID本身则不会发生冲突,从而迫使PID查找过程中遇到大量不匹配的条目。
四、信息泄露漏洞(CVE-2018-17972)
4.1 寻找发生抢占的地方
此时,我们可以重复抢占任务A,其中的一个抢占将直接在spin_unlock_irqrestore()的位置发生。但是,我们需要弄清楚在那个位置发生了哪些抢占事件,以便我们了解应该在什么时候进行竞争。要做到这一点,一种方法是观察调度的延迟,如果我们让哈希桶变得足够大,这种方式应该可行。但是,我们还有一种更好的方法。
在2018年9月,我报告了/proc/$pid/stack借口中存在的一个安全漏洞,并提供了一个补丁将该接口限制为仅root用户可以访问。这个补丁很快就在主代码上进行了应用,以修复上游稳定版本树和Android通用内核。并且,这个漏洞还被分配了编号CVE-2018-17972。但是,在Android生态系统中,在上游的修复并不意味着已经在设备内核中实现修复,至少在我们所分析的Samsung手机中,这一漏洞在2019年11月1日更新的版本中还仍然存在(已在2020年2月的安全更新中实现修复)。
因此,我们仍然可以在PoC中使用/proc/$pid/stack。这个文件允许普通用户转储他们拥有的任意任务的内核栈跟踪,它包含一个符号化的栈跟踪,如下所示(末尾使用0xffffffffffffffff标记):
a50:/ $ cat /proc/$$/stack [] __switch_to+0xbc/0xd8 [] sigsuspend+0x3c/0x74 [] SyS_rt_sigsuspend+0xa0/0xf8 [] __sys_trace+0x68/0x68 [] 0xffffffffffffffff
这表明,每次在我们抢占任务A时,都可以读取它的/proc/$pid/stack,并检查其中是否包含“proca_table_get_by_pid+”。如果包含,就表明我们已经进入到了竞争窗口之中。
尽管利用/proc/$pid/stack可以更加轻松地编写出针对内核竞争条件的漏洞利用程序,但这并不是一个仅仅局限于root上游的安全漏洞,我们将在下一节中详细描述。
4.2 使用用户页面的直接映射绕过PAN
在Linux上,内核的直接映射区域(安全人员也将其称为“physmap”会将几乎所有内存都映射为RW,包括作为普通匿名内存映射的一部分映射到用户空间的页面。如果我们可以在直接映射中找到这样的页面,那么我们将获得一个已知的内核地址,可以在该地址直接从用户空间中任意读写数据。
在/proc/$pid/stack接口中存在的安全漏洞是,它对可能同时运行的任务进行堆栈跟踪,从最后一次在CPU外调度任务时保存的帧指针开始。这意味着,如果堆栈同时发生变化,内核的栈跟踪器最终可能会将随机堆栈数据解释为堆栈帧。更糟糕的是,当堆栈跟踪器无法符号化已保存的指令指针时,它会简单地将原始值打印为十六进制数字。我们可以在存在这一漏洞的Android手机上轻松复现此代码,如下所示:
130|a50:/ $ cat /dev/zero > /dev/null & [1] 8559 a50:/ $ while true; do grep ' 0x' /proc/8559/stack | grep -v 0xffffffffffffffff; done [] 0x285ab42995 [] 0xffffff8009b63b80 [] 0x285ab42995 [] 0x285ab42995 [] 0x285ab42995 [] 0x285ab42995 [] 0x285ab42995 [] 0x285ab42995 [] 0x285ab42995 [] 0x285ab42995 [] 0xffffffc874325280 [] 0xffffffc874325280 [] 0xffffffc874325280 [] 0xffffffc874325280 [] 0x80000000
如果我们可以专门针对例如用户空间拥有页面的虚拟地址进行尝试,这会对漏洞利用的编写非常有帮助。
在花费几天时间弄清楚堆栈框架布局之后,我发现可以通过交替下面的两个系统调用来实现:
1、在不存在的页面上调用getcwd(),并使其在mmap信号量上阻塞。
(1)首先,需要让另一个线程在循环中执行VMA分配和释放,以在mmap信号量上创建竞争。
(2)尝试获取已经在写入模式下获取的信号量时,将计划调用该进程。这时,arch/arm64/kernel/entry.S中的cpu_switch_to()会将被调用者保存的寄存器(包括帧指针)保存到{task}->thread.cpu_context中。
(3)保存的帧指针({task}->thread.cpu_context.fp)指向堆栈帧,该堆栈帧保存的链接寄存器与另一个系统调用期间存储X1的位置相同。
2、使用指向不存在页面的输出指针,调用process_vm_readv()。
(1)页面错误将在copyout()的过程中发生,将完整的寄存器状态保存到异常帧中。
(2)异常帧中保存的X1包含内核地址,内核将任务页面映射到该地址。由于我们可以使用process_vm_readv()控制访问的偏移量,因此我们也可以使用地址的较低12位作为标志,该标志会告诉我们堆栈跟踪操作是否以正确的方式运行,并且是否泄露了页面指针。
有关调用堆栈和帧大小的信息,请参见我们bugtracker中的addr_leak.c。
同样,我们可以使用sched_yield()和pipe-to-pipe splice()泄漏与管道关联的结构文件的地址。该堆栈的布局如下所示:
在这种情况下,由于我们不能在这里指定一个任意的偏移量作为区分目标值的信号,因此我们对两个不同的管道文件执行相同的操作,并过滤掉这两种情况下堆栈跟踪出现的任何数字,从而确定了正确的文件。
五、堆喷射
发生UAF的proca_task_descr实际上是使用kzalloc()进行分配的,因此它必须位于kmalloc-* slabs之中。Slab是根据对象的大小进行选择的,proca_task_descr对应0x60=96字节大小,如果是在X86桌面系统上,该分配位于kmalloc-96 slab之中。但是,在ARM64上不存在这样的slab,最小的slab是kmalloc-128,适用于所有kmalloc()分配,顾名思义,最大为128个字节。
通常,为了通过slab分配器进行喷射分配,需要使用一个原语,该原语分配在对应大小存储桶中的内存块,使用攻击者控制的数据进行填充,然后以某种方式保留对该分配的引用,以保证分配不会立即被释放。但是,根据SLUB分配器的分配模式,我们可以组合两个原语:其中一个分配一些内存,进行填充,然后立即释放。另一个重新分配该内存,并保留对其的引用,同时保留大部分未初始化的空间。我们可以尝试交替调用recvmsg()和控制消息中的Payload,并调用signalfd()。其中,recvmsg()为控制消息分配一个临时缓冲区,将控制消息复制到其中,并释放该缓冲区,然后释放signalfd()将128字节的堆块重新分配为8字节的分配,其余部分保持未初始化的状态。
由于UAF将发生在g_hook_workqueue的proca_task_descr上,我们无法控制其调度,因此我们并不清楚它分配到哪个CPU slab上。最重要的是,由于在创建需要UAF的分配之后,我们还要缓慢地创建虚拟proca_task_descr实例来填充哈希存储桶,因此在proca_task_descr和UAF的分配之间要花费大量时间。我们编写的PoC尝试在所有CPU内核上执行重新分配,以避免丢失每个CPU上的slab,但是这仍然不能非常可靠的工作,但我们没有花费更多时间来详细研究这一点。我们现在不能确定,是这一过程不可靠,还是漏洞利用的其他部分不可靠。
六、实现任意读取/写入
在这里,我们已经具备了可以实现任意内核读取/写入的概念证明所需的一切。
我们可以触发竞争,使用堆喷射重新分配释放的缓冲区。喷射的伪proca_task_descr可以用于对两个任意地址执行链表中的取消链接操作,从而使它们指向对方。在取消链接之后,我们还可以使用指针指向已经泄露地址的管道文件的->private_data成员。同时,也可以使用用户空间拥有的页面的内核映射,该页面的内核地址已经泄漏。这样一来,我们就可以直接、完整地控制这个管道文件的pipe_inode_info结构。通过控制pipe_inode_info指向的pipe_buffer实例,我们可以让管道代码读写任意地址,只要保证它们在直接映射中,并且访问范围不跨越页面边界即可。
至此,要将漏洞利用扩展到完全任意的读/写是相当简单的。我们可以覆盖各种指向内核将返回数据的指针。
七、利用任意读取/写入漏洞
一般情况下,我们到此就可以结束了。然而,任意内核读写表明用户空间可以访问所有用户数据(除非是文件已经加密,并且用户没有输入PIN的情况下),并且内核存在严重的风险。但是,由于Samsung尝试阻止这一漏洞的成功利用,我们认为有必要尝试证明可以利用这种任意内核读/写漏洞来成功访问敏感数据。因此,我编写了一些代码,可以使用任意读/写操作执行dentry缓存中的路径遍历(就像内核在fastpath上进行遍历一样),根据其在文件系统中的路径查找一个inode,然后将其->i_mapping作为攻击者拥有结构文件实例的->f_mapping。PoC利用这一原理,将帐户数据库的内容转储到/data/system_ce/0/accounts_ce.db,其中包含敏感的身份验证令牌。其代码非常简单,并且没有涉及到任何凭据结构。
八、总结
Linux内核代码具有相当重要的意义,对此类代码库进行修改(特别是在未经上游维护人员审计的情况下进行的分叉中)很容易引入细微的问题,即便这些修改是为了实现“安全”功能。
我认为,Samsung所添加的一些自定义功能是不必要的,可以直接删除,而不会产生任何损失。我们无法确定PROCA应该做什么,但是像SEC_RESTRICT_SETUID这样看起来限制已经获得任意内核读/写权限的攻击者的机制,在我看来是徒劳的。实际上,在通用版本中,已经实现了更好的防护方案。
如果要针对特定设备进行内核修改,最理想的方式是在上游实现,或者在用户空间驱动程序中实现,在这些位置它们可以用更安全的编程语言、更安全的沙箱机制来实现,并且不会导致内核版本的更新过程变得复杂。
在这次研究中,我成功利用了一年前修复的信息泄露漏洞,这表明,当前维护Android设备分支的方式存在安全隐患。尽管我在此前批评过一些Linux发行版本没有及时从上游获取补丁,但Android生态系统的整体状况显然要更糟糕。在理想情况下,所有厂商都应该及时、全面地应用上游内核的更新。
本文翻译自:https://googleprojectzero.blogspot.com/2020/02/mitigations-are-attack-surface-too.html如若转载,请注明原文地址: