了解该漏洞的最简单方法是查看差异,挖掘代码,并找出如何利用它。
易受攻击的方法的签名如下所示:
status_t GraphicBuffer::unflatten(
void const*& buffer, size_t& size, int const*& fds, size_t& count) {
这里的重要参数是void const*& buffer
,它是用于展开的数据缓冲区,以及size_t& size
,它指定缓冲区的大小。
请记住这size_t
是 typedef unsigned int
,这意味着它是一个无符号整数。这意味着它不能代表负值。
该方法开始如下:
// if the size of the buffer is less than 8 integers, return because it's too small
if (size < 8*sizeof(int)) return NO_MEMORY;
// cast the buffer to an integer pointer type (think "array of ints")
// remember that the size argument is NOT an int, but a size_t - therefore unsigned int.
int const* buf = static_cast<int const*>(buffer);
// check that the buffer contains the right magic number at the start
if (buf[0] != 'GBFR') return BAD_TYPE;
// populate two size_t variables - these are the ones which cause the integer overflow later!
// note that this data is supplied by the calling application, so it can control it.
const size_t numFds = buf[8];
const size_t numInts = buf[9];
因此,正如您所看到的,有一些粗略的检查以查看缓冲区的大小是否正确。然后它继续从缓冲区读取更多数据并填充两个计数(Fds 和 Ints)。这些数字指定在标题数据之后有多少个值。
让我们继续...
// check that the buffer is large enough to contain the number of ints specified.
// notice that 10 is the number of ints read so far (buf[9] was our last access)
// so in order to store numInts ints, the buffer needs to be 10 + numInts ints long.
const size_t sizeNeeded = (10 + numInts) * sizeof(int);
// if the buffer isn't big enough, fail
if (size < sizeNeeded) return NO_MEMORY;
// if the count argument is less than zero, fail
// this is meaningless because count is size_t, so can't be negative
size_t fdCountNeeded = 0;
if (count < fdCountNeeded) return NO_MEMORY;
if (handle) {
// free previous handle if any
free_handle();
}
到目前为止一切顺利,主要是。检查缓冲区是否足够大,可以容纳标头声明的“整数”数量。对负数进行毫无意义的检查,但无论如何这是不可能的。最后,它会清理任何旧手柄以防万一,以防止手柄泄漏。
现在漏洞来了...
// if either numFds or numInts is nonzero...
if (numFds || numInts) {
// read a bunch of fields...
width = buf[1];
height = buf[2];
stride = buf[3];
format = buf[4];
usage = buf[5];
// native_handle_create is just a wrapper for malloc'ing a struct which
// contains a few metadata fields and the fds/ints data.
// the function takes two ints as its parameters, so it can take negatives.
// the underlying code does malloc(...+((numFds+numInts)*sizeof(int)))
// and since nothing has checked if numFds < 0, this will make a buffer
// which is smaller than expected!
//
// example: if numFds is -1 and numInts is 20, the allocated data buffer
// will be 19*sizeof(int) bytes long.
//
// this is potentially bad already, but it gets worse.
native_handle* h = native_handle_create(numFds, numInts);
// this will copy the list of file descriptors (fds) from the fds pointer
// to the h->data pointer. the number of bytes to copy is numFds*sizeof(int).
// it is important to note that the third argument of memcpy takes a size_t,
// which is unsigned. numFds is a signed int, but when you multiply it with
// sizeof(int), which is type size_t, you get a size_t, which is interpreted
// as an UNsigned integer. as such, -1 (0xFFFFFFFF) in signed becomes
// 4294967295 when translated over to size_t. so, it attempts to copy way
// too much data from fds to h->data, leading to heap corruption.
memcpy(h->data, fds, numFds*sizeof(int));
// <-- CRASH HERE, HEAP IS CORRUPTED
memcpy(h->data + numFds, &buf[10], numInts*sizeof(int));
handle = h;
} else {
width = height = stride = format = usage = 0;
handle = NULL;
}
本质上,问题在于文件描述符的数量(numFds)被读取为有符号整数,但随后被转换为 memcpy 的无符号整数,这会将一个小的负数变成一个大的正数并超出堆缓冲区。这都是由于缺少对 和 的值的检查造成numInts
的numFds
。
根据FullDisclosure 帖子,您可以通过制作一个派生自 的类来利用它,该类可以BufferQueue
访问最终传递给unflatten
'buffer
参数的数据。
现在,由于所有这些易受攻击的代码都位于在用户system_server
下运行的进程内,因此system
从用户应用程序(即应用程序)破坏其堆是一件大事。如果您可以覆盖堆上的结构或类中的函数指针(例如,来自回调或事件),则可以在 中获得任意代码执行system_server
,从而获得特权。
要回答您的澄清请求...
首先,函数 native_handle_create 和变量 native_handle 在哪里声明(我可以看到它们的用途,但既不是局部声明也不是全局声明)。
Android 交叉参考对于查找这类东西很有用。该函数在native_handle.c中声明,native_handle
结构(不是变量 -h
是 type 的指针变量native_handle
)在native_handle.h中声明。
其次,您如何确定缓冲区的实际大小应该是多少(以便确定 numFds 或 numInts 的当前大小是否不正确,即不应为-1)?
好吧,我们知道列表中不能有 -1 项,所以这是无效的。如果您查看补丁 diff,他们会添加一些检查:
// compute the maximum possible number of ints that you can store in a buffer
// whose size is represented by a size_t.
const size_t maxNumber = UINT_MAX / sizeof(int);
// if numFds exceeds the max number of possible ints (e.g. if 0xFFFFFFFF is passed)
// this check will fail. the second check is the same, except it accounts for the
// fact that 10 ints in the buffer are already used for other purposes.
if (numFds >= maxNumber || numInts >= (maxNumber - 10)) {
// clear data and fail
width = height = stride = format = usage = 0;
handle = NULL;
ALOGE("unflatten: numFds or numInts is too large: %d, %d",
numFds, numInts);
return BAD_VALUE;
}
稍后我们可以看到更多的检查...
// originally this was set to 0, which essentially did nothing.
size_t fdCountNeeded = numFds;
if (count < fdCountNeeded) return NO_MEMORY;
调用后的最终检查native_handle_create
可防止分配失败导致问题 -即使为 null,它也会执行memcpy
to ,从而导致 null 取消引用错误(DoS 条件)。h->data
h
因此,这些检查确保输入值不会超出范围,因此不能像以前那样被利用。
第三,h->data 是什么意思,因为我找不到名为 data 的变量的声明,而且我不知道 -> 是什么意思?什么是 sizeof(int)?
->
运算符是取消引用。左边的操作数是指向结构的指针。所以h->data
dereferences h
,它是一个指向类型结构的指针native_handle
(我们之前提到过)并访问该data
结构的字段。相比之下,如果h
不是指针而只是一个普通的结构,你会这样做h.data
。
该sizeof
宏为您提供类型的大小(以字节为单位)。因此将为您的编译器实现和目标平台sizeof(int)
返回本机大小。int
在大多数情况下,它应该返回 4 或 8 来指定 32 位或 64 位整数。
最后我认为 numFds 是 unsigned const size_t numFds = buf[8];,如果 numFds 是 size_t 类型,我认为它不应该能够保持负数开始(除非我错过了什么)?
是的,numFds
是size_t
,但这只是意味着该值中的原始字节被解释为无符号整数。如果它包含 0xFFFFFFFF,则其直接解释的十进制值将是 4294967295。但是,当您将 asize_t
转换为 an时,问题就出现了int
,此时其中的原始字节被解释为有符号整数,在这种情况下,0xFFFFFFFF 突然变成 -1 .
另外,我不明白我们如何从创建堆损坏或数组索引越界异常等转移到任意代码执行?
这是一个复杂的话题,但我会给你一个总结。堆是您的大部分应用程序数据所在的位置。这意味着当您制作缓冲区和类实例以及其他各种东西时,大部分数据都在堆中。现在,假设您有一个带有事件的类 - 让我们称之为Dog
,并将事件称为Bark
。当某些条件出现时,该类的一个实例将引发该Bark
事件,以便其他对象可以对该树皮作出反应。事件只是一个指针。其他对象通过为其分配一个指向方法的指针(即指向某些代码的指针)来处理该事件。因此,当Dog
实例想要 raiseBark
时,它会查看Bark
指针并开始执行那里的代码。在汇编中,这看起来像call [eax+0x0...]
.
由于Dog
实例存在于内存中,在堆上,它的Bark
指针也存在。如果您可以覆盖Bark
指向其他可执行内存的指针,然后导致Dog
实例引发事件,它将在其他位置运行代码。如果您控制该位置的代码(例如,通过用代码填充缓冲区并将该地址用作指针),那么您将获得任意代码执行。
您可以通过简单地用可能以有意义的方式覆盖目标数据的数据结构来充斥堆来利用这一点。它并不总是 100% 可靠,但有时就是这样。
NX 和 ASLR 让事情变得更加复杂,您必须使用指针泄漏和 ret2libc/ROP 来利用它,但这是一个更大的话题。
如果你想进入漏洞利用写作,我强烈推荐Corelan 的漏洞利用写作教程。它们有点过时了——你需要一个禁用硬件 NX 的 WinXP VM——但它仍然是一个强大的资源。