从技术角度来看,2012 年 9 月发现的 Internet Explorer 零日漏洞是如何工作的?

信息安全 Web应用程序 网页浏览器 恶意软件 已知漏洞
2021-08-20 15:29:09

我正在寻找一个从开发人员/工程师的角度解释问题的答案。究竟是什么被利用了,为什么它会起作用?

请注意,我不是在寻找有关如何利用的说明,也没有兴趣以任何方式复制它;我只是好奇它背后的技术背景。

一些相关链接:

Microsoft 安全公告 2757760
CVE-20012-4969,MITRE
CVE-20012-4969,NVD
CVE-20012-4969,CVE 详细信息

2个回答

CVE-2012-4969,又名最新的 IE 0-day,是一个基于 IE 渲染引擎中的 use-after-free 漏洞。当动态分配的内存块在被释放(即释放)后被使用时,就会发生释放后使用。可以通过创建内部结构包含指向敏感内存位置(例如堆栈或可执行堆块)的指针的情况来利用此类错误,这种情况会导致程序将 shellcode 复制到可执行区域中。

在这种情况下,问题出CMshtmlEd::Execmshtml.dll. CMshtmlEd对象被意外释放,然后在释放操作Exec对其调用方法

首先,我想介绍一些理论。如果您已经知道 use-after-free 的工作原理,请随意跳过。

在低层次上,一个类可以等同于一个包含其状态(例如字段、属性、内部变量等)和一组对其进行操作的函数的内存区域。这些函数实际上带有一个“隐藏”参数,它指向包含实例状态的内存区域。

例如(请原谅我可怕的伪 C++):

class Account
{
    int balance = 0;
    int transactionCount = 0;

    void Account::AddBalance(int amount)
    {
        balance += amount;
        transactionCount++;
    }

    void Account::SubtractBalance(int amount)
    {
        balance -= amount;
        transactionCount++;
    }
}

上面的内容实际上可以表示为:

private struct Account
{
    int balance = 0;
    int transactionCount = 0;
}

public void* Account_Create()
{
    Account* account = (Account*)malloc(sizeof(Account));
    account->balance = 0;
    account->transactionCount = 0;
    return (void*)account;
}

public void Account_Destroy(void* instance)
{
    free(instance);
}

public void Account_AddBalance(void* instance, int amount)
{
    ((Account*)instance)->balance += amount;
    ((Account*)Account)->transactionCount++;
}

public void Account_SubtractBalance(void* instance, int amount)
{
    ((Account*)instance)->balance -= amount;
    ((Account*)instance)->transactionCount++;
}

public int Account_GetBalance(void* instance)
{
    return ((Account*)instance)->balance;
}

public int Account_GetTransactionCount(void* instance)
{
    return ((Account*)instance)->transactionCount;
}

void*用来演示参考的不透明性质,但这并不重要。关键是我们不希望任何人能够Account手动更改结构,否则他们可以任意添加资金,或者在不增加交易计数器的情况下修改余额。

现在,想象我们做这样的事情:

void* myAccount = Account_Create();
Account_AddBalance(myAccount, 100);
Account_SubtractBalance(myAccount, 75);
// ...
Account_Destroy(myAccount);
// ...
if(Account_GetBalance(myAccount) > 1000) // <-- !!! use after free !!!
    ApproveLoan();

现在,当我们到达Account_GetBalance时,in 中的指针值myAccount实际上指向了处于不确定状态的内存。现在,假设我们可以执行以下操作:

  1. 可靠地触发调用Account_Destroy
  2. 在此之后 Account_Destroy之前 执行任何操作Account_GetBalance允许我们分配合理数量的内存,以及我们选择的内容。

通常,这些调用是在不同的地方触发的,因此实现这一点并不难。现在,会发生以下情况:

  1. Account_Create分配一个 8 字节的内存块(每个字段 4 个字节)并返回一个指向它的指针。该指针现在存储在myAccount变量中。
  2. Account_Destroy释放内存。myAccount变量仍然指向相同的内存地址。
  3. 我们触发我们的内存分配,包含重复的39 05 00 00 01 00 00 00. 此模式与balance = 1337相关transactionCount = 1由于旧内存块现在被标记为空闲,内存管理器很可能会将我们的新内存写入旧内存块。
  4. Account_GetBalance被调用,期望指向一个Account结构。实际上,它指向我们被覆盖的内存块,导致我们的余额实际上是1337,所以贷款被批准了!

当然,这都是一种简化,真正的类会创建更迟钝和复杂的代码。关键是类实例实际上只是一个指向数据块的指针,类方法与任何其他函数一样,但它们“默默地”接受指向实例的指针作为参数。

这个原则可以扩展到控制堆栈上的值,这反过来会导致程序执行被修改。通常,目标是将 shellcode 放入堆栈,然后覆盖返回地址,使其现在指向一条jmp esp指令,然后该指令运行 shellcode。

这个技巧适用于非 DEP 机器,但是当 DEP 启用时,它会阻止堆栈的执行。相反,必须使用面向返回的编程 (ROP)来设计 shellcode ,它使用来自应用程序及其模块的小块合法代码来执行 API 调用,以绕过 DEP。

无论如何,我有点跑题了,所以让我们来看看 CVE-2012-4969 的有趣细节吧!

在野外,有效载荷是通过一个打包的 Flash 文件丢弃的,旨在一次性利用 Java 漏洞和新的 IE 错误。AlienVault也对它进行了一些有趣的分析

metasploit模块说明如下:

此模块利用 Microsoft Internet Explorer (MSIE) 中发现的漏洞。渲染 HTML 页面时,CMshtmlEd 对象以意外方式被删除,但稍后在 CMshtmlEd::Exec() 函数中再次重用相同的内存,导致释放后使用情况。

还有一篇关于这个错误的有趣博客文章,虽然英文很差——我相信作者是中国人。无论如何,博客文章会详细介绍:

execCommandIE函数执行命令事件时,会CMshtmlEd通过AddCommandTarget函数分配对应的对象,然后调用mshtml@CMshtmlEd::Exec()函数执行。但是,在execCommand函数添加了相应的事件之后,会立即触发并调用相应的事件函数。通过document.write("L")函数重写html中相应的事件函数被调用。从而导致IE调用CHTMLEditor::DeleteCommandTarget释放原来的应用对象CMshtmlEd,然后在后面执行该msheml!CMshtmlEd::Exec()函数时触发了used-after-free漏洞

让我们看看我们是否可以将其解析为更具可读性的内容:

  1. 将事件应用于文档中的元素。
  2. 事件通过 via 执行,execCommandCMshtmlEd通过AddCommandTarget函数分配一个对象
  3. 目标事件用于document.write修改页面。
  4. 不再需要该事件,因此该CMshtmlEd对象通过CHTMLEditor::DeleteCommandTarget.
  5. execCommand在它被释放之后调用CMshtmlEd::Exec()该对象。

崩溃现场的部分代码如下所示:

637d464e 8b07            mov     eax,dword ptr [edi]
637d4650 57              push    edi
637d4651 ff5008          call    dword ptr [eax+8]

use-after-free 允许攻击者控制 的值edi,可以将其修改为指向攻击者控制的内存。假设我们可以01234f00通过内存分配将任意代码插入到内存中。我们按如下方式填充数据:

01234f00:    01234f08
01234f04:    41414141
01234f08:    01234f0a
01234f0a:    cccccccc // int3 breakpoint
  1. 我们通过 use-after-free 错误设置edi为。01234f00
  2. mov eax,dword ptr [edi]导致在eax地址中填充内存edi,即01234f00
  3. push edi01234f00入堆栈。
  4. call dword ptr [eax+8]eax(即01234f00)并在其上加 8,得到01234f08. 然后它取消引用该内存地址,给我们01234f0a. 最后,它调用01234f0a.
  5. 处的数据01234f0a被视为指令。cc转换为int3,这会导致调试器引发断点。我们已经执行了代码!

这使我们可以控制eip,因此我们可以将程序流修改为我们自己的 shellcode,或者修改为 ROP 链。

请记住,以上只是一个示例,实际上利用此漏洞还有许多其他挑战。这是一个非常标准的 use-after-free,但是 JavaScript 的性质产生了一些有趣的计时和堆喷射技巧,DEP 迫使我们使用 ROP 来获得一个可执行的内存块。

无论如何,这对研究很有趣,我希望它有所帮助。

如果您查看 technet 文章,您会看到对CVE-2012-4969的引用。CVE 页面有几个参考资料,其中最有帮助的(恕我直言)是这个英文不是很好,但很好读。

另请参阅El Reg以获得更通俗易懂的语言解释。

我不打算在这里发布一个巨大的解释,因为它只是在抄袭其他人的工作,但总结一下,这是一个关于处理死对象的编码错误,导致任意代码执行。