Skip to content →

COM劫持:持久性机制分析

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}在注册表中可能存在如下表项:

  1. \HKEY_CLASSES_ROOT\CLSID\{B5F8350B-0548-48B1-A6EE-88BD00B4A5E7}\InprocServer32
  2. \HKEY_CLASSES_ROOT\Wow6432Node\CLSID\{B5F8350B-0548-48B1-A6EE-88BD00B4A5E7}\InprocServer32
  3. \HKEY_CURRENT_USER\Software\Classes\CLSID\{B5F8350B-0548-48B1-A6EE-88BD00B4A5E7}\InprocServer32
  4. \HKEY_CURRENT_USER\Software\Classes\Wow6432Node\CLSID\{B5F8350B-0548-48B1-A6EE-88BD00B4A5E7}\InprocServer32
  5. \HKEY_LOCAL_MACHINE\Software\Classes\CLSID\{B5F8350B-0548-48B1-A6EE-88BD00B4A5E7}\InprocServer32
  6. \HKEY_LOCAL_MACHINE\Software\Classes\Wow6432Node\CLSID\{B5F8350B-0548-48B1-A6EE-88BD00B4A5E7}\InprocServer32
  7. \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

注:本文仅用作学习交流,请勿非法滥用。

Published in 未分类