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)) < 0)
问题出现在_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) <= (int)(buf->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() < 0 and source.asExpr() = call and DataFlow::localFlow(source, sink) and sink.asExpr().getFullyConverted().getType().getUnderlyingType().(IntegralType).isUnsigned() and lowerBound(sink.asExpr()) < 0 select sink
查询代码如下:
r_len = _libssh2_get_c_string(&buf, &r); if(r_len <= 0) return -1;
该查询会返回负整数常量的函数。例如,_libssh2_get_c_string在773行上执行此操作,然后,它将查找对该函数的调用,这些调用返回值并将其转换为无符号类型。
0x03 触发漏洞
漏洞的源位置是packet.c:480:
if(message_len < datalen-13) {
值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) { source.asExpr().(FunctionCall).getTarget().getName().matches("_libssh2_ntoh%") } override predicate isSink(DataFlow::Node sink) { convertedExprMightOverflowNegatively(sink.asExpr()) and exists(RelationalOperation cmp | cmp.getAnOperand() = sink.asExpr()) } override predicate isAdditionalFlowStep(DataFlow::Node source, DataFlow::Node target) { exists(Field f | source.asExpr() = f.getAnAssignedValue() and target.asExpr() = f.getAnAccess()) or target.asExpr().(AddExpr).getAnOperand() = source.asExpr() or target.asExpr().(SubExpr).getAnOperand() = source.asExpr() } } from Config cfg, DataFlow::PathNode source, DataFlow::PathNode sink where cfg.hasFlowPath(source, sink) select sink, source, sink, "possible integer overflow of tainted expression in bounds check"
该查询与我的同事FermínSerna针对U-Boot NFS漏洞编写的查询非常相似。该isSource可以找出_libssh2_ntohu32和_libssh2_ntohu64,用于网络主机字节顺序转换。它查找包含可能会溢出的子表达式的比较操作。例如,message_len < datalen-13是一个比较表达式,其中子表达式datalen-13可能会溢出。我的查询还覆盖了可选isAdditionalFlowStep,我对该谓词进行了调整,以生成精确的近似变体漏洞列表。
0x05 缓解措施
这不是openssh中的漏洞,因此它不会影响ssh(https://linux.die.net/man/1/ssh)。 libssh2是客户端C库,使应用程序可以连接到SSH服务器。其次,这不是libssh的漏洞,后者是一个不相关的C库,提供与libssh2类似的功能。
该漏洞存在于libssh2 1.8.2和更早版本中,已在libssh2版本1.9.0中得到修复。除了升级到1.9.0版之外,没有任何缓解此漏洞的方法。
该漏洞是越界读取,可能导致远程信息泄露。使用libssh2连接到恶意SSH服务器时将触发该事件。溢出发生在Diffie Hellman密钥交换期间,可以在完成身份验证之前在连接过程的早期触发漏洞。libssh2接收uint32_t来自恶意服务器的请求,并且对此没有任何限制。然后,libssh2从所指定的偏移量中读取uint32_t内存。我写了一个漏洞PoC,其中恶意的SSH服务器返回非常大的偏移值,这会导致libssh2因分段错误而崩溃。但是,我认为,更仔细选择的偏移量可能会导致信息泄露,因为读取的内存随后会返回给服务器。可利用性将取决于堆栈布局,由于libssh2只是一个库,因此将根据使用该库的应用程序而有所不同。