用户
搜索
  • TA的每日心情
    奋斗
    2018-4-15 17:23
  • 签到天数: 2 天

    连续签到: 1 天

    [LV.1]初来乍到

    i春秋-核心白帽

    Rank: 4

    5

    主题

    8

    帖子

    83

    魔法币
    收听
    0
    粉丝
    0
    注册时间
    2017-5-3

    i春秋推荐小组

    发表于 2018-5-15 10:41:45 02025
    本帖最后由 TGhost 于 2018-5-15 11:02 编辑

    在我之前提交了关于7.zip的漏洞CVE-2017-17和CVE-2018-5996之后,我继续花时间去分析反病毒的软件。当它发生的时候,我发现了一个新的漏洞(像前两次一样)也对7.zip产生了影响。因为反病毒的厂商还没有发布补丁,一旦发生这种情况,我将在此更新中添加受影响产品的名称。

    介绍
    7.zip的rar代码大多数是基于最近的unrar版本,但是尤其是被大量修改的高层次部分的代码。我们能从我最些时间提交的博客中可以看出,UnRAR的代码非常脆弱。因此,及其令人惊讶的是任何对这个代码带来的改变都有可能引入新的漏洞。

    这个漏洞能够被抽象的描述为以下几点:RAR解码类最初的成员数据结构基于RAR处理器在解一些代码之前去正确的配置解码器。不幸的是,RAR的处理器不能去清理输入的数据并把不正确配置传递到解码器,造成了没有被初始化的内存的使用。

    现在你也许会认为着听起来无害但是无聊。我承认,当我最初发现这个漏洞的时候所想的。令人惊讶的是,这是无害的。

    在下面,我将详细的列出漏洞。接着我们将会简短的看7-Zip的补丁。最后,我们将会看到代码会怎么会被远程的代码执行利用。

    漏洞(CVE-2018-20115)
    在处理固态的压缩的时候这个新的漏洞产生。固态压缩的注意很简单:假设一些文件,我们能够将他们解释为对单个数据块的串接,然后压缩整个块(而不是压缩每一个文件)。这可以放弃一个高的压缩率,尤其是当那里有很多文件在某些方面相同。

    在RAR格式(在第五版本之前),固态压缩方式能够被用在一个非常便利的方式:每一个物体(代表一个文件)的档案能够被标记为固态的,独立的于所有其他的物品。如果有一个固态的比特位解码的物品,这个解码器将不会再一次初始化它的状态,基本上继续物体之前的状态。

    显然,需要确保解码器对象在开始时初始化其状态(对于正在解码的第一个项目)。让我们看看这是如何在7-Zip中实现的。 RAR处理程序有一个方法NArchive :: NRar:: CHandler :: Extract1,它包含一个循环,该循环遍历所有项目的变量索引。在这个循环中,我们可以找到以下代码:
    [AppleScript] 纯文本查看 复制代码
    f (solidStart) {[/align][align=left]  isSolid = 0;
      solidStart = false;
    }
    RINOK(compressSetDecoderProperties->SetDecoderProperties2(&isSolid, 1));
    

    基本的想法是有一个布尔类型的标志solidStart,它被初始化为true(在循环之前),确保为解码的第一项解码器配置isSolid == false。此外,每当使用isSolid == false调用解码器时,解码器将(重新)初始化其状态(在开始解码之前)。

    这似乎是正确的,对吗?那么,问题是RAR支持三种不同的编码方法(不包括版本5),并且每个项目可以用不同的方法编码。具体而言,对于这三种编码方法中的每一种,都存在不同的解码器对象。有趣的是,这些解码器对象的构造函数将其状态的很大一部分未初始化。这是因为无论如何该状态需要重新初始化为非固定项目,隐含的假设是解码器的调用者将确保解码器的第一次调用是isSolid == false。我们可以通过构建如下的RAR存档轻松推翻这一假设2:
    第一项使用编码方法v1。
    第二项使用编码方法v2(或v3),并设置固体位。

    第一项将导致solidStart标志设置为false。然后,对于第二项,创建一个新的Rar2解码器对象,并且(由于固体标志被设置),解码在大部分解码器状态未初始化的情况下运行。

    乍一看,这可能不会太糟糕。但是,未初始化状态的各个部分可能会导致内存损坏:
    保存基于堆的缓冲区大小的成员变量。这些变量现在可以保持比实际缓冲区大的大小,从而允许基于堆的缓冲区溢出。
    具有用于索引到其他数组中的索引的数组,用于读取和写入数值。
    在我以前的帖子中讨论过的PPMd状态。回想一下,代码在很大程度上依赖于模型状态的正确性,现在可以很容易地违反。
    显然,这个清单并不完整。

    修正
    实质上,错误在于解码器类不能保证它们的状态在第一次被使用之前被正确初始化。相反,他们依赖调用者在解码第一个项目之前用isSolid == false配置解码器。正如我们所看到的,这并不是很好。

    有两种不同的方法来解决这个错误:
    1、使解码器类的构造函数初始化完整状态。
    2、向每个解码器类添加一个额外的布尔成员solidAllowed(初始化为false)。如果isSolid == true,即使solidAllowed ==false,解码器也会失败(或设置isSolid = false)。

    UnRAR似乎实施了第一种选择。然而,Igor Pavlov选择了7-Zip的第二种选择。

    如果你想修补7-Zip的分支,或者你只是对修复的细节感兴趣,你可能想看看这个文件,它总结了这些修改。

    减缓剥削
    在上一篇关于7-Zip错误CVE-2017-17969和CVE-2018-5996的文章中,我提到了在版本18.00之前的7-Zip中缺少DEP和ASLR(测试版)。在该博客发布后不久,Igor Pavlov发布了带有/ NXCOMPAT标志的7-Zip 18.01,履行了他在所有平台上启用DEP的承诺。此外,所有动态库(7z.dll,7-zip.dll,7-zip32.dll)都有/ DYNAMICBASE标志和一个重定位表。因此,大部分运行代码都受到ASLR的约束。

    但是,所有主要可执行文件(7zFM.exe,7zG.exe,7z.exe)都没有/ DYNAMICBASE,并且具有剥离的重定位表。这意味着他们不仅不受ASLR约束,而且甚至不能使用EMET或其后继者Windows DefenderExploit Guard等工具强制执行A​​SLR。

    显然,ASLR只有在所有模块都被正确随机化的情况下才有效。我和Igor讨论了这个问题,并说服他用新的7-Zip 18.05的主要可执行文件发布/ DYNAMICBASE和重定位表。 64位版本仍然使用标准的非高熵ASLR(大概是因为图像基础小于4GB),但这是一个小问题,可以在未来的版本中解决。

    另外,我想指出7-Zip从不分配或映射额外的可执行内存,使其成为任意代码防护(ACG)的理想选择。如果您使用的是Windows 10,则可以通过在Windows Defender安全中心中添加主要可执行文件7z.exe,7zFM.exe和7zG.exe(应用程序和浏览器控制 - >漏洞利用防护 - >程序设置)。这基本上会执行W ^ X策略,因此使代码执行的开发更加困难。

    编写代码执行漏洞
    通常情况下,我不会花太多时间考虑实际的武器利用。但是,如果只是为了了解在特定情况下实际取得成功需要多少时间,编写漏洞利用有时可能是有益的。
    我们的目标平台是全面更新的Windows 10Redstone 4(RS4,Build 17134.1)64位,运行7-Zip 18.01 x64。

    选择一个适当的开发情景
    使用7-Zip提取档案有三种基本方法:

    1、使用GUI打开存档并分别提取文件(使用拖放操作)或使用“提取”按钮提取整个存档。
    2、右键单击存档并从上下文菜单中选择“7-Zip->Extract Here”或“7-Zip-> Extract tosubfolder”。
    3、使用7-Zip的命令行版本。

    这三种方法中的每一种都会调用不同的可执行文件(7zFM.exe,7zG.exe,7z.exe)。由于我们想要利用这些模块中缺少的ASLR,我们需要修复提取方法。
    第二种方法(通过上下文菜单提取)似乎是最有吸引力的方法,因为它是一种可能经常使用的方法,同时它应该给我们一个相当可预测的行为(不同于第一种方法,其中a用户可能会决定打开存档,但然后提取“错误”的文件)。因此,我们采用第二种方法。

    开发战略
    使用上面的错误,我们可以创建一个Rar解码器,该解码器在(大部分)未初始化的状态下运行。因此,让我们看看哪个Rar解码器可以让我们以攻击者控制的方式破坏堆。

    一种可能性是使用Rar1解码器。 NCompress ::NRar1 :: CDecoder :: HuffDecode3方法包含以下代码:
    [C] 纯文本查看 复制代码
    int bytePlace = DecodeNum(...);
    // some code omitted
    bytePlace &= 0xff;
    // more code omitted
    for (;;)
    {
      curByte = ChSet[bytePlace];
      newBytePlace = NToPl[curByte++ & 0xff]++;
      if ((curByte & 0xff) > 0xa1)
        CorrHuff(ChSet, NToPl);
      else
        break;
    }
    ChSet[bytePlace] = ChSet[newBytePlace];
    ChSet[newBytePlace] = curByte;
    return S_OK;
    

    这非常有用,因为Rar1解码器的未初始化状态包括uint32_t数组ChSet和NtoPl。因此,newBytePlace是攻击者控制的uint32_t,curByte也是如此(限制最低有效字节不能大于0xa1)。而且,bytePlace由输入流决定,所以它也是攻击者控制的(但不能大于0xff)。

    所以这会给我们一个相当不错的(虽然不是完美的)读写原语。但是请注意,我们处于64位地址空间,所以我们将无法到达具有32位偏移量(即使乘以来自ChSet的sizeof(uint32_t))的Rar1解码器对象的vtable指针。因此,我们将把放在Rar1解码器之后的对象的vtable指针指向堆。

    我们的想法是为此使用Rar3解码器对象,我们将同时使用它来保存我们的有效载荷。特别是,我们使用上面的RW-primitive来交换作为Rar3解码器的成员变量的指针_windows与同一个Rar3解码器对象的vtable指针。 _窗口指向一个4MB大小的缓冲区,该缓冲区保存已用解码器提取的数据(即,它完全由攻击者控制)。

    当然,我们将填充_window缓冲区的堆栈数据透视表(xchg rax,rsp)的地址,然后是一个ROP链来获得可执行的内存并执行shellcode(我们也将其放入_window缓冲区中)。

    在堆上放置一个替换对象
    为了取得成功,我们需要完全控制解码器的未初始化内存。粗略地说,我们将通过分配Rar1解码器对象的大小,将所需数据写入其中,然后在实际Rar1解码器分配之前的某个时间释放它。

    显然,我们需要确保Rar1解码器的分配实际上重用了我们之前释放的同一块内存。实现这一目的的直接方法是在相应的分配大小上激活低碎片堆(LFH),然后用多个替换对象喷射LFH。这实际上起作用,但是因为自Windows 8以来LFH上的分配随机化,所以此方法将永远无法将Rar1解码器对象放置在与任何其他对象保持恒定距离的位置。因此,我们尽量避免LFH,并将我们的对象放在常规堆上。非常粗略的分配策略如下:
    1、创建大约18个小于Rar1解码器对象的所有(相关)尺寸的未决分配。这将激活这些分配大小的LFH,并防止这样的小分配破坏我们的清理堆结构。
    2、分配替换对象并释放它,确保它被繁忙的分配包围(因此不会与其他空闲块合并)。
    3、Rar3解码器被分配(由于Rar3解码器比Rar1解码器大,所以替换对象不被重用。
    4、分配Rar1解码器(重新使用替换对象)。

    请注意,在分配Rar1解码器之前分配一些解码器是不可避免的,因为只有这样solidStart标志将被设置为false,并且下一个解码器将不会被正确初始化(见上文)。
    如果一切按计划进行,则Rar1解码器会重用我们的替换对象,并且Rar3解码器对象会在Rar1解码器对象之后以一定的偏移量放置。

    在堆上分配和释放
    显然,上述分配策略要求我们能够以合理控制的方式进行堆分配。通过RAR处理程序的整个代码,我找不到很多好方法对默认进程堆进行动态分配,这些进程堆具有攻击者控制的大小并存储攻击者控制的内容。事实上,似乎做这种动态分配的唯一方法是通过存档项目的名称。让我们看看这是如何工作的。

    打开存档时,方法NArchive :: NRar:: CHandler :: Open21会使用以下代码(简化)读取存档的所有项目:
    [C] 纯文本查看 复制代码
    CItem item;
    for (;;)
    {
      // some code omitted
      bool filled;
      archive.GetNextItem(item, getTextPassword, filled, error);
      // some more code omitted
      if (!filled) {
        // some more code omitted
        break;
      }
      if (item.IgnoreItem()) { continue; }
      bool needAdd = true;
      // some more code omitted
      _items.Add(item);
    }
    
    类CItem有一个名为AString的成员变量名,它将相应项的(ASCII)名称存储在堆分配的缓冲区中。

    不幸的是,项目的名称在NArchive :: NRar:: CInArchive :: ReadName1中设置如下:
    [C] 纯文本查看 复制代码
    for (i = 0; i < nameSize && p[i] != 0; i++) {}
    item.Name.SetFrom((const char *)p, i);
    

    我不幸地说,因为这意味着我们不能将完全任意的字节写入缓冲区。特别是,我们似乎无法写入空字节。这很糟糕,因为我们想要放在堆上的替换对象需要几个零字节。所以,我们能做些什么?那么,让我们看看AString ::SetFrom4:
    [C] 纯文本查看 复制代码
    void AString::SetFrom(const char *s, unsigned len)
    {
      if (len > _limit)
      {
        char *newBuf = new char[len + 1];
        delete []_chars;
        _chars = newBuf;
        _limit = len;
      }
      if (len != 0)
        memcpy(_chars, s, len);
      _chars[len] = 0;
      _len = len;
    }
    

    好的,所以这个方法总是会以空字节结束字符串。此外,我们看到AString保持相同的底层缓冲区,除非它太小而不能容纳所需的字符串。这产生了以下想法:假设我们要将十六进制字节DEAD00BEEF00BAAD00写入某个堆分配缓冲区。然后,我们将只保存一个包含以下名称的项目的存档(按列出的顺序):
    • DEAD55BEEF55BAAD
    • DEAD55BEEF


    基本上,我们让方法SetFrom写入我们需要的所有空字节。请注意,我们用一些任意非零字节(本例中为0x55)替换了我们数据中的所有空字节,确保将完整字符串写入缓冲区。

    这工作相当好,我们可以用它来写任意字节序列,有两个小的限制。首先,我们必须用空字节结束我们的序列。其次,我们的字节序列中不能有太多的空字节,因为这会导致档案大小的二次爆炸。幸运的是,我们可以在特定情况下轻松处理这些限制。

    最后,请注意,我们可以制定两种类型的分配:
    • 用项目分配item.IgnoreItem()== true。这些项目不会被添加到列表_items中,因此只是暂时的。这些分配具有最终将被释放的属性,并且他们可以(使用上述技术)填充几乎任意字节的序列。由于这些分配都是通过同一个堆栈分配的对象项来完成的,因此使用相同的AString对象,因此这种类型的分配大小需要严格增加其大小。我们将主要使用这种分配类型来将替换对象放在堆上。
    • 用项目分配item.IgnoreItem()== false。这些项目将被添加到列表_items中,导致相应名称的副本。这特别有用于导致许多待定的特定大小的分配以激活LFH。请注意,复制的字符串不能包含任何空字节,这对我们的目的来说很好。

    仔细组合所概述的方法,我们可以构建一个存档,实现上一节中的堆分配策略。

    ROP
    我们利用主要可执行文件7zG.exe上缺少ASLR来绕过带有ROP链的DEP。 7-Zip从不调用VirtualProtect,所以我们读取VirtualAlloc,memcpy的地址,并从导入地址表中退出,以编写以下ROP链:
    [C] 纯文本查看 复制代码
    // pivot stack: xchg rax, rsp;
    exec_buffer = VirtualAlloc(NULL, 0x1000, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    memcpy(exec_buffer, rsp+shellcode_offset, 0x1000);
    jmp exec_buffer;
    exit(0);
    

    由于我们在x86_64上运行(大多数指令的编码比x86中的编码更长),并且二进制文件不是很大,所以对于我们要执行的一些操作,没有整洁的小工具。这不是一个真正的问题,但它使得ROP链条有点难看。例如,为了在调用VirtualAlloc之前将寄存器R9设置为PAGE_EXECUTE_READWRITE,我们使用以下链接的小工具:
    0x40691e, #pop rcx; add eax,0xfc08500; xchg eax, ebp; ret;
    PAGE_EXECUTE_READWRITE,#value that is popped into rcx
    0x401f52, #xor eax, eax;ret; (setting ZF=1 for cmove)
    0x4193ad, #cmove r9, rcx;imul rax, rdx; xor edx, edx; imul rax, rax, 0xf4240; div r8; xor edx, edx; divr9; ret;


    演示
    下面的演示视频简要介绍了在新安装并全面更新的Windows 10 RS4(Build 17134.1)64位和7-Zip 18.01 x64上运行的漏洞利用情况。如上所述,目标利用情景是通过上下文菜单7-Zip->Extract Here和7-Zip-> Extract tosubfolder进行提取。

    可靠性
    在对辅助堆分配大小进行了一些微调后,该漏洞似乎非常可靠地工作。

    为了获得更多关于可靠性的信息,我编写了一个小脚本,它以与通过上下文菜单提取制作的存档时调用二进制7zG.exe相同的方式重复调用该二进制文件。此外,该脚本检查calc.exe是否实际启动并且过程7zG.exe以代码0退出。在不同的Windows操作系统(全部更新)上运行该脚本,结果如​​下所示:
    • Windows 10 RS4(内部版本17134.1)64位:漏洞利用失败5 17在100 000次之内。
    • Windows 8.1 64位:漏洞利用在10万次中失败12次。
    • Windows 7 SP1 64位:漏洞利用在100 000次以内失败90次。


    请注意,在所有操作系统中,都使用完全相同的制作存档。这很有效,大概是因为Windows 7和Windows 10堆实现之间的大多数变化都会影响低碎片堆,而其余部分并没有太多变化。此外,LFH仍然触发相同数量的待决分配。

    无可否认,通过实证确定利用的可靠性是不太可能的。不过,我认为这比“我跑了几次,似乎是可靠的”更好。

    结论
    在我看来,这个错误是从UnRAR设计(部分)继承的结果。如果一个类依赖于它的客户正确使用它以防止使用未初始化的类成员,那么注定会失败。
    我们已经看到这个(乍一看)无辜的外观错误可以变成可靠的武器代码执行漏洞。由于主要可执行文件缺少ASLR,攻击的唯一困难部分是在RAR提取的限制范围内执行堆处理。

    幸运的是,新的7-Zip 18.05不仅解决了这个错误,而且还在所有主要的可执行文件中启用了ASLR。
    你有任何意见,反馈,怀疑或投诉吗?我很乐意听到他们。您可以在关于页面找到我的电子邮件地址。
    或者,您被邀请加入关于HackerNews/ r / netsec的讨论。

    披露时间表
    2018-03-06  - 发现
    2018-03-06  - 报告
    2018-04-14  - MITRE分配给CVE-2018-10115
    2018-04-30  - 发布7-Zip 18.05,修复CVE-2018-10115并在可执行文件上启用ASLR。

    感谢和致谢
    我想感谢Igor Pavlov修复这个错误并在7-Zip中启用进一步的开发缓解措施。

    作者:landave
    翻译:i春秋翻译小组-Neo
    翻译来源:
    https://landave.io/2018/05/7-zip-from-uninitialized-memory-to-remote-code-execution/

    发新帖
    您需要登录后才可以回帖 登录 | 立即注册