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接口,并尝试执行
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 # 使用creatshortcut方法返回一个Shortcut对象 # 此路径是保存lnk文件的路径 $Shortcut = $WshShell.CreateShortcut("C:\my.lnk" ) # 指定链接指向的位置(当我们单击文件时将执行的内容) $Shortcut.TargetPath = "C:\windows\system32\cmd.exe" $Shortcut.Save()
执行后会在生成一个cmd的快捷方式
IPID结构 IPID结构并不是一个随机的GUID
COM对象标识符
CLSID - 类ID,是标识COM类的全局唯一标识符(GUID)
ProgID - 程序ID,COM类的别名,可以像CLSID一样使用,上面通过powershell脚本调用的ProgID「Wscript.Shell」
AppID - 应用程序ID,全局唯一标识符,用于包含单个或一组OCM类的各种配置的注册表
IID - 接口ID,标识COM接口的全局唯一标识符
激活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, NULL , CLSCTX_INPROC_SERVER, &IID_ITaskbarList3, (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服务器的位置。
每个注册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启动本地可执行文件,它在启动时注册了一个类工厂,其接口指针可供系统和客户端使用。
这里远程方面指的是DCOM,是分布式COM,可将其视为网络上的COM(DCE-RPC)。它是激活或访问远程计算机上的COM对象的行为,本地SCM从远程计算机上运行的SCM获取类工厂接口指针。
进程内服务器(DLL) 以Wscript.Shell为例
CLSID-{72C24DD5-D70A-438B-8A42-98424B88AFB8}
ProgID - WScript.Shell
这两处的有区别吗???
1 2 3 4 5 6 7 8 9 10 11 Get-ChildItem -Path "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Classes\CLSID\{72C24DD5-D70A-438B-8A42-98424B88AFB8}" $comobj = [System.Activator ]::CreateInstance([Type ]::GetTypeFromCLSID("72C24DD5-D70A-438B-8A42-98424B88AFB8" ))或者 $comobj = [System.Activator ]::CreateInstance([Type ]::GetTypeFromProgID("WScript.Shell" ))$comobj | Get-Member $comobj .Run("calc.exe" )
进程外服务器(EXE) 以MMC20为例 这个COM对象既可以在本地使用,也可以远程使用
CLSID - {49B2791A-B1AE-4C90-9B8E-E860BA07F889}
ProgID - MMC20.Application
1 2 3 4 5 6 7 8 9 10 Get-ChildItem -Path "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Classes\CLSID\{49B2791A-B1AE-4C90-9B8E-E860BA07F889}" $comobj = [System.Activator ]::CreateInstance([Type ]::GetTypeFromCLSID("49B2791A-B1AE-4C90-9B8E-E860BA07F889" ))或者 $comobj = [activator ]::CreateInstance([type ]::GetTypeFromProgID("MMC20.Application.1" ))$comobj | Get-Member $comobj .Document.ActiveView.ExecuteShellCommand("cmd" ,$null ,"/c calc" ,"7" )
远程进程外 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用于创建远程实例
IUnknown接口 IUnknown 接口是COM中的底层接口,包含所有对象的基本操作和所有接口的基本操作。所有的COM对象都必须继承IUnknown接口并实现它。
IUnknown接口结构大体如下
1 2 3 4 5 6 7 8 9 10 struct IUnknown { virtual HRESULT STDMETHODCALLTYPE QueryInterface ( REFIID riid, _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> 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 ; 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 ); } 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);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);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);VARIANT args = { VT_EMPTY }; args.vt = VT_BSTR; args.bstrVal = SysAllocString (L"calc.exe " ); 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---
实现方法:
初始化COM对象,设置COM安全等级
创建ITaskService对象,该对象运行在指定文件夹中创建任务
References https://learn.microsoft.com/en-us/windows/win32/api/unknwn/
https://tttang.com/archive/1824
https://paper.seebug.org/1624/