G DATA SecurityLabs的专家们发现了一种新的Remote Administration Tool,被称为COMpfun,它支持32位和64位的Windows系统版本,最高可支持Windows 8。其特性对于目前的间谍工具而言相当常见:文件管理,截屏,键盘记录,代码执行,等等。最大的新奇之处在于持久性机制:恶意程序会劫持一个合法的COM,进而注入到被感染系统的进程中。该劫持行为不需要管理员权限,使用这个工具,攻击者可以避开检测,隐藏在被感染系统中很长一段时间!
一、什么是COM?
COM的英文全拼是Component Object Model,即组件对象模型,被微软描述为“跨平台、分布式、用于创建可交互二进制软件组件的面向对象系统”。这项技术的目的是提供一个接口,允许开发人员控制和操纵其他应用程序的对象。每一个COM都被惟一的ID所标识,称为CLSID,例如,创建一个Internet Explorer实例的CLSID是{0002df01-0000-0000-c000-000000000046}。对COM不太了解的读者建议花点时间学习一下,下面是两个博客链接,它们对COM的讲解还是比较清楚且通俗易懂的:
http://blog.csdn.net/wangqiulin123456/article/category/1235759
http://www.cnblogs.com/fangyukuan/category/247067.html
二、COM劫持分析
前面提到,COMpfun的新奇之处在于其持久性机制,具体过程为:
第一步,在安装阶段,恶意程序会释放两个文件到以下目录:%APPDATA%\Roaming\Microsoft\Installer\{BCDE0395-E52F-467C-8E3D-C4579291692E}\,这两个文件分别是32位和64位版本的DLL,其命名格式为:api-ms-win-downlevel-[4char-random]-l1-1-0._dl,其中[4char-random]是指随机产生的4个字母。
第二步,创建两个注册表项:
- \HKEY_CURRENT_USER\Software\Classes\CLSID{B5F8350B-0548-48B1-A6EE-88BD00B4A5E7}\InprocServer32
- \HKEY_CURRENT_USER\Software\Classes\Wow6432Node\CLSID{BCDE0395-E52F-467C-8E3D-C4579291692E}\InprocServer32
默认项的项值分别是上一步释放的两个DLL的全路径。上述键中的两个CLSID是被微软预定义过的:
{B5F8350B-0548-48B1-A6EE-88BD00B4A5E7} => the CLSID of the class CAccPropServicesClass {BCDE0395-E52F-467C-8E3D-C4579291692E} => the CLSID of the class MMDeviceEnumerator
前者通过提供一种从任意UI元素提取信息的有效方法,实现了对UI元素的程序式访问;后者则提供了对应的媒体音频视频操作。借助这两个类来提供相关辅助功能的应用程序,例如Internet Explorer和Windows Media Player,在实例化组件对象的时候,进程会通过注册表项值去加载组件DLL,于是恶意库就被加载到相应进程中去了。
三、COM实例化机制
上面提到,在实例化组件对象的时候,相应的DLL文件会被加载到进程中,这里简单介绍一下其中原理。应用程序在创建一个COM对象时,都需要类似地用到下面这段代码:
CoInitialize(NULL); // 初始化COM IXX* pIXX = NULL; HRESULT hr = CoCreateInstance(CLSID_XX, NULL, CLSCTX_SERVER, IID_IXX, (void **)&pIXX); if(SUCCEEDED(hr)) { ... // 通过COM接口指针pIXX调用接口里的方法 pIXX->Release(); // 通知COM对象不再使用它 } CoUninitialize(); // 释放COM
CoInitialize初始化COM库后,就可以调用COM库的API函数CoCreateInstance,后面就是直接使用组件接口。不妨一起来看看CoCreateInstance的实现:
/* ** clsid -- 待创建组件的CLSID ** pUnkOuter -- 用于聚合组件 ** dwClsContext -- 所使用的COM服务器的种类 ** iid -- 待使用的组件接口的IID ** ppv -- 组件接口指针 */ HRESULT CoCreateInstance(CLSID& clsid, IUnknown* pUnkOuter, DWORD dwClsContext, IID& iid, void** ppv) { ... IClassFactory* pIClassFactory = NULL; HRESULT hr = CoGetClassObject(clsid, dwClsContext, NULL, IID_IClassFactory, (void**)&pIClassFactory); if (SUCCEEDED(hr)) { // 使用类厂对象来创建组件 hr = pIClassFactory->CreateInstance(pUnkOuter, iid, ppv); pIClassFactory->Release()(); } return hr; }
因为CoGetClassObject源代码没有公开,所以只能从前后文推测它做了什么事情:
HRESULT CoGetClassObject(CLSID& clsid, DWORD dwClsContext, COSERVERINFO* pServerInfo, IID& iid, void** ppv) { // 通过clsid查注册表,获得待创建组件的DLL路径 // 加载DLL到当前进程中 // 调用函数GetProcAddress()得到DLL库中函数DllGetClassObject()的首地址 // 调用函数DllGetClassObject()创建一个与待创建组件相对应的类厂对象,并返回其指针 }
组件DLL中的函数DllGetClassObject()是用来获得类厂对象的,类厂是一种用来创建其他组件的简单组件,只有先得到类厂才能去创建组件。下面是DllGetClassObject()的伪码:
/* 注:该函数中的参数都是由外层函数传进来的,因此iid应该是IID_IClassFactory,ppv应该是pIClassFactory. */ HRESULT DllGetClassObject(CLSID& clsid, IID& iid, void** ppv) { ... CFactory* pFactory= new CFactory(clsid); // 实例化一个与待创建组件相对应的类厂对象 pFactory->QueryInterface(iid, ppv); // 查询接口 pFactory->Release(); // 释放对象 ... }
四、COM劫持测试
注册表项\HKEY_LOCAL_MACHINE\Software\Classes\CLSID 以及 \HKEY_LOCAL_MACHINE\Software\Classes\Wow6432Node\CLSID 下面的每个子键都对应了一个COM组件(微软预定义好的),项值都是组件DLL所在路径。根据上一步的分析,应用程序要创建某个COM对象的时候,都是根据其CLSID去查询注册表,获得对应的组件DLL所在路径,然后加载到进程中。这其中的关键之处就在于,应用程序不会(也无法)去判断这个DLL的真伪。也就是说,即使别有用心的人将DLL替换掉,应用程序仍会机械地去加载这个已经被替换掉的DLL;或者只是简单的修改注册表中的路径值,应用程序也会根据修改后的路径值去加载DLL。COMpfun对组件的劫持正是源于此。
1. 设计组件
前面第一部分内容给了两个博客链接,里面都介绍了如何利用ATL来设计组件,这里不再赘述。设计好一个组件(假设名称为ATLComTest)之后,在入口点(即DLLMain()函数)加上下面这一段代码:
if(count == 0) { ShellExecuteA(0, "open", "cmd.exe", "", "", SW_SHOWNORMAL); count = count + 1; }
其中,count是初值为0的全局静态变量。编译项目,成功的话会弹出一个CMD。该组件下载地址:ATLComTest.dll
可以自己写个测试程序来检验该组件DLL能否被正确加载,测试代码可以是:
#include <Windows.h> #include <ExDisp.h> #include <assert.h> #include <iostream> using namespace std; extern "C" const GUID clsid = { 0xB5F8350B, 0x0548, 0x48B1,{ 0xA6, 0xEE, 0x88, 0xBD, 0x00, 0xB4, 0xA5, 0xE7 } }; int main() { OleInitialize(NULL); IUnknown *pIUk = NULL; HRESULT hr = CoCreateInstance(clsid, NULL, CLSCTX_INPROC_SERVER, IID_IUnknown, (void**)&pIUk); if (hr != S_OK) { cout << "Error Code: "; cout << hex << hr << endl; return -1; } pIUk->Release(); CoUninitialize(); }
其中的clsid是所创建组件的clsid。如果测试失败,可以根据错误码去查找错误原因:https://msdn.microsoft.com/en-us/library/cc704587.aspx,然后纠正,直到可以加载成功。
2. 修改注册表
例如,{B5F8350B-0548-48B1-A6EE-88BD00B4A5E7}在注册表中可能存在如下表项:
- \HKEY_CLASSES_ROOT\CLSID\{B5F8350B-0548-48B1-A6EE-88BD00B4A5E7}\InprocServer32
- \HKEY_CLASSES_ROOT\Wow6432Node\CLSID\{B5F8350B-0548-48B1-A6EE-88BD00B4A5E7}\InprocServer32
- \HKEY_CURRENT_USER\Software\Classes\CLSID\{B5F8350B-0548-48B1-A6EE-88BD00B4A5E7}\InprocServer32
- \HKEY_CURRENT_USER\Software\Classes\Wow6432Node\CLSID\{B5F8350B-0548-48B1-A6EE-88BD00B4A5E7}\InprocServer32
- \HKEY_LOCAL_MACHINE\Software\Classes\CLSID\{B5F8350B-0548-48B1-A6EE-88BD00B4A5E7}\InprocServer32
- \HKEY_LOCAL_MACHINE\Software\Classes\Wow6432Node\CLSID\{B5F8350B-0548-48B1-A6EE-88BD00B4A5E7}\InprocServer32
- \HKEY_LOCAL_MACHINE\Software\Wow6432Node\Classes\CLSID\{B5F8350B-0548-48B1-A6EE-88BD00B4A5E7}\InprocServer32
其中,a/e同步变化,b/f/g同步变化。而应用程序根据CLSID查询注册表获取DLL路径的时候,是按照HKCU -> HKLM的顺序。也就是说,在32位的机器中,先查找c若不存在再去查找a/e;在64位机器中,先查找d若不存在再去查找b/f/g。
这样不管你是32位还是64位机器,只需将编译好的ATLComTest.dll所在路径作为默认项值添加到以下注册表项(若不存在则新建)中:
- \HKEY_CURRENT_USER\Software\Classes\CLSID\{B5F8350B-0548-48B1-A6EE-88BD00B4A5E7}\InprocServer32
- \HKEY_CURRENT_USER\Software\Classes\Wow6432Node\CLSID\{B5F8350B-0548-48B1-A6EE-88BD00B4A5E7}\InprocServer32
这时候打开Internet Explorer,就会弹出一个CMD。借助Process Explorer工具查看Internet Explorer的进程,可以在进程所加载的DLL列表中找到自定义的ATLComTest.dll。
通过另一个CLSID{BCDE0395-E52F-467C-8E3D-C4579291692E}也可以达到上述效果,只不过它影响的是Windows Media Player。
在这个例子当中,ATLComTest.dll只是简单的弹出一个CMD,而在现实中只要修改入口点的代码,那就不只是一个CMD这么简单了。攻击者只需要修改注册表,不需要进行DLL注入(这种行为通常会被杀毒软件检测到),这个过程甚至不会被Sysinternals的Autoruns工具检测到,对攻击者来说,克服了在大多数情况下都要面临的一个难题。
结束
参考文章:https://blog.gdatasoftware.com/2014/10/23941-com-object-hijacking-the-discreet-way-of-persistence
注:本文仅用作学习交流,请勿非法滥用。