U-Boot NFS RCE漏洞(CVE-2019-14192)

U-Boot NFS RCE漏洞(CVE-2019-14192)

原文:https://blog.semmle.com/uboot-rce-nfs-vulnerability/

翻译:看雪翻译小组 - lipss

校对:看雪翻译小组 - Nxe


这篇文章是关于U-Boot引导加载程序中的13个远程执行代码漏洞的,我和我的同事Pavel Avgustinov和Kevin Backhouse发现了这些漏洞。当U-Boot配置为使用网络来获取下一阶段的启动资源时,可以触发这些漏洞。

请注意,该漏洞尚未通过https://gitlab.denx.de/u-boot/u-boot进行修补,并且我应U-Boot的主要托管人Tom Rini的要求将这些漏洞公开。有关更多信息,请查看下面的时间表。

MITER已针对这13个漏洞发布了以下CVE:CVE-2019-14192,CVE-2019-14193,CVE-2019-14194,CVE-2019-14195,CVE-2019-14196,CVE-2019-14197,CVE-2019 -14198,CVE-2019-14199,CVE-2019-14200,CVE-2019-14201,CVE-2019-14202,CVE-2019-14203和CVE-2019-14204

什么是U-Boot?

Das U-Boot(通常称为“通用引导加载程序”)是一种流行的主引导加载程序,广泛用于嵌入式设备中,以从不同来源获取数据并运行下一阶段的代码,通常(但不限于)Linux内核。IoT,Kindle和ARM ChromeOS设备通常使用它。

U-Boot支持从不同的文件分区格式(例如ext4),以及网络(TFTP和NFS)获取下一阶段的代码。请注意,U-boot支持验证启动,在其中检查获取的映像是否被篡改。这样可以减轻使用不安全的明文协议(例如TFTP和NFS)的风险。因此签名检查之前的任何漏洞都可能意味着设备越狱。

我正在使用U-boot,会受到影响吗?

这些漏洞影响非常特定的U-Boot配置,其中指示U-Boot使用网络。这些漏洞中的一些存在于NFS解析代码中,而其他一些则存在于通用TCP / IP堆栈中。

此配置通常用于无盘IoT部署和快速开发过程中。

有什么影响?

通过这些漏洞,同一网络(或控制恶意NFS服务器)中的攻击者可以在U-Boot驱动的设备上执行代码。由于此漏洞的性质,利用似乎并不十分复杂,尽管可以通过使用堆栈cookie,ASLR或其他运行时和编译时的内存保护机制来提高其挑战性。

明白了,这些漏洞是什么?

通过源代码审查在2个非常相似的事件中发现了第一个漏洞,我们使用了Semmle的LGTM.comQL来查找其他漏洞。它是普通的memcpy溢出,攻击者控制的大小来自网络数据包,没有任何验证。

该问题存在于nfs_readlink_reply解析来自网络的nfs答复的函数中。它解析4个字节,并且无需进一步验证,就在两个不同位置中将它们用作memcpy的长度。

static int nfs_readlink_reply(uchar *pkt, unsigned len)
{
	[...]

	/* new path length */
	rlen = ntohl(rpc_pkt.u.reply.data[1 + nfsv3_data_offset]);

	if (*((char *)&(rpc_pkt.u.reply.data[2 + nfsv3_data_offset])) != '/') {
		int pathlen;

		strcat(nfs_path, "/");
		pathlen = strlen(nfs_path);
		memcpy(nfs_path + pathlen,
		       (uchar *)&(rpc_pkt.u.reply.data[2 + nfsv3_data_offset]),
		       rlen);
		nfs_path[pathlen + rlen] = 0;
	} else {
		memcpy(nfs_path,
		       (uchar *)&(rpc_pkt.u.reply.data[2 + nfsv3_data_offset]),
		       rlen);
		nfs_path[rlen] = 0;
	}
	return 0;
}

目标缓冲区nfs_path是一个全局缓冲区,最多可容纳2048个字节。

使用QL的变异分析

以下查询为我们提供了9个列表,以便手动进行跟踪。查询背后的想法是从任何辅助函数(例如ntohl()/ ntohs()...)到memcpy的size参数来执行数据流分析。

import cpp

import semmle.code.cpp.dataflow.TaintTracking
import semmle.code.cpp.rangeanalysis.SimpleRangeAnalysis

class NetworkByteOrderTranslation extends Expr {
  NetworkByteOrderTranslation() {
    // On Windows, there are ntoh* functions.
    this.(Call).getTarget().getName().regexpMatch("ntoh(l|ll|s)")
    or
    // On Linux, and in some code bases, these are defined as macros.
    this = any(MacroInvocation mi |
        mi.getOutermostMacroAccess().getMacroName().regexpMatch("(?i)(^|.*_)ntoh(l|ll|s)")
      ).getExpr()
  }
}

class NetworkToMemFuncLength extends TaintTracking::Configuration {
  NetworkToMemFuncLength() { this = "NetworkToMemFuncLength" }

  override predicate isSource(DataFlow::Node source) {
       source.asExpr() instanceof NetworkByteOrderTranslation
  }
  
  override predicate isSink(DataFlow::Node sink) {
    exists (FunctionCall fc |
        fc.getTarget().getName().regexpMatch("memcpy|memmove") and
        fc.getArgument(2) = sink.asExpr() )
  } 
 
}

from Expr ntoh, Expr sizeArg, NetworkToMemFuncLength config
where config.hasFlow(DataFlow::exprNode(ntoh), DataFlow::exprNode(sizeArg))
select ntoh.getLocation(), sizeArg

能不能找到相似函数产生的漏洞?

虽然有些数据在从源到接收器的数据流之间进行了大小检查,但发现有些数据是可利用的。另外我还通过源代码审计发现了其他相似函数。

nfs_lookup_reply中失败的长度检查导致未限界memcpy

nfs_lookup_reply函数再次解析来自网络的nfs答复时存在问题。解析4个字节并在两个不同位置中用作memcpy的长度。

指定长度的部分代码,让拷贝的部分不会大于分配的缓冲区。但是依然可以用负值绕过这个判断语句,之后还是可以缓冲区溢出。

filefh3_length = ntohl(rpc_pkt.u.reply.data\[1]);
if (filefh3_length > NFS3_FHSIZE)
    filefh3_length  = NFS3_FHSIZE;

memcpy(filefh, rpc_pkt.u.reply.data + 2, filefh3_length);

目标缓冲区filefh是一个全局缓冲区,最多可容纳64个字节。

nfs_read_reply/store_block中失败的长度检查导致未限界memcpy

nfs_read_reply读取文件并将其存储到另一种介质(闪存或物理存储器)中以供以后处理时,该函数中存在此问题。同样,数据和长度由攻击者完全控制,并且从未验证。

static int nfs_read_reply(uchar *pkt, unsigned len)
{        [...]

	if (supported_nfs_versions & NFSV2_FLAG) {
		rlen = ntohl(rpc_pkt.u.reply.data[18]); // <-- rlen is attacker-controlled could be 0xFFFFFFFF
		data_ptr = (uchar *)&(rpc_pkt.u.reply.data[19]);
	} else {  /* NFSV3_FLAG */
		int nfsv3_data_offset =
			nfs3_get_attributes_offset(rpc_pkt.u.reply.data);

		/* count value */
		rlen = ntohl(rpc_pkt.u.reply.data[1 + nfsv3_data_offset]); // <-- rlen is attacker-controlled
		/* Skip unused values :
			EOF:		32 bits value,
			data_size:	32 bits value,
		*/
		data_ptr = (uchar *)
			&(rpc_pkt.u.reply.data[4 + nfsv3_data_offset]);
	}

	if (store_block(data_ptr, nfs_offset, rlen)) // <-- We pass to store_block source and length controlled by the attacker
			return -9999;

	[...]
}

关注store_block函数的物理内存部分,尝试使用特定于arch的函数map_physmem保留一些内存,最终调用phys_to_virt。正如在x86实现中所看到的那样,在保留物理内存时显然忽略了长度,函数返回值提供了原始指针,而没有增加判断内存区域里还有没有保留其他数据。

static inline void *phys_to_virt(phys_addr_t paddr)
{
        return (void *)(unsigned long)paddr;
}

其后在store_block中有一个攻击者控制的源和长度的memcpy缓冲区溢出。

static inline int store_block(uchar *src, unsigned offset, unsigned len)
{

[...]

 void *ptr = map_sysmem(load_addr + offset, len); // <-- essentially this is ptr = load_addr + offset
 memcpy(ptr, src, len); // <-- unrestricted overflow happens here
 unmap_sysmem(ptr);

[...]

}

flash_write代码路径中也可能存在类似的问题。

由于整数下溢,在解析UDP数据包时出现未限界memcpy

函数net_process_received_packet未经验证的情况下使用ip->udp_len会导致整数下溢。其后,此字段将用于通过net_set_udp_handler(DNS,dhcp,...)设置的memcpyatnc_input_packet和所有udp数据包处理函数中。

#if defined(CONFIG_NETCONSOLE) && !defined(CONFIG_SPL_BUILD)
                nc_input_packet((uchar *)ip + IP_UDP_HDR_SIZE,
                                src_ip,
                                ntohs(ip->udp_dst),
                                ntohs(ip->udp_src),
                                ntohs(ip->udp_len) - UDP_HDR_SIZE); // <- integer underflow
#endif
                /*
                 * IP header OK.  Pass the packet to the current handler.
                 */
                (*udp_packet_handler)((uchar *)ip + IP_UDP_HDR_SIZE,
                                      ntohs(ip->udp_dst),
                                      src_ip,
                                      ntohs(ip->udp_src),
                                      ntohs(ip->udp_len) - UDP_HDR_SIZE); // <- integer underflow

请注意,我们并未审计为不同目的(DNS,DHCP等)设置的所有潜在udp处理程序。但是,我们确实对nfs_handler进行了审计,如下所述。

响应辅助函数nfs_handler中多个基于堆栈的缓冲区溢出

这是上述漏洞的代码审查变体。在此,当解析很大的ip->udp_len参数的udp数据包时会发生整数下溢,随后又调用nfs_handler。在此函数中,同样没有对长度的验证,我们将调用辅助函数,例如nfs_readlink_reply。该函数盲目使用长度而不进行验证,从而导致基于堆栈的缓冲区溢出。

static int nfs_readlink_reply(uchar *pkt, unsigned len)
{
    struct rpc_t rpc_pkt;

    [...]

        memcpy((unsigned char *)&rpc_pkt, pkt, len);

我们确定了5个不同的易受攻击的函数,它们遵循相同的代码模式,从而导致基于堆栈的缓冲区溢出。除了nfs_readlink_reply之外,还有:

  • rpc_lookup_reply
  • nfs_mount_reply
  • nfs_umountall_reply
  • nfs_lookup_reply

nfs_read_reply中读取越界数据

这与以前的漏洞非常相似。开发人员试图在复制来自套接字的数据时执行大小检查,以保持谨慎。当他们检查以防止缓冲区溢出时,他们没有检查源缓冲区中是否有足够的数据,从而导致潜在的读取越界访问冲突。

static int nfs_read_reply(uchar *pkt, unsigned len)
{
    struct rpc_t rpc_pkt;

    [...]

        memcpy(&rpc_pkt.u.data[0], pkt, sizeof(rpc_pkt.u.reply));

攻击者可以向NFS数据包提供读取请求和发送给套接字的较小数据包请求。

有什么建议吗?

为了缓解这些漏洞,只有两种选择:

  • 补丁发布后立即应用,或者
  • 处于可攻击时,请勿通过NFS或任何U-Boot网络功能挂载文件系统

披露时间表

此漏洞报告受披露政策的约束,在此处可见https://lgtm.com/security/#disclosure_policy

  • 2019年5月15日-Fermín Serna最初发现了两个漏洞,并编写了一个QL查询,以发现另外三个有问题的调用处。
  • 2019年5月16日-Pavel Avgustinov带来了一些QL魔术,概括了查询并找到了更多解析ip和udp标头的函数。
  • 2019年5月23日-Kevin Backhouse通过nfs_handler向Pavel和Fermín警告有关基于堆栈的缓冲区溢出的疏忽。
  • 2019年5月23日-Semmle安全团队结束调查并通过电子邮件联系维护者。
  • 2019年5月24日-Tom Rini(U-Boot的主要托管人)确认收到安全报告。
  • 2019年7月19日-Tom Rini请求在其公共邮件列表u-boot@lists.denx.de上公开此报告。
  • 2019年7月22日-为了避免周末披露,Fermin将该报告通过u-boot@lists.denx.de公开。
posted @ 2019-10-14 15:02  17bdw  阅读(693)  评论(0编辑  收藏  举报