none
ActiveX 권한 상승에 대한 일반적인 이야기와 솔루션들 RRS feed

  • 일반 토론

  • 최근 고객사의 요청으로 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"%s\r\n", wszCLSID);
     wprintf(L"%s\r\n", 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 Explorer\\iexplore.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을 수행하도록 유도하는 코드를 웹에서 작성할 수 있을 것입니다.

     

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


    남정현 (rkttu@rkttu.com) - Visual Studio 2010 한국 공식 팀 블로그 멤버 - http://www.vsts2010.net | Windows Azure Cafe SYSOP - http://cafe.naver.com/wazure | DEVPIA C# Forum SYSOP - http://www.devpia.com/CSharp.MAEUL
    2010년 11월 3일 수요일 오후 1:52