Kernel从0开始

VSole2021-12-10 13:42:20

简介

网上一大堆教编译内核的,但很多教程看得特别迷糊。第一次编译内核时,没设置好参数,直接把虚拟机编译炸开了。所以就想着能不能先做个一键获取内核源码和相关vmlinux以及bzImage的脚本,先试试题,后期再深入探究编译内核,加入debug符号,所以就有了这个一键脚本。

这个直接看我的项目就好了,我是直接拖官方的docker,然后把编译所需要的环境都重新安装了一遍,基本可以适配所有环境,安装各个版本的内核,外加调试信息也可以配置。

PIG-007/kernelAll (github.com)

(https://github.com/PIG-007/kernelAll)

前置环境,前置知识啥的在上面已经足够了,如果还是感觉有点迷糊可以再去搜搜其他教程。看雪的钞sir师傅和csdn上的ha1vk师傅就很不错啊,还有安全客上的ERROR404师傅。

钞sir师傅:Ta的论坛 (pediy.com)(https://bbs.pediy.com/user-818602.htm)

ha1vk师傅:kernel- CSDN搜索(https://so.csdn.net/so/search?q=kernel&t=blog&u=seaaseesa)

error404师傅:Kernel Pwn 学习之路(一) - 安全客,安全资讯平台 (anquanke.com)(https://www.anquanke.com/post/id/201043)

这个系列记录新的kernel解析,旨在从源码题目编写,不同内核版本来进行各式各样的出题套路解析和exp的解析。另外内核的pwn基本都是基于某个特定版本的内核来进行模块开发,而出漏洞地方就是这个模块,我们可以借助这个模块来攻破内核,所以我们进行内核pwn的时候,最应该先学习的就是一些简单内核驱动模块的开发。

例子编写

首先最简单和经典的的Hello world。

//注释头 //由于基本都是用下载的内核编译,所以这里的头文件直接放到正常的编译器中可能找不到对应的头文件。//在自己下载的编译好的内核中自己找对应的,然后Makefile中来设置内核源码路径#include #include #include MODULE_LICENSE("Dual BSD/GPL");static int __init hello_init(void){    printk("PIG007:Hello world!");    return 0;}static void __exit hello_exit(void){    printk("PIG007:Bye,world");}module_init(hello_init);module_exit(hello_exit);

1、头文件简介

module.h:包含可装载模块需要的大量符号和函数定义。

init.h:指定初始化模块方面和清除函数。

另外大部分模块还包括moduleparam.h头文件,这样就可以在装载的时候向模块传递参数。而我们常常用的函数_copy_from_user则来自头文件uaccess.h。

2、模块许可证

//注释头 MODULE_LICENSE("Dual BSD/GPL");

这个就是模块许可证,具体有啥用不太清楚,如有大佬恳请告知。可以通过下列命令查询。

grep "MODULE_LICENSE" -B 27 /usr/src/linux-headers-`uname -r`/include/linux/module.h

或者网址Linux内核许可规则 — The Linux Kernel documentation

(https://www.kernel.org/doc/html/latest/translations/zh_CN/process/license-rules.html)

3、模块加载卸载

加载

一般以 __init标识声明,返回值为0表示加载成功,为负数表示加载失败。用来初始化,定义之类的。

static int __init hello_init(void)

在整个模块的最后加上:

module_init(hello_init);

来通过这个init函数加载模块。

卸载

一般以 __exit标识声明,用来释放空间,清除一些东西的。

static void __exit hello_exit(void)

同样的模块最后加上以下代码来卸载。

module_exit(hello_exit);

▲其实加载和卸载有点类似于面向对象里的构造函数和析构函数。

以上是一个最简单的例子,下面讲讲实际题目的编写,实际的题目一般涉及驱动的装载。

题目编写

由于模块装载是在内核启动时完成的(root下也可以设置再insmod装载),所以一般需要安装驱动,通过驱动来启动模块中的代码功能。而驱动类型也一般有两种,一种是字符型设备驱动,一种是globalmem虚拟设备驱动。

1、字符型设备驱动

(1)安装套路

首先了解一下驱动设备的结构体:

///linux/cdev.h   kernel 5.14.8 struct cdev {    struct kobject kobj;    // 内嵌的kobject对象    struct module *owner;    // 所属模块    const struct file_operations *ops;    // 文件操作结构体,用来进行交互    struct list_head list;    dev_t dev;                // 设备号    unsigned int count;} __randomize_layout;

然后就是套路编写:

// 设备结构体struct xxx_dev_t {    struct cdev cdev;} xxx_dev; // 设备驱动模块加载函数static int __init xxx_init(void){    // 初始化cdev    cdev_init(&xxx_dev.cdev, &xxx_fops);    xxx_dev.cdev.owner = THIS_MODULE;     // 获取字符设备号    if (xxx_major) {         //register_chrdev_region用于已知起始设备的设备号的情况        register_chrdev_region(xxx_dev_no, 1, DEV_NAME);    } else {        alloc_chrdev_region(&xxx_dev_no, 1, DEV_NAME);    }    //申请设备号常用alloc_chrdev_region,表动态申请设备号,起始设备设备号位置。     // 注册设备    ret = cdev_add(&xxx_dev.cdev, xxx_dev_no, 1); }// 设备驱动模块卸载函数static void __exit xxx_exit(void){    // 释放占用的设备号    unregister_chrdev_region(xxx_dev_no, 1);    cdev_del(&xxx_dev.cdev);}

这样简单的驱动就安装完了,安装完了之后,我们想要使用这个驱动的话,还需要进行交互,向驱动设备传递数据,所以上面的xxx_fops,即file_operations这个结构体就起到了这个功能。

有的时候安装注册设备驱动需要用到class来创建注册,原因未知:

static int __init xxx_init(void){    buffer_var=kmalloc(100,GFP_DMA);    printk(KERN_INFO "[i] Module xxx registered");    if (alloc_chrdev_region(&dev_no, 0, 1, "xxx") < 0)    {        return -1;    }    if ((devClass = class_create(THIS_MODULE, "chardrv")) == NULL)    {        unregister_chrdev_region(dev_no, 1);        return -1;    }    if (device_create(devClass, NULL, dev_no, NULL, "xxx") == NULL)    {        printk(KERN_INFO "[i] Module xxx error");        class_destroy(devClass);        unregister_chrdev_region(dev_no, 1);        return -1;    }    cdev_init(&cdev, &xxx_fops);    if (cdev_add(&cdev, dev_no, 1) == -1)    {        device_destroy(devClass, dev_no);        class_destroy(devClass);        unregister_chrdev_region(dev_no, 1);        return -1;    }     printk(KERN_INFO "[i] : <%d, %d>", MAJOR(dev_no), MINOR(dev_no));    return 0; }

(2)交互套路

安装完成之后还需要交互,用到file_operations结构体中的成员函数,首先了解下这个结构体。

///linux/fs.h     kernel 5.14.8 struct file_operations {    struct module *owner;    loff_t (*llseek) (struct file *, loff_t, int);    ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);    ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);    ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);    ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);    int (*iopoll)(struct kiocb *kiocb, bool spin);    int (*iterate) (struct file *, struct dir_context *);    int (*iterate_shared) (struct file *, struct dir_context *);    __poll_t (*poll) (struct file *, struct poll_table_struct *);    long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);    long (*compat_ioctl) (struct file *, unsigned int, unsigned long);    int (*mmap) (struct file *, struct vm_area_struct *);    unsigned long mmap_supported_flags;    int (*open) (struct inode *, struct file *);    int (*flush) (struct file *, fl_owner_t id);    int (*release) (struct inode *, struct file *);    int (*fsync) (struct file *, loff_t, loff_t, int datasync);    int (*fasync) (int, struct file *, int);    int (*lock) (struct file *, int, struct file_lock *);    ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);    unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);    int (*check_flags)(int);    int (*flock) (struct file *, int, struct file_lock *);    ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);    ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);    int (*setlease)(struct file *, long, struct file_lock **, void **);    long (*fallocate)(struct file *file, int mode, loff_t offset,              loff_t len);    void (*show_fdinfo)(struct seq_file *m, struct file *f);#ifndef CONFIG_MMU    unsigned (*mmap_capabilities)(struct file *);#endif    ssize_t (*copy_file_range)(struct file *, loff_t, struct file *,            loff_t, size_t, unsigned int);    loff_t (*remap_file_range)(struct file *file_in, loff_t pos_in,                   struct file *file_out, loff_t pos_out,                   loff_t len, unsigned int remap_flags);    int (*fadvise)(struct file *, loff_t, loff_t, int);} __randomize_layout;

其中常用的就是read,write等函数。

之后也是正常的调用函数,套路编写。

// 读设备ssize_t xxx_read(struct file *filp, char __user *buf, size_t count,                loff_t *f_pos){    ...    copy_to_user(buf, ..., ...); // 内核空间到用户空间缓冲区的复制    ...}// 写设备ssize_t xxx_write(struct file *filp, const char __user *buf,                 size_t count, loff_t *f_pos){    ...    copy_from_user(..., buf, ...); // 用户空间缓冲区到内核空间的复制    ...} // ioctl函数命令控制long xxx_ioctl(struct file *filp, unsigned int cmd, unsigned long arg){    ...    switch (cmd) {    case XXX_CMD1:        ...        break;    case XXX_CMD2:        ...        break;    default:        // 不支持的命令        return -ENOTTY;    }    return 0;}

然后需要file_operations结构体中的函数来重写用户空间的write,open,read等函数:

static struct file_operations xxx_fops =        {                .owner = THIS_MODULE,                // .open = xxx_open,                // .release = xxx_close,                .write = xxx_write,                .read = xxxx_read        };

这样当用户空间打开该设备,调用该设备的write函数,就能通过.write进入到xxx_write函数中。

▲这样一些常规kernel题的编写模板就总结出来了。

(3)具体的题目

原题:https://github.com/black-bunny/LinKern-x86_64-bypass-SMEP-KASLR-kptr_restric

①代码和简单的解析

#include #include #include #include #include #include #include #include #include #include  //正常的设置了dev_t和cdev,但是这里使用的class这个模板来创建设备驱动static dev_t first; // Global variable for the first device numberstatic struct cdev c_dev; // Global variable for the character device structurestatic struct class *cl; // Global variable for the device classstatic char *buffer_var; //打开关闭设备的消息提示函数static int vuln_open(struct inode *i, struct file *f){  printk(KERN_INFO "[i] Module vuln: open()");  return 0;}static int vuln_close(struct inode *i, struct file *f){  printk(KERN_INFO "[i] Module vuln: close()");  return 0;} //从buffer_var中读取数据static ssize_t vuln_read(struct file *f, char __user *buf, size_t len, loff_t *off){  if(strlen(buffer_var)>0) {    printk(KERN_INFO "[i] Module vuln read: %s", buffer_var);    kfree(buffer_var);    buffer_var=kmalloc(100,GFP_DMA);    return 0;  } else {    return 1;  }}//向buffer中写入数据,然后拷贝给buffer_var,这里就是漏洞存在点。//由于len和buf都是我们可以控制的,而buffer是栈上的数据,长度为100。//所以我们可以通过len和buf,将数据复制给buffer从而进行栈溢出。static ssize_t vuln_write(struct file *f, const char __user *buf,size_t len, loff_t *off){  char buffer[100]={0};  if (_copy_from_user(buffer, buf, len))    return -EFAULT;  buffer[len-1]='\0';  printk("[i] Module vuln write: %s", buffer);  strncpy(buffer_var,buffer,len);  return len;} //file_operations结构体初始化static struct file_operations pugs_fops ={  .owner = THIS_MODULE,  .open = vuln_open,  .release = vuln_close,  .write = vuln_write,  .read = vuln_read}; //驱动设备加载函数static int __init vuln_init(void) /* Constructor */{  buffer_var=kmalloc(100,GFP_DMA);  printk(KERN_INFO "[i] Module vuln registered");  if (alloc_chrdev_region(&first, 0, 1, "vuln") < 0)  {    return -1;  }  if ((cl = class_create(THIS_MODULE, "chardrv")) == NULL)  {    unregister_chrdev_region(first, 1);    return -1;  }  if (device_create(cl, NULL, first, NULL, "vuln") == NULL)  {    printk(KERN_INFO "[i] Module vuln error");    class_destroy(cl);    unregister_chrdev_region(first, 1);    return -1;  }  cdev_init(&c_dev, &pugs_fops);  if (cdev_add(&c_dev, first, 1) == -1)  {    device_destroy(cl, first);    class_destroy(cl);    unregister_chrdev_region(first, 1);    return -1;  }   printk(KERN_INFO "[i] : <%d, %d>", MAJOR(first), MINOR(first));  return 0;} //驱动设备卸载函数static void __exit vuln_exit(void) /* Destructor */{    unregister_chrdev_region(first, 3);    printk(KERN_INFO "Module vuln unregistered");} module_init(vuln_init);module_exit(vuln_exit); MODULE_LICENSE("GPL");MODULE_AUTHOR("blackndoor");MODULE_DESCRIPTION("Module vuln overflow");

②内核函数解析

printk
printk(日志级别 "消息文本");

其中日志级别定义如下:

#defineKERN_EMERG "<0>"/*紧急事件消息,系统崩溃之前提示,表示系统不可用*/#defineKERN_ALERT "<1>"/*报告消息,表示必须立即采取措施*/#defineKERN_CRIT "<2>"/*临界条件,通常涉及严重的硬件或软件操作失败*/#define KERN_ERR "<3>"/*错误条件,驱动程序常用KERN_ERR来报告硬件的错误*/#define KERN_WARNING "<4>"/*警告条件,对可能出现问题的情况进行警告*/#define KERN_NOTICE "<5>"/*正常但又重要的条件,用于提醒。常用于与安全相关的消息*/#define KERN_INFO "<6>"/*提示信息,如驱动程序启动时,打印硬件信息*/#define KERN_DEBUG "<7>"/*调试级别的消息*/
kmalloc
static inline void *kmalloc(size_t size, gfp_t flags)

其中flags一般设置为GFP_KERNEL或者GFP_DMA,在堆题中一般就是GFP_KERNEL模式,如下:

 

 |– 进程上下文,可以睡眠 GFP_KERNEL

 |– 进程上下文,不可以睡眠 GFP_ATOMIC

 | |– 中断处理程序 GFP_ATOMIC

 | |– 软中断 GFP_ATOMIC

 | |– Tasklet GFP_ATOMIC

 |– 用于DMA的内存,可以睡眠 GFP_DMA | GFP_KERNEL

 |– 用于DMA的内存,不可以睡眠 GFP_DMA |GFP_ATOMIC

 

具体可以看:

Linux内核空间内存申请函数kmalloc、kzalloc、vmalloc的区别【转】 - sky-heaven - 博客园 (cnblogs.com)

(https://www.cnblogs.com/sky-heaven/p/7390370.html)
kfree

这个就不多说了,就是简单的释放。

copy_from_user
copy_from_user(void *to, const void __user *from, unsigned long n)
copy_to_user
copy_to_user(void __user *to, const void *from, unsigned long n)

这两个就不讲了,顾名思义。

注册函数

剩下的好多就是常见的注册函数了。

alloc_chrdev_region(&t_dev, 0, 1, "xxx");unregister_chrdev_region(t_dev, 1);xxx_class = class_create(THIS_MODULE, "xxx");device_create(xxx_class, NULL, devno, NULL, "xxx");cdev_init(&c_dev, &pugs_fops);cdev_add(&c_dev, t_dev, 1)

2、globalmem虚拟设备驱动

这个不太清楚,题目见的也少,先忽略。

printk()函数的总结 - 深蓝工作室 - 博客园 (cnblogs.com)

https://www.cnblogs.com/king-77024128/articles/2262023.html

Linux kernel pwn notes(内核漏洞利用学习) - hac425 - 博客园 (cnblogs.com)

https://www.cnblogs.com/hac425/p/9416886.html

Linux设备驱动(二)字符设备驱动 | BruceFan's Blog (pwn4.fun)

http://pwn4.fun/2016/10/21/Linux%E8%AE%BE%E5%A4%87%E9%A9%B1%E5%8A%A8%EF%BC%88%E4%BA%8C%EF%BC%89%E5%AD%97%E7%AC%A6%E8%AE%BE%E5%A4%87%E9%A9%B1%E5%8A%A8/

infodev
本作品采用《CC 协议》,转载必须注明作者和本文链接
在 Kerberos 身份验证中,客户端必须在 KDC为其提供票证授予票证 之前执行“预验证”,该票证随后可用于获取服务票证。使用时间戳而不是静态值有助于防止重放攻击。客户端有一个公私钥对,并用他们的私钥对预认证数据进行加密,KDC 用客户端的公钥对其进行解密。该属性的值是 Key Credentials这种信任模型消除了使用无密码身份验证为每个人颁发客户端证书的需要。PKINIT 允许 WHfB 用户或更传统的智能卡用户执行 Kerberos 身份验证并获得 TGT。
BEServer - BattlEye服务器,收集上传的信息,并判定作弊行为。本次分析的是BEDaisy,也就是BE内核驱动中的各种检测。上传的内容主要是一些黑名单特征,这些特征应该是从服务器下发的,因此可以在不重新编译驱动的情况下,动态调整检测的特征。没有给定偏移量的特征,BE在检测时会按子串匹配的方式尝试所有位置进行匹配。
敏感信息泄露对于学校站点的信息搜集,一般来说外网能拿直接权限的点已经很少了,web应用大多是放在vpn后面,因此能弄到一个vpn账号可以说是事半功倍,这时候可以通过语法对此类信息进行挖掘常用命令如下:#google语法。弱口令默认口令对于部分站点,在搭建完成后可能没有更改默认账号密码,这时候可以尝试使用默认账密登录下面列举一些常见的web站点默认口令账号:。对于一些应用广泛的系统,可以通过google语法搜索其默认密码这里通过sysadmin/1?
显示机器的处理器架构。/proc/cpuinfo 显示CPU info的信息。/proc/swaps 显示哪些swap被使用。/proc/net/dev 显示网络适配器及统计。/proc/mounts 显示已加载的文件系统。显示2007年的日历表。设置日期和时间 - 月日时分年.秒。将时间修改保存到 BIOS. 取消按预定时间关闭系统。的目录并同时删除其内容。f -atime +100 搜索在过去100天内未被使用过的执行文件。结尾的文件并定义其权限。halt 显示一个二进制文件或可执行文件的完整路径。file.iso /mnt/cdrom 挂载一个文件或ISO镜像文件
敏感信息泄露对于学校站点的信息搜集,一般来说外网能拿直接权限的点已经很少了,web应用大多是放在vpn后面,因此能弄到一个vpn账号可以说是事半功倍,这时候可以通过语法对此类信息进行挖掘常用命令如下:#google语法。弱口令默认口令对于部分站点,在搭建完成后可能没有更改默认账号密码,这时候可以尝试使用默认账密登录下面列举一些常见的web站点默认口令账号:。对于一些应用广泛的系统,可以通过google语法搜索其默认密码这里通过sysadmin/1?
编译make x86_64_defconfig # 加载默认configmake menuconfig # 自定义config. 编译选项添加调试信息, 需要以下几行[*] Compile the kernel with debug info [*] Generate dwarf4 debuginfo [*] Provide GDB scripts for kernel debugging. 由于本虚拟机是只有很基本的环境,在调试漏洞之前还需要做一些操作, 创建/etc/passwd,?漏洞原理在调试之前首先根据补丁来简单了解一下漏洞造成的原因。Exp分析根据exp分析漏洞利用的细节,删除了部分检测利用条件、备份密码等漏洞利用不相关代码。const unsigned pipe_size = fcntl; static char buffer[4096];for { unsigned n = r > sizeof ?int main() { const char *const path = "/etc/passwd";const int fd = open; if { perror; return EXIT_FAILU
影响版本:Linux v5.10-rc1 ~ v5.14.15。v5.14.16已修补。高危,可导致远程提权,评分9.8。 默认不加载,需用户配置。
上述操作均在gitlab默认配置情况下,若漏洞利用无法复现可留言,一起讨论研究。
稍后将会基于linux 2.4.x内核环境,演示这种感染技术。不过,由于内核模块是elf格式的,所以阅读后续内容之前,先要熟悉elf格式,并且要重点理解其符号表,之后才好明白,向正常内核模块注入感染代码的原理。----[ 2.1 - The .symtab section.symtab节区(符号表)的内容,是一个供链接器使用的Elf32_Sym结构数组,Elf32_Sym结构定义在内核的/usr/include/elf.h头文件:typedef struct
VSole
网络安全专家