CoInitializeSecurity의 전역 설정을 재정의하는 CoSetProxyBlanket 함수 사용법
지난 글에서,
역공학을 통한 lxssmanager.dll의 ILxssSession 사용법 분석
; https://www.sysnet.pe.kr/2/0/12677
드디어 COM 개체를 직접 사용해 WSL을 제어하는 방법을 (QueryState 함수에 한해) 살펴봤는데요. 혹시나, 나중에 Type Library 및 문서가 공개돼 모든 인터페이스의 함수들을 사용할 수 있게 된다면 CoInitializeSecurity 문제를 가진 Win32 API 래퍼 함수에 더 이상 얽매이지 않을 수 있습니다.
왜냐하면, 프로세스 수준에서 단 한 번만 호출 가능한 CoInitializeSecurity의 보안 설정을 인터페이스 단위로 재정의할 수 있는 CoSetProxyBlanket 함수를 사용하면 되기 때문입니다.
CoSetProxyBlanket function (combaseapi.h)
; https://learn.microsoft.com/en-us/windows/win32/api/combaseapi/nf-combaseapi-cosetproxyblanket
IClientSecurity::SetBlanket method (objidl.h)
; https://learn.microsoft.com/en-us/windows/win32/api/objidl/nf-objidl-iclientsecurity-setblanket
이에 관해서는 다음의 문서가 설명하고 있는데요,
Client-Side Requirements for Impersonation
; https://learn.microsoft.com/en-us/windows/win32/cossdk/client-side-requirements-for-impersonation
The client can indicate the impersonation level programmatically, using either CoSetProxyBlanket—equivalent to IClientSecurity::SetBlanket, which can be called as often as necessary—or CoInitializeSecurity, which can be called once per process.
한번 테스트를 해볼까요? ^^
우선, 다음과 같이 CoInitializeSecurity를 호출해 두면 QueryState 함수가 E_ACCESSDENIED를 반환하며 실패하게 될 것입니다.
#include <Windows.h>
#include <stdio.h>
#include "lxssmanager.h"
HRESULT OleMain();
BOOL WslIsDistributionRegistered(ILxssSession* pSession, const wchar_t* distroName);
int main()
{
HRESULT hr = CoInitializeEx(nullptr, 0);
if (hr != S_OK)
{
return 1;
}
hr = OleMain();
CoUninitialize();
return (hr == S_OK) ? 0 : hr;
}
HRESULT OleMain()
{
HRESULT hr = S_FALSE;
hr = CoInitializeSecurity(nullptr,
-1,
nullptr,
nullptr,
RPC_C_AUTHN_LEVEL_DEFAULT,
RPC_C_IMP_LEVEL_IDENTIFY,
nullptr,
EOAC_STATIC_CLOAKING,
nullptr);
if (hr != S_OK)
{
return hr;
}
ILxssSession* pSession = nullptr;
do
{
hr = CoCreateInstance(CLSID_LxssManager, nullptr, CLSCTX_LOCAL_SERVER,
__uuidof(ILxssSession), (PVOID*)&pSession);
if (hr != S_OK)
{
break;
}
printf("IsRegistered: %d\n", WslIsDistributionRegistered(pSession, L"Ubuntu20.04"));
} while (false);
if (pSession != nullptr)
{
pSession->Release();
}
return hr;
}
BOOL WslIsDistributionRegistered(ILxssSession* pSession, const wchar_t* distroName)
{
IID iid;
HRESULT hr = pSession->QueryState(distroName, 0, &iid);
// hr == E_ACCESSDENIED 반환
if (hr != S_OK)
{
return FALSE;
}
return TRUE;
}
자, 그럼 전역 보안 설정을 override하는 CoSetProxyBlanket을 위의 예제 코드에서 CoCreateInstance 호출 후에 사용하면,
do
{
hr = CoCreateInstance(CLSID_LxssManager, nullptr, CLSCTX_LOCAL_SERVER,
__uuidof(ILxssSession), (PVOID*)&pSession);
if (hr != S_OK)
{
break;
}
hr = CoSetProxyBlanket(pSession, RPC_C_AUTHN_WINNT, RPC_C_AUTHZ_NONE,
nullptr, RPC_C_AUTHN_LEVEL_CALL, RPC_C_IMP_LEVEL_IMPERSONATE, nullptr, EOAC_STATIC_CLOAKING);
if (hr != S_OK)
{
break;
}
printf("IsRegistered: %d\n", WslIsDistributionRegistered(pSession, L"Ubuntu20.04"));
} while (false);
이제 정상적으로 (CoInitializeSecurity 함수의 호출 여부에 상관없이) WslIsDistributionRegistered/QueryState 함수가 동작하는 것을 확인할 수 있습니다.
그럼 코드를 좀 다듬어서,
using System;
using System.Runtime.InteropServices;
using WslSdk.Interop;
using static WslSdk.Interop.NativeMethods;
namespace WslSdk
{
public class LxSession : IDisposable
{
public static Guid CLSID_LxssManager = new Guid("4f476546-b412-4579-b64c-123df331e3d6");
public static Guid IID_ILxssSession = new Guid("536A6BCF-FE04-41D9-B978-DCACA9A9B5B9");
public static Guid IID_ILxInstance = new Guid("8f9e8123-58d4-484a-ac25-7ef7d5f7448f");
ILxssSession pSession;
public LxSession()
{
IntPtr pUnknown = IntPtr.Zero;
try
{
int hr = NativeMethods.CoCreateInstance(CLSID_LxssManager, IntPtr.Zero, NativeMethods.CLSCTX.LOCAL_SERVER, IID_ILxssSession,
ref pUnknown);
if (hr != 0)
{
return;
}
hr = NativeMethods.CoSetProxyBlanket(pUnknown, RpcAuthN.WINNT, RpcAuthZ.NONE,
IntPtr.Zero, RpcAuthnLevel.Call, RpcImpLevel.Impersonate, IntPtr.Zero, EoAuthnCap.StaticCloaking);
if (hr != 0)
{
return;
}
pSession = Marshal.GetTypedObjectForIUnknown(pUnknown, typeof(ILxssSession)) as ILxssSession;
}
finally
{
Marshal.Release(pUnknown);
}
}
public bool WslIsDistributionRegistered(string distroName)
{
if (pSession == null)
{
return false;
}
Guid instanceId = Guid.Empty;
pSession.QueryState(distroName, 0, ref instanceId);
return instanceId != Guid.Empty;
}
public void Dispose()
{
if (pSession != null)
{
Marshal.ReleaseComObject(pSession);
pSession = null;
}
}
}
}
해보는 김에,
WebBrowser를 얹은 Windows Forms 예제에도,
using System;
using System.Windows.Forms;
using WslSdk;
namespace WindowsFormsApp1
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void Form1_Load(object sender, EventArgs e)
{
this.webBrowser1.Navigate("https://forum.dotnetdev.kr");
string distroName = Wsl.GetDefaultDistro().DistroName;
using (LxSession lxSession = new LxSession())
{
MessageBox.Show(lxSession.WslIsDistributionRegistered(distroName).ToString());
}
}
}
}
적용해 보면 QueryState를 직접 호출한 WslIsDistributionRegistered 버전으로는 잘 동작합니다.
결국, 전역적으로 설정되는 CoInitializeSecurity와 wslapi.dll의 Win32 API 호출 간의 문제를 극복하기 위해 별도의 프로세스로 호스팅하는 노력들이,
COM+ 서버 응용 프로그램을 이용해 CoInitializeSecurity 제약 해결
; https://www.sysnet.pe.kr/2/0/12667
애당초 마이크로소프트 측에서 ILxssSession의 Type Library를 공개했다면 없었을 문제였던 것입니다.
(
첨부 파일은 이 글의 예제 코드를 포함합니다.)
또 다른 예제 코드로,
C++ (Cpp) CoSetProxyBlanket Examples
; https://cpp.hotexamples.com/examples/-/-/CoSetProxyBlanket/cpp-cosetproxyblanket-function-examples.html
WMI의 IWbemServices 인터페이스 개체에 대해 CoSetProxyBlanket을 호출한 사례를 볼 수 있습니다.
참고로 아래의 문서를 보면,
Win32_TSExpandEnvironmentVariables class
; https://learn.microsoft.com/en-us/windows/win32/termserv/win32-tsexpandenvironmentvariables
To connect to the \root\CIMV2\TerminalServices namespace, the authentication level must include packet privacy. For C/C++ calls, this is an authentication level of RPC_C_AUTHN_LEVEL_PKT_PRIVACY, which can be set by using the CoSetProxyBlanket COM function.
CoSetProxyBlanket이 요구하는 RPC_C_AUTHN_LEVEL_??? 설정이 상황마다 다르다는 것을 알 수 있습니다. 그리고, 일부 옵션의 값을 적절치 않게 설정하면,
hr = CoSetProxyBlanket(pSession, RPC_C_AUTHN_NONE, RPC_C_AUTHZ_NONE,
nullptr, RPC_C_AUTHN_LEVEL_CALL, RPC_C_IMP_LEVEL_IMPERSONATE, nullptr, EOAC_STATIC_CLOAKING);
// hr 0x800706d4 : The authentication level is unknown.
친절하게 그에 대한 옵션을 알려줘서 그나마 쉽게 문제점을 발견할 수 있습니다. ^^
휴~~~ 이제야 CoInitializeSecurity로부터 시작한 기나긴 여정이 끝났군요. 바로 이 글을 쓰기 위해 그동안 다음의 모든 주제가 쓰인 것입니다. ^^;
C# - CoCreateInstance 관련 Inteop 오류 정리
; https://www.sysnet.pe.kr/2/0/12678
Wslhub.Sdk 사용으로 알아보는 CoInitializeSecurity 사용 제약
; https://www.sysnet.pe.kr/2/0/12665
별도 DLL에 포함된 타입을 STAThread Main 메서드에서 사용하는 경우 CoInitializeSecurity 자동 호출
; https://www.sysnet.pe.kr/2/0/12666
COM+ 서버 응용 프로그램을 이용해 CoInitializeSecurity 제약 해결
; https://www.sysnet.pe.kr/2/0/12667
ionescu007/lxss github repo에 공개된 lxssmanager.dll의 CLSID_LxssUserSession/IID_ILxssSession 사용법
; https://www.sysnet.pe.kr/2/0/12676
역공학을 통한 lxssmanager.dll의 ILxssSession 사용법 분석
; https://www.sysnet.pe.kr/2/0/12677
C# - DLL Surrogate를 이용한 Out-of-process COM 개체 제작
; https://www.sysnet.pe.kr/2/0/12668
DLL Surrogate를 이용한 Out-of-process COM 개체에서의 CoInitializeSecurity 문제
; https://www.sysnet.pe.kr/2/0/12670
CoInitializeSecurity의 전역 설정을 재정의하는 CoSetProxyBlanket 함수 사용법
; https://www.sysnet.pe.kr/2/0/12679
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]