ATL ActiveX 만들기 - Part4. 관리자권한 UAC Elevation
요즘은 확실히 ActiveX 사용이 눈에 띄게 줄어들긴 했지만, 아직도 정신 못차리고(-_-) ActiveX를 요구하는 사람들도 가끔 있다. 아마 앞으로도 한동안은 ActiveX가 필요한 경우가 간혹 있을지도 모르니까, 방법을 다 잊어버리기 전에 요점만 정리해 두자.
2010/06/23 - [소프트웨어 개발] - ATL ActiveX 만들기 - Part2. 컨트롤 구현
2010/07/05 - [소프트웨어 개발] - ATL ActiveX 만들기 - Part3. 이벤트(Connection point) 구현
오늘은 연재의 마지막으로, Windows Vista 이상의 운영체제에서 동작하기 위한 작업을 진행하겠다.
관리자 권한: UAC(User Access Control)
아시다시피, Windows Vista부터 UAC라는 개념이 도입 되었다.
UAC는 한마디로, "시스템에 중대한 영향을 끼치는 작업을 하려면 적절한 권한을 갖고 있을 것"이라고 할 수있다. 보통때는 "일반 사용자 권한"으로 사용하다가, 새로운 어플리케이션의 설치나 레지스트리 편집처럼 시스템에 중대한 변경을 가하는 작업을 하기 위해서는 "관리자 권한"을 획득해야 한다.
인터넷 익스플로러는 여기서 한단계 더 나가서 "보호모드(Low IL)"에서 동작하게 되었고, 이 위에서 동작하는 ActiveX도 많은 제약을 받게 되었다. 이러한 변화는 최근 대부분의 바이러스가 ActiveX형태로 전파되는 점을 감안하면 어쩌면 좀 늦은감 마저 있긴 하다.
아무튼, 이제 사용자 PC에 있는 파일을 읽고 쓰거나 하는 저수준 작업은 원칙적으로 ActiveX에서 허용되지 않는다. 이러한 작업을 하고 싶다면, ActiveX가 관리자 권한을 획득해야 한다.
프로그램 흐름
최종 결과물 Javascript를 통해 전체 프로그램의 흐름을 파악해 보자.
여러가지 방법이 있지만, 나는 Javascript에서 프로그램의 전체 흐름을 제어하고, ActiveX는 필요한 기능만을 제공하는 방법을 선호한다.
- function runInUAC() {
- var helloCtrl = null;
- try {
- //
- // 1. ActiveX 컨트롤의 인스턴스를 생성.
- //
- helloCtrl = new ActiveXObject("GreenmaruX.HelloCtrl");
- if (helloCtrl == null) {
- // HelloCtrl이 시스템에 설치되지 않은 경우의 처리.
- alert("GreenmaruX HelloCtrl was not installed.");
- }
- //
- // 2. UAC가 필요한 OS인지를 판단.
- //
- if (helloCtrl.IsNeedElevation) {
- //
- // 3. 관리자 권한이 적용된 개체를 구함.
- //
- helloCtrl = helloCtrl.GetElevationObject();
- }
- //
- // 4. 관리자 권한이 필요한 저수준 기능을 사용.
- //
- helloCtrl.SomeFunction();
- }
- catch (ex) {
- var em = "ERROR:" + ex.message + "(" + ex.number + ")";
- alert(em);
- }
- }
대충 감을 잡으셨는가? 그러면 하나씩 구현해 보도록 하자. 다만, 관리자 권한을 획득하는 과정까지가 중요하기 때문에 4번 SomeFunction은 실제로 구현하지 않겠다.
UAC 필요여부 판단하기
아직도 Windows XP는 많이 사용되고 있기 때문에, 무조건 UAC를 진행할 필요는 없다. Windows OS를 판별해서 UAC가 필요한 운영체제인지를 판단하도록 하자. OS버전을 알아내기 위해서 COSVersion이라는 클래스를 사용했다. 이 클래스에 대한 설명은 다음 포스트를 참조하시기 바란다.
2009/04/13 - [소프트웨어 개발] - Windows OS Version 알아내기
m_isNeedElevation이라는 BOOL형 멤버변수를 추가하고, FinalConstruct에서 UAC의 필요 여부에 따라 값을 설정하도록 했다. 참고로, FinalConstruct는 이름처럼 개체 생성이 완료되는 시점에서 호출되는 함수다.
- HRESULT FinalConstruct()
- {
- // Vista이상의 운영체제는 UAC elevation이 필요합니다.
- Greenmaru::COSVersion osv;
- _isNeedElevation = osv.IsWindowsVistaOrLater();
- return S_OK;
- }
그리고 이 값을 개체 외부로 알리기 위한 IsNeedElevation COM Property를 추가해 준다. 이 값은 개체 외부에서 설정할 필요는 없으므로 Get함수만 구현하도록 하겠다.
- STDMETHODIMP CHelloCtrl::get_IsNeedElevation(VARIANT_BOOL* pVal)
- {
- *pVal = m_isNeedElevation;
- return S_OK;
- }
UAC Elevation
UAC 권한상승(Elevation)의 원리는 사실 간단하다. ActiveX객체가 자신의 새로운 인스턴스를 권한상승을 거쳐 만들도록 한다.
GetElevationObject라는 COM Method를 IHelloCtrl에 추가하자. 반환형은 VARIANT*형으로 지정한다. 이 함수는 권한 상승된 HelloCtrl의 새로운 인스턴스를 반환해 준다.
HelloCtrl.cpp에서 strsafe.h를 Include해 준 다음, 함수를 다음과 같이 구현하록 한다.
- STDMETHODIMP CHelloCtrl::GetElevationObject(VARIANT* lpvObject)
- {
- HRESULT hr;
- BIND_OPTS3 bo;
- WCHAR wszCLSID[64];
- WCHAR wszMonikerName[512];
- IHelloCtrl * objElevator = NULL;
- if ( ! m_isNeedElevation )
- {
- lpvObject->vt = VT_DISPATCH;
- lpvObject->pdispVal = this;
- return S_OK;
- }
- // 권한 상승을 시도합니다.
- StringFromGUID2( CLSID_HelloCtrl, wszCLSID, sizeof(wszCLSID)/sizeof(wszCLSID[0]) );
- hr = StringCchPrintfW( wszMonikerName, sizeof(wszMonikerName)/sizeof(wszMonikerName[0]), L"Elevation:Administrator!new:%s", wszCLSID );
- if ( FAILED(hr) )
- {
- lpvObject->vt = VT_NULL;
- return S_FALSE;
- }
- memset( &bo, NULL, sizeof(bo) );
- bo.cbStruct = sizeof(bo);
- bo.hwnd = NULL;
- bo.dwClassContext = CLSCTX_LOCAL_SERVER;
- hr = CoGetObject( wszMonikerName, &bo, IID_IHelloCtrl, (void**)&objElevator );
- if ( FAILED(hr) )
- {
- lpvObject->vt = VT_NULL;
- return S_FALSE;
- }
- lpvObject->vt = VT_DISPATCH;
- lpvObject->pdispVal = objElevator;
- lpvObject->pdispVal->AddRef();
- return S_OK;
- }
천천히 살펴보면 알겠지만, 핵심은 CoGetObject를 통해 COM개체 자신의 새로운 인스턴스를 만들어 내는 것이다. 다만 일반적인 CoCreateObject등의 방식으로는 Elevation처리를 거칠 수 없기 때문에 조금 더 복잡해 졌다.
그 다음, 문자열 리소스를 하나 추가한다. 나는 IDS_ELEVATION이라고 이름지었는데, 이 문자열은 Elevation과정에서 사용자에게 컨트롤의 이름으로 표시된다.
그리고, DLL이름.rgs파일(Greenmaru.rgs)을 다음과 같이 만든다.
아시다시피, rgs파일의 내용은 COM개체를 시스템에 등록할 때 레지스트리에 기록된다. 다른 컨트롤을 만들때는 대충 어디를 어떻게 바꿔야 할지를 짐작할 수 있을 것이다.
- HKCR
- {
- NoRemove AppID
- {
- '%APPID%' = s 'GreenmaruX'
- {
- val DllSurrogate = s ''
- }
- 'GreenmaruX.DLL'
- {
- val AppID = s '%APPID%'
- }
- }
- }
다음으로, 컨트롤이름.rgs파일(HelloCtrl.rgs)의 내용은 다음과 같다.
- HKCR
- {
- GreenmaruX.HelloCtrl.1 = s 'Greenmaru Example ActiveX Control'
- {
- CLSID = s '{4C80B2EE-6D19-4F77-9946-DF7AD17EEE67}'
- }
- GreenmaruX.HelloCtrl = s 'Greenmaru Example ActiveX Control'
- {
- CLSID = s '{4C80B2EE-6D19-4F77-9946-DF7AD17EEE67}'
- CurVer = s 'GreenmaruX.HelloCtrl.1'
- }
- NoRemove CLSID
- {
- ForceRemove {4C80B2EE-6D19-4F77-9946-DF7AD17EEE67} = s 'Greenmaru Example ActiveX Control'
- {
- ProgID = s 'GreenmaruX.HelloCtrl.1'
- VersionIndependentProgID = s 'GreenmaruX.HelloCtrl'
- ForceRemove Programmable
- InprocServer32 = s '%MODULE%'
- {
- val ThreadingModel = s 'Apartment'
- }
- val AppID = s '%APPID%'
- Elevation
- {
- val Enabled = d 1
- }
- val LocalizedString = s '@%MODULE%,-101'
- ForceRemove Control
- ForceRemove 'ToolboxBitmap32' = s '%MODULE%, 106'
- MiscStatus = s '0'
- {
- '1' = s '%OLEMISC%'
- }
- TypeLib = s '{C5133EA0-1CE5-4FFC-9C7D-79AE48AD2233}'
- Version = s '1.0'
- }
- }
- }
기본적으로 생성되는 rgs파일의 내용이 Visual Studio의 버전에 따라 조금씩 차이가 있으므로, 위의 내용을 참고해서 적절히 편집해 주도록 한다.
주의할 부분은 32번째 라인의 LocalizedString이다. LocalizedString은 UAC화면에서 표시할 문자열 리소스의 숫자형 ID를 의미한다. 101은 리소스에 추가한 IDS_ELEVATION을 의미한다.
디버그
F5를 누르고 디버거 컨트롤을 실행해 보면, UAC Elevation화면이 표시되지 않는다. 이 경우는 보호모드가 작동하지 않기 때문이다. 윈도우 탐색기를 통해 HelloCtrl.htm을 직접 실행해야 예상된 결과를 볼 수 있다.
여기서 중요한 문제!
SomeFunction의 구현부에 아무런 처리나 집어넣고 중단점(Break point)을 걸어보자. 이 상태에서 실행하면 디버거에서 중단점을 잡지 못한다.
이유는 두가지다.
첫째로, UAC를 거치면서 디버거가 실행한 컨트롤의 인스턴스가 아니라, (GetElevationObject함수에서) Elevation을 거쳐서 새롭게 생성된 컨트롤의 인스턴스를 통해 Logic이 진행되기 때문이다.
또한, 내가 사용한 방식은 Javascript에서 CreateObject를 통해 ActiveX인스턴스를 만들어낸 것이므로 디버거가 추적할 수 없게 되버린다.
이런 상태로 디버그를 한다는건 무척 짜증스러운 일이다. Visual Studio의 강력한 디버그 기능을 사용하지 못하게 되므로, 전통적인 디버그 방식 - 실행하고 출력해서 확인하고 수정하는 - 밖에는 쓸 수 없는 것이다.
따라서, 고전적인(?) object 태그를 통해 개체를 생성하고, 디버그를 위한 Javascript를 별도로 작성하는 쪽을 추천하겠다. 조금 귀찮기는 해도, 디버거 없이 테스트 하는 쪽보다는 훨씬 효과적인 방법이다.
- <script type="text/javascript">
- function doDebug() {
- var helloCtrl = document.getElementById("HelloCtrl");
- //
- // UAC를 통한 개체생성 부분을 제거하고, 필요한 함수들을 테스트한다.
- //
- helloCtrl.SomeFunction();
- }
- </script>
- <object id="HelloCtrl" classid="CLSID:4C80B2EE-6D19-4F77-9946-DF7AD17EEE67"></object>
마무리
이제 남은 작업은 컨트롤에 서명(Signing)하고 CAB으로 만들어서 배포하는 것이다. 이 과정에 관한 정보는 참고할만한 문서를 찾기 쉬운 편이므로 여기서는 다루지 않겠다.
소스코드를 다운로드 하게 되면, Build한 다음 프로젝트를 닫았다가 다시 로드해서 보시기 바란다.