【译】BINDER - ANALYSIS AND EXPLOITATION OF CVE-2020-0041
2019年12月,在Linux内核中推送了新的Binder提交。 此补丁修复了用于处理Binder事务中特定类型的对象的索引的计算。
本文研究了已更正问题的含义,为什么是安全漏洞以及如何利用它。
在阅读本文之前,强烈建议先阅读有关粘合剂内部的文章。
相关补丁和版本
CVE-2020-0041发布于2020年3月的Android安全公告中。
附加到此CVE的修补程序是提交16981742 ,其说明如下:
活页夹:修复num_valid的错误计算 对于BINDER_TYPE_PTR和BINDER_TYPE_FDA交易, num_valid local计算错误,导致 在binder_validate_ptr()中进行范围检查以错过越界 抵消。 修复:bde4a19(“活页夹:使用用户空间指针作为缓冲区空间的基础”)
描述中提到了binder_valid_ptr()
函数out-of-bounds
。 这似乎是安全修复程序!
该错误是通过代码重构(commit bde4a19 )在2019年2月引入的。 实际上,几乎没有设备受到影响,因为大多数供应商都使用旧的内核,并且此漏洞仅影响最新的内核。 据我所知,只有Android 10上的Pixel 4和Pixel 3 / 3a XL受到影响:
- 像素4-内核msm-coral-4.14-android10
- Pixel 3 / 3a XL-内核msm-bonito-4.9-android10
补丁概述
差异--git a / drivers / android / binder.cb / drivers / android / binder.c 索引e9bc9fc..b2dad43 100644 --- a /驱动程序/android/binder.c +++ b / drivers / android / binder.c @@ -3310,7 +3310,7 @@ 活页夹大小 struct binder_fd_array_object * fda = to_binder_fd_array_object(hdr); -size_t num_valid =(buffer_offset-off_start_offset)* + size_t num_valid =(buffer_offset-off_start_offset)/ sizeof(binder_size_t); struct binder_buffer_object *父= binding_validate_ptr(target_proc,t-> buffer, @@ -3384,7 +3384,7 @@ t->缓冲区-> user_data + sg_buf_offset; sg_buf_offset + = ALIGN(bp-> length,sizeof(u64)); -num_valid =(buffer_offset-off_start_offset)* + num_valid =(buffer_offset-off_start_offset)/ sizeof(binder_size_t); ret = binding_fixup_parent(t,线程,bp, off_start_offset,
该修补程序修复了num_valid
索引的计算,该索引在binder_fixup_parent()
的调用中用作参数。 乘号*
被除法/
代替。
当活页夹事务包含活页夹对象时,偏移量列表将给出不同活页夹对象在事务缓冲区中的位置。
活页夹交易的冲销缓冲区
让我们举个例子,如果对象的偏移量为0x10
(上图中的对象BINDER_TYPE_PTR C),则索引的正确值应为0x2 :
//正确的版本 size_t num_valid = (buffer_offset - off_start_offset) / sizeof (binder_size_t); / * 如果(buffer_offset-off_start_offset)= 0x10 num_valid = 0x10 / 0x8 num_valid = 0x2 * /
在易受攻击的版本中,计算得出的值为0x80 。
//版本不正确 size_t num_valid = (buffer_offset - off_start_offset) * sizeof (binder_size_t); / * 如果(buffer_offset-off_start_offset)= 0x10 num_valid = 0x10 * 0x8 num_valid = 0x80 * /
那是完全不同的! 越野车版本允许通过偏移缓冲区(蓝色)发送带有父索引的活页夹对象。
偏移缓冲区越界
函数binder_validate_ptr()
使用num_valid
并检查两件事:
- 如果给定索引(此处为
off_start_offset
)小于num_valid
,则该函数仅信任已处理的对象。 - 在索引(
off_start_offset
)中找到的偏移量处是否存在有效的binder_buffer_object
(使用幻数进行检查)。
//drivers/android/binder.c 静态 整数 bind_fixup_parent ( struct bind_transaction * t, struct binder_thread * 线程 , struct binder_buffer_object * bp, binding_size_t off_start_offset, binding_size_t num_valid, binding_size_t last_fixup_obj_off, 活 页 夹 大小( t last_fixup_min_off) { // [...] 如果 ( ! ( bp- > 标志 和 BINDER_BUFFER_FLAG_HAS_PARENT)) 返回 0 ; 父 = 粘结剂有效期_ptr(target_proc, b 和 对象, bp- > 父, off_start_offset 和 parent_offset, num_valid);
//drivers/android/binder.c 静态 结构 binder_buffer_object * binder_validate_ptr ( struct binder_proc * proc, 结构 binder_buffer * b, struct binder_object * 对象, 活页夹大小 索引 binding_size_t start_offset, 活页夹大小 *对象 偏移量, binding_size_t num_valid) { // [...] 如果 (索引 > = num_valid) 返回 NULL; buffer_offset = 起始偏移量 + sizeof (binder_size_t) * 索引; binding_alloc_copy_from_buffer( & proc- > alloc, & object_offset, b, buffer_offset, sizeof (object_offset)); object_size = binding_get_object(proc, b, object_offset, object); 如果 ( ! object_size || object- > hdr.type != BINDER_TYPE_PTR) 返回 NULL; // [...]
即使父索引存在出界(读访问),也只能访问内存的有限部分,因为内核会验证内存在接收方事务缓冲区中。 此外,在对象的开始处需要一个幻数,因此偏移量必须指向该幻数。 此外,如果魔术不正确,内核不会泄漏该值。
目前,该错误的影响很难看到。 要了解可能的利用,我们需要对Binder中的对象父系统有更好的了解。
有活页夹的父母
活页夹对象BINDER_TYPE_PTR
和BINDER_TYPE_FDA
具有一个字段parent
和parent_offset
,该字段允许在父缓冲区内修补指针。 此功能由HIDL语言(硬件服务)使用,并且在上一则有关资料夹内部的文章中进行了解释。
HIDL_STING示例
hidl_string
结构是使用BINDER_TYPE_PTR
父母的一个很好的例子。
//从system / libhidl / base / include / hidl / HidlSupport.h中提取 struct hidl_memory { // ... 私人的: hidl_handle mHandle __attribute__ ((aligned( 8 ))); uint64_t mSize __attribute__ ((aligned( 8 ))); hidl_string mName __attribute__ ((aligned( 8 ))); };
当进程A hidl_string
进程B发送hidl_string
,该结构包含一个属于进程A的指针。为了使该结构在接收方进程内存中有效,Binder驱动程序必须通过属于该内存空间的指针对其进行更改。这是由BINDER_TYPE_PTR
parent
和parent_offset
字段完成的。
// BINDER_TYPE_PTR的结构 structinder_object_header { __u32 类型; }; structinder_buffer_object { 结构 binder_object_header hdr; __u32 标志; binding_uintptr_t 缓冲区; binding_size_t 长度; binding_size_t 父对象; //父对象的索引(在偏移缓冲区中) 活页夹大小 //偏移以修补父缓冲区 };
为了发送hidl_string
,第一个缓冲区(A)用于发送hidl_memory
C结构,第二个缓冲区(B)用于存储实际字符串(在这种情况下为“我的演示字符串”)。 缓冲区A是B的父级,而A的偏移量0是使用目标过程存储器中字符串的地址修补的。
带有hidl_string的Binder父示例
家长修正规则
一些规则限制了在绑定对象中使用父对象。 当然,在调用此检查之前,绑定程序内核已经检查了指向缓冲区及其大小的指针是否有效(指向调用者内存的指针)。
通过调用binder_fixup_parent()
检查有关父代活页夹对象层次结构的这些规则。
内核应用的规则如下:
通过活页夹检查规则()
- [1]父索引必须小于或等于
num_valid
。 内核已验证num_valid
之前的所有对象。
通过活页夹检查规则()
- [2]仅允许对已验证的最后一个缓冲区对象或其父对象进行修复
- [3]我们仅允许缓冲区内的修正在偏移增加时发生
对于本文的下一部分,这些以前的规则已由[1]到[3]的数字标识。
规则检查示例(有效)
为了验证事务的所有绑定程序对象,内核会按照它们在偏移缓冲区中注册的顺序检查它们。 请记住,此缓冲区包含一个偏移量列表,其中活页夹对象存储在事务数据缓冲区中。
有效的活页夹父母
此示例有效,并遵守所有规则。
规则检查示例(无效1)
无效的父级偏移
该示例无效,因为它违反了规则[3]:
我们只允许缓冲区内的修正以增加的偏移量发生
规则检查示例(无效2)
在下图中,内核检查与对象D对应的偏移量时,验证失败。
无效的父母
该示例无效,因为它违反了规则[2]:
仅允许对最后一个经过验证的缓冲区对象或其父对象进行修复
在我们的例子中,最后一个验证的对象是C ,而对象D的父对象是B。 但是B不是C或A (C的父母)。 层次结构无效。
如何利用漏洞?
利用此错误的一种有趣方法是使父缓冲区的字段buffer
和length
具有任意值。
该错误允许轻松绕过规则[1],但是很难绕过规则[3]。
开发理念
诀窍是在验证过程中更改父级层次结构。 这可以使用extra
缓冲区来完成! 确实,内核使用缓冲区的这一部分来存储与BINDER_TYPE_PTR
对象有关的数据。 如果活页夹对象的父索引指向该多余部分,则当内核在此位置复制数据时,其父对象将被更改。
内核验证-初始配置
要执行此漏洞利用,需要具有以下层次结构的三个缓冲区对象(添加一个未在偏移列表中注册的缓冲区):
对象D包含任意数据。
我们在计算num_valid
使用此漏洞为对象B和C设置相同的父对象,并设置其父索引引用额外的数据部分(紫色区域)。 在验证之前,多余的部分未初始化,并且包含以前的活页夹交易记录的数据。 这些数据可以通过发送事务并使用所需的offset A
值向缓冲区喷洒来控制。
内核验证-选中C时暂停
内核从offset A
开始检查偏移量列表中包含的对象。 直到C的所有对象都是有效的。 每次处理对象时,都会如下图所示在多余部分中复制其相关缓冲区。
该图显示了算法的状态。 内核已经检查了缓冲区A和B ,但未检查对象C。 此时, parent_index
的偏移量值尚未修改(设置为与offset A
对应的值),并且最后一个验证的对象为B。
内核验证-对象C
活页夹驱动程序处理对象C时,它首先在多余的部分复制其缓冲区。 但是,此副本将覆盖parent_index
的先前值。 准备缓冲器C的数据以用与对象D相对应的新值来改变该值。
此时,父母的等级会发生变化!
遵守所有规则! 实际上, C的父对象(此处为D )必须是最后一个经过验证的对象或其父对象之一。
使用此配置,我们已绕过了binder_validate_ptr()
和binder_validate_fixup()
内核验证-父修补
一旦对象C通过所有检查,内核便通过调用函数binder_alloc_copy_to_buffer()
修补父缓冲区。
//提取binder.c 静态 诠释 粘结剂_固定up_父母 (...){ //在这里检查规则[...] buffer_offset = bp- > parent_offset + ( uintptr_t ) parent- > 缓冲区 - ( uintptr_t )b- > user_data; binding_alloc_copy_to_buffer( & target_proc- > alloc, b, buffer_offset, & bp- > 缓冲区, sizeof (bp- > 缓冲区));
请记住,执行此代码时,不会映射目标进程。
所有/ dev / binder设备都可以在内核内存中访问。 用户界面进程映射了活页夹文件描述符时。 内核分配页面(在此处使用kzalloc),并将这些页面映射到进程内存中。
在绑定程序事务期间,内核可以通过在接收方进程的内存地址上应用偏移量来检索此分配的内核地址。 在正常调用中, parent->buffer
的值属于目标进程的/dev/binder
内存,因为该值先前是在处理父对象时由内核修补的。 驱动程序可以通过以下计算获得相应的内核地址:
kernel_proc_buffer = 父 -> 缓冲区 -b- > user_data
使用我们的漏洞,我们可以部分控制kernel_proc_buffer
因为b->user_data
是未知的。
写入父缓冲区的值(加上偏移量)是当前对象的地址(在我们的示例中,是目标进程内存中对象C的地址:另外)
不幸的是,对于我们的利用而言,函数binder_alloc_copy_to_buffer()
对要修补的地址执行了附加检查。
//在drivers / android / binder_alloc.c中 int binding_alloc_copy_to_buffer ( struct binder_alloc * alloc, struct binder_buffer * 缓冲区, binding_size_t buffer_offset, 无效 * src, size_t 字节) { return binding_alloc_do_buffer_copy(alloc, true, buffer, buffer_offset, src, 字节); } 静态 整数 bind_alloc_do_buffer_copy ( struct binder_alloc * alloc, bool to_buffer, struct binder_buffer * 缓冲区, binding_size_t buffer_offset, 无效 * ptr, size_t 字节) { / *所有副本必须对齐32位且大小为32位* / BUG_ON( ! check_buffer(分配, 缓冲区, buffer_offset, 字节)); // [...]
函数binder_alloc_do_buffer_copy()
检查要修补的缓冲区是否在当前绑定程序事务的当前接收缓冲区内。
此漏洞利用不允许针对内核内存 。
可以注意到,如果地址无效,内核将调用BUG_ON
,这将停止内核执行。
通过绕过所有检查,我们可以将任意值设置为parent->buffer
但是我们只能尝试一次,否则内核将停止! 我们需要内存泄漏才能知道目标进程中/dev/binder
的地址。
为了验证该理论,让我们来看一下所描述的利用工作。 由于尚未发生泄漏,因此我们在Android仿真器中运行了经过修改的内核。
静态 void binder_alloc_do_buffer_copy ( struct binder_alloc * alloc, bool to_buffer, struct binder_buffer * 缓冲区, binding_size_t buffer_offset, 无效 * ptr, size_t 字节) { 如果 ( ! check_buffer(alloc, buffer, buffer_offset, bytes)){ size_t buffer_size = binder_alloc_buffer_size(alloc, buffer); pr_info( “ [JB] check_buffer buffer_size:0x%lx字节= 0x%lx偏移量= 0x%lx \ n ” , buffer_size, 字节, buffer_offset); } / *所有副本必须对齐32位且大小为32位* / BUG_ON( ! check_buffer(分配, 缓冲区, buffer_offset, 字节));
添加了调试打印(调用pr_info()
,以检查buffer_offset
值是否无效)
POC
自定义内核(基于msm-bonito-4.9-android10)与Pixel 3a XL固件一起启动。 PoC使用先前图中描述的父层次结构将绑定程序事务发送到servicemanager
器。
。 / 模拟器 -AVD Pixel_3a_XL_API_29_64b- 内核 custom_bzImage- 显示 - 内核 - 否 - 窗口 - 详细 - 随机 - 否 - 快照
[148.291702]活页夹:3410:3410 ioctl c0306201 7fff98cb5f20返回-22 [148.295022] binding_alloc:[JB] check_buffer buffer_size:0x10e0字节= 0x8偏移量= 0x71829fdc8b8 [148.299460] ------------ [在此处剪切] ------------ [148.301159]内核BUG位于drivers / android / binder_alloc.c:1133! [148.303042]无效的操作码:0000 [#1] PREEMPT SMP NOPTI [148.304537]模块链接在: [148.305422] CPU:0 PID:3410 Comm:poc未受污染4.14.150HELLO +#28 [148.307397]硬件名称:QEMU Standard PC(i440FX + PIIX,1996),BIOS rel-1.11.1-0-g0551a4be2c-prebuilt.qemu-project.org 04/01/2014 [148.311690]任务:0000000086b3eedc task.stack:0000000000a1c204 [148.313730] RIP:0010:binder_alloc_do_buffer_copy + 0x8d / 0x15e [148.315692] RSP:0018:ffffa11501effa48 EFLAGS:00010246 [148.317540] RAX:0000000000000000 RBX:ffff9e98a62079c0 RCX:0000000000000008 [148.320403] RDX:ffff9e98aa0e5dd8 RSI:0000000000000000 RDI:ffff9e98aa0e5da0 [148.323268] RBP:ffffa11501effaa0 R08:0000000000000ff4 R09:0000000000000000 [148.325435] R10:0000000000000000 R11:0000000000000000 R12:0000000000000008 [148.328290] R13:0000071829fdc8b8 R14:ffff9e98aa0e5da0 R15:ffff9e98a62079c0 [148.330194] FS:000000000048d648(0000)GS:ffff9e98bfc00000(0000)knlGS:0000000000000000 [148.331780] CS:0010 DS:0000 ES:0000 CR0:0000000080050033 [148.332740] CR2:00007435311239a0 CR3:0000000010ee2000 CR4:00000000000006b0 [148.333848]通话跟踪: [148.334207] binding_alloc_copy_to_buffer + 0x1a / 0x1c [148.334895] binding_fixup_parent + 0x186 / 0x1ac
调试字符串证明PoC起作用,因为偏移量值无效(0x71829fdc8b8的偏移量很大!)
活页夹分配器: [JB] check_buffer buffer_size : 0x10e0 字节 = 0x8 偏移量 = 0x71829fdc8b8
/ dev / binder的内存映射泄漏
在不知道目标进程的内存映射的情况下,此PoC几乎是没有用的:(。但是,没有丢失任何信息!
Android Java应用程序具有特定性,它们都是Zygote
或Zygote64
(取决于32/64位)。
Zygote
是带有预初始化的Java虚拟机的进程。 当系统需要启动新的Java应用程序时, Zygote
被派生并开始执行该应用程序。 这种设计可以减少初始化步骤。 Java应用程序可以尽快启动。 但是,在执行对fork()
的调用时,虚拟内存将被克隆,因此其内存映射也将被克隆。 因此,Zygote的所有孩子都共享相同的映射。
让我们检查一下模拟器:
根1612 1 4758476 190144 poll_schedule_timeout 0 S zygote64 ... u0_a103 3891 1612 4927284 124964 SyS_epoll_wait 0 S com.foo.mypoc
cat / proc / $(pidof com.foo.mypoc)/ maps | grep“ / dev / binder” 7a6242192000-7a6242290000 r--p 00000000 00:12 7315 / dev
假设我们可以将任意代码作为com.foo.mypoc
包执行,通过检查进程内存映射,可以找到/dev/binder
的映射位置。 在我们的情况下,它映射为0x7a6242192000 。
进程com.foo.mypoc
是从zygote64
分叉的。 其他与同一个母亲一起的过程如下:
generic_x86_64:/#ps -e | grep $(pidof zygote64) 根1612 1 ... zygote64 系统1845 1612 ... system_server u0_a89 1996 1612 ... com.android.systemui network_stack 2118 1612 ... com.android.networkstack radio 2199 1612 ... com.android.phone 系统2210 1612 ... com.android.settings u0_a55 2261 1612 ... android.ext.services u0_a84 2296 1612 ... com.android.launcher3 u0_a102 2321 1612 ... com.android.inputmethod.latin u0_a87 2436 1612 ... com.android.dialer u0_a37 2465 1612 ... android.process.acore secure_element 2553 1612 ... com.android.se 无线电2586 1612 ... com.android.ims.rcsservice 系统2626 1612 ... com.android.emulator.multidisplay u0_a77 2686 1612 ... com.android.smspush u0_a67 2705 1612 ... com.android.printspooler u0_a40 2787 1612 ... android.process.media u0_a97 2884 1612 ... com.android.email u0_a78 2947 1612 ... com.android.messaging u0_a81 2971 1612 ... com.android.onetimeinitializer u0_a52 3005 1612 ... com.android.packageinstaller u0_a54 3027 1612 ... com.android.permissioncontroller u0_a39 3050 1612 ... com.android.providers.calendar u0_a62 3075 1612 ... com.android.traceur u0_a41 3097 1612 ... com.android.externalstorage 系统3134 1612 ... com.android.localtransport 系统3230 1612 ... com.android.keychain u0_a103 3891 1612 ... com.foo.mypoc
软件包com.android.settings
看起来很有趣,因为它作为system
运行。
generic_x86_64:/#cat / proc / $(pidof com.android.settings)/ maps | grep“ / dev / binder” 7a6242192000-7a6242290000 r--p 00000000 00:12 7315
实际上,绑定器设备与我们的应用程序com.foo.mypoc
映射在同一位置!
开发思路
使用先前的PoC和目标进程的内存映射,可以覆盖与已验证的活页夹对象有关的数据。
Userland绑定程序库( libbinder.so
和libhwbinder.so
)信任绑定程序驱动程序处理,并认为所有绑定程序对象均已正确打补丁。 如果修补程序未正确完成,则在宗地反序列化步骤中,应用程序可能很容易受到攻击。
文件描述符
覆盖BINDER_TYPE_FDA
修补的对象,使其指向BINDER_TYPE_FDA
且未经检查的文件描述符列表。 我们可以想象在目标进程中关闭任意文件描述符,以将其替换为受控文件描述符。
活页夹缓冲区
覆盖BINDER_TYPE_PTR
对象的大小。 如果使用该漏洞更改了嵌入式缓冲区结构(如hidl_string
)的大小字段,则新值将是一个指针,并且对于正确的缓冲区无效。
详细信息 :: hidl_pointer < const char > mBuffer; uint32_t mSize; //尝试覆盖大小 bool mOwnsBuffer;
活页夹/句柄对象
覆盖BINDER_TYPE_HANDLE
/ BINDER_TYPE_WEAK_HANDLE
对象的指针。 当绑定程序内核模块处理这些对象时,它将在远程进程中用其原始指针值替换处理程序。 内核在其内存中保留处理程序/指针之间的映射,并使用此表修复BINDER_TYPE_HANDLE
或BINDER_TYPE_BINDER
。 有时,远程服务需要实例化对象以执行命令。 要使用此对象,客户端将发送一个对象句柄( BINDER_TYPE_HANDLE
)。 内核将其替换为BINDER_TYPE_BINDER
,该对象在目标接收缓冲区中包含实际指针。
如果攻击者使用该漏洞替换了BINDER_TYPE_BINDER
对象指针,则他可以控制该对象类型的所有字段以获取代码执行BINDER_TYPE_BINDER
。
正常交易
对象指向受控数据
结论
对这个错误及其利用方法的分析非常有趣且新颖。 它允许与活页夹父母一起玩。
即使此漏洞不允许定向内核内存,也可能导致利用特权更高的应用程序。
本文完成的分析只是利用此漏洞所需的第一步。 将这些原语转换为实际的权限提升漏洞需要大量的工作。
我认为,在将代码添加到Linux内核源代码之前,请仔细检查一下该错误。 我的陈述与我先前对secctx patch进行的补丁分析相同,最近在内核源代码中插入了几个“简单”的错误。
幸运的是,此最新漏洞仅影响了少数设备(Android 10上的Pixel 4和3a)。