对 libssh2 整数溢出漏洞 (CVE-2019-17498)的分析
0x01 漏洞挖掘
在2019年3月18日,Canonical Ltd.的Chris Coulson披露了libssh2中的九个漏洞(CVE-2019-3855至CVE-2019-3863)。这些漏洞已在libssh2 v1.8.1中修复。当时,我的同事Pavel Avgustinov注意到,修复漏洞的报告在LGTM上引入了多个新告警。这些告警是由于像下面的代码:
if((p_len = _libssh2_get_c_string(&buf, &p))
问题出现在_libssh2_get_c_string返回 -1是一个error code,但是p_len没有符号,因此错误条件将被忽略。libssh2团队已经在后面的的报告中修复了这些问题,但是它促使我们仔细查看代码以查看其包含明显错误的漏洞。我们很快发现了这个用于边界检查的函数:
int _libssh2_check_length(struct string_buf *buf, size_t len)
{
return ((int)(buf->dataptr - buf->data) len - len)) ? 1 : 0;
}
此函数的问题在于强制转换int可能会溢出。左侧的强制转换是安全的,因为的字段buf是受信任的值,但是右侧的强制转换是不安全的,因为的值len是不受信任的。创建漏洞利用exp并非难事,该漏洞利用使值len大于绕过此溢出检查buf->len + 0x80000000。可以在GitHub上找到PoC 。
我后来了解到,该问题_libssh2_check_length是在1.8.2版发布后在主要开发分支上引入的,因此漏洞范围检查在1.8.2版中不存在。不幸的是,版本1.8.2不包含任何边界检查,因此PoC仍然有效。在1.8.2版中,漏洞的源位置是kex.c:1675。问题在于其中p_len包含一个不受信任的值,因此后续的读取s可能会超出范围。因为_libssh2_check_length在1.8.2版中不存在,所以不需要将值p_len大于0x80000000触发漏洞。这意味着较小的值len可以触发越界读取,该漏洞更有可能被利用来实现远程信息泄露。
0x02 negative error codes 转换为 unsigned
当我和我的同事正在查看漏洞补丁报告时,我们注意到一种常见的错误模式,其中将negative error返回值强制转换为unsigned。这是一个非常容易犯的错误,并且它没有被编译器警告所](https://godbolt.org/z/CvqwDm)捕获。所以我写了这个简单的查询来查找漏洞实例:
import cpp
import semmle.code.cpp.dataflow.DataFlow
import semmle.code.cpp.rangeanalysis.SimpleRangeAnalysis
from Function f, FunctionCall call, ReturnStmt ret, DataFlow::Node source, DataFlow::Node sink
where call.getTarget() = f
and ret.getEnclosingFunction() = f
and ret.getExpr().getValue().toInt()
查询代码如下:
r_len = _libssh2_get_c_string(&buf, &r);
if(r_len
该查询会返回负整数常量的函数。例如,_libssh2_get_c_string在773行上执行此操作,然后,它将查找对该函数的调用,这些调用返回值并将其转换为无符号类型。
0x03 触发漏洞
漏洞的源位置是packet.c:480:
if(message_len
值datalen不受信任,因为它是由远程SSH服务器控制的。例如,如果使用datalen == 11,则减法将溢出并且的边界检查message_len无效。message_len是一个32位无符号整数,它也由远程SSH服务器控制,因此这可能导致在第485行上越界读取:
language_len =
_libssh2_ntohu32(data + 9 + message_len);
越界读取通常只会导致分段错误,但是LIBSSH2_DISCONNECT在 第499行的调用中也有可能导致其他类型的问题:
if(session->ssh_msg_disconnect) {
LIBSSH2_DISCONNECT(session, reason, message,
message_len, language, language_len);
}
取决于libssh2库的使用方式,因为session->ssh_msg_disconnect是一个回调函数,默认情况下为null,但可以由该库的用户设置(通过调用libssh2_session_callback_set)。
我编写了一个 漏洞PoC ,其中恶意SSH服务器使用datalen == 11和返回断开连接消息message_len == 0x41414141,会导致libssh2因分段错误而崩溃。
0x04 libssh2中整数溢出的变体分析
我在2019年6月底之前写了关于libssh2的最后一篇博客文章。为了准确了解该博客文章的技术细节,我需要回过头来再看一下libssh2代码。我注意到许多粗心的边界检查代码,例如上述错误。
变体分析是关于漏洞的所有变体,并在可能的情况下,创建可在多个代码库之间重用的查询。但是我的目标集中在libssh2和我发现的漏洞上。
当我向供应商报告安全漏洞时,通常会尝试在报告中包括两点:
1. 一个漏洞的PoC。
2. 一个QL查询,它标识了我认为存在漏洞的的所有代码位置。
QL查询和PoC有几个好处:
1. 如果代码中包含几个非常相似的错误,那么我可以编写一个列举所有漏洞的查询。
2. 该查询使我能够轻松地检查漏洞是否已修复(在90天的最后期限即将到期时,这非常方便)。
3. 我可以将QL查询及其结果列表作为单个URL包括在内,这对我来说很方便,希望对接收者也很方便。
编写PoC通常需要大量工作,因此,如果存在多个非常相似的漏洞,那么我通常只为其中一个编写PoC。我的感觉是,一个PoC足以证明安全影响是真实的。下面的查询针对此用例进行了调整,该查询的目的不是在libssh2中找到所有整数溢出漏洞,而且由于查询中编码了某些libssh2特定的细节,它也不会扩展到其他代码库。
/**
* @kind path-problem
*/
import cpp
import semmle.code.cpp.rangeanalysis.SimpleRangeAnalysis
import semmle.code.cpp.dataflow.TaintTracking
import DataFlow::PathGraph
class Config extends DataFlow::Configuration {
Config() { this = "_libssh2_ntohl bounds check overflow" }
override predicate isSource(DataFlow::Node source) {