正文
}
到目前为止一切顺利,问题出在对
malloc
的调用中。
nameValue = (char *)Malloc(nameLen + valueLen + 2);
可能会在最终的分配计算中添加 +2,以存储键和值之间的“=”字符,以及字符串末尾的空字节。
然而,在
0x7ffffffff + 0x7ffffffff + 1 = 0xffffffff
...
0x7ffffffff + 0x7ffffffff + 2 = 0
.
这种等式只能在 32 位机器上验证。事实上,这个计算的结果将被存储在一个整数中,其类型将不由
nameLen
和
valueLen
类型定义,而是由
malloc
参数的类型定义。在
stdlib
下,这个参数是
size_t
。
size_t
的定义取决于目标机器,但你可以将
size_t
视为
unsigned long long
。这意味着在 64 位机器上,参数的大小(8 字节)足以正确存储计算的最大结果。在 32 位机器上,只会分配 4 个字节,从而产生整数溢出。
请注意,当
malloc
分配小于
0x10
的大小时,将分配
0x10
字节。
如果提供的两个大小是
0xfffffffff
,在 32 位机器上,结果将分配
0x10
,用于从二进制文件中已知的键/值大小
0x7ffffffff
。 事实上,第一个字节上的掩码会将值从 0xff 减少到 0x7f。
这种整数溢出导致了一个更重要的漏洞,也是本文将要利用的漏洞:
一个堆溢出
。
一旦分配,
malloc
返回的指针将直接用于容纳用户输入。
if(FCGX_GetStr(nameValue, nameLen, stream) != nameLen) {
//...
}
FCGX_GetStr
函数接受一个指针、一个大小和要从中读取的
FCGX_Stream
,然后从
FCGX_Stream
中读取与第一个参数提供的指针一样多的字节。
在操作层面,这可能会带来问题。 实际上,通过提供大小
0x7ffffffff
,在缓冲区之外写入将到达堆的末尾,并在写入未映射区域时生成崩溃。
然而,
FCGX_Stream
系统允许您控制写入目标缓冲区的大小。
int FCGX_GetStr(char *str, int n, FCGX_Stream *stream)
{
int m, bytesMoved;
if (stream->isClosed || ! stream->isReader || n <= 0) {
return 0;
}
/*
* Fast path: n bytes are already available
*/
if(n <= (stream->stop - stream->rdNext)) {
memcpy(str, stream->rdNext, n);
stream->rdNext += n;
return n;
}
/*
* General case: stream is closed or buffer fill procedure
* needs to be called
*/
bytesMoved = 0;
for (;;) {
if(stream->rdNext != stream->stop) {
m = min(n - bytesMoved, stream->stop - stream->rdNext);
memcpy(str, stream->rdNext, m);
bytesMoved += m;
stream->rdNext += m;
if(bytesMoved == n)
return bytesMoved;
str += m;
}
if(stream->isClosed || !stream->isReader)
return bytesMoved;
stream->fillBuffProc(stream);
if (stream->isClosed)
return bytesMoved;
stream->stopUnget = stream->rdNext;
}
}
FCGX_Stream
结构包含一个指向要读取的下一个字节的指针
rdNext
,以及一个指向套接字读取的最后一个字节的指针
stop
。
FCGX_GetStr
函数将使用它来避免读取超出已插入流的内容。因此,如果流已完成,则写入目标缓冲区将终止。 这种停止条件将允许我们控制写入目标缓冲区的字节数,尽管在
FCGX_GetStr
中作为参数传递了大小。我们需要确保使用的参数是流中的最后一个参数。
简而言之,参数处理函数中的整数溢出导致堆中缓冲区溢出,而该缓冲区的大小是可以控制的。
演示环境