[업데이트] ActiveX 권한 상승에 대한 일반적인 이야기와 솔루션들

최근 고객사의 요청으로 ActiveX 컨트롤 하나를 유지보수하고 있습니다. ActiveX 컨트롤에서 탈피하려는 추세가 있지만, 별 다른 대안이 없어서 ActiveX 컨트롤을 유지보수해야 하는 경우도 아직 우리나라에서는 상당히 많은것 같습니다.


 


Internet Explorer 7.0부터는 보호 모드라는 개념이 새로 소개되었습니다. 보호 모드란, 일종의 Sand-box 개념으로 기존과 같이 현재 로그온한 사용자의 권한을 그대로 물려받아 무분별하게 실행되는 것을 방어하는 안전 장치입니다. Windows XP와는 달리 Windows Vista부터는 일반 사용자를 단순히 관리자로 분류하지 않고, UAC를 통하여 작업에 대해 허가/거절 여부를 정할 수 있게 하였습니다.


 


우리가 권한 상승이라고 이야기하는 기능은 사실 권한 상승을 사용자에게 요청하는 것입니다. 이러한 권한 상승 요청을 구현하기 위하여 이제까지 참고할 수 있는 보편적인 리소스는 EXE 파일과 함께 매니페스트 파일을 배포하는 것이 대표적인 것이었습니다. 그리고 ActiveX 컨트롤의 경우 Elevation Moniker를 통하여 권한 할당을 받는 것이 대표적입니다.


 


ActiveX 컨트롤에서 어떻게 권한 상승을 구현할 수 있는지에 대하여 잘 정리한 권용휘 MVP님의 아티클을 참고하시면 어떻게 권한 상승이 이루어지고 관리될 수 있는지에 대한 컨셉을 확인하실 수 있습니다. (http://www.devpia.com/MAEUL/Contents/Detail.aspx?BoardID=51&MAEULNO=20&no=7669) 저는 권용휘 MVP님의 아티클 위에 몇 가지 내용을 더 첨언해보고자 합니다.


 


1. Visual C++ 6.0 (SP6)에서의 권한 상승 구현


 


놀랍게도, 그리고 안타깝게도, Visual C++ 6.0 개발 도구를 업그레이드하지 못하는 이슈는 도처에 널려있습니다. 기존에 개발되어있던 소프트웨어나 라이브러리가 특정 문자 세트에 완벽하게 맞추어져있지만, 업그레이드할 수 없을만한 이슈 (개발 업체의 부도 – 또는 – 계약 해지 / 담당자의 연락 두절과 같은)로 인하여, 올해부터는 더 이상 일체의 기술 지원을 받을 수 없는 (서비스 팩 다운로드도 MS 공식 홈페이지에서는 더이상 받으실 수 없습니다.) 그런 개발 플랫폼위에서 고군분투해야 하는 상황은 생각보다 자주 있습니다.


 


사실 권한 상승을 제대로 프로그래밍하려면 Windows Vista나 Windows 7 SDK가 필요합니다. 하지만 이들 SDK의 코드를 가져다 사용하려면 개발 도구를 Visual C++ 2005나 2008로 업그레이드할 필요가 있습니다. 하지만 개발 도구나 Windows SDK를 설치하지 않고 간단히 적용할 수 있다고 소개된 방법은 다행히 Visual C++ 6.0에서 온전하게 동작합니다.


 



typedef struct tagBIND_OPTS3 : tagBIND_OPTS2 {
 HWND hwnd;
} BIND_OPTS3, *LPBIND_OPTS3;

위의 코드를 자주 사용하는 헤더에 선언해두면 권한 상승을 위하여 호출하는 CoGetObject에 정확히 바인딩할 수 있습니다. 그리고, 아래의 두 함수를 이용하여, 권한 상승을 지원하는 운영 체제 (Windows Vista 이상의 운영 체제)를 식별하고 실제로 객체를 생성할 수 있습니다.

 




HRESULT CoCreateInstanceAsAdmin(HWND hwnd, REFCLSID rclsid, REFIID riid, OUT void ** ppv)
{
 BIND_OPTS3 bo;
 OLECHAR wszCLSID[50];
 OLECHAR wszMonikerName[300];
 const int CHARS_IN_GUID = 39;


 StringFromGUID2(rclsid, wszCLSID, CHARS_IN_GUID);
 swprintf(wszMonikerName, L”Elevation:Administrator!new:%s”, wszCLSID);
 wprintf(L”%srn”, wszCLSID);
 wprintf(L”%srn”, wszMonikerName);


 memset(&bo, 0, sizeof(bo));


 bo.cbStruct = sizeof(bo);
 bo.hwnd = hwnd;
 bo.dwClassContext = CLSCTX_LOCAL_SERVER;


 return CoGetObject((LPCWSTR)wszMonikerName, &bo, riid, ppv);
}


 


BOOL IsUACRequiredOperatingSystem(void)
{
 BOOL bIsUACRequired = FALSE;
 OSVERSIONINFO sInfo;
 sInfo.dwOSVersionInfoSize = sizeof(OSVERSIONINFO);


 if (GetVersionEx(&sInfo))
 {
  if( sInfo.dwPlatformId == VER_PLATFORM_WIN32_NT &&
   sInfo.dwMajorVersion >= 6 &&
   sInfo.dwMinorVersion >= 0) { // Windows VISTA or Higher
   bIsUACRequired = TRUE;
  }
 }


 return bIsUACRequired;
}


 


Visual C++ 6.0 기반의 프로젝트들은 거의 대부분 ANSI나 Multibyte Character Set 기반의 문자 세트를 기준으로 프로그램이 구성되어있습니다. 확장성을 위하여 Transition이 가능한 데이터 타입 (LPTSTR, LPCTSTR, TCHAR 등)은 거의 고려하지 않았을 확률이 높습니다. 하지만 우리가 호출해야 할 API들은 유니코드 문자열을 필요로 하기 때문에 직접 유니코드를 사용하도록 기존 코드를 수정하였습니다. 그리고 더불어서, StringCchPrintf 함수를 대신하여 sprintf 계열의 함수를 직접 이용하였습니다.


 


2. 실제 ActiveX 컨트롤에 권한 상승을 적용하는 또 다른 방법


 


권용휘 MVP님의 아티클에서 설명하는 방법은, 특정 메서드나 프로퍼티의 호출 과정에서 필요로하는 권한 상승을, 권한 상승을 외부에서 요구하고 결과를 받기 위한 프록시 멤버와, 실제 처리하는 멤버로 이원화하여 구현하는 방식입니다. 대부분의 경우 이 방법으로 해결할 수 있다고 생각합니다. 하지만 간혹, ActiveX 컨트롤의 동작이나 상태, 속성을 정의하는 작업 전체가 시스템에 종속적으로 구성된 경우도 있을 수 있습니다. 이 경우에는, 격리모드에서 실행되는 ActiveX 컨트롤과는 별도로 새로운 인스턴스를 노출시켜야 할 필요가 있습니다.


 


이에 대한 솔루션은 인터넷 검색중에 발견한 어떤 블로그의 아티클에서 찾을 수 있었습니다. (http://hbesthee.tistory.com/620) 이 아티클은 델파이 기반의 솔루션을 제시하고 있었으며 저는 이것을 Visual C++ 기반의 코드로 옮겼습니다.




STDMETHODIMP CtrusEBANK::trusElevate(VARIANT *ret)
{
 AFX_MANAGE_STATE(AfxGetStaticModuleState())


 if (IsUACRequiredOperatingSystem())
 {
  ItrusEBANK *pElevatedObj = NULL;
  HRESULT hr = CoCreateInstanceAsAdmin(NULL,
   CLSID_trusEBANK, IID_ItrusEBANK,
   (void **)&pElevatedObj);


  if (SUCCEEDED(hr))
  {
   ret->vt = VT_DISPATCH;
   ret->pdispVal = pElevatedObj;
   ret->pdispVal->AddRef();
   return S_OK;
  }
  else
  {
   ret->vt = VT_NULL;
   return hr;
  }
 }
 else
 {
  ret->vt = VT_BOOL;
  ret->boolVal = VARIANT_FALSE;
 }


 return S_OK;
}


 


STDMETHODIMP CtrusEBANK::trusNeedElevate(VARIANT *ret)
{
 AFX_MANAGE_STATE(AfxGetStaticModuleState())


 ret->vt = VT_BOOL;


 if (::IsUACRequiredOperatingSystem())
  ret->boolVal = VARIANT_TRUE;
 else
  ret->boolVal = VARIANT_FALSE;


 return S_OK;
}


 


위의 코드에서는 trusNeedElevate를 이용하여 권한 상승이 지원되는 운영 체제인지 파악하는 부분과, trusElevate를 이용하여 권한 상승이 적용될 개체를 반환하는 방법 두 가지를 보여주고 있습니다. 그리고 아래의 코드는 자바스크립트에서의 실제 사용 예시입니다.


 




var obj = new ActiveXObject(“EBANK.trusEBANK”);


if (obj.trusNeedElevate())
{
 var result = obj.trusElevate();
 if (result) {
  obj = result;
 }
}


// 여기서부터 obj 객체 사용

 


이와 같은 방법을 통하여, 권한 상승이 온전하게 ActiveX 컨트롤에 적용될 수 있도록 하여, 원활한 동작 환경을 구현할 수 있을 것입니다.


 


3. Internet Explorer Host Window를 Elevation하는 방법 [업데이트]


 


그리고 최근에 저는 여러가지 방법을 적용해보던 중에 가장 현실적인 타협안 하나를 발견하였습니다. 바로, Internet Explorer Host Window 자체를 Runtime 도중에 Elevation 처리하는 방법으로, 기존의 응용프로그램 기반 매니페스트와 유사하게 작동합니다. 그리고 이 방법은, Host Window를 Elevation하는 데에 사용할 수 있는것 뿐만 아니라, 대리 실행해야 하는 응용프로그램의 Manifest 보유 여부에 관계없이 Elevation에도 사용될 수 있습니다.


 


참고로, 이 방법은 http://www.softblog.com/2008-02/vista-tools/ 에서 소개한 VistaTools.cxx 파일의 코드를 일부 발췌한 것임을 밝혀둡니다.


 



typedef struct _TOKEN_ELEVATION {
    DWORD TokenIsElevated;
} TOKEN_ELEVATION, *PTOKEN_ELEVATION;


 


위의 구조체는 Elevation 상태를 점검하기 위하여 필요한 구조체로 이미 Elevation 처리가 되어있는 호스트 위에서 실행되는 경우 Elevation을 다시 실행하지 않도록 하기 위하여 필요합니다. 이 구조체는 Visual C++ 6.0 기반에서 프로그램을 업데이트할 때 수동으로 지정해야 합니다.


 



#ifndef CSIDL_PROGRAM_FILES
  #define CSIDL_PROGRAM_FILES 0x0026
#endif // CSIDL_PROGRAM_FILES


 


위의 Special Folder Constant는 %PROGRAMFILES% 폴더의 경로를 가져오기 위하여 필요합니다. 마찬가지로 Visual C++ 6.0 기반에서 프로그램을 업데이트할 때 수동으로 지정해야 합니다.


 



#include <shellapi.h>
#include <shlobj.h>


 


마지막으로 위의 두 헤더 파일을 참조하도록 선언하면 일단 준비는 끝납니다.


 




BOOL IsVistaOrHigher(void)
{
 OSVERSIONINFO versionInfo;
 versionInfo.dwOSVersionInfoSize = sizeof(OSVERSIONINFO);


 if (GetVersionEx(&versionInfo) &&
  versionInfo.dwPlatformId == VER_PLATFORM_WIN32_NT &&
  versionInfo.dwMajorVersion >= 6)
  return TRUE;
 else
  return FALSE;
}


 


HRESULT IsElevated(BOOL *pElevated)
{
 HRESULT hResult = E_FAIL;
 HANDLE hToken = NULL;


 if (!IsVistaOrHigher())
  return hResult;


 if (!OpenProcessToken(
  GetCurrentProcess(),
  TOKEN_QUERY,
  &hToken))
  return hResult;


 TOKEN_ELEVATION te = { 0 };
 DWORD dwReturnLength = 0;
 const int TokenElevation = 20;


 if (GetTokenInformation(
  hToken,
  (TOKEN_INFORMATION_CLASS)TokenElevation,
  &te,
  sizeof(te),
  &dwReturnLength))
 {
  hResult = te.TokenIsElevated ? S_OK : S_FALSE;


  if (pElevated)
   *pElevated = (te.TokenIsElevated != 0);
 }


 CloseHandle(hToken);
 return hResult;
}


 


BOOL ShellExecWithVerb(HWND hWnd, LPCTSTR lpVerb, LPCTSTR lpPath, LPCTSTR lpParameters, LPCTSTR lpDirectory)
{
 SHELLEXECUTEINFO executeInfo;
 memset(&executeInfo, 0, sizeof(executeInfo));


 executeInfo.cbSize = sizeof(SHELLEXECUTEINFO);
 executeInfo.fMask = 0;
 executeInfo.hwnd = hWnd;
 executeInfo.lpVerb = lpVerb;
 executeInfo.lpFile = lpPath;
 executeInfo.lpParameters = lpParameters;
 executeInfo.lpDirectory = lpDirectory;
 executeInfo.nShow = SW_NORMAL;


 return ShellExecuteEx(&executeInfo);
}


 


BOOL ShellExecWithElevation(HWND hWnd, LPCTSTR lpPath, LPCTSTR lpParameters, LPCTSTR lpDirectory)
{
 return ShellExecWithVerb(hWnd, _T(“runas”), lpPath, lpParameters, lpDirectory);
}


 


BOOL OpenUrlWithElevation(HWND hWnd, LPCTSTR lpUrl)
{
 _TCHAR lpBuffer[MAX_PATH + 1];


 if (!SHGetSpecialFolderPath(hWnd, lpBuffer, CSIDL_PROGRAM_FILES, 0))
  return FALSE;


 _tcscat(lpBuffer, _T(“Internet Exploreriexplore.exe”));
 return ShellExecWithElevation(hWnd, lpBuffer, lpUrl, _T(“”));
}


 


위의 코드의 내용들 중에서 강조표시된 것이 실제 Elevation을 위하여 필요한 코드입니다. 그리고 실제 사용법은 아래와 같습니다.


 




STDMETHODIMP CMyAXControl::RunElevatedWeb(VARIANT *url)
{
 AFX_MANAGE_STATE(AfxGetStaticModuleState())
 USES_CONVERSION;


 CComBSTR targetUrl;
 url->vt = VT_BSTR;
 targetUrl = CComBSTR(url->bstrVal);


 _TCHAR buffer[MAX_PATH + 1];
 _tcscpy(buffer, (_TCHAR*)OLE2T(targetUrl.m_str));
 BOOL bResult = OpenUrlWithElevation(NULL, buffer);


 if (bResult)
  return S_OK;
 else
  return E_FAIL;
}


 


그리고 위의 메서드가 실제로 실행되어야 할 때와, 그렇지 않을 때를 구분하기 위하여 아래와 같이 상태 확인을 위한 메서드를 배치하는 것도 도움이 됩니다. 🙂


 




STDMETHODIMP CMyAXControl::NeedElevate(VARIANT *ret)
{
 AFX_MANAGE_STATE(AfxGetStaticModuleState())


 ret->vt = VT_I4;
 ret->intVal = 0;


 if (IsVistaOrHigher())
 {
  BOOL bResult = FALSE;


  if (SUCCEEDED(IsElevated(&bResult)))
  {
   if (bResult == TRUE)
    ret->intVal = 4; // 이미 Elevation이 완료됨
   else
    ret->intVal = 3; // Elevation이 필요함
  }
  else
   ret->intVal = 2; // 상태 정보를 조회할 수 없음
 }
 else
  ret->intVal = 1; // UAC가 지원되지 않는 운영체제로 판단함


 return S_OK;
}


 


위의 메서드를 통하여, 반환값이 1 – 또는 – 4로 반환되는 경우에 한정하여 실제로 필요한 코드를 실행하고, 그렇지 않은 경우 Elevation을 수행하도록 유도하는 코드를 웹에서 작성할 수 있을 것입니다.


 


덧) 지적하거나 수정이 필요한 부분이 있으시면 댓글로 남겨주시면 바로 반영하도록 하겠습니다. 감사합니다. 🙂

[업데이트] ActiveX 권한 상승에 대한 일반적인 이야기와 솔루션들”에 대한 7개의 생각

  1. 핑백: Prac's World~ ~
  2. 안녕하세요. 잘 읽었습니다. 혹시 질문을 좀 드려도 될까요?

    현재 만들어진 프로젝트가 VC6 MFC ActiveX (ocx)입니다. 이를 웹에서 실행시 비스타 이상에서
    UAC에 걸리잖아요.. ㅜㅜ 그래서 이를 해결해야하는데.

    2005 이상의 ATL로 변환하기가 쪼금 힘든상황입니다.( 추가된 프로젝트가 옛날 옛날 꺼라.. )

    하여 여기에 설명해주신 코드를 이용해보려고 하는데.. 그게… ATL인것 같은데… MFC AvtiveX상태에서
    권한요청 박스가 나와서 관리자를 얻어 웹에서 원하는 함수를 호출( 바탕화면 아이콘 저장)를 호출 할 수
    있을까요..

    여러가지 방법을 써봤습니다.
    1. manifest 생성+리소스추가 ( 안됨 )
    2. CoCreateInstanceAsAdmin 를 이용한 권한상승 요청 ( 안됨 = 로그를 찍어보면 권한요청 박스가 안나오지만 다 성공했따고 나오네요.. 그러나 바탕화면에 아이콘이 생성은 안되네요.. )
    3. 남정현님께서 쓰신 1번,, ( 안됨 )
    4. “” 2번 ( 웹에서 호출 할 수 있는 리턴 STDMETHODIMP 생성 불가 )
    5. “” 3번 ( “” ) – 리턴으로 ret에 관리자 권한을 줘서 웹에서 그 obj를 이용해서
    다시 ocx 내부에 있는 함수를 호출( 아이콘 생성) 하라느느 것 같은데 그 리턴 값을 넣지 못하겟습니다.

    도움 부탁 드리겠습니다.

    • 여러모로 어려움이 많으신것 같네요. 🙂

      우선 시도해보셨다고 했던 1번은 Inprocess Server 기반의 ActiveX 컨트롤에 대해서는 해당 사항이 없습니다. 이 방법을 적용하려면 Internet Explorer에 대해서 Manifest를 작성해야하는 우스운 상황이 연출됩니다.

      제가 작성했었던 내용들은 모두 MFC 프로젝트 위에서 테스트하고 작성했었던 내용들입니다. 혹시 구체적으로 어떤 부분에서 어려움이 있으셨었는지 말씀해주시면 좋겠습니다. 🙂

  3. 음. 추가적으로요..
    우선 6.0 MFC ActiveX에서 코드 추가로만 자신의 ocx에서는 권한 상승후 파일 쓰기 등을 할 수 가 없는것 같은데.. 맞나요?

    대안으로 2005 Atl로 권한 상승 dll을 만들고 이 dll에서 권한 상승을 요구하는 exe 나 dll을 따로 만들어서
    호출을 하라는 것 같은데. 맞나요?

    그렇다면 권한상승용dll + 작업용(exe,dll)을 cab 만들때 같이 배포를 하라는 것인가요?
    개념이 잘 잡히지 않네요. 어짜피 파일 다운로드 설치가 권한상승dll에서는 불가능할테니 이 방법 밖에 없겠죠?

    • 별도로 추가 프로그램을 작성하라는 의미는 아니었습니다. 대신, 현재 로드된 URL을 그대로 가져와서 관리자 권한을 획득한 Internet Explorer를 대리 실행할 수 있도록 해준다는 것이 제가 이 글을 통하여 설명했던 결론의 주된 골자입니다.

  4. 답변 감사드립니다

    우선 저의 주된 질문은 이미만들어진
    vc60 mfc activex(ocx)에 1 2 혹은 3번의 코드 추가로만으로

    이 ocx에 관리자 권한을 주는 것이 가능한건지 입니다

    ocx에서는 단순히 웹에서 아이콘파일을 다운받아 바탕화면에 바로가기 아이콘을 만드는 일늘 합니다

    리턴을로 관리자토큰을 넘겨 var obj에서 obj에 대입하여 사용이 가능한지입니다

    이것을 하기위해 위의 시도 들을 해봤는데 안되네요

    2005 atl dll로는 간단한 프로그램을 만들어 성공했습니다

댓글 남기기