Love for Microsoft Component Object Model, RPC and AMSI attack surface
免责声明:本博客文章仅用于教育和研究目的。提供的所有技术和代码示例旨在帮助防御者理解攻击手法并提高安全态势。请勿使用此信息访问或干扰您不拥有或没有明确测试权限的系统。未经授权的使用可能违反法律和道德准则。作者对因应用所讨论概念而导致的任何误用或损害不承担任何责任。
内容概要
-
对不熟悉 COM 的读者:本文涉及大量 COM 技术,如果你不了解 COM,请先阅读 《Inside COM》 by Dale Rogerson,然后再回来! -
通过攻击 AMSI 的 COM 架构来绕过检测,包括破坏 vftable 和 IID/GUID -
针对 Defender 和 AMSI 之间的 RPC 通信通道进行攻击
AMSI 组件分析
如何识别一个组件?
实现 COM 接口的组件需要在 Windows 注册表中注册为 ComputerHKEY_CLASSES_ROOTCLSID{<GUID>}
。这是一种便捷的定位 dll/exe 组件的方式,使得像 CoCreateinstance()
这样的 API 可以通过库中的 GUID 值获取并实例化接口。
本节我们将讨论 AMSI 的 COM 特性。为了开始对其内部机制的研究,我们先检查注册表以确认 AMSI 确实是一个组件。手动检查 CLSID 键中的条目将是一项繁琐(且无意义)的工作,我编写了一个简单的 Python 代码来完成这个任务!
如下图所示,我们可以看到组件的 CLSID 和文件位置。这意味着 AMSI 是通过 COM 实现的。
以下是我分享的 reg.py 代码
import winreg
import itertools
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("-m", "--mod",nargs="+", type=str)
args = parser.parse_args()
ar = args.mod
aReg = winreg.ConnectRegistry(None, winreg.HKEY_CLASSES_ROOT)
aKey = winreg.OpenKey(aReg, r'CLSID' )
for i in itertools.count():
try:
aValue_name = winreg.EnumKey(aKey, i)
oKey = winreg.OpenKey(aKey, aValue_name )
infokey = winreg.QueryInfoKey(oKey)
for x in range(infokey[0]):
subkey = winreg.EnumKey(oKey, 0)
kn = "CLSID" +"\" +aValue_name+"\"+subkey
sKey = winreg.OpenKey(aReg, kn)
if subkey == "InprocServer32":
sValue = winreg.QueryValueEx(sKey, None)
#print(sValue[0])
if ar[0] in sValue[0]:
print(f"Module : {sValue[0]}")
print(f"CLSID : {aValue_name}")
except Exception as e:
if "WinError 259" in str(e):
break
continue
下图展示了 amsi.dll 的导出函数。核心的 AMSI 功能通过以 Amsi* 开头的函数向客户端提供。高亮显示的函数用于 COM 操作。
唯一以纯文本形式公开的是头文件 amsi.h。这足以让我们理解代码结构并识别接口和 IID/GUID 值。CAntimalware 类实现了 amsi.dll 导出的函数。该类具有唯一标识符 fdb00e52-a214-4aa1-8fba-4357bb0072ec,我们将在后续章节中经常看到这个 ID。这个 ID 与我们在 reg.py 输出中看到的 ID 相同。该类不是一个组件,它只是通过实例化组件调用 AMSI COM 方法的包装器,我们稍后会看到这一点。
现在让我们检查所有 AMSI 接口,在众多接口中,下面展示的 IAntimalware2
是最重要的一个。该接口继承自 IAntimalware。请注意该接口的接口 ID,我们将在后续章节详细讨论这个接口。
下图展示了 IAntimalwa
的接口声明,其中包含有趣的方法——Scan。
针对 vfTable 的攻击
组件实现的方法保存在内存中的一个特殊表中,称为虚表 (virtual table) 或虚函数表 (virtual function table)。vftable 中的每个条目都是指向方法实现的指针。作为攻击者,这是一个有趣的目标,如果表中的指针条目被攻击者提供的代码覆盖会怎样?
编写 AMSI 扫描器
在深入探讨之前,让我们编写一个非常基础的 AMSI 客户端来进行试。客户端代码如下所示。在我们的测试中,我们将 Seatbelt.exe 提供给 AMSI。
/*
HRESULT AmsiInitialize(
[in] LPCWSTR appName,
[out] HAMSICONTEXT *amsiContext
);
HRESULT AmsiScanBuffer(
[in] HAMSICONTEXT amsiContext,
[in] PVOID buffer,
[in] ULONG length,
[in] LPCWSTR contentName,
[in, optional] HAMSISESSION amsiSession,
[out] AMSI_RESULT *result
);
*/
typedefHRESULT(WINAPI* AmsiInitializeT)(LPCWSTR, HAMSICONTEXT*);
typedefHRESULT(WINAPI* AmsiScanBufferT)(HAMSICONTEXT, PVOID, ULONG, LPCWSTR, HAMSISESSION, AMSI_RESULT*);
//<Seatbelt, Version = 1.0.0.0, Culture = neutral, PublickeyToken = null>
const DWORD assembly_size = 610304;
const BYTE assembly[] = { 0x4d, 0x5a, 0x90, 0x00, 0x03, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* truncated*/ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
voidmain()
{
HAMSICONTEXT AmsiCtx;
AMSI_RESULT amsiResult;
HMODULE hAmsi = LoadLibrary(L"amsi");
AmsiInitializeT AmsiInitialize = (AmsiInitializeT)GetProcAddress(hAmsi, "AmsiInitialize");
AmsiScanBufferT _AmsiScanBuffer = (AmsiScanBufferT)GetProcAddress(hAmsi, "AmsiScanBuffer");
HRESULT hr = AmsiInitialize(L"Test", &AmsiCtx);
if(FAILED(hr))
{
std::cout << "Failed";
}
hr = _AmsiScanBuffer(AmsiCtx, (PVOID)assembly, assembly_size, L"MAL_BUFF", nullptr, &amsiResult);
if (FAILED(hr))
{
std::cout << "Failed";
}
}
AMSI 初始化与 HAMSICONTEXT
AMSI 初始化是反恶意软件扫描接口的重要步骤,它将帮助创建 CAmsiAntimalware
组件的实例。该组件实现了扫描功能。我们将在后续章节深入探讨。AmsiInitialize
的函数原型如下所示。AMSI 客户端在调用 AmsiScanBuffer
等其他 API 之前必须先调用此 API。这是因为 AMSI 函数需要客户端提供 HAMSICONTEXT
。
回到我们的 AMSI 客户端,在调用 AmsiInitialize()
后中断执行,检查变量 AmsiCtx
的指针。成功调用 AmsiInitialize()
后,我们将收到一个指向 HAMSICONTEXT
类型内存块的指针。有趣的是,这种类型没有官方文档。如下图所示,amsi context 位于 0x0200F0B73580
。
MSDN 文档中提到 HAMSICONTEXT
只是一个句柄。但我们会发现事实并非如此。它不仅仅是一个简单的句柄,而是 CAmsiAntimalware
组件的实例。为什么这个信息如此重要?让我们一探究竟! 🙂
如前所述,HAMSICONTEXT
的内容如下所示。由于我们不知道 HAMSICONTEXT
的结构[或大小],理解其内容有些困难。但当我们开始调用一些 AMSI 函数时,就会有所了解。让我们通过 amsi.dll!AmsiScanBuffer()
来探索这个神秘的 HAMSICONTEXT
类型!
AmsiScanBuffer
在我们的 AMSI 扫描客户端中,Seatbelt 程序集通过 AmsiScanBuffer()
API 传递给 AMSI。AmsiInitialize()
创建的 amsi context 作为第一个参数传递给该函数。AmsiScanBuffer
的 IDA 反编译代码如下所示。
CAmsiBufferStream
是实现IAmsiStream
接口的组件。调用CAmsiBufferStream::CAmsiBufferStream()
会使用传递给AmsiScanBuffer()
的数据实例化相应对象。尾调用看起来非常有趣,我们可以看到使用amsiContext
引用的内存,这是由AmsiInitialize()
创建的HAMSICONTEXT
类型的上下文。尾调用的 IDA 反汇编如下所示。现在代码更清晰了,mov rcx,[rbx + 0x10]
从amsiContext
偏移 0x10 处检索指针并存储在 rcx 中。这个指针再次被解引用以检索另一个指针,该指针再次使用偏移量 0x18 解引用并存储在 rax 中。最后通过调用 rax 来执行尾调用。
要查看使用 amsiContext
的指针解引用和尾调用的实际操作,我们需要运行如下所示的代码。以下代码与上述 IDA 反汇编相同。
虚函数表 (vftable)
当我们单步执行 mov rcx, [rbc + 0x10]
时,存储在 rcx 中的值如下所示。借助符号,我们可以看到 amsiContext + 0x10
指向 CAmsiAntiMalware
的 vfTable。
让我们重新审视 AmsiCtx 的内存布局,根据我们的分析,CAmsiAntiMalware
实例位于偏移量 0x10 (16)
处。这意味着我们可以使用 AmsiInitialize()
创建的 HAMSICONTEXT
"句柄" 来访问 CAmsiAntiMalware
的 vfTable。
借助符号,我们可以清楚地看到地址 0x200F0CA1720
指向 CAmsiAntiMalware::vfTable
。
回到 AmsiScanBuffer
的尾调用,代码 move rax, [rax + 0x18]
将 CAmsiAntiMalware::Scan()
的地址存储在 rax 中。
CAmsiAntiMalware::vfTable
如下所示。
当单步进入尾调用时,我们可以看到 jmp rax,rax
指向 CAmsiAntiMalware::Scan()
。所以基本上 amsi.dll!AmsiScanBuffer
调用了 COM 方法 CAmsiAntiMalware::Scan()
。
尾调用前存储在 rax 中的数据如下所示。
vfTable 存储在只读部分。
现在我们已经看到了 vftable,并且知道 CAmsiAntiMalware::Scan(
) 的位置和表中的指针条目,为什么不直接将表中的指针条目指向 amsi.dll 中的某个 ret gadget 呢? 🙂 调用 VirtualProtect
来授予 amsi.dll 写访问权限有点冒险。
针对 AMSI 初始化的攻击
破坏 CLSID
AmsiInitialize()
代码封装了对象创建和各种检查,用于识别 AMSI 组件的 CLSID 存储在对象映射 (ATL) 中。以下代码检索值为 fdb00e52-a214-4aa1-8fba-4357bb0072ec的 CLSID_AntiMalware
。如下图所示,代码从第一个四字节 FD B0 0E 52开始验证 CLID。
让我们仔细看看内存中的 ATL 对象映射 __pobjmap_CAmsiAntimalware
。__pobjmap_CAmsiAntimalware
映射保存了指向映射条目的指针。
映射条目如下所示,这是指向位于 0x7FFD16ED07A0的实际数据的另一个指针。
CLSID 存储在 0x7FFD16ED07A0。
映射条目指针位于 .data 段,该段具有读写保护。这意味着我们不需要调用 VirtualProtect来使内存页可写,就可以破坏指针。
找到此映射条目指针的简单方法是在 .data 段中搜索 0x7FFD16ED07A0的逆序字节。搜索返回的内存地址就是映射条目指针。现在我们可以简单地用指向 clsid 起始地址后 4 个字节的地址覆盖此指针。
这将导致初始化失败,AmsiInitialize() 将返回如下所示的错误代码。
破坏 IID
初始化的重要步骤是创建 IAntimalware2
接口对象,如下所示。IAntimalware2
的 guid 被传递给 r8 并调用 ATL::CComClassFactory::CreateInstance()
。
篡改 GUID 指针将导致以下错误。不幸的是,可写段中没有指针可以篡改此 guid,您将不得不依赖 VirtualProtect。
Defender RPC 调用
虽然这与 AMSI 没有直接关系,但出于好奇,我想探究杀毒软件 (AV) 和 AMSI 之间的通信机制。AMSI 会加载额外的 DLL 文件,如 mpoav.dll 和 mpclient.dll,这些都是与 Defender 相关的文件,并会调用各种内部 API,如下图所示。在下面的堆栈跟踪中,有趣的是我们可以看到 mpclient.dll(Defender 客户端)中的一个函数正在向 Defender 服务发起 RPC 调用。
这让我想到,如果我们干扰 RPC 会发生什么? Andrea Bocchetti 在这篇文章中已经回答了这个问题,他称这种技术为 Ghosting AMSI。
原文始发于微信公众号(securitainment):对微软组件对象模型 (COM)、RPC 和 AMSI 攻击面的研究 – Sabotage Sec
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论