一 漏洞信息
对这类漏洞的启发是来自于https://bugs.python.org/issue27945bugs.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;