0x01 前言
C / C++主要使用 malloc,calloc,zalloc,realloc和专门版本kmalloc来进行内存分配。例如,malloc具有void *malloc(size_t size) 签名,可以从堆中申请任何数量的字节,该函数会返回一个指针。然后使用释放内存函数free()。即使在2019年,这些函数仍然是黑客进行漏洞利用的入口点。例如,最近在WhatsApp中出现的UAF漏洞,我将在后续文章中讨论。
我的朋友Alexei提供了一个非常酷的基于Ghidra的脚本来发现常见的malloc漏洞。我从中得到启发,并使用名为Ocular的工具编写了一些简单的查询,该工具可以帮助更快地发现此类问题。Ocular及其开源代码平台Joern是由我们的ShiftLeft团队开发的。这将是一个学习Ocular和Joern如何使用并了解其内部运作的机会。
0x02 malloc()Bugs
回到malloc()中,在某些情况下,看似有效使用了malloc可能也会出错:
· 缓冲区溢出漏洞:size参数可能是由malloc函数的其他一些外部函数计算的。例如下面,这是从另一个函数返回大小的情况:
int getNumber() { int number = atoi("8"); number = number + 10; return number; } void *scenario3() { int a = getNumber(); void *p = malloc(a); return p; }
在这种情况下,虽然mallocsize参数的来源只是atoi(),但情况并非总是如此。如果整数(number + 10)的值溢出并且比随后所需的值小得多(memcpy例如),访问或写入缓冲区时可能会导致缓冲区溢出。
· Zero Allocation:(https://openwall.info/wiki/_media/people/jvanegue/files/woot10.pdf)中所述,提供零作为size参数虽然有效,但可能会导致越界错误。在某些算术运算(例如乘法)之后确定大小的情况下,这很有可能会发生
void *scenario2(int y) { int z = 10; void *p = malloc(y * z); return p; }
如果外部控制了y为零怎么办?在这种情况下,malloc可以返回NULL指针,但是它由用户决定NULL使用分配的内存之前要进行检查。
· Intra-Chunk Heap Overflow:我最喜欢的堆利用方式之一,这种漏洞利用情况是在给定的分配内存块中,覆盖了一部分但是对另一个无关的部分进行操作。克里斯·埃文斯(Chris Evans)的博客中的一个例子很好地说明了这一点:
struct goaty { char name[8]; int should_run_calc; }; int main(int argc, const char* argv[]) { struct goaty* g = malloc(sizeof(struct goaty)); g->should_run_calc = 0; strcpy(g->name, "projectzero"); if (g->should_run_calc) execl("/bin/gnome-calculator", 0); }
· UAF,内存泄漏漏洞: 这也很常见,忘记free()在循环结构中分配内存可能会导致泄漏,在某些情况下可以用来引起恶意崩溃或性能下降。另一种情况是记住释放内存,但是之后又尝试使用它,导致释放后重引用,尽管漏洞利用的可重复性不高,但是当free接近malloc并尝试重新分配时,仍然可能会导致这种情况(通常返回相同的值)或附近的地址),从而使我们能够访问以前释放的内存。
在这篇文章中,我将介绍使用Ocular清理malloc's size参数的前两种情况,看看它们是否最终会导致缓冲区溢出或零分配错误。
0x03 Ocular
Ocular允许将代码(C / C ++ / Java / Scala / C#等)表示为代码属性图 – CPG的图(它类似于AST,控制流和数据流图的混合)。我称之为半编译器,我们采用源代码(C / C ++ / C#)或字节码(Java)并将其编译为IR。该IR是图形(CPG)。无需将其进一步编译,将其加载到内存中,并允许向此IR提出问题,以评估函数的数据泄漏问题,数据流分析,保证关键部分中的变量正确使用,检测缓冲区溢出,UAF等。
而且,由于它是一个图形,因此这种查询会非常有趣,就像GDB或Radare一样,可以在Ocular Shell上编写。例如,您可以说,
Hey Ocular, list all functions in the source code that have “alloc” in their name and give me the name of its parameter
将在Ocular Shell上执行为:
ocular> cpg.method.name(".*alloc.*").parameter.name.l res1: List[String] = List("a")
这是我在不到一分钟的时间内创建的代码图并列出代码中的所有函数:
0x04 使用Ocular / Joern检测内存分配漏洞
稍微升级一下,尝试进行一些malloc()的简单查询。
使用下面的代码在Ocular或其开源平台Joern中执行(https://joern.io/)
#include <stdio.h> #include <stdlib.h> int getNumber() { int number = atoi("8"); number = number + 10; return number; } void *scenario1(int x) { void *p = malloc(x); return p; } void *scenario2(int y) { int z = 10; void *p = malloc(y * z); return p; } void *scenario3() { int a = getNumber(); void *p = malloc(a); return p; }
在上面的代码中,可以列出malloc调用的文件名和行号,我们可以在Ocular shell使用以下查询:
Ocular Query:
ocular> cpg.method.callOut.name("malloc").map(x => (x.location.filename, x.lineNumber.get)).l
Result:
List[(String, Integer)] = List( ("../../Projects/tarpitc/src/alloc/allocation.c", 23), ("../../Projects/tarpitc/src/alloc/allocation.c", 17), ("../../Projects/tarpitc/src/alloc/allocation.c", 11) )
在示例代码中,方案2或方案3清楚地表明了可能会发生零分配错误,在数据流中进行算术运算,一直达到malloc调用位置。因此,我们尝试建立一个查询,该查询列出带有源数据流作为“方案”方法的参数,并作为所有malloc调用站点的接收器。然后,我们找到所有数据流,并过滤对数据流中的数据进行算术运算的流,这会显示出分配为零或不正确的bugs。
Ocular Query:
ocular> val sink = cpg.method.callOut.name("malloc").argument ocular> var source = cpg.method.name(".*scenario.*").parameter ocular> sink.reachableBy(source).flows.passes(".*multiplication.*").p
Result:
在上面的查询中,我们在Ocular shell上创建了名为source和的局部变量sink。语言是scala,它很冗长因此不必过多解释,但是为了完整起见,这是可以用英语解释Ocular查询的第一条语句的方法:
To identify the sink, find all call-sites (callOut) for all methods in the graph (cpg) with name as malloc and mark their arguments as sink.
在上面的代码中,它们将是x,(y * z)和a。
如果我们不想遍历所有方法然后发现它们是否存在漏洞,该怎么办?如果可以从任意调用站点作为源转到malloc调用站点作为接收器,以查找进行算术运算的数据流怎么办?我们可以制定英文查询,首先遍历所有方法的所有调用站点,过滤掉具有malloc和任何操作的源,然后将其作为实际方法的返回值(methodReturn)来定义源站点。然后找到从这些源到作为接收器的malloc调用站点参数的数据流,该数据流对流中的数据进行任何算术运算。听起来有些难,但Ocular Query可以以编程方式解释这一点:
Ocular Query:
ocular> val sink = cpg.method.callOut.name("malloc").argument ocular> var source = cpg.method.callOut.nameNot(".*(<operator>|malloc).*").calledMethod.methodReturn ocular> sink.reachableBy(source).flows.passes(".*(multiplication|addition).*").p
Result:
通过这种方法可以挖掘很多内存漏洞,并修复它们提升安全性。
在下一篇文章中,我将分析如何使用Ocular / Joern在Scala中生成一个Double Free Detector。