原文:https://lgtm.com/blog/apple_xnu_nfs_vfsops_CVE-2018-4259

这篇文章讲述了我在Apple的macOS操作系统内核中发现的几个堆栈和堆缓冲区溢出。Apple将这些漏洞归类为内核中的远程代码执行漏洞,因此它们非常严重。攻击者可能会利用它们远程入侵Mac。或者,通过物理访问计算机,攻击者可以作为访客用户登录(无需密码),然后使用这些漏洞提升其权限并控制计算机。

这些漏洞存在于客户端NFS实现中,用于将网络驱动器(如NAS)安装到Mac的文件系统中。

严重程度和缓解

NFS被广泛使用,特别是在大型企业中,它用于共享驱动器和联网的主目录。但它也用于家庭NAS设备,通常用作媒体服务器。在macOS中,安装NFS共享不需要特殊权限,因此任何用户都可以触发漏洞,甚至是访客帐户(不需要密码)。此外,许多计算机(尤其是企业环境中的计算机)配置为在启动时自动挂载NFS共享。这意味着这些漏洞至少有两个攻击媒介:

  1. 这些漏洞可能被用于在使用NFS文件管理器的公司中快速传播病毒。要做到这一点,攻击者需要在文件管理器上安装恶意版本的NFS服务器软件,或者找到一种方法将恶意文件管理器放在网络上并开始拦截某些NFS流量。
  2. 这些漏洞可用于获取升级的权限。例如,有人可能以guest用户身份登录,然后发出单行命令连接到恶意NFS服务器(可能是Internet上的任何位置),并在计算机上获得内核级访问权限。

Apple分配了五个CVE,因为代码包含多个类似的错误:CVE-2018-4259,CVE-2018-4286,CVE-2018-4287,CVE-2018-4288和CVE-2018-4291。在我于2018年5月21日发送给Apple的漏洞报告中,我在源代码中列出了14个不同的位置,我认为这些位置是错误的。我只为其中两个发送了工作漏洞,所以我不完全确定这五个CVE对应的代码行。实际上,Apple最近才发布了macOS版本10.13.6的源代码,所以我还没有完成所有源代码更改的审核。(macOS版本10.13.4,10.13.3和10.13.6的源代码于2018年10月3日发布。)因此,为避免意外泄露任何可能未修复的错误,在本文中我只会谈关于我发送Apple工作概念验证漏洞的两个漏洞。

概念验证漏洞利用

我编写了一个概念验证漏洞,它用0覆盖4096字节的堆内存并导致内核崩溃。我做了一个简短的视频来证明这一点。4096是一个随意的选择 - 我本可以改变利用率来发送尽可能多的数据。任何大于128字节的数字都会触发堆缓冲区溢出。我也完全控制了写入的字节值。因此,尽管我的PoC只破坏了内核,但可以合理地假设可以使用这些缓冲区溢出来实现远程代码执行和本地权限提升。

当我第一次发现漏洞时,我必须编写自己的NFS服务器来创建PoC的想法似乎相当令人生畏。但在我学习了一些关于NFS协议以及如何使用rpcgen之后,结果却非常简单。我的漏洞利用PoC只包含46行C语言和63行RPC语言。我不会立即发布漏洞利用PoC,因为我想让Apple用户有机会首先升级他们的设备。但是,在不久的将来,我将在SecurityExploits存储库中发布漏洞利用PoC的源代码。

漏洞

我编写PoC的两个漏洞是在这个看似无害的代码行中(nfs_vfsops.c:4151):

nfsm_chain_get_fh(error, &nmrep, nfsvers, fh);

这行代码的目的是fh从nmrepNFS服务器发送回Mac 的回复消息()中读取文件句柄()。文件句柄是NFS共享上的文件或目录的不透明标识符。NFSv3中的文件句柄最多为64个字节,NFSv4中的文件句柄最多为128个字节(搜索FHSIZE)。fhandle_tXNU中的类型有足够的空间容纳128字节的文件句柄,但是他们忘记检查nfsm_chain_get_fh宏中的缓冲区溢出:

/* get the size of and data for a file handle in an mbuf chain */
#define nfsm_chain_get_fh(E, NMC, VERS, FHP) \
  do { \
    if ((VERS) != NFS_VER2) \
      nfsm_chain_get_32((E), (NMC), (FHP)->fh_len); \
    else \
      (FHP)->fh_len = NFSX_V2FH;\
    nfsm_chain_get_opaque((E), (NMC), (uint32_t)(FHP)->fh_len, (FHP)->fh_data);\
    if (E) \
      (FHP)->fh_len = 0;\
  } while (0)

由于宏的大量使用,这段代码很难遵循,但它的作用实际上非常简单:它从消息中读取一个32位无符号整数(FHP)->fh_len,然后直接从消息中读取该字节数(FHP)->fh_data。没有边界检查,因此攻击者可以使用他们选择的任何字节序列覆盖任意数量的内核堆。被覆盖的文件句柄的内存在nfs_socket.c:1401中分配。

我写的PoC for的第二个错误是nfsm_chain_get_opaque宏中的整数溢出:

/* copy the next consecutive bytes of opaque data from an mbuf chain */
#define nfsm_chain_get_opaque(E, NMC, LEN, PTR) \
  do { \
    uint32_t rndlen; \
    if (E) break; \
    rndlen = nfsm_rndup(LEN); \
    if ((NMC)->nmc_left >= rndlen) { \
      u_char *__tmpptr = (u_char*)(NMC)->nmc_ptr; \
      (NMC)->nmc_left -= rndlen; \
      (NMC)->nmc_ptr += rndlen; \
      bcopy(__tmpptr, (PTR), (LEN)); \
    } else { \
      (E) = nfsm_chain_get_opaque_f((NMC), (LEN), (u_char*)(PTR)); \
    } \
  } while (0)

此代码使用nfsm_rndup宏圆LEN高达4的倍数,但它使用的原始值LEN的调用bcopy。如果值LEN是0xFFFFFFFF然后加入在nfsm_rndup将溢出和的值rndlen将是0。这意味着与比较(NMC)->nmc_left会成功,bcopy将与被调用0xFFFFFFFF作为大小参数。这当然会导致内核崩溃,因此它只能用作拒绝服务攻击。

使用QL查找错误

QL的一大优势是能够找到已知错误的变种。今年早些时候,我的同事Jonas Jensen 在Apple的NFS无盘启动实现中发现了两个漏洞,CVE-2018-4136和CVE-2018-4160。我发布了一篇关于这些漏洞和发现它们的查询的博客文章。该查询旨在查找bcopy可能具有用户控制的大小参数的调用,该参数可能为负。一个简单的变体是查找bcopy用户控制源缓冲区的调用。这样的调用可能很有趣,因为它们将用户控制的数据复制到内核空间中。

/**
 * @name bcopy of network data
 * @description Copying a variable-sized network buffer into kernel memory
 * @kind path-problem
 * @problem.severity warning
 * @id apple-xnu/cpp/bcopy-negative-size
 */

import cpp
import semmle.code.cpp.dataflow.DataFlow
import semmle.code.cpp.rangeanalysis.SimpleRangeAnalysis
import DataFlow::PathGraph

class MyCfg extends DataFlow::Configuration {
  MyCfg() {
    this = "MyCfg"
  }

  override predicate isSink(DataFlow::Node sink) {
    exists (FunctionCall call
    | sink.asExpr() = call.getArgument(1) and
      call.getTarget().getName() = "__builtin___memmove_chk" and
      not call.getArgument(2).isConstant())
  }

  override predicate isSource(DataFlow::Node source) {
    source.asExpr().(FunctionCall).getTarget().getName() = "mbuf_data"
  }
}

from DataFlow::PathNode sink, DataFlow::PathNode source, MyCfg dataFlow
where dataFlow.hasFlowPath(source, sink)
select sink, source, sink, "bcopy of network data"

上面的查询相当简单,因为它查找对bcopy从mbuf内核空间复制数据的任何调用。只要正确检查size参数的边界,这样的调用就没有错。然而事实证明,很大一部分结果是nfsm_chain_get_fh宏的使用,它不进行任何边界检查。因此,尽管此查询很简单,但它发现了许多重要的安全漏洞。在其当前形式中,即使修复了错误,查询仍将继续报告相同的结果。最好改进查询,以便在有适当的边界检查时不会报告结果。

实现边界检查的常用方法是这样的:

if (n < limit) {
  bcopy(src, dst, n);
}

我写了这个谓词来检测上面的模式:

/**
 * Holds if `guard` is a bounds check which ensures that `size` is less than
 * `limit`. For example:
 * 
 *   if (size < limit) {
 *     ... size ...
 *   }
 */
predicate guardedSize(GuardCondition guard, Expr size, Expr limit,
                      RelationStrictness strict) {
  exists (boolean branch, Expr sz, BasicBlock block
  | guard.controls(block, branch) and
    block.contains(size) and
    globalValueNumber(size) = globalValueNumber(sz) and
    relOpWithSwapAndNegate(guard, sz, limit, Lesser(), strict, branch))
}

它使用Guards库来查找在受控制的控制流位置中使用的大小表达式guard。然后它使用globalValueNumber库来检查条件本身是否出现相同大小的表达式。该GlobalValueNumbering库使谓词能够检测非平凡大小表达式的相等性,如下所示:

if (packet.data.size < limit) {
  ... packet.data.size ...
}

最后,它使用一个名为的实用程序relOpWithSwapAndNegate来检查size表达式是否小于限制。它使谓词也能处理这样的场景:

if (packet.data.size >= limit) {
  return -1;
} else {
  ... packet.data.size ...
}

有时实现边界检查的另一种方式是通过调用min,如此谓词所检测到的:

/**
 * Holds if `size` is bounds checked with a call to `min`:
 * 
 *    size = min(n, limit);
 *
 *    ... size ...
 */
predicate minSize(Expr size) {
  exists (DataFlow::Node source, DataFlow::Node sink
  | DataFlow::localFlow(source, sink) and
    source.asExpr().(FunctionCall).getTarget().getName() = "min" and
    size = sink.asExpr())
}

我将这两个谓词组合如下:

/**
 * Holds if `size` has been bounds checked.
 */
predicate checkedSize(Expr size) {
  lowerBound(size) >= 0 and
  (guardedSize(_, size, _, _) or minSize(size))
}

请注意,我还习惯lowerBound确保不存在负整数溢出的可能性。唯一剩下要做的就是checkedSize在isSink方法中使用,以减少误报的数量。这是完成的查询:

/**
 * @name bcopy of network data
 * @description Copying a variable-sized network buffer into kernel memory
 * @kind path-problem
 * @problem.severity warning
 * @id apple-xnu/cpp/bcopy-negative-size
 */

import cpp
import semmle.code.cpp.valuenumbering.GlobalValueNumbering
import semmle.code.cpp.controlflow.Guards
import semmle.code.cpp.dataflow.DataFlow
import semmle.code.cpp.dataflow.TaintTracking
import semmle.code.cpp.rangeanalysis.RangeAnalysisUtils
import semmle.code.cpp.rangeanalysis.SimpleRangeAnalysis
import DataFlow::PathGraph

/**
 * Holds if `guard` is a bounds check which ensures that `size` is less than
 * `limit`. For example:
 * 
 *   if (size < limit) {
 *     ... size ...
 *   }
 */
predicate guardedSize(GuardCondition guard, Expr size, Expr limit,
                      RelationStrictness strict) {
  exists (boolean branch, Expr sz, BasicBlock block
  | guard.controls(block, branch) and
    block.contains(size) and
    globalValueNumber(size) = globalValueNumber(sz) and
    relOpWithSwapAndNegate(guard, sz, limit, Lesser(), strict, branch))
}

/**
 * Holds if `size` is bounds checked with a call to `min`:
 * 
 *    size = min(n, limit);
 *
 *    ... size ...
 */
predicate minSize(Expr size) {
  exists (DataFlow::Node source, DataFlow::Node sink
  | DataFlow::localFlow(source, sink) and
    source.asExpr().(FunctionCall).getTarget().getName() = "min" and
    size = sink.asExpr())
}

/**
 * Holds if `size` has been bounds checked.
 */
predicate checkedSize(Expr size) {
  lowerBound(size) >= 0 and
  (guardedSize(_, size, _, _) or minSize(size))
}

class MyCfg extends DataFlow::Configuration {
  MyCfg() {
    this = "MyCfg"
  }

  override predicate isSink(DataFlow::Node sink) {
    exists (FunctionCall call
    | sink.asExpr() = call.getArgument(1) and
      call.getTarget().getName() = "__builtin___memmove_chk" and
      not checkedSize(call.getArgument(2)))
  }

  override predicate isSource(DataFlow::Node source) {
    source.asExpr().(FunctionCall).getTarget().getName() = "mbuf_data"
  }
}

from DataFlow::PathNode sink, DataFlow::PathNode source, MyCfg dataFlow
where dataFlow.hasFlowPath(source, sink)
select sink, source, sink, "bcopy of network data"

在XNU上尝试QL
与大多数其他开源项目不同,XNU无法在LGTM上查询。这是因为LGTM使用Linux工作程序来构建项目,但XNU只能在Mac上构建。即使在Mac上,XNU也非常容易构建。如果我没有找到杰里米·安德鲁斯这篇非常有用的博客文章,我就无法做到。使用Jeremy Andrus的说明和脚本,我为三个最新发布的XNU版本手动构建了快照。:您可以从这些链接下载快照10.13.4,10.13.5,10.13.6。不幸的是,Apple尚未发布10.14(Mojave / iOS 12)的源代码,因此我无法创建QL快照来运行针对它的查询。要在这些QL快照上运行查询,您需要下载QL for Eclipse。可以在此处找到有关如何使用QL for Eclipse的说明。

源链接

Hacking more

...