引用计数相关漏洞案例

垃圾分类 是最近非常火爆的一个话题,其经常出现在各种新闻报道中,碰巧的是笔者最近也在研究 垃圾回收 ,但此垃圾非彼垃圾,笔者研究的是程序设计语言中的 Garbage Collection(下文简称 GC)。

关于 GC 的入门读物,这里强烈推荐日本人在 2010 年出版的书籍《垃圾回收的算法与实现》。笔者在阅读该书的部分内容时,刚好想到了几个相关的漏洞,因此写篇文章记录一下。

0x01. Python 析构函数

Python 基于引用计数来实现 GC 算法。在引用计数法中,各个对象的内部都有一个专门用于引用计数的成员,以及相应的操作函数 inc_ref_countdec_ref_count 来增减引用计数。特别的,如果在 dec_ref_count 中,引用计数递减之后等于零,那么该对象就需要被回收掉了。

在 Python 中定义一个对象时,可以通过 __init__ 定义构造函数、通过 __del__ 定义析构函数。在引用计数变为 0 之后、销毁对象之前,Python 会检查对象是否定义了 __del__ 函数,如果有,则调用之。相关的代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static void slot_tp_del(PyObject *self) {
static PyObject *del_str = NULL;
PyObject *del, *res;

self->ob_refcnt = 1;

/* 如果有 __del__ 就执行它 */
del = lookup_maybe(self, "__del__", &del_str);
if (del != NULL) {
res = PyEval_CallObject(del, NULL);

/* 省略部分:错误检查和后处理等 */
}

if (--self->ob_refcnt == 0)
return; /* 退出函数 */

/* 省略部分:最终化时有引用的情况下的应对处理 */
}

这里在进入 slot_tp_del 之前,对象的引用计数已经为 0 了,但是在该函数中,在调用 __del__ 函数之前将引用计数重新设置为 1 ,而在调用之后则将引用计数减去 1 并判断结果是否为 0 ,这里主要是判断在调用 __del__ 的过程中是否又产生了新的引用。

像这样的判断是很有必要的,如果没有这样的判断,那么后面可能就直接把对象回收了,而如果 __del__ 中产生了新的引用,那么就会导致 UAF

0x02. VBScript Class_Terminate UAF

CVE-2018-8174 是 VBScript Class_Terminate 函数(析构函数)中引用计数处理不当导致的 UAF 漏洞,漏洞的细节可以参考 安全客卡巴斯基 的分析文章。漏洞的 PoC 代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Dim arr(1)
Dim o

Class MyClass
Private Sub Class_Terminate
Set o = arr(0)
arr(0) = &h12345678
End Sub
End Class

Set arr(0) = new MyClass
Erase arr

' Trigger UAF
MsgBox o

这里在清空数组时,会依次清空数组中的元素。arr(0) 保存了 MyClass 对象的引用,因此最终会触发 MyClass 的析构函数,而在该析构函数中又重新引用了对象本身;由于 VBScript 引擎没有处理好这种情况,这里对象会被回收,后续再次使用对象时便会触发 UAF 。

这里触发漏洞的模式,和上面提到的 Python 代码非常相似,只是这里 VBScript 引擎并没有考虑到对象在析构函数中被重新引用这一情况。

0x03. 整数溢出问题

大家可能会想,引用计数不会存在整数溢出问题吗?理论上是可以的,但实际上通常不会出现这样的情况!

引用计数器的类型可以使用 平台相关 的整数类型,比如 32 位环境下使用 32 位的计数器,64 位环境下使用 64 位的寄存器。这里以前者为例分析溢出时的情况:

假如使用无符号类型的引用计数,那么要存在 2^32 个引用才会产生溢出,这里 2^32 = 4GB ;而产生一个引用计数至少要占用一定的内存(比如在脚本语言中定义一个变量),即便是 4 字节就需要占用 16GB 的内存,而实际情况则肯定是 16GB 的数倍。

32 位环境下,在触发整数溢出前,早就触发了 OOM (Out-Of-Memory)异常;在 64 位下,同样也会导致 OOM ;此外,实现整数溢出的时间,可能也是难以接受的。

相关案例可以参考 Firefox 的一个 Issue :Use-after-free due to ref counter overflow in CanvasRenderingContext2D

0x04. Firefox SharedArrayBuffer UAF

上面提到,通常情况下 不存在引用计数溢出的问题,但 特殊情况下 这也是需要考虑的一个问题!Share with care: Exploiting a Firefox UAF with shared array buffers 就是一个很好的例子。

所谓特殊情况,无非就是在可以接受的时间消耗和内存占用的情况下,不断增加对象的引用计数并最终导致其溢出的情况。

在上述文章中提到,Firefox 的 SharedArrayBuffer 使用了 uint32_t 类型的引用计数器,并且不会检查溢出问题,这就存在整数溢出的可能性。

此外,在特定的漏洞场景下,利用 Web Worker 序列化(使用 结构化克隆算法SharedArrayBuffer 对象时,可以在不创建新的 SharedArrayBuffer 的情况下(也不需要定义额外的变量)增加原有 SharedArrayBuffer 的引用计数,这就消除了触发整数溢出需要占用大量内存的情况。

最后,文章使用了一个小技巧来缩短触发漏洞所需要的时间,即通过 postMessage 到自身(不是发送给其他 Web Worker )来创建指向同一个 SharedArrayBuffer 的许多引用(可以减少序列化时的循环次数),最后通过上面提到的漏洞场景来增加 SharedArrayBuffer 的引用计数,以实现在可控的时间内触发引用计数的整数溢出。

有了上述条件,就可以通过引用计数的溢出来触发 UAF 漏洞,之后再通过利用 UAF 漏洞来实现代码执行。

0x05. 小结

以上,就是在学习基于引用计数的 GC 时想到的几个漏洞案例,后续如有找到新的案例再进行补充。

Garbage Collector

听说这是 Java 的垃圾回收?哈哈哈

请作者喝杯咖啡☕