0%

Windows COM初探[未完待续]

COM基础

参考橙神文章 这里直接引用过来一些COM的基础知识(以下两段) 作为前置基础以便后面理解

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
1.在设计层面,COM模型分为`接口`与`实现`。 
例如计划任务示例代码中的`ITaskService`。
2.区分COM组件的唯一标识为`Guid`,分别为针对接口的`IID(Interface IDentifier)`与针对类的`CLSID(CLaSs IDentifier)`。
例如`CLSID_TaskScheduler`定义为`0F87369F-A4E5-4CFC-BD3E-73E6154572DD`。
3.COM组件需要在注册表内进行注册才可进行调用。通常情况下,系统预定义组件注册于`HKEY_LOCAL_MACHINE\SOFTWARE\Classes`,用户组件注册于`HKEY_CURRENT_USER\SOFTWARE\Classes`。`HKEY_CLASSES_ROOT`为二者合并后的视图,在系统服务角度等同于`HKEY_LOCAL_MACHINE\SOFTWARE\Classes`。
例如计划任务组件的注册信息注册于`HKEY_CLASSES_ROOT\CLSID\{0f87369f-a4e5-4cfc-bd3e-73e6154572dd}`。
4.Windows最小的可独立运行单元是进程,最小的可复用的代码单元为类库,所以COM同样存在`进程内(In-Process)`与`进程外(Out-Of-Process)`两种实现方式。多数情况下,进程外COM组件为一个exe,进程内COM组件为一个dll。
例如计划任务的COM对象为进程内组件,由`taskschd.dll`实现。
5.为方便COM组件调用,可以通过`ProgId(Programmatic IDentifier)`为`CLSID`指定别名。
例如计划任务组件的ProgId为`Schedule.Service.1`。
6.客户端调用`CoCreateInstance`、`CoCreateInstanceEx`、`CoGetClassObject`等函数时,将创建具有指定`CLSID`的对象实例,这个过程称为`激活(Activation)`。
例如微软示例代码中的`CoCreateInstance(CLSID_TaskScheduler,....)`。
7.COM采用`工厂模式(class factory)`对调用方与实现方进行解耦,包括进程内外COM组件激活、通信、转换,`IUnknown::QueryInterface`和`IClassFactory`始终贯穿其中。
例如微软示例代码中的一大堆`QueryInterface`。
1.com程序一般是dll文件,被提供给主程序调用。不同的com程序具有不同的接口,但是所有的接口都是从class factory 和 IUnknown接口获得的。所以com程序必须实现 class factory 和 Iunknown接口

2.接口是实现对对象数据访问的函数集,而接口的函数称为方法。每个接口都有自己的唯一接口标识符,叫IID, IID也是一个GUID(全局唯一标识符)。
在定义接口时,用IDL来定义,使用MIDL编译会生成对应的都文件,根据头文件我们自己实现编程调用

3.IUnKnown接口
所有COM接口都继承自IUnKnown接口,该接口具有3个成员函数,QueryInterface、AddRef、Release.

4.CoCreateInstance 函数创建com实例并返回客户端请求的接口指针。客户端指的是将CLSID传递给系统并请求com对象实例的调用方,这里个人理解为编程人员的代码获取com服务器的指针,并调用接口的方法使用com服务,服务器端指的是向系统提供COM对象的模块
com服务器主要有两种,进程内和进程外,进程内服务器在dll中实现,进程外服务器在exe中实现。
如果要创建com对象,com服务器需要提供 IClassFactory 接口的实现,而且 IClassFactory 包含 CreateInstance方法。
IUnknown::QueryInterface和IClassFactory始终贯穿在com组件的调用中。

5.在注册com服务器的时候,如果是进程内注册,即dll,dll必须导出以下函数
DllRegisterServer
DllUnregisterServer
注册是将com对象写进注册表,自然离不开注册表的一系列函数
RegOpenKey
RegCreateKey
......

6.几乎所有的COM函数和接口方法都返回HRESULT类型的值,但HRESULT不是句柄

COM对象是对象定义(类)的一个实例,COM类包含对象的定义及各种接口。客户端仅通过COM对象的接口与其交互。

所有的COM接口都继承自IUnknown即可。IUnknown接口包含用于多台性和实例声明周期管理的基本COM操作。IUnknown接口有三个成员函数,分别为QueryInterface、AddRef、Release。

可以使用OleViewDotNet 查看Wscript,使用powershell测试IWshShell接口,并尝试执行

img

img

1
2
3
4
5
6
7
8
9
10
11
12
# powershell下调用COM对象 
# (需管理员权限)
# 使用ProgID (WScript.Shell)激活Windows script host COM对象
# 我们返回一个指向IWshShell接口的指针
$WshShell = New-Object -comObject WScript.Shell //WScript.Shell CLSID对应的别名

# 使用creatshortcut方法返回一个Shortcut对象
# 此路径是保存lnk文件的路径
$Shortcut = $WshShell.CreateShortcut("C:\my.lnk")
# 指定链接指向的位置(当我们单击文件时将执行的内容)
$Shortcut.TargetPath = "C:\windows\system32\cmd.exe"
$Shortcut.Save()

执行后会在生成一个cmd的快捷方式

img

IPID结构

IPID结构并不是一个随机的GUID

img

COM对象标识符

  • CLSID - 类ID,是标识COM类的全局唯一标识符(GUID)
  • ProgID - 程序ID,COM类的别名,可以像CLSID一样使用,上面通过powershell脚本调用的ProgID「Wscript.Shell」
  • AppID - 应用程序ID,全局唯一标识符,用于包含单个或一组OCM类的各种配置的注册表
  • IID - 接口ID,标识COM接口的全局唯一标识符

img

img

激活COM对象

在使用某个COM对象之前,需要先把它加载进内存,该过程称为激活。

在不同的编程语言中,用多个不同的函数来创建实例:

  • C/C++ CreateInstance | CoCreateInstance | CoCreateInstanceEx
  • VBScript/JScript CreateObject | ActiveXObject
  • Powershell New-Object -ComObject
1
2
3
4
5
6
7
8
 hr = CoCreateInstance(
&CLSID_TaskbarList, // CLSID
NULL,
CLSCTX_INPROC_SERVER,
&IID_ITaskbarList3, //IID
(LPVOID *)&infoPtr->lpTaskbarList3);
# Create Instance from CLSID
[System.Activator]::CreateInstance([Type]::GetTypeFromCLSID("56FDF344-FD6D-11d0-958A-006097C9A090"))

当使用这些函数创建COM类的实例时,需要传递CLSID或ProgID作为参数。

在以下注册表为COM相关的主要关键项:

当我们提供CLSID或ProgID作为参数时,控制流将传递给处理客户端请求的SCM(WIndows服务管理器),SCM查询注册表中的相关位置,并搜索我们传递的标识符。

注册表键包含传递的标识符,它的值会映射到COM服务器的位置。

img

img

每个注册CLSID表象中都会有一个InProcServer32或者LocalServer32的子项,该子项内映射到该Com二进制文件的键值对,操作系统通过该键值对将Com二进制文件载入进程。

InProcServer32表示的是Dll的实现路径,LocalServer32表示的是Exe的实现路径。

SCM服务管理器请求处理顺序:

  • 客户端通过使用COM对象的CLSID调用 CoCreateInstance等函数 从COM 库(ole32.dll)请求指向 COM 对象的接口指针。
  • COM库查询SCM 以找到与请求的CLSID对应的服务器
  • SCM定位服务器并请求从服务器提供的类工厂创建COM对象
  • 如果成功,COM库返回一个指向客户端的接口指针

COM服务器激活类型

当注册一个COM服务器时,有三种类型激活方法

  • In-Process(InprocServer32)

绝大多数的本地COM服务是在进程中激活的,这意味着COM服务器是一个DLL文件,将被加载到实例化COM对象的客户端进程中。这种激活方法中,SCM返回包含对象服务器实现的DLL的文件路径,COM库加载DLL并查询它的类工厂接口指针。

  • Out-of-Process/Local (LocalServer32)

当一个COM对象被配置为本地服务器时,这意味着COM服务器是一个EXE文件。它将作为一个与实例化COM对象的客户端不同的进程执行,SCM启动本地可执行文件,它在启动时注册了一个类工厂,其接口指针可供系统和客户端使用。

  • Out-of-Process/Remote

这里远程方面指的是DCOM,是分布式COM,可将其视为网络上的COM(DCE-RPC)。它是激活或访问远程计算机上的COM对象的行为,本地SCM从远程计算机上运行的SCM获取类工厂接口指针。

进程内服务器(DLL)

以Wscript.Shell为例

CLSID-{72C24DD5-D70A-438B-8A42-98424B88AFB8}

ProgID - WScript.Shell

img这两处的有区别吗???

1
2
3
4
5
6
7
8
9
10
11
#在注册表中查询 COM 类 ID (CLSID) 并查看其配置
Get-ChildItem -Path "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Classes\CLSID\{72C24DD5-D70A-438B-8A42-98424B88AFB8}"

#Create Instance from CLSID
$comobj = [System.Activator]::CreateInstance([Type]::GetTypeFromCLSID("72C24DD5-D70A-438B-8A42-98424B88AFB8"))
或者
#Create Instance from ProgID
$comobj = [System.Activator]::CreateInstance([Type]::GetTypeFromProgID("WScript.Shell"))

$comobj | Get-Member
$comobj.Run("calc.exe")

imgimg

进程外服务器(EXE)

以MMC20为例 这个COM对象既可以在本地使用,也可以远程使用

CLSID - {49B2791A-B1AE-4C90-9B8E-E860BA07F889}

ProgID - MMC20.Application

1
2
3
4
5
6
7
8
9
10
#在注册表中查询 COM 类 ID (CLSID) 并查看其配置
Get-ChildItem -Path "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Classes\CLSID\{49B2791A-B1AE-4C90-9B8E-E860BA07F889}"
#Create Instance from CLSID
$comobj = [System.Activator]::CreateInstance([Type]::GetTypeFromCLSID("49B2791A-B1AE-4C90-9B8E-E860BA07F889"))
或者
#Create Instance from ProgID
$comobj = [activator]::CreateInstance([type]::GetTypeFromProgID("MMC20.Application.1"))

$comobj | Get-Member
$comobj.Document.ActiveView.ExecuteShellCommand("cmd",$null,"/c calc","7")

img

远程进程外 DCOM
1
2
3
$comobj = [System.Activator]::CreateInstance([Type]::GetTypeFromCLSID("49B2791A-B1AE-4C90-9B8E-E860BA07F889", "127.0.0.1"))
或者
$com =[activator]::CreateInstance([type]::GetTypeFromProgID("MMC20.Application","127.0.0.1"))

只是加了个目标IP 当使用.Net或者Win32API时,CoCreateInstance创建本地实例,而CoCreateInstanceEx用于创建远程实例

COM Object & Interface

IUnknown接口

IUnknown接口是COM中的底层接口,包含所有对象的基本操作和所有接口的基本操作。所有的COM对象都必须继承IUnknown接口并实现它。

IUnknown接口结构大体如下

1
2
3
4
5
6
7
8
9
10
struct IUnknown
{
virtual HRESULT STDMETHODCALLTYPE QueryInterface(
/* [in] */ REFIID riid,
/* [iid_is][out] */ _COM_Outptr_ void __RPC_FAR *__RPC_FAR *ppvObject) = 0;

virtual ULONG STDMETHODCALLTYPE AddRef( void) = 0;

virtual ULONG STDMETHODCALLTYPE Release( void) = 0;
}

其中,

AddRef()

AddRef() 表示此对象引用计数加一,如果被外面持有时,需要调用AddRef()。返回之后的引用计数。

Release()

Release() 表示此对象引用计数减一,一旦引用计数为0,实现者必须要释放此对象

QueryInterface()

QueryInterface() 是IUnknown的核心接口,由于所有的接口都是从IUnknoen继承的,所以所有的接口都支持QueryInterFace。
QueryInterface有两个参数,一个HRESULT返回值:

  • 第一个参数 接口标识符(IID)
  • 第二个参数 存放所请求接口指针的地址
  • 返回值:查询成功返回S_OK,如果不成功则返回相应错误码。

举例:

如果有一个结构 接口IRead和IWrite都继承自IUnknown接口,组件ReadWrite实现了IRead和IWrite接口,那么其QueryInterface的实现如下:

1
2
3
4
5
6
7
8
struct IRead : public IUnknown
{
virtual byte* read() = 0;
};
struct IWrite: public IUnknown
{
virtual void write(byte) = 0;
}
1
2
3
4
5
6
7
8
class ReadWrite : public IRead, public IWrite
{
ULONG AddRef();
ULONG Release();
HRESULT QueryInterface(REFIID iid, void** ppvObject);
byte* read();
void write(byte);
};

通过如下方法获取到这个对象的IRead接口:

1
IRead* reader = GetReader();

拿到IReader后再去获取IWrite

1
2
3
IWrite* write;
reader->QueryInterface(IID_IWRITE,(void**)&write);
......

这样就从IReader中拿到了Iwrite,在ReadWrite中实现了Iread和Iwirte,其QueryInterface伪代码可以写成如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
HRESULT __stdcall ReadWrite::QueryInterface(const IID& iid, void** ppv)
{
if (iid == IID_IREAD)
{
trace("QueryInterface: Return pointer to IREAD.");
*ppv = static_cast<IREAD*>(this);
}

else if (iid == IID_IWRITE)
{
trace("QueryInterface: Return pointer to IWRITE.");
*ppv = static_cast<IWRITE*>(this);
}

... ...
... ...

reinterpret_cast<IUnknown*>(*ppv)->AddRef();

return S_OK;
}

ClassFactory接口

COM利用

利用思路:

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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
#include <windows.h>
#include <stdio.h>
#include <initguid.h>
#include <stdint.h>

//The CLSID {13709620-C279-11CE-A49E-4445535400} associated with the data and code that we will use to create the object.
DEFINE_GUID(clsid, 0x13709620, 0xc279, 0x11ce, 0xa4, 0x9e, 0x44, 0x45, 0x53, 0x54, 0x00, 0x00);

int main(char argc, char **argv) {

LPOLESTR clsidstr = NULL;
StringFromCLSID(&clsid, &clsidstr);
printf("Our targeted CLSID is %ls\n", clsidstr);
HRESULT hr;
hr = CoInitialize(NULL);

FARPROC DllGetClassObject = GetProcAddress(LoadLibrary("shell32.dll"), "DllGetClassObject");
printf("DllGetClassObject is at 0x%p\n\n", DllGetClassObject);

IClassFactory *icf = NULL;
// Get shell32.DLL's IClassFactory
// 1.通过DLLGetClassObject获取目标CLSID的 类工厂
hr = DllGetClassObject(&clsid, &IID_IClassFactory, (void **)&icf);

if (hr != S_OK) {
printf("DllGetClassObject failed to do something. Error %d HRESULT 0x%08x\n", GetLastError(), (unsigned int)hr);

CoUninitialize();
ExitProcess(0);
}
//For debugging purposes
HMODULE shell32address = GetModuleHandle("shell32.dll");
printf("shell32.dll address is :%p \tIClassFactory's Vtable address is:%p \n", shell32address, icf->lpVtbl);
uint64_t val = (uint64_t)icf->lpVtbl - (uint64_t)shell32address;
printf("The offset is 0x%p - 0x%p = 0x%llx", icf->lpVtbl, shell32address, val);

// Create an IDispatch object
// 2.通过CreateInstance方法实例化类的对象IDispatch
IDispatch *id = NULL;
hr = icf->lpVtbl->CreateInstance(icf, NULL, &IID_IDispatch, (void **)&id);
if (hr != S_OK) {
printf("CreateInstance failed to do something. Error %d HRESULT 0x%08x\n", GetLastError(), (unsigned int)hr);

CoUninitialize();
ExitProcess(0);
}
printf("[+]IDispatch's Vtable address is:%p \n", id->lpVtbl);
uint64_t val1 = (uint64_t)id->lpVtbl - (uint64_t)shell32address;
printf("The offset IDispatch is 0x%p - 0x%p = 0x%llx\n", id->lpVtbl, shell32address, val1);


// get function ID
// 3.得到IDispatch对象后 通过调用GetIDsOfNames函数获取ShellExecutte的COM调度标识符DISPID
WCHAR *member = L"ShellExecute";
DISPID dispid = 0;

hr = id->lpVtbl->GetIDsOfNames(id, &IID_NULL, &member, 1, LOCALE_USER_DEFAULT, &dispid);
if (hr != S_OK) {
printf("GetIDsOfNames failed to do something. Error %d HRESULT 0x%08x\n", GetLastError(), (unsigned int)hr);

CoUninitialize();
ExitProcess(0);
}

printf("DISPID 0x%08x\n", dispid);

// initialize parameters
// 4.初始化invoke方法上使用的参数
/*
HRESULT Invoke(
[in] IDispatch *this
[in] DISPID dispIdMember,
[in] REFIID riid,
[in] LCID lcid,
[in] WORD wFlags,
[in, out] DISPPARAMS *pDispParams,
[out] VARIANT *pVarResult,
[out] EXCEPINFO *pExcepInfo,
[out] UINT *puArgErr
);
*/
//VARIANT描述在disparams中传递的参数。
VARIANT args = { VT_EMPTY };
args.vt = VT_BSTR;
args.bstrVal = SysAllocString(L"calc.exe ");
//包含传递给ShellExecute方法的参数。
DISPPARAMS dp = { &args, NULL, 1, 0 };

VARIANT output = { VT_EMPTY };
hr = id->lpVtbl->Invoke(id, dispid, &IID_NULL, LOCALE_USER_DEFAULT, DISPATCH_METHOD, &dp, &output, NULL, NULL);
if (hr != S_OK) {
printf("Invoke failed to do something. Error %d HRESULT 0x%08x\n", GetLastError(), (unsigned int)hr);
CoUninitialize();
ExitProcess(0);
}

id->lpVtbl->Release(id);
icf->lpVtbl->Release(icf);
SysFreeString(args.bstrVal);
CoUninitialize();

return 0;
}

COM劫持

dll劫持的原理主要是利用加载dll的路径顺序,而com劫持思路也是类似的

com组件的加载过程如下:

  • HKCU\Software\Classes\CLSID
  • HKCR\CLSID
  • HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\shellCompatibility\Objects\

优先级: HKCU > HKCR > HKLM

所以要劫持的目标选择HKCU\Software\Classes\CLSID 这样就可以去加载我们的恶意dll

与DLL劫持不同的是 dll劫持只能劫持dll,Com劫持可以劫持com文件、pe文件、Api文件等

步骤就是修改注册表路径指向我们的恶意文件路径,和dll劫持的白加黑相似。

可参考文章: https://www.4hou.com/posts/Mo51

COM进程注入

利用com实现进程注入,没有调用CreateProcess等常规的API,而是调用oleacc!GetProcessHandleFromHwnd(),利用IRundown:DoCallback()执行命令,并且该接口需要一个IPID和OXID值来执行代码。该接口并不是公开方法,需要手动去逆…

参考Project Zero的詹姆斯 福肖使用 COM 将代码注入 Windows 受保护的进程:

https://googleprojectzero.blogspot.com/2018/11/injecting-code-into-windows-protected.html

https://googleprojectzero.blogspot.com/2018/10/injecting-code-into-windows-protected.html

及mdsec的 https://www.mdsec.co.uk/2022/04/process-injection-via-component-object-model-com-irundowndocallback/

COM写计划任务

参考

https://github.com/skvl/com-taskschd-example/blob/master/main.cpp

https://learn.microsoft.com/en-us/windows/win32/taskschd/registration-trigger-example--c---

实现方法:

    1. 初始化COM对象,设置COM安全等级
    1. 创建ITaskService对象,该对象运行在指定文件夹中创建任务

References

https://learn.microsoft.com/en-us/windows/win32/api/unknwn/

https://tttang.com/archive/1824

https://paper.seebug.org/1624/

欢迎关注我的其它发布渠道

------------- 💖 🌞 本 文 结 束 😚 感 谢 您 的 阅 读 🌞 💖 -------------