Let_go
人生如棋,我愿为卒。行动虽慢,可谁曾见我后退一步。

CVE-2018-9568

2019/03/25 Android_Kernel

CVE-2018-9568


前言

漏洞信息

漏洞原理

漏洞原理

如果把一个IPV6类型的套接字句柄通过setsockopt函数使用IPV6_ADDRFORM选项转换成IPV4套接字,那么该套接字的sk->sk_prot会被改变为tcp_prot,此时套接字的sk->sk_prot和sk->sk_prot_creator就不一样了(sk->sk_prot指向tcp_prot,而sk->sk_prot_creator还是原来的tcpv6_prot,sk_prot_creator指向的proto结构体中的slab变量指向申请当前sk结构体的那块专用高速缓存)。
再使用accept函数去监听上面被转换成IPV4的套接字,底层函数sk_clone_lock会分配一个新的IPV4套接字,使用被监听套接字的sk->sk_prot(tcp_prot)中的slab变量指向的高速缓存申请一个新的sock结构体变量sk,再把被监听套接字的所有内容都拷贝到新的sock结构体变量sk中,由于这属于完全拷贝导致新套接字的sk->sk_prot_creator和被监听套接字的sk->sk_prot_creator一致(都指向tcpv6_prot),并且后面的代码也没有把新套接字的sk->sk_prot_createor更新为创建sock结构体时使用的sk_prot,后续关闭新套接字调用sk_prot_free函数释放sock结构体时使用的是sk_prot_creator指向的slab(也就是tcpv6_prot的slab,但实际应该使用tcp_prot的slab),导致释放这个sock结构体时使用的高速缓存不是申请这个结构体时使用的高速缓存,存在类型混淆的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* sk->sk_prot指向的结构体原型 */
struct proto {
/*--------------------------skip--------------------------*/
int max_header;
bool no_autobind;
struct kmem_cache *slab; /* 指向当前套接字协议对应的高速缓存*/
unsigned int obj_size;
slab_flags_t slab_flags;
unsigned int useroffset; /* Usercopy region offset */
unsigned int usersize; /* Usercopy region size */
struct percpu_counter *orphan_count;
struct request_sock_ops *rsk_prot;
struct timewait_sock_ops *twsk_prot;
/*--------------------------skip--------------------------*/
char name[32]; /* TCP/TCPv6 */
struct list_head node;
int (*diag_destroy)(struct sock *sk, int err);
} __randomize_layout;

大概的触发链如下:
1.首先创建一个IPV6的套接字 底层会创建一个sk结构体,在inet6_create函数中,通过sk_alloc函数调用sk_prot_alloc函数在prot->slab指向的slab中申请,并且初始化sk->sk_prot = sk->sk_prot_creator = prot; 此时使用的是IPV6的初始化

1
socket()->__sys_socket()->sock_create()->__sock_create()->pf->create(__sock_create内部的函数指针)->inet6_create()->sk_alloc()->sk_prot_alloc()->kmem_cache_alloc()

2.把IPV6套接字转换为IPV4套接字,在do_ipv6_setsockopt函数中通过选项IPV6_ADDRFORM,把第一步申请的sk->sk_prot赋值成IPV4的prot,此时该sk的prot和sk_prot_creator已经不一致了,所以可以看出为什么在释放函数kmem_cache_free中传入的是sk_prot_creator,这是为了能够在正确的kmem_cache slab上做释放

1
do_ipv6_setsockopt()->case IPV6_ADDRFORM:

3.用转换的IPV4套接字通过accept生成一个真正的IPV4套接字,在accept底层函数sk_clone_lock中会把监听套接字的sk结构体通过sock_copy拷贝到新的套接字的sk中,因为这里拷贝完成后没有对新套接字sk的sk_prot_creator变量做初始化,实际新套接字的该成员还是指向了监听套接字的sk_prot_creator,而监听套接字sk的sk_prot_creator是属于IPV6的,它的sk实际在sk_prot中的slab中申请的(newsk = sk_prot_alloc(sk->sk_prot, priority, sk->sk_family);)

1
sk_clone_lock()

4.释放真正的IPV4套接字触发kmem_cache_free,错误使用sk->sk_prot_creator在错误的kmem_cache slab上释放sk

1
sk_free()->__sk_free()->sk_prot_free()->kmem_cache_free()->

漏洞造成的影响

该漏洞造成的结果:由于混淆释放使用了错误的偏移值导致受害者page的freelist链表损坏,会出现重复申请的情况,一个sk对象内存被多个套接字描述符重复指向,精心构造逻辑会出现释放后重引用的问题。

漏洞模型

通过对一个结构体对象完全拷贝来生成另一个新结构体变量时,由于一时疏忽忘记对新结构体变量中的某些值做初始化,导致使用新结构体变量时错误的使用了原结构体的值。和CVE-2017-8890有些类似,只是8890没初始化的是内存指针导致了浅拷贝的问题,而这个洞是使用了不该使用的变量导致的类型混淆的问题。

补丁用意

补丁就是在accept底层函数sk_clone_lock拷贝完成之后,对新套接字的sk_prot_creator变量初始化为sk_prot指向正确的高速缓存,避免最后释放时使用错误的缓存进行释放。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Diffstat (limited to 'net/core/sock.c')
-rw-r--r-- net/core/sock.c 2
1 files changed, 2 insertions, 0 deletions
diff --git a/net/core/sock.c b/net/core/sock.c
index 9b7b6bbb2a23..7d55c05f449d 100644
--- a/net/core/sock.c
+++ b/net/core/sock.c
@@ -1654,6 +1654,8 @@ struct sock *sk_clone_lock(const struct sock *sk, const gfp_t priority)
sock_copy(newsk, sk);
+ newsk->sk_prot_creator = sk->sk_prot;
+
/* SANITY */
if (likely(newsk->sk_net_refcnt))
get_net(sock_net(newsk));

漏洞利用

基础知识

Slab分配器

什么是Slab分配器

在linux内核中一般是通过buddy system进行物理内存的分配的,其分配单位是页。之所以引入Slab分配器就是因为buddy system只能按页对齐来分配内存,而大多数情况下内核申请的内存大小不需要一页。如果直接通过buddy system分配内存就会造成大量的内存碎片也就是分配了而没有被用到并且无法再次分配的内存。而Slab却可以满足内核的小内存分配并且不会出现过多的内存碎片,虽然Slab分配比buddy system分配灵活但是Slab分配器还是基于buddy system实现的。

Slab分配器的优点
  • 不会出现过多的内存碎片
  • 内存每次申请内存和释放内存都不会和伙伴系统直接打交道,提高了分配效率
Slab分配器的工作
  • 首先会向伙伴系统申请一块内存
  • 然后Slab分配器将申请的内存分成相同大小的对象(slab)放到高速缓存中(通过struct kmem_cache结构体来描述一个高速缓存)
  • 当内核需要申请内存时通过调用kmalloc函数向Slab分配器申请内存,Slab分配器会查找当前高速缓存中是否有可分配的内存,若没有可分配的内存则向伙伴系统索要新的内存块以供分配,否则直接返回查找到的空闲内存给内核

伙伴系统,Slab管理器和内核之间的关系我感觉就像[供货商(buddySystem)-采购商(Slab管理器)-商铺(高速缓存)-商品(slab对象)-客户(内核kmalloc)]一样。

高速缓存

前面讲了Slab分配器的工作原理提到了高速缓存,那什么是高速缓存呢?
其实高速缓存就是一个数据结构(struct kmem_cache),用来管理Slab分配器从buddy system那申请过来的若干大小相同的对象(object)
内核在系统启动初始化阶段会创建多个通用高速缓存和专用高速缓存,这两种高速缓存管理方式是一致的,只是它们的用途不一样而已。

  • 通用高速缓存:一般用于常规的内存分配,如内核需要申请一块200 byte大小的内存,Slab管理器会在通用高速缓存中匹配和申请的大小最接近的高速缓存,由于对齐问题,只会匹配到空间大于我们申请的大小的缓存块而不会小于,这里我们申请的大小是200那么会匹配到”kmalloc-256”这块高速缓存,并从中分配一个对象返回给内核以供使用。
  • 专用高速缓存:一般用于存放特定的结构体对象如当内核创建一个新任务时,他会从task_struct的专用缓存中获得struct task_struct对象所需要的内存,缓存上一般会有已分配好的并标记为空闲的struct task_struct对象来满足请求。

在linux上一般可以通过读取/proc/slabinfo这个文件来获取当前系统存在的一些高速缓存,不过android上一般没有该文件。

slab申请/释放

在进行漏洞利用之前我们还得来了解一些slab对象的申请流程与释放流程。
首先我们需要了解几个内核结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
struct kmem_cache {
struct kmem_cache_cpu __percpu *cpu_slab; /*一个per cpu变量,对于每个cpu来说,相当于一个本地内存缓存池。当分配内存的时候优先从本地cpu分配内存以保证cache的命中率*/
/* Used for retriving partial slabs etc */
unsigned long flags;
unsigned long min_partial; /*限制struct kmem_cache_node中的partial链表slab的数量。虽说是mini_partial,但是代码的本意告诉我这个变量是kmem_cache_node中partial链表最大slab数量,如果大于这个mini_partial的值,那么多余的slab就会被释放。*/
int size; /*实际分配的大小 size(按对齐的方式分配出来的)*/
int object_size; /*申请时的大小 object size,就是创建kmem_cache时候传递进来的参数。和size的关系就是,size是各种地址对齐之后的大小。因此,size要大于等于object_size。*/
int offset; /* Free pointer offset. */
int cpu_partial; /*per cpu partial中所有slab的free object的数量的最大值,超过这个值就会将所有的slab转移到kmem_cache_node的partial链表。*/
struct kmem_cache_order_objects oo;
/* Allocation and freeing of slabs */
struct kmem_cache_order_objects max;
struct kmem_cache_order_objects min;
gfp_t allocflags; /* gfp flags to use on each alloc */
int refcount; /* Refcount for slab cache destroy */
void (*ctor)(void *);
int inuse; /*object_size按照word对齐之后的大小。*/
int align; /*字节对齐大小。*/
int reserved; /* Reserved bytes at the end of slabs */
const char *name; /* Name (only for display!) */
struct list_head list; /*系统有一个slab_caches链表,所有的slab都会挂入此链表。*/
int red_left_pad; /* Left redzone padding size */
#ifdef CONFIG_SYSFS
struct kobject kobj; /* For sysfs */
#endif
#ifdef CONFIG_MEMCG_KMEM
struct memcg_cache_params memcg_params;
int max_attr_size; /* for propagation, maximum size of a stored attr */
#ifdef CONFIG_SYSFS
struct kset *memcg_kset;
#endif
#endif
#ifdef CONFIG_NUMA
int remote_node_defrag_ratio;
#endif
#ifdef CONFIG_SLAB_FREELIST_RANDOM
unsigned int *random_seq;
#endif
#ifdef CONFIG_KASAN
struct kasan_cache kasan_info;
#endif
struct kmem_cache_node *node[MAX_NUMNODES]; /*slab节点。在NUMA系统中,每个node都有一个struct kmem_cache_node数据结构*/
};

1
2
3
4
5
6
7
8
9
struct kmem_cache_cpu {
void **freelist; /* Pointer to next available object */ /*指向下一个可用的object。*/
unsigned long tid; /* Globally unique transaction id */ /*一个神奇的数字,主要用来同步作用的。*/
struct page *page; /* The slab from which we are allocating */ /*slab内存的page指针。*/
struct page *partial; /* Partially allocated frozen slabs */ /*本地slab partial链表。主要是一部分使用object的slab。*/
#ifdef CONFIG_SLUB_STATS
unsigned stat[NR_SLUB_STAT_ITEMS];
#endif
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct kmem_cache_node {
spinlock_t list_lock;
#ifdef CONFIG_SLAB
struct list_head slabs_partial; /* partial list first, better asm code */
struct list_head slabs_full;
struct list_head slabs_free;
unsigned long free_objects;
unsigned int free_limit;
unsigned int colour_next; /* Per-node cache coloring */
struct array_cache *shared; /* shared per node */
struct alien_cache **alien; /* on other nodes */
unsigned long next_reap; /* updated without locking */
int free_touched; /* updated without locking */
#endif
#ifdef CONFIG_SLUB
unsigned long nr_partial; /*slab节点中slab的数量*/
struct list_head partial; /*slab节点的slab partial链表,和struct kmem_cache_cpu的partial链表功能类似。*/
#ifdef CONFIG_SLUB_DEBUG
atomic_long_t nr_slabs;
atomic_long_t total_objects;
struct list_head full;
#endif
#endif
};
slab申请流程

分配流程如下:优先从cpu本地高速缓存中分配,如果per cpu freelist中没有空闲的内存可供分配了,那么就从per cpu partial链表中分配,如果per cpu partial也没有可以被分配的对象那么继续查看per node partial链表中是否有可供分配的,如果很不幸也没找到可以使用的对象那Slab管理器就从伙伴系统中申请一个空闲的slab对象链表,并挂入到per cpu freelist中以供内核的申请。
内存申请顺序:per cpu freelist -> per cpu partial -> per node partial

slab释放流程

释放流程如下:如果当前被释放的slab对象所在的page和cpu本地高速缓存的page一致,那么直接通过快释放路径释放到per cpu freelist,否则进入慢释放路径,慢释放路径首先把释放对象释放到该对象所在page的freelist上,然后page的引用计数(inuse)减1,接着判断释放掉当前obj后的slab page是否为empty,如果属于empty slab(inuse为0),那么在满足kmem_cache_node的nr_partial大于kmem_cache的min_partial的情况下,释放该slab page到伙伴系统。否则如果slab对象所属链表状态为full,那么释放之后该slab对象链表就属于partial empty链表,就从full链表中删除并且添加到per cpu partial链表中。

利用思路

前面提到过该漏洞的造成的结果就是触发漏洞时破坏了当时高速缓存的freelist指针,导致当时page空闲的所有slab对象都会被重复分配,也就是一块内存被两个sock对象同时指向,我们可以通过close第一个socket描述符去释放该内存,但是第二个socket描述符对该内存还有引用,这就把类型混淆转换成了释放后重引用问题,我们可以在第一次close之后对释放的内存进行喷射占位,第二次close的时候使用的就是我们前面喷射的数据了,从而达到控制内核执行流的效果。但由于涉及到的对象使用的是专用缓存,所以我们通过常规的堆喷射无法喷射成功,需要先通过堆风水把该对象从专用缓存转为通用缓存,然后再去喷射才能如我们所愿顺利控制内存中的内容。
如何把专用缓存转换为通用缓存呢?
通过申请大量的专用缓存,当Slab管理器发现过多的空闲缓存后会把这些空闲内存释放回伙伴系统,这样就有机会被通用缓存分配到。

Heap Fenshui

大概的一个布局流程

1
2
3
4
5
6
7
8
9
10
11
创建大量的酱油线程,构造full slab
触发漏洞,实现双重分配
释放第一重分配的sock,促使受害者slab对象对应的page的inuse变量为0
new.inuse需要为0
间隔释放酱油线程,构造大量的partial slab
n->nr_partial >= s->min_partial(node结构体的nr_partial的数量必须大于等于keme_cache的min_partial的数量)
系统会把空闲slab对象返回给buddy系统
discard_slab(s, page);
然后通过kmalloc喷射占位原始的sock-A
接着释放sock-B,就会触发UAF
inet_release -> sk->sk_prot->close >>>>>> good

关键姿势

1
2
3
4
5
6
7
8
9
10
11
12
13
14
一个kmem_cache中的一个page能够分配多少个sock变量; ---> 0x12;
keme_cache -> per cpu -> page
每个page 可分配多少个 slab对象(sock变量)
酱油sock的个数; ---> 50 * 12; 一共50组每组12个,这个应该只要是2的倍数就行吧
重复分配的个数; ---> 36; 为什么是36? 一个page最多分配0x12个slab对象,由于会出现重复分配,所以为了把稳直接使用0x12 * 2 = 0x2236
喷射数据的大小 slab->size的值即可
第二重释放时因为sock内存被损坏,所以每释放一次需要检查一下喷射利用成功没,如果成功了 后面的sock就不能释放了不然会出现崩溃情况
喷射的字符串首地址可能和内存中的sock首地址不完全重合,可以通过gef的pattern命令生成随机字符串来定位构造目标位置
因为漏洞能够直接覆盖掉函数指针,并且是由用户进程调用的被修改函数,所以可以直接把修改函数设置为kernel_sock_ioctl函数来修改addr_limit

劫持内核执行流

基于我们喷射成功的情况下,通过close函数去关闭socket描述符会触发以下的函数链:

1
2
3
4
应用层:close()
------------------------------------------------
内核层:inet_release
\__ sk->sk_prot->close

由于sk的内容已经被我们控制了,所以只需要简单布局下内存就能控制住内核的执行流程了。

实现提权

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
bullhead:/data/local/tmp $ ./SwAK
[+] uid:2000 pid:6082
_____________ _______________ _______________ ____ ______ ________.________ ________ ______
\_ ___ \ \ / /\_ _____/ \_____ \ _ \/_ |/ __ \ / __ \ ____// _____/ / __ \
/ \ \/\ Y / | __)_ ______ / ____/ /_\ \| |> < ______ \____ /____ \/ __ \ > <
\ \____\ / | \ /_____/ / \ \_/ \ / -- \ /_____/ / // \ |__\ \/ -- \
_\______ / \___/ /_______ / \_______ \_____ /___\______ / /____//______ /\_____ /\______ /
\/ \/ \/ \/ \/ \/ \/ \/
[+] CreateHoldSock Begin
[+] CreateHoldSock End
[+] MaskSK:0x7b0ba0d000 : Mmap_A:0x7b0ba14000
[+] Exploit Begin
[+] FreeHoldSock Begin
[+] FreeHoldSock End
[+] close succe 18 :
[*] test for r/w selinux_enforcing @ffffffc001b8fa8c success
[*] get root,patch cred && sid
[+] uid:0 pid:6082
bullhead:/data/local/tmp # id
uid=0(root) gid=0(root) groups=0(root),1004(input),1007(log),1011(adb),1015(sdcard_rw),1028(sdcard_r),3001(net_bt_admin),3002(net_bt),3003(inet),3006(net_bw_stats),3009(readproc),3011(uhid) context=u:r:toolbox:s0
bullhead:/data/local/tmp # getprop ro.vendor.build.fingerprint
google/bullhead/bullhead:8.1.0/OPM6.171019.030.B1/4768815:user/release-keys
bullhead:/data/local/tmp #

总结

对Linux内核的好多机制还不够熟悉,很多漏洞都需要结合内核机制才能完成利用,还需要多看看内核源码啊。
该漏洞还可以发挥更大的作用比如绕过一些保护机制,不过还没想到怎么玩o(╥﹏╥)o
有了想法就去验证,不要嫌麻烦偷懒啊。

参考

https://blog.csdn.net/u014089131/article/details/72782624 slub释放/申请
http://blog.chinaunix.net/uid-7494944-id-3833334.html slub/slab中的一些问题 释放/申请
https://awakening-fong.github.io/posts/mm/slub_partial/ slub : node partial和cpu partial
http://blog.chinaunix.net/uid-26859697-id-5512117.html slub分配算法
https://mp.weixin.qq.com/s/3eR3f8RfjCstYMqvPZayHw 图解slub

Author: Let_go

Link: http://github.com/2019/03/25/CVE-2018-9568/

Copyright: All articles in this blog are licensed under CC BY-NC-SA 3.0 unless stating additionally.

< PreviousPost
CVE-2018-12232-And-CVE-2019-8912
NextPost >
CVE-2018-8120
CATALOG
  1. 1. CVE-2018-9568
    1. 1.1. 前言
      1. 1.1.1. 漏洞信息
    2. 1.2. 漏洞原理
      1. 1.2.1. 漏洞原理
      2. 1.2.2. 漏洞造成的影响
      3. 1.2.3. 漏洞模型
      4. 1.2.4. 补丁用意
    3. 1.3. 漏洞利用
      1. 1.3.1. 基础知识
        1. 1.3.1.1. Slab分配器
          1. 1.3.1.1.1. 什么是Slab分配器
          2. 1.3.1.1.2. Slab分配器的优点
          3. 1.3.1.1.3. Slab分配器的工作
        2. 1.3.1.2. 高速缓存
        3. 1.3.1.3. slab申请/释放
          1. 1.3.1.3.1. slab申请流程
          2. 1.3.1.3.2. slab释放流程
      2. 1.3.2. 利用思路
      3. 1.3.3. Heap Fenshui
        1. 1.3.3.1. 关键姿势
      4. 1.3.4. 劫持内核执行流
      5. 1.3.5. 实现提权
    4. 1.4. 总结
    5. 1.5. 参考