作者zplusplus

BitDefender:由7z PPMD产生的远程栈溢出漏洞

发布于:2017-08-11 18:14:59 阅读:101 回帖:0

如果你读我之前的一篇文章觉得无聊的话,那么这篇可能是你想要的。在该系列的第二篇中,我将履行讨论在更复杂环境设置中发生错误的承诺。

需要非常小心的处理提取特定文档格式(例如7z)软件模块中的错误。这不仅对于软件本身,而且对于共享同一个库或基于相同参考实现的许多不同的软件产品来说,这一点至关重要。

所以,来解决这个问题:我相信这个bug并不影响Igor Pavlov的7z的参考实现。但是,如果BitDefender以外的产品受到这个影响,那不会使我感到意外。


简介



在小型供应商的反病毒产品中发现重大漏洞后,我最终决定来探索一下Bitdefender的反病毒产品。因此,我开始了对引擎的模糊测试,一段时间之后就遇到了第一次崩溃,涉及到7z文件格式的崩溃。

7z确实相当复杂。文件格式本身就不简单,而且它还支持相当多的压缩方式。

幸运的是,这个bug只牵扯到了文件格式的一部分以及所谓的PPMd编码。PPMd是一个最初由Dmitry Shkarin开发的压缩算法。它通过部分匹配来使用预测,并将其与范围编码相结合。

实质上,部分匹配预测是建立一个试图给定n个前导符号预测下一个符号的模型。context存储了包含由n个最新符号组成的序列,常量n被称作模型的阶。

我希望这些基础信息足以理解接下来的内容。如果你想阅读更多的关于PPM的信息,我强烈建议你去读读Cleary和Witten的论文。或者Mark Nelson的博客文章也是不错的选择。


深入细节


如果没有调试符号,那么调试发生在7z代码中的反病毒软件会是一场噩梦!!

可能的补救措施是参考一下参考实现,并尝试来匹配函数名称。尽管BitDefender似乎重用了7-Zip的代码,但这也不是一件简单的事,因为编译器已经应用了大量的内联甚至函数内部优化。

一旦匹配了最重要的7-Zip函数,我们就可以在WinDbg中仔细来单步,并且轻松的观察到CreateSuccessors函数中栈分配ps时的缓冲区溢出。

在最新的7-Zip版本中,这个函数(上半部分)看起来是这样的:

static CTX_PTR CreateSuccessors(CPpmd7 *p, Bool skip) {
  CPpmd_State upState;
  CTX_PTR c = p->MinContext;
  CPpmd_Byte_Ref upBranch = (CPpmd_Byte_Ref)SUCCESSOR(p->FoundState);
  CPpmd_State *ps[PPMD7_MAX_ORDER]; /* PPMD7_MAX_ORDER==64 */
  unsigned numPs = 0;

  if (!skip) { ps[numPs++] = p->FoundState; }

  while (c->Suffix) {
    CPpmd_Void_Ref successor;
    CPpmd_State *s;
    c = SUFFIX(c); /* SUFFIX(c) == c->Suffix */
    if (c->NumStats != 1) {
      for (s = STATS(c); s->Symbol != p->FoundState->Symbol; s++);
    } else {
      s = ONE_STATE(c);
    }
    successor = SUCCESSOR(s);
    if (successor != upBranch) {
      c = CTX(successor);
      if (numPs == 0) return c;
      break;
    }
    ps[numPs++] = s;
  }

  /* ### Rest of function omitted. ### */
}

我们看到当前正在遍历context(一个链表),并且填充ps缓冲区。

令人惊讶的是,这里没有任何限制的检查。因此,如果这是原始7-Zip实现中的代码,这正确吗?

回想一下,模型的阶是指该context种能够存储的符号的个数。如果该context总是被正确的更新,它不应该包含比模型的阶更多的元素。

那么如何确保正确的更新呢?不管实际的机制如何,一定要知道模型的阶数。变量PPMD7_MAX_ORDER已经暗示了64是最大的阶。然而实际的阶数可能有所不同。7-Zip的源代码展示了我们正在寻找的内容:

STDMETHODIMP CDecoder::SetDecoderProperties2(const Byte *props, UInt32 size) {
  if (size < 5) { return E_INVALIDARG; }
  _order = props[0]; // <---------------------------------
  UInt32 memSize = GetUi32(props + 1);
  if (_order < PPMD7_MIN_ORDER ||
      _order > PPMD7_MAX_ORDER ||
      memSize < PPMD7_MIN_MEM_SIZE ||
      memSize > PPMD7_MAX_MEM_SIZE)
    return E_NOTIMPL;
  if (!_inStream.Alloc(1 << 20)) { return E_OUTOFMEMORY; }
  if (!Ppmd7_Alloc(&_ppmd, memSize, &g_BigAlloc)) { return E_OUTOFMEMORY };
  return S_OK;
}

(注意:该代码不是Bitdefender引擎中的代码)

可以看到,阶是从props数组中读取的。事实证明,props是直接从输入文件中获取的。更进一步,这是7z文件格式Folder结构中包含的Properties数组。

此外,我们还看到了参考实现确保了阶不会大于PPMD7_MAX_ORDER。

我的使其崩溃的输入文件中的阶是0x5D,并且Bitdefender的7z模块无论如何都可以解压缩。所以他们忽略了这个检查。于是导致了栈缓冲区溢出。


攻击者控制



攻击者可以完全控制阶的值,最大值是255。这允许攻击者插入最多255个指针到缓冲区中,产生191个溢出。这些指针指向下边这个类型的CPpmd_State结构(在Ppmd.h中定义)。

typedef struct {
  Byte Symbol;
  Byte Freq;
  UInt16 SuccessorLow;
  UInt16 SuccessorHigh;
} CPpmd_State;

注意:所有的结构体成员都是由攻击者控制的。


Exploitation 和碰撞



Bitdefender使用了statck canary(请问这是什么意思?),同时也使用了ASLR和DEP。

有趣的是,他们似乎并没有在系统的大部分地方使用SafeSEH。这样做的原因是Bitdefender的核心动态的从专有二进制插件文件格式(非Windows DLL)中加载大部分模块(比如7z模块)。更具体的说,引擎包含了一个用来分配内存的加载器,从文件系统中读取插件文件,将其解密和解压缩,最终进行代码重定位。因此,Bitdefender没有使用Windows PE映像加载器来执行大部分的代码,这样就非常难以应用完整的SafeSEH保护机制了。似乎Bitdefender通过避免在插件的代码使用异常处理来解决这个限制。

引擎是在没有沙箱保护的情况下以NT Authority\SYSTEM用户运行的。此外,由于软件在文件系统中使用了minifilter,因此可以轻松的远程触发漏洞完成利用,例如通过发送带有精心设计的文件作为邮件的附件发送给受害者。

还要注意,Bitdefender的引擎许可许多不同的反病毒供应商,如F-Secure或G Data,所有这些都可能受到这个bug的影响。


修复



Bitdefender 决定通过确保CreateSuccessors中一旦numPs(ps缓冲区的索引)达到了PPMD7_MAX_ORDER就抛出异常的方式来修复该bug。代码中仍然接受大于PPMD7_MAX_ORDER的值的阶数,但是,当解压过程中numPs==PPMD7_MAX_ORDER时就会中断解压缩过程。

选择这种设计是反病毒行业中常见的做法。这些人都喜欢以轻松的方式分析和处理文件。特别的,他们倾向于接受各种(部分)无效文件。

原因在于,消费者软件处理流行的文件格式(例如rar,zip或者7z)有许多不同的变体。放松文件解析和处理的目的是尽可能多的覆盖各种实现,并避免消费者软件可以成功处理而反病毒产品将文件解析为无效的这种场景。

这种观念来对待无效文件时,对PPMd阶的检查压根就被忽略了。


结论



我们已经看到,在软件中嵌入外来代码而不引发严重Bug是一个巨大的挑战。

当不能避免使用外部C/C++代码时,你应该竭尽全力的来review这些代码。比如,就这个bug,就可以相当容易的通过review来解决。

还要注意的是,它需要一个相当由争议的参数来解释为什么CreateSuccessors中的缓冲区不能溢出,因为阶的值不能大于PPMD7_MAX_ORDER。我甚至不想作出这样的论据,因为我认为不应该这样做。如果有没有缓冲区溢出的函数很大程度上取决于其他的函数,以及他们是如何更新状态的,我们会不会做一些极其错误的事情?

您有任何意见,反馈,疑问或投诉吗? 我很乐意听到他们的声音。 您可以在关于页面上找到我的电子邮件地址。


披露时间表


  • 02/11/2017 - 发现

  • 02/13/2017 - 厂商致谢及等待确认

  • 02/21/2017 - 确认并发布补丁

  • 02/28/2017 - 支付赏金


致谢


我要对BitDefender的快速响应以及快速发布补丁尤其是Marius表示感谢。在今天的反病毒行业,这(不幸)不是可以想当然的事情。


分析


如果你想快速测试7z实现是否是脆弱的,你可以尝试解压一下这个文件。这是一个7z压缩包,里边由一个仅包含ASCII字符串bar名为foo.txt的文本文件。foo.txt使用了阶为65(回想下参考实现中的PPMD7_MAX_ORDER=64)的PPMd压缩。

请注意,即使阶没有正确检查,该文件也不会产生栈缓冲区溢出。但是,如果软件成功的提取了文件foo.txt(恢复了bar这个字符串),这是阶数没有正确检查的非常明确的标志,你应该进一步检查它是否容易受到栈缓冲区溢出的影响。

1. http://www.7-zip.org/

2. http://compression.ru/ds/

3. https://en.wikipedia.org/wiki/Prediction_by_partial_matching

4. https://en.wikipedia.org/wiki/Range_encoding

5. http://ieeexplore.ieee.org/document/1096090/

6. http://marknelson.us/1991/02/01/arithmetic-coding-statistical-modeling-data-compression/#part2

7. 好吧,代码非常相似,但是有一些微笑的区别。例如, 他们已将大部分的C代码一直到C++中。

8. CreateSuccessors 位于 C/Ppmd7.c中。 7-Zip 17.00。请注意,Bitdefender中运行的实际代码略有不同。但是,我认为差异与这个bug无关。

9. CDecoder::SetDecoderProperties2 位于 CPP/7zip/Compress/PpmdDecoder.cpp 中。 7-Zip 17.00.

10.7z文件格式(部分) DOC/7zFormat.txt of the 7-Zip source package.

11. 这需要重写大量的并入产品的C++代码。 例如,7-Zip依赖于不同地方的异常处理。

12. G Data’s 的反病毒产品是我唯一明确检查过的, 绝对是受影响的。 如果他们使用Bitdefender的7z模块,其他产品也很可能受到影响。


返回