Kernel從0開始
簡介
網上一大堆教編譯內核的,但很多教程看得特別迷糊。第一次編譯內核時,沒設置好參數,直接把虛擬機編譯炸開了。所以就想著能不能先做個一鍵獲取內核源碼和相關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/