首页
论坛
课程
招聘
[原创] Linux内核代码审计之CVE-2018-9568(WrongZone)
2021-7-3 15:19 6224

[原创] Linux内核代码审计之CVE-2018-9568(WrongZone)

2021-7-3 15:19
6224

目录

 

之前说过代码审计是漏洞挖掘的一个重要方法,因此本文就尝试用这种方法“挖”出一个知名漏洞。

前言

在很多会议或者文章中,经常能看到大佬分享他们找到的 0day,也有一些原理解释和综述的,但是漏洞挖掘过程,几乎很少有所展现。通常一篇议题的结构就是背景介绍、原理介绍,然后 BOOM 的一下,这里有个漏洞被我发现了,然后是漏洞利用和危害,最后可能秀个视频。对于资深研究人员来说,可以补全自己对某块知识的缺失,但是对于初学者而言,心里一直有个疑问: 没错,这里是漏洞点,但是是怎么想到的?心路历程如何?走了哪些弯路?简而言之,相比于结果,有人更关心的是过程。

漏洞“挖掘”

本文是笔者初学 Linux 内核漏洞时的一篇笔记,遵循(漫谈漏洞挖掘)中提到的学习方法:

  1. 寻找历史漏洞通告等公开信息;
  2. 根据漏洞标题自己去尝试审计对应子系统看能否找到漏洞;
  3. 如果无法找到,再回头看漏洞的细节,思考自己为什么没有能够发现这个问题;
  4. 不断重复,直到说服自己漏洞挖掘不过是时间问题,而不是能力问题。

这里选取的是 CVE-2018-9568,即 WrongZone 漏洞。当然这个漏洞的漏洞利用过程更为精妙,阿里的王勇、360、百度 XLab 都有详细的利用介绍,不过这不是本文的主题,我们只关注如何通过代码审计找出这个漏洞。

 

已知的信息如下:

  1. 该漏洞和 socket 有关
  2. 该漏洞是 TCP4 和 TCP6 socket 的类型混淆漏洞

根据这些信息尝试自己去找到具体的漏洞点。

入口

一开始真的很难,没有方向,不知道从哪里下手,甚至不知道应该看哪个文件。不过冷静想象就知道了,漏洞要触发还是从用户态进来,所以应该从系统调用开始看。

 

系统调用的定义为SYSCALL_DEFINEn(name, type, args...),n是参数的个数,根据socket的定义:

1
int socket(int domain, int type, int protocol);

推测系统调用定义为:

1
SYSCALL_DEFINE3(socket, xxx....)

或者直接用正则表达式搜索:

1
2
3
4
5
6
7
8
9
$ egrep "SYSCALL_DEFINE[[:digit:]]" -n -r net/
net/socket.c:1325:SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol)
net/socket.c:1366:SYSCALL_DEFINE4(socketpair, int, family, int, type, int, protocol,
net/socket.c:1447:SYSCALL_DEFINE3(bind, int, fd, struct sockaddr __user *, umyaddr, int, addrlen)
net/socket.c:1476:SYSCALL_DEFINE2(listen, int, fd, int, backlog)
net/socket.c:1509:SYSCALL_DEFINE4(accept4, int, fd, struct sockaddr __user *, upeer_sockaddr,
net/socket.c:1583:SYSCALL_DEFINE3(accept, int, fd, struct sockaddr __user *, upeer_sockaddr,
net/socket.c:1601:SYSCALL_DEFINE3(connect, int, fd, struct sockaddr __user *, uservaddr,
...

SYSCALL_DEFINEn(socket) 宏展开实际上是定义了一个名为sys_socket的函数。

 

为什么在 net 目录下搜索?因为 socket 和网络相关,显然是是在网络子系统中。还有其他常见的子系统如下:

  • mm: 内存管理子系统
  • kernel: 任务调度、进程管理、用户管理
  • fs: 各种文件系统的实现
  • arch: 处理器架构相关的代码,比如中断向量表,CPU和SOC的定义
  • ...

这个小技巧有助于快速定位某个子系统的入口。

socket子系统

严格来说socket不是一个子系统,网络才是一个子系统。但我这里还是这样称呼它,将其看作是围绕socket数据结构的一系列操作,比如创建、删除、修改,等等。

 

socket内核数据结构为:

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
/**
*  struct socket - general BSD socket
*  @state: socket state (%SS_CONNECTED, etc)
*  @type: socket type (%SOCK_STREAM, etc)
*  @flags: socket flags (%SOCK_ASYNC_NOSPACE, etc)
*  @ops: protocol specific socket operations
*  @file: File back pointer for gc
*  @sk: internal networking protocol agnostic socket representation
*  @wq: wait queue for several uses
*/
struct socket {
  socket_state        state;
 
  kmemcheck_bitfield_begin(type);
  short           type;
  kmemcheck_bitfield_end(type);
 
  unsigned long       flags;
 
  struct socket_wq __rcu  *wq;
 
  struct file     *file;
  struct sock     *sk;
  const struct proto_ops  *ops;
};

注释还比较清楚,值得注意的是这里说sk为内部网络协议的实现。

socket创建

socket创建流程为:

  • sock_create
    • sock_alloc()
      • new_inode_pseudo
        • alloc_inode
    • pf->create(net, sock, protocol, kern)

没看到有创建socket结构体的地方,只看到初始化了inode,然后通过

1
sock = SOCKET_I(inode);

获得socket指针,alloc_inode实现为:

1
2
3
4
5
6
7
8
9
10
static struct inode *alloc_inode(struct super_block *sb)
{
  struct inode *inode;
 
  if (sb->s_op->alloc_inode)
      inode = sb->s_op->alloc_inode(sb);
  else
      inode = kmem_cache_alloc(inode_cachep, GFP_KERNEL);
// ...
}

有时候静态分析不太好确定走的是哪个分支,但根据SOCKET_I宏得知从inode获取sock是根据结构体偏移来的,所以显然inode的分配不单单是从inode_cachep从获取,而是走的上面一个分支,搜索.alloc_inode发现socket.c中果然定义了自己的分配函数sock_alloc_inode,内部通过sock_inode_cachepslub分配socket:

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
static struct inode *sock_alloc_inode(struct super_block *sb)
{
  struct socket_alloc *ei;
  struct socket_wq *wq;
 
  ei = kmem_cache_alloc(sock_inode_cachep, GFP_KERNEL);
  if (!ei)
      return NULL;
  wq = kmalloc(sizeof(*wq), GFP_KERNEL);
  if (!wq) {
      kmem_cache_free(sock_inode_cachep, ei);
      return NULL;
  }
  init_waitqueue_head(&wq->wait);
  wq->fasync_list = NULL;
  wq->flags = 0;
  RCU_INIT_POINTER(ei->socket.wq, wq);
 
  ei->socket.state = SS_UNCONNECTED;
  ei->socket.flags = 0;
  ei->socket.ops = NULL;
  ei->socket.sk = NULL;
  ei->socket.file = NULL;
 
  return &ei->vfs_inode;
}

sock_alloc是不带参数的,所以对于TCP/UDP/ICMP都是一样的过程,因此实际协议的初始化操作应该是在后面,即pf->create(net, sock, protocol, kern),这里的create也是个虚函数,grep搜索其实现:

1
2
3
4
$ grep "\.create" -r -n net/
net/ipv4/af_inet.c:1016:    .create = inet_create,
net/ipv6/af_inet6.c:613:    .create = inet6_create,
...

当然正常的查找方法还是根据代码逻辑,通过family(PF_INET)去定位具体的实现。

Tips: 虽然可以从代码中精确查看初始化的过程,但借助经验或者动态调试可以比较快定位到目标关键点。

sock创建

接下来就是具体协议对应的sock创建过程了,先看ipv4:

  • inet_create(struct net *net, struct socket *sock, int protocol, int kern)
    • sk = sk_alloc(net, PF_INET, GFP_KERNEL, answer_prot, kern)
      • sk = sk_prot_alloc(prot, priority | __GFP_ZERO, family)
        • slab = prot->slab
        • sk = kmem_cache_alloc(slab, priority & ~__GFP_ZERO);

对于ipv6也是一样的,因为sk_alloc定义在net/core/sock.c中,后面的实现已经做了抽象,从代码中也可以看到,实际使用的slab为prot->slab

 

确定prot的过程也有些tricky,inet_create中prot是answer->prot,什么是answer?这里暂时还不清楚,只知道它是通过查找inetsw数组确定的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
  /* The inetsw table contains everything that inet_create needs to
   * build a new socket.
   */
  static struct list_head inetsw[SOCK_MAX];
// inet_create
list_for_each_entry_rcu(answer, &inetsw[sock->type], list) {
           /* Check the non-wild match. */
          if (protocol == answer->protocol) {
              if (protocol != IPPROTO_IP)
                  break;
          } else {
              /* Check for the two wild cases. */
              if (IPPROTO_IP == protocol) {
                  protocol = answer->protocol;
                  break;
              }
              if (IPPROTO_IP == answer->protocol)
                  break;
          }
          err = -EPROTONOSUPPORT;
}

既然如此,就找找inetsw初始化的地方,如下:

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
45
46
47
48
49
50
51
static int __init inet_init(void)
{
  struct inet_protosw *q;
  struct list_head *r;
  int rc = -EINVAL;
 
  sock_skb_cb_check_size(sizeof(struct inet_skb_parm));
 
  rc = proto_register(&tcp_prot, 1);
  if (rc)
      goto out;
 
  rc = proto_register(&udp_prot, 1);
  if (rc)
      goto out_unregister_tcp_proto;
 
  rc = proto_register(&raw_prot, 1);
  if (rc)
      goto out_unregister_udp_proto;
 
  rc = proto_register(&ping_prot, 1);
  if (rc)
      goto out_unregister_raw_proto;
// ...
 
      /* Register the socket-side information for inet_create. */
  for (r = &inetsw[0]; r < &inetsw[SOCK_MAX]; ++r)
      INIT_LIST_HEAD(r);
 
  for (q = inetsw_array; q < &inetsw_array[INETSW_ARRAY_LEN]; ++q)
      inet_register_protosw(q);
 
// ...
      /*
   *  Set the ARP module up
   */
 
  arp_init();
 
  /*
   *  Set the IP module up
   */
 
  ip_init();
 
  /* Setup TCP slab cache for open requests. */
  tcp_init();
 
  /* Setup UDP memory threshold */
  udp_init();
}

inet中初始化tcp、udp、raw、icmp等网络协议。

 

tcp_prot、udp_prot都是全局变量,定义在各自的头文件中。例如tcp4:

1
2
3
4
5
6
7
8
9
10
// net/ipv4/tcp_ipv4.c
struct proto tcp_prot = {
  .name           = "TCP",
  .owner          = THIS_MODULE,
  .close          = tcp_close,
  .connect        = tcp_v4_connect,
  .disconnect     = tcp_disconnect,
  .accept         = inet_csk_accept,
   // ...
}

其中inetsw_array是静态数组,如下:

1
2
3
4
5
6
7
8
9
10
11
12
static struct inet_protosw inetsw_array[] =
{
  {
      .type =       SOCK_STREAM,
      .protocol =   IPPROTO_TCP,
      .prot =       &tcp_prot,
      .ops =