python调用compare工具_[漏洞调试笔记] Python PyObject_RichCompareBool UaF 细节

一 漏洞信息

对这类漏洞的启发是来自于https://bugs.python.org/issue27945​bugs.python.org

第二个漏洞,我们先来复现下面这段PoC :

class X:

def __eq__(self, other):

d.clear()

return NotImplemented

d = {0: set()}

(0, X()) in d.items()

这段PoC 对应的修复代码如下,我们在Python 3.8 上复现该漏洞时,需要注意以下的Patch 代码,把对应修改的代码改回来

diff --git a/Objects/dictobject.c b/Objects/dictobject.cindex 4bcc3db..56b06db 100644--- a/Objects/dictobject.c+++ b/Objects/dictobject.c@@ -3942,6 +3942,7 @@ dictitems_iter(_PyDictViewObject *dv) static int

dictitems_contains(_PyDictViewObject *dv, PyObject *obj)

{

+ int result; PyObject *key, *value, *found;

if (dv->dv_dict == NULL)

return 0;

@@ -3955,7 +3956,10 @@ dictitems_contains(_PyDictViewObject *dv, PyObject *obj) return -1;

return 0;

}

- return PyObject_RichCompareBool(value, found, Py_EQ);+ Py_INCREF(found);+ result = PyObject_RichCompareBool(value, found, Py_EQ);+ Py_DECREF(found);+ return result; }

static PySequenceMethods dictitems_as_sequence = {

修改为

static int

dictitems_contains(_PyDictViewObject *dv, PyObject *obj)

{

int result;

PyObject *key, *value, *found;

if (dv->dv_dict == NULL)

return 0;

if (!PyTuple_Check(obj) || PyTuple_GET_SIZE(obj) != 2)

return 0;

key = PyTuple_GET_ITEM(obj, 0);

value = PyTuple_GET_ITEM(obj, 1);

found = PyDict_GetItemWithError((PyObject *)dv->dv_dict, key);

if (found == NULL) {

if (PyErr_Occurred())

return -1;

return 0;

}

return PyObject_RichCompareBool(value, found, Py_EQ);

}

F5 启动Visual Studio ,可以看到崩溃点如下:

二 深入分析PoC

接下来分析PoC :

class X:

def __eq__(self, other):

d.clear()

return NotImplemented

d = {0: set()}

(0, X()) in d.items()

字典d 包含key 为0 ,内容为一个set() 对象.然后新构造一个tuple() 对象判断它是否被d 对象包含.那么就是??? in d 的代码语句触发dictitems_contains() 函数的调用,进而在后面的代码逻辑里面调用到PyObject_RichCompareBool() 函数.

此外,声明的类X 里面包含一个__eq__() 函数,这个__eq__() 函数内部竟然要对字典d 进行clear() ,究竟是为何可以导致UaF 产生呢?

三 源码调试

我们在dictitems_contains() 下断点,执行PoC ,然后可以看到key 和value 变量的值.

// obj 为tuple() 对象,(0,X()) in d 语句的左边那个值 key = PyTuple_GET_ITEM(obj, 0); // key 值为0 value = PyTuple_GET_ITEM(obj, 1); // value 为类X

接下来继续往下执行,代码会把key 值到d 对象里面寻找是否存在也有相同值.

found = PyDict_GetItemWithError((PyObject *)dv->dv_dict, key); // 这个是set() 对象 if (found == NULL) { // 这里肯定不为空 if (PyErr_Occurred())

return -1;

return 0;

}

最后就对value 和found 变量进行两值判断,特别特别要注意Py_EQ 这个关键字,它的意思是要调用__eq__() 函数进行值判断.

return PyObject_RichCompareBool(value, found, Py_EQ);

最后PyObject_RichCompareBool() 会递归调用到do_richcompare() 函数,我们就直接关注它的代码:

static PyObject *

do_richcompare(PyObject *v, PyObject *w, int op)

{

richcmpfunc f;

PyObject *res;

int checked_reverse_op = 0;

if (v->ob_type != w->ob_type && // ob_type 是指对象类型 PyType_IsSubtype(w->ob_type, v->ob_type) &&

(f = w->ob_type->tp_richcompare) != NULL) {

checked_reverse_op = 1;

res = (*f)(w, v, _Py_SwappedOp[op]);

if (res != Py_NotImplemented)

return res;

Py_DECREF(res);

}

if ((f = v->ob_type->tp_richcompare) != NULL) { // tp_richcompare 就是__eq__ 函数地址 // 如果对象里面已经声明__eq__ 函数,那么它就不为NULL res = (*f)(v, w, op); // 调用__eq__ 函数,实际上是调用到slot_tp_richcompare() if (res != Py_NotImplemented)

return res;

Py_DECREF(res);

}

// 省略无关代码 return res;

}

而slot_tp_richcompare() 函数,其实就是通过lookup_maybe_method() 来查询类里面是否存在某个函数对象,进而再通过call_unbound() 调用该函数.

static _Py_Identifier name_op[] = {

{0, "__lt__", 0},

{0, "__le__", 0},

{0, "__eq__", 0},

{0, "__ne__", 0},

{0, "__gt__", 0},

{0, "__ge__", 0}

};

static PyObject *

slot_tp_richcompare(PyObject *self, PyObject *other, int op)

{

int unbound;

PyObject *func, *res;

func = lookup_maybe_method(self, &name_op[op], &unbound);

if (func == NULL) {

PyErr_Clear();

Py_RETURN_NOTIMPLEMENTED;

}

PyObject *args[1] = {other};

res = call_unbound(unbound, func, self, args, 1);

Py_DECREF(func);

return res;

}

至此,我们基本上对整个执行过程的相关细节已经有所了解,那么还有最后两步细节,我们就直接F5 执行程序,让它执行到崩溃点,可以看到,崩溃代码也在do_richcompare() 里面.

static PyObject *

do_richcompare(PyObject *v, PyObject *w, int op) {

// 省略无关代码..... if ((f = v->ob_type->tp_richcompare) != NULL) { // <-- 这里调用到__eq__() 函数 res = (*f)(v, w, op);

if (res != Py_NotImplemented) // <-- TIPS1 .. return res;

Py_DECREF(res);

}

if (!checked_reverse_op && (f = w->ob_type->tp_richcompare) != NULL) { // <-- 这里崩溃 res = (*f)(w, v, _Py_SwappedOp[op]);

if (res != Py_NotImplemented)

return res;

Py_DECREF(res);

}

第一步细节,我们看注释TIPS1 对应的代码位置,当__eq__()函数返回的对象不为NotImplemented时,那就继续往下执行.此时w 对象已经被释放,这里的内容就已经为0xDDDDDDDD .

要理解最关键的一步细节,我们就需要深入理解Python 的对象引用机制.

四 Python 对象引用机制

Python 对象引用机制的意义在于,通过引用计数器ob_refcnt 来对对象的引用进行计数,在对象不再被引用时(引用计数为0)则立即释放对象.对Python 对象进行加引用,就调用Py_INCREF ,反之则调用Py_DECREF .那么,要想让一个对象被释放,那就必须要通过Py_DECREF让它被释放,而且此时的对象引用必须是ob_refcnt=1.

我们重新对代码进行调试,在dictitems_contains() 调用PyObject_RichCompareBool(value, found, Py_EQ); 时下断点.我们通过上面的分析知道,do_richcompare() 的v,w 参数分布对应的是value 和found 变量.我们观察断点处的内容.

可以看到,found 的ob_refcnt 为1 ,然后调用到X() 对象的__eq__() 函数,把字典对象d 清空,我们再来阅读clear() 函数的实现代码.

static PyObject *

dict_clear(PyDictObject *mp, PyObject *Py_UNUSED(ignored))

{

PyDict_Clear((PyObject *)mp);

Py_RETURN_NONE;

}

void

PyDict_Clear(PyObject *op)

{

PyDictObject *mp;

PyDictKeysObject *oldkeys;

PyObject **oldvalues;

Py_ssize_t i, n;

if (!PyDict_Check(op))

return;

mp = ((PyDictObject *)op);

oldkeys = mp->ma_keys; // 字典对象的key 内容 oldvalues = mp->ma_values;

// 省略无关代码 if (oldvalues != NULL) { // <-- 此时oldvalues == NULL // 省略无关代码 }

else {

assert(oldkeys->dk_refcnt == 1);

dictkeys_decref(oldkeys);

}

}

static inline void

dictkeys_decref(PyDictKeysObject *dk)

{

assert(dk->dk_refcnt > 0);

_Py_DEC_REFTOTAL;

if (--dk->dk_refcnt == 0) {

free_keys_object(dk);

}

}

static void

free_keys_object(PyDictKeysObject *keys)

{

PyDictKeyEntry *entries = DK_ENTRIES(keys);

Py_ssize_t i, n;

for (i = 0, n = keys->dk_nentries; i < n; i++) {

Py_XDECREF(entries[i].me_key); // <-- 对对象进行减引用 Py_XDECREF(entries[i].me_value);

}

if (keys->dk_size == PyDict_MINSIZE && numfreekeys < PyDict_MAXFREELIST) {

keys_free_list[numfreekeys++] = keys;

return;

}

PyObject_FREE(keys); // 对内容链表进行释放,但是没有释放里面的对象}

所以,我们可以看到,在__eq__() 函数里面调用了d.clear() ,把d 内部的所有对象都进行了一次减引用,此时found 对象(d 对象内保存的那个value )从ob_refcnt=1 被减引用为ob_refcnt=0 ,然后被_Py_Dealloc() 调用进行释放.回来继续看do_richcompare()的代码,注释里面就标得很清楚了.

static PyObject *

do_richcompare(PyObject *v, PyObject *w, int op) {

// 省略无关代码..... if ((f = v->ob_type->tp_richcompare) != NULL) { // <-- 这里调用到__eq__() 函数 res = (*f)(v, w, op); // <-- 这里已经把found 对象释放了 if (res != Py_NotImplemented) // <-- 返回NotImplemented ,没有return return res;

Py_DECREF(res);

}

if (!checked_reverse_op && (f = w->ob_type->tp_richcompare) != NULL) { // <-- 这里崩溃 res = (*f)(w, v, _Py_SwappedOp[op]);

if (res != Py_NotImplemented)

return res;

Py_DECREF(res);

}

五 回归diff

diff 代码为什么这样写,请大家自行思考

- return PyObject_RichCompareBool(value, found, Py_EQ);+ Py_INCREF(found);+ result = PyObject_RichCompareBool(value, found, Py_EQ);+ Py_DECREF(found);+ return result;

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值