프로그래밍

    하바나 2008. 8. 7. 23:22

    윈도우 비스타 지원 프로그래밍 가이드

     

    1.      개요

     

    윈도우 XP(이하 XP) 기반에서 개발된 프로그램를 윈도우 비스타(이하 비스타) 기반에서 동작하도록 수정하는 과정에서 파악한 프로그래밍 가이드라인을 설명합니다.

    이 글을 통해서 다른 분들은 비스타에서의 강화된 보안(UAC), 다중 사용자 환경 지원, NT 서비스 및 IE 프로그래밍에 대해서 좀 더 쉽게 이해하실 수 있기를 바랍니다. 저 처럼 고생하지 마시구요^^

     

    2.      비스타에서의 문제점 파악하기

    비스타에서의 프로그램 오동작의 증상과 원인을 크게 8가지로 나열해 보았습니다.

     

    A.        HKCU 또는 HKLM Run 키를 사용하여 자동 시작시킨 프로그램들 중 admin 권한이 필요한 프로그램들은 아래 그림과 같이 윈도우 시작 시 차단됩니다. 

     

    B.        Winlogon Notification Package가 작동하지 않습니다..

    XP까지는 HKEY_LOCAL_MACHINE|Software\Microsoft\Windows NT\CurrentVersion\Winlogon\Notify 키에 dll을 등록함으로써 세션 로그온 이벤트를 감지하여 프로그램을 실행시킬 수 있었지만, 비스타부터는 보안 문제 때문에 이 기능이 더 이상 지원되지 않습니다. 첫 번째 문제를 해결하기 위해 이 기술을 사용해보려고 괜히 시도했다가는 시간만 날립니다.

     

    C.       Admin 권한을 요구하는 프로그램은 실행 시마다 사용자의 허락 여부를 묻습니다.

      

    D.        Admin 권한이 필요한 프로그램, 특히 C:\Windows 이하 폴더나 C:\Program Files 이하의 폴더 또는 HKLM 이하의 레지스트리를 변경해야 하는 프로그램인데 비스타가 그렇게 인식하지 못하는 경우, 그냥 아이콘을 더블클릭해서 실행하면 기능이 제대로 작동하지 않거나 실행되지 않습니다. 그래서 매번 아이콘을 우측 마우스로 클릭해서 아래 그림과 같이 “관리자 권한으로 실행(A)” 메뉴를 선택하여 실행해 줘야 합니다. 

          

     

    E.        Admin 권한을 가진 사용자로 로그인한 상태이더라도 실행되는 모든 프로그램들은 Admin 권한으로 실행되지 않고 일반 User 권한으로 실행되므로 응용 프로그램은 쓰기 가능한 폴더와 레지스트리 위치에 있어서 많은 제약을 받게 됩니다. 쓰기 가능한 폴더는 Users 폴더의 개인 ID 폴더 이하와 윈도우가 설치되지 않은 다른 드라이브안의 폴더들이며 레지스트리는 HKCU 이하에만 쓰기 가능합니다.

     

    F.        아래와 같이 기존의 중복 실행 방지 기술로 작성된 프로그램은 두 번째 로그인 세션에서 프로그램이 실행되지 않습니다.

    If App.PreInstance Then ‘중복 실행이면 빠져나가기

    Goto Exit_Sub

    End If

     

    A.        IE 안에서 실행되는 플러그인은 사용자 계정 폴더와 사용자 레지스트리(HKCU 이하) 안의 데이터도 변경하지 못하고, 다른 응용 프로그램 윈도우에 윈도우 메시지를 전달할 수도 없으며 Ping 테스트를 위한 IcmpCreateFile API 호출도 실패합니다.

     

    B.        Visual Basic 6에서 호출한 HeapAlloc HeapFree 함수가 오류를 일으킬 수 있습니다이번 프로젝트의 경우 맥어드레스를 구하기 위한 아래 소스 중 HeapFree에서 오류가 발생하였습니다.

    pASTAT = HeapAlloc(GetProcessHeap(), HEAP_GENERATE_EXCEPTIONS Or HEAP_ZERO_MEMORY, NCB.ncb_length)

              

       If pASTAT <> 0 Then

          NCB.ncb_buffer = pASTAT

          Call Netbios(NCB)

         

          CopyMemory AST, NCB.ncb_buffer, Len(AST)

                 

         'convert the byte array to a string    

          GetMACAddress = MakeMacAddress(AST.adapt.adapter_address(), sDelimiter)

          HeapFree GetProcessHeap(), 0, pASTAT

     

        Else

          Debug.Print "memory allocation failed!"

          Exit Function

       End If

     

    3.      해결 방안

     

    이번 프로젝트를 수행하기 한달 전에 “서비스를 이용한 프로그램 실행에 관한 연구”를 통해 나름 위의 문제점들에 관한 대부분의 해결책을 이미 가지고 있었던 게 큰 도움이 되었습니다. 그 당시 핵심은 서비스 등록과 서비스에서 특정 세션에 그 세션의 로그온 계정으로 프로그램을 실행시키는 기술이었고, 그 중에서도 핵심은 XYNTService라는 프로그램이었습니다. 이에 대해서는 아래에서 다시 설명하기로 하겠습니다.

     

    해결 방안의 핵심은 크게 1) 서비스를 이용하여 UAC 회피하기, 2) 다중 사용자 환경을 위하여 세션 이벤트 감지하여 실행시키기, 3) 권한 범위 내에서 쓰기 가능한 리소스 찾기 등으로 나뉠 수 있습니다.

     

    4.      UAC 이해하기

     

    A.        보안을 강화하기 위해서 비스타의 UAC(User Account Control)는 윈도우 로그온 시 맨 처음 실행되는 쉘 프로세스(기본은 explorer.exe)를 로그온한 사용자 계정이 설혹 admin 권한이 있다 하더라도 항상 일반 User 권한으로 실행시킵니다

     

    B.        그 외 UAC가 하는 일은 다음과 같습니다.

           admin 권한이 필요한 프로그램이 자동으로 실행되는 것을 차단

           admin 권한이 필요한 프로그램을 사용자가 직접 실행시킬 때에도 항상 허락 여부를 묻는 팝업창을 표시

           admin 권한이 없는 프로그램의 Windows, Program Files 이하 폴더 변경 차단.

           admin 권한이 없는 프로그램의 HKLM 이하 레지스트리 변경 차단

     

    C.       그럼 비스타는 어떻게 특정 프로그램이 admin 권한을 필요로 하는지 판단하는 것일까요? 답은 아래와 같습니다.

           설치본 작성 툴(InstallShield )로 작성된 프로그램일 때.

           실행파일명에 update 또는 setup이라는 단어가 들어 있을 때.

           실행파일명과 동일한 manifest 파일이나, 실행파일에 리소스로 추가된 manifest 파일 안에 권한 상승 요청 태그가 기록되어 있을 때.

           정확하지는 않지만 프로그램이 사용하는 API를 알아내서 판단하는 듯도 함. 귀찮아서 더 알고 싶지 않네요ㅡㅡ;;

     

    5.      NT 서비스 이해하기

     

    A.        NT 서비스(이하 서비스)윈도우 NT이후 운영체제에서 사용자 로그온 전에 자동으로 실행될 수 있도록 고안된 프로그램 구조입니다.

    B.        서비스는 사용자 UI를 지원하지 않습니다. 비스타까지는 UI를 지원하지만 차기 윈도우에서는 더 이상 지원되지 않을 것으로 알려져 있습니다.

    C.       모두 로컬 SYSTEM 계정으로 실행되므로 admin 권한을 기본으로 가지며 다행스럽게도 비스타의 UAC에 의하여 제약을 받지 않습니다.

    D.        서비스는 다중 사용자 환경을 지원하기 위하여 세션 로그온, 로그오프 등의 세션 관련 이벤트들을 모니터링할 수 있습니다. 참고로 일반 응용프로그램에서는 WTSRegisterSessionNotification 함수를 이용하여 세션 이벤트를 감지할 수 있습니다.

     

    6.      XYNTService 활용

     

    A.        위에서 admin 권한이 있는 프로그램을 부팅 시 자동으로 실행시킬 수 있으려면 응용 프로그램을 서비스로 등록해야 한다는 것을 알았습니다. 그런데 서비스로 등록해 놓고 보면 다음과 같은 부작용을 발견하게 됩니다. 첫째, UI가 표시되지 않습니다. 둘째, 응용 프로그램에 서비스 관련 코드를 추가해야 합니다. 셋째, 그래서 프로그램 디버그가 어렵게 됩니다.

    B.        다행히도 서비스에서 어떤 프로그램을 실행시키면 이 프로그램도 마치 서비스인 것처럼 동작하게 됩니다. UI가 있는 서비스라고 말해도 무방하겠죠. 이를 구현하기 위해서는 먼저 작고 안정적인 서비스 프로그램을 만들어서 NT 서비스로 등록한 후 사용해야 하는데 이 서비스를 직접 제작할 수도 있겠지만 대신 공개 소스인 XYNTService를 이용하는 편이 빠를 것 같습니다.

    C.       XYNTService는 서비스 관리 API Service Control Manager(SCM) 인터페이스를 준수하여 스스로 서비스에 등록될 수 있으면 실행 시 XYNTService.ini 파일 안의 정보에 따라 특정 프로그램을 실행시키는 기능을 수행합니다.

    D.        하지만, 이 프로그램을 그대로 사용할 수는 없는데, 그 이유는 바로 XYNTService가 모든 프로그램을 세션 0에 실행시키기 때문이죠.

     

    7.      세션 이해하기

     

    A.        위에서 세션 0에 프로그램이 실행된다는 것은 무슨 의미일까요? 하나의 세션은 한 사용자가 윈도우에 로그온되어 있는 환경을 의미합니다. 보통 우리가 PC를 부팅하여 처음 뜨는 세션을 Console 세션이라고 하고 XP에서는 0, Vista에서는 1 ID를 가집니다. XP에서의 세션 0은 부팅 시의 서비스 실행 환경과 첫 번째 사용자 로그온 환경을 모두 포함합니다. , 첫 번째 사용자가 실행시킨 모든 프로그램들이 세션 0에서 실행되게 된다는 뜻이죠. 하지만 비스타에서의 세션 0은 서비스만 실행될 수 있는 보이지 않는 세션으로 용도가 축소되었고, 사용자 로그온 세션은 항상 세션1부터 시작하도록 변경되었습니다.

    B.        , 비스타에서 세션 0에서 실행된다는 것은 첫째, 실행된 프로그램을 아무도 보지 못하게 된다는 것, 둘째, 여러 사용자가 동시에 로그온한 경우 각 사용자의 세션별로 프로그램이 실행되지 않는다는 것을 의미합니다.

    C.       그리고, 요즘 자주 사용하는 원격 데스크톱을 이용한 다중 접속 시나 XP Home과 비스타의 Fast User Switching 기술은 모두 터미널 서비스를 기반으로 사용자 접속 시 신규 세션을 생성해 주는 기술입니다.

     

    8.      다중 로그온 세션 지원하기

     그럼 XYNTService가 사용자 세션에 프로그램을 실행시키도록 수정해 보겠습니다. 첫 번째로 할 일은 세션 로그온 이벤트 감지하기, 두 번째로 할 일은 특정 세션에 프로그램 실행시키기입니다.

     

    A.        세션 로그온 이벤트 감지하기

    VOID WINAPI XYNTServiceMain( DWORD dwArgc, LPTSTR *lpszArgv )

    {

    DWORD   status = 0;

    DWORD   specificError = 0xfffffff;

     

        serviceStatus.dwServiceType        = SERVICE_WIN32;

        serviceStatus.dwCurrentState       = SERVICE_START_PENDING;

        serviceStatus.dwControlsAccepted   = SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_SHUTDOWN |

                           SERVICE_ACCEPT_PAUSE_CONTINUE | SERVICE_ACCEPT_SESSIONCHANGE;

        serviceStatus.dwWin32ExitCode      = 0;

        serviceStatus.dwServiceSpecificExitCode = 0;

        serviceStatus.dwCheckPoint         = 0;

        serviceStatus.dwWaitHint           = 0;

     

        hServiceStatusHandle = RegisterServiceCtrlHandlerEx(pServiceName,                                                    (LPHANDLER_FUNCTION_EX)XYNTServiceHandler, 0);

    // Initialization complete - report running status

        serviceStatus.dwCurrentState       = SERVICE_RUNNING;

        serviceStatus.dwCheckPoint         = 0;

        serviceStatus.dwWaitHint           = 0; 

        SetServiceStatus(hServiceStatusHandle, &serviceStatus);

    }

           XYNTServiceMain 함수는 서비스가 처음 실행되는 시점에 호출되는 함수이며 여기서 서비스는 서비스 제어 명령과 세션, 드라이버, 하드웨어 변경, 전원 이벤트 등을 모니터링하고 처리할 수 있도록 하기 위해 HandlerEx 함수(이 예제에서는 XYNTServiceHandler) 를 등록해야 합니다.

           핸들러 등록이 된 직후 SetServiceStatus 함수를 이용하여 서비스 관리자에게 이 서비스의 상태를 알려줘야 하며 이 때 dwControlsAccepted 속성에 SERVICE_ACCEPT_SESSIONCHANGE 값을 지정하여 세션 이벤트를 수신할 의사가 있음을 서비스 관리자에게 알려줘야 합니다.

    DWORD WINAPI XYNTServiceHandler(DWORD dwControl, DWORD dwEventType, LPVOID lpEventData, LPVOID lpContext)

    {

            DWORD ret = ERROR_CALL_NOT_IMPLEMENTED;

     

            switch(dwControl)

            {

            case SERVICE_CONTROL_SESSIONCHANGE:

                   WTSSESSION_NOTIFICATION wtsno;

                   CopyMemory(&wtsno, lpEventData, sizeof(WTSSESSION_NOTIFICATION));

                   switch(dwEventType)

                   {

                   case WTS_SESSION_LOGON:

                           OSVERSIONINFO osi;

                           osi.dwOSVersionInfoSize = sizeof(osi);

                           if (GetVersionEx(&osi))

                           {      

                                  //XP에서는새로운세션로그인시바로프로그램을실행시키면

                                  //CreateProcessAsUser 에서233번오류가발생한다.

                                  //XP 자체의 오류로 Vista에서는 해결되었다.

                                  //그래서, 로그온화면이사라질때까지기다린후프로그램들을실행시킨다.

                                  if ((osi.dwMajorVersion <= 5) && (wtsno.dwSessionId > 0))

                                  {

                                          Sleep (3000);

                                  }

                           }

                           //프로그램을실행시킨다.

                           StartProcesses(wtsno.dwSessionId);

                           break;

                   };

                   ret = NO_ERROR;

                   break;

            default:

                   ret = ERROR_CALL_NOT_IMPLEMENTED;

            };

     

            SetServiceStatus(hServiceStatusHandle,  &serviceStatus);

    }

           XYNTServiceHandler 함수는 dwControl 인자로 서비스 제어 명령과 세션 이벤트를 수신하며, 세션 이벤트인 경우 이벤트의 종류는 lpEventData 변수를 보고 알 수 있습니다. 이 소스에서는 사용자 로그온 시에 자동으로 실행되는 기능을 구현하기 위해서 WTS_SESSION_LOGON 이벤트만을 처리하고 있는데, , XP의 경우 세션 1이후(, 두 번째 이후 사용자 로그온)부터는 이벤트 발생 시 바로 프로그램을 실행시키면 named pipe 오류(233)가 발생하므로 잠시 쉬었다가 실행시켜줘야 합니다. StartProcesses 함수는 ini 파일 안의 프로그램 실행 경로를 읽어서 로그온된 세션에 실행시켜주는 역할을 담당하며 인자로는 방금 로그온된 세션 ID를 전달받는다. 구현 원리는 아래 설명됩니다.

     

    B.        특정 세션에 프로그램 실행시키기

           StartProcesses 함수는 원본 소스에는 존재하지 않는 함수로 ini 파일 안의 프로그램 정보를 열거해서 주어진 세션에 실행시키는 역할을 수행합니다. 그럼, 어떻게 프로그램을 특정 세션 안에서 실행되도록 할 수 있을까요? 주어진 세션 ID로 이미 실행되어 있는 winlogon.exe 프로세스의 Access Token(이하 토큰)을 이용하면 됩니다.

           winlogon.exe는 각 세션별로 가장 먼저 실행되며 항상 SYSTEM 계정으로 실행됩니다.

     

           모든 winlogon.exe 프로세스들 중에서 주어진 세션 ID에 실행되는 프로세스만 구합니다. (ProcessIdToSessionId)

           구해진 winlogon.exe 프로세스를 열고(OpenProcess) 토큰을 구합니다. (OpenProcessToken)

           구해진 토큰을 CreateProcessAsUser에 전달하여 프로그램을 실행시킵니다.

           SYSTEM 계정으로 주어진 세션에서 프로그램이 실행됩니다.

           여기서 토큰이 뭔지 잠깐 살펴보도록 하죠. Access Token은 프로세스나 쓰레드의 Security Context(현 상황에서의 보안 정보)을 기술하는 개체입니다. 이 토큰에는 로그온 시 인증된 사용자 및 그룹의 계정 정보와 프로그램이 실행된 세션 ID 정보를 담고 있습니다. 이 토큰은 사용자 로그온 시 자동으로 생성된 후 실행되는 모든 프로세스/쓰레드에 상속되고, 각 프로세스/쓰레드가 특정 보안 개체에 접근할 수 있는지 여부를 신속히 판단하는 기준으로 사용됩니다.

    출처: http://www.codeproject.com/KB/vista-security/VistaSessions.aspx

     

    C.       로그오프 시 프로그램 종료

           이렇게 실행시킨 프로그램들은 SYSTEM 계정으로 실행되었기 때문에 세션 로그오프 시에 윈도우에 의해서 자동으로 종료되지 않는 경우가 발생할 수 있습니다. 윈도우 셀 프로세스(explorer.exe) User 권한으로 실행되어서 충분한 권한이 없기 때문이죠. 종료되지 않고 방치되어 고아가 된 프로그램들은 다음에 다른 사용자가 로그온할 때 비록 세션 ID가 같다고 하더라도 화면에 표시되지 않습니다(이유는 모르겠지만). 그래서 세션 로그오프 시에 실행시킨 프로그램들을 잘 종료하는 것도 중요한 이슈가 되었습니다.

           그래서, 처음에는 XYNTService가 자기가 실행시킨 프로그램들을 로그오프 이벤트 수신 시에 모두 강제로 종료시키도록 하려고 생각을 했으나, 실행시킨 프로그램이 또 실행시킨 자식 프로세스는 종료시키지 못하는 문제가 발생했습니다. 어렵게 돌아가긴 했지만 간단한 해결책이 있는데 바로 실행된 프로그램들이 스스로 종료하도록 수정하는 것이었습니다. VB6 프로그램이라면 그냥 Form_QueryUnload 이벤트에서 UnloadMode 2이면 스스로 종료(Unload Me)하면 되는 것이죠. 여기서 2는 세션 로그오프로 인한 프로그램 종료 요청을 의미한답니다. 이 간단한 걸 생각해 내는데 반나절이나 걸렸다는 게(ㅡㅡ) 한심하긴 하지만 해결되었으니 그나마 다행^^.

     

    D.        중복 실행 방지

           문제점 F에서 지적했듯이 세션에서 유일하게 실행되는 프로그램을 작성하는 일은 이전 방식으로는 불가능합니다. 그래서 선택한 방법이 창 검색(FindWindow 함수)입니다. 다행히도 창 검색은 한 세션 안에서만 가능하므로 창이 발견되면 이 세션에 동일한 프로그램이 이미 실행 중인 것으로 판단하여 실행되지 않도록 하고, 창이 검색되지 않은 경우에는 최소한 이 세션에는 이 프로그램이 실행되어 있지 않다고 판단하여 그대로 프로그램을 실행시키면 됩니다.

     

    9.      쓰기 가능 폴더 찾기

     

    A.        이제까지 admin 권한이 필요한 프로그램을 어떻게 SYSTEM 계정으로, 그리고 윈도우 부팅 시 자동으로 실행되게 할 수 있는지에 대해서 알아보았습니다. 하지만, admin 권한이 필요 없는 프로그램인데도 굳이 SYSTEM 계정으로 프로그램을 실행시키는 것은 보안상의 문제와 불필요한 코딩의 번거로움을 수반하므로 적절하지 않습니다. 웬만하면 그냥 일반 권한으로 실행되도록 하는 것이 바람직하겠죠.

    B.        하지만 이러한 바람직한 프로그램 동작을 위해서는 XP 이전까지는 너무 당연하게 생각하던 폴더와 레지스트리 쓰기에 대해서 심각하게 고민을 해야만 합니다. UAC에 의해서 사용자 계정용 폴더와 레지스트리(HKCU 이하)가 아니면 쓰기가 불가능하게 되었기 때문입니다.

    C.       SHGetKnownFolderPath 함수로 쓰기 가능 폴더 구하기

    hr = SHGKFPfunction(FOLDERID_LocalAppDataLow, KF_FLAG_CREATE, NULL, &pwszCacheDir);

           위의 함수에서 인자로 FOLDERID_LocalAppData, FOLDERID_LocalAppDataLow, FOLDERID_ProgramData 등을 전달하여 구해진 폴더에는 프로그램의 각종 설정을 저장할 수 있습니다. LocalAppData LocalAppDataLow는 사용자 계정별로 데이터를 저장할 때 사용하는 폴더이고, ProgramData 폴더는 사용자 계정과는 무관하게 응용 프로그램의 전역 설정 등을 저장하고자 할 때 사용할 수 있습니다.

           전체 폴더 상수는 MSDN KNOWNFOLDERID 열거 상수를 참고하기 바랍니다.

           그리고 XP 이전에서 사용하던 SHGetSpecialFolderPath 함수는 그대로 사용 가능합니다. 하지만 비스타에서 새로 추가된 특수 폴더들은 구할 수 없으며 SHGetKnownFolderPath 함수를 통해서 SHGetSpecialFolderPath 함수의 기능을 모두 수행할 수 있으므로 앞으로는 SHGetKnownFolderPath 함수를 사용하는 것이 바람직합니다.

    D.        IEGetWriteableFolderPath 함수로 Protected Mode에서 쓰기 가능한 폴더 구하기

           비스타에서 IE Protected 모드가 기본으로 활성화되어 있고 이 경우에는 IE 플러그인 또는 ActiveX들은 특별히 낮은 IL(Low Integrity Level)로 실행되게 됩니다. 그래서 심지어 사용자 계정 폴더나 HKCU 이하마저도 변경할 수 없게 되었습니다. IE에서 쓰기 가능한 폴더를 구할 때는 먼저 Protected Mode가 켜져 있는지 확인할 필요가 있습니다.

    HRESULT IEIsProtectedModeProcess(BOOL* pbResult);

           Protected Mode인 경우 IEGetWriteableFolderPath 함수를 사용하여 인터넷 캐시 폴더를 찾아서 파일을 기록할 수 있습니다.

    HRESULT IEGetWriteableFolderPath(GUID clsidFolderID, LPWSTR* lppwstrPath);

    E.        IEGetWriteableHKCU 함수로 쓰기 가능한 레지스트리 경로 구하기

           레지스트리는 시스템 운영에 핵심적인 설정이 담겨 있으므로 비스타에서는 IE에서 레지스트리를 변경하지 못하도록 강력히 단속하고 있다. 하지만, 무조건 불가능한 것은 아니고, 특별히 HKCU의 하단 저 깊숙한 공간에는 쓰기가 가능한 곳도 있는데 바로 이 장소를 찾아 주는 함수가 IEGetWriteableHKCU 함수입니다.

    HRESULT IEGetWriteableHKCU(HKEY *phKey );

    F.        이제 admin 권한이 없는 프로그램에서 특히 IL이 가능 낮은 IE 플러그인에서도 어떻게 파일이나 레지스트리를 변경할 수 있는지 알 수 있게 됐습니다. 하지만, IE 플러그인의 경우 이렇게 제한적인 공간에만 쓰기가 가능하다면 너무나 기능에 제약이 많은 거 아닐까요? 예를 들어 BHO에서 Program Files 하위 폴더에 파일을 꼭 쓰고 싶다면 어떻게 해야 할까요? 해결책은 admin 권한으로 실행되는 프로그램을 별도로 제작하고 BHO에서 그 프로그램과 통신하여 작업을 대신하도록 요청하는 것입니다.

     

    10.   IE 플러그인으로부터 윈도우 메시지 수신하기

     

    A.        위에서 BHO와 일반 프로그램과의 통신이 필요하다고 했는데요, 그냥 FindWindow 한 후 반환된 윈도우 핸들에 SendMessage 함수로 메시지를 날리면 끝이라고 생각할 수 있습니다. 하지만, 역시나 비스타는 다릅니다. 비스타에는 UAC같은 보안 정책이 두 개 더 있는데, 바로 MIC(Mandatory Integrity Control) UIPI(User Interface Privilege Isolation)입니다.

    B.        MIC: 통합 레벨(IL) Low, Medium, High, System 네 가지로 구분하고 프로세스 토큰에 IL 정보를 저장시켜서 낮은 IL 프로세스에서 높은 IL 데이터에 접근하지 못하도록 하는 정책

    C.       UIPI: 낮은 IL 프로세스에서 높은 IL 프로세스의 윈도우에 메시지를 보내거나, 후킹하지 못하도록 하는 정책

    D.        이 두 가지 정책에 의하여 Low IL 프로세스인 IE BHO는 다른 응용 프로그램에 윈도우 메시지를 전송하지 못하게 되는 것입니다.

    E.        이런 제약을 해결하기 위한 방법으로 ChangeWindowMessageFilter API를 사용하여 admin 권한을 가진 프로그램 측에서 낮은 IL 프로세스에서 전송했음에도 수신을 허용할 윈도우 메시지를 차단 필터의 예외 목록에 추가할 수 있습니다.

    Call ChangeWindowMessageFilter(WM_COPYDATA, MSGFLT_ADD) 프로그램 시작 직후

    Call ChangeWindowMessageFilter(WM_COPYDATA, MSGFLT_REMOVE) 프로그램 종료 직전

           위에서 보는 바와 같이 응용 프로그램 시작 직후에 허용되기 원하는 메시지를 추가하고, 종료 전에 제거하면 됩니다.

           참고로 SYSTEM 계정으로 실행된 프로그램이 시스템 트레이에 아이콘을 생성시킨 경우, explorer.exe가 죽고 재실행될 때 전송하는 WM_TASKBARCREATED 메시지를 수신하지 못하게 됩니다. 당연히 시스템 트레이에서 아이콘이 보이지 않는 증상이 발생하게 되겠죠. 이 역시 쉘 프로세스의 IL이 이 프로그램의 IL보다 낮아서 윈도우 메시지가 차단되기 때문입니다. 이 때도 ChangeWindowMessageFilter 함수를 이용하여 WM_TASKBARCREATED 메시지를 필터에 추가해 주면 해결됩니다.

     

    11.   사용자 계정 리소스 접근하기

     

    A.        이제 어려운 부분은 다 끝난 것 같았습니다. 소스를 컴파일한 후 프로그램을 XYNTService.ini에 등록하여 서비스에 의하여 실행시켰습니다. 음…… 대부분 잘 동작하는데 뭔가 이상한 점이 있네요. 설정 정보를 담은 파일과 레지스트리가 저장되어 있지 않습니다. 왜 일까요? SYSTEM 계정이므로 실행되었으니 admin 권한이 있고, 그렇다면 UAC에 의한 권한 부족 오류는 아닐 텐데…… 이 문제 때문에 또 한참을 디버그 로그를 추적하면서 궁리를 해 봅니다. 그리고 반짝하면서 그간의 경험을 통해서 얻은 통찰력이 발휘되었습니다. 바로 SYSTEM 계정으로 실행된 것이 문제였습니다. 로그온된 사용자의 폴더와 HKCU가 아닌, SYSTEM 계정의 사용자 폴더와 HKCU에 접근하여 기록하니 아무리 탐색기로 현재 사용자 폴더와 HKCU를 찾아봐도 변경된 내용을 찾지 못하는 것이 당연한 것이죠. 알고 나면 당연하지만 그 전까지는 얼마나 헤매야 했던지^^. 하여튼 발견한 걸로 만족하면서 추가 코딩에 들어가 보도록 하죠.

     현재 세션 ID 구하기

    If WTSQuerySessionInformation(WTS_CURRENT_SERVER_HANDLE, WTS_CURRENT_SESSION, _

                           WTSSessionId, lpBuffer, ByteRet) = 0 Then Goto EXIT_SUB

    Call CopyMemory(dwSessionID, ByVal lpBuffer, Len(dwSessionID))

    Call WTSFreeMemory(lpBuffer) 반드시 리소스 해제

    현재 세션의 토큰을 구한다.

    If WTSQueryUserToken(dwSessionID, hToken) = 0 Then GOTO EXIT_SUB

    이 토큰으로 Impersonate 시킨다.

    If ImpersonateLoggedOnUser(hToken) = 0 Then

    Call CloseHandle(hToken)

    GoTo EXIT_SUB

    End If

    HKCU Root 키를 구한다.

    If RegOpenCurrentUser(KEY_ALL_ACCESS, hKeyCU) = ERROR_SUCCESS Then

    lRet = RegOpenKeyEx(hKeyCU, _

                           "Software\Microsoft\Windows\CurrentVersion\Ext\Settings\", _

                           0, KEY_ALL_ACCESS, hKey)

        If lRet = ERROR_SUCCESS Then

            필요한 레지스트리 작업 수행

            Call RegCloseKey(hKey)

    End If

    End If

    원래 계정으로 환원시킨다.

    Call RevertToSelf

    토큰 핸들을 닫는다.

    Call CloseHandle(hToken)

    B.        원인은 알았는데, 어떻게 해야 SYSTEM 계정으로 실행된 상태에서 이미 공들여 작성한 SHGetKnownFolderPath 함수나 RegOpenKeyEx 함수 관련 코딩들을 그대로 사용해서 사용자 계정 리소스에 접근할 수 있을까요? 이럴 때 사용하라고 만들어진 API가 바로 ImpersonateLoggedOnUser 입니다. 이 함수는 인자로 토큰을 필요로 하므로 현재 세션 ID를 구하고(WTSQuerySessionInformation) 이 세션의 토큰을 구한 후(WTSQueryUserToken) 그 토큰을 전달하면 됩니다. , WTSQueryUserToken 함수를 호출할 때는 이 응용 프로그램이 SE_TCB_NAME 권한이 있어야 하지만, 우리 프로그램은 이미 SYSTEM 계정이므로 실행되어 있어서 권한에 제약이 없으므로 걱정할 건 없겠죠. 특정 계정으로 Impersonate가 되면 이 함수를 호출한 쓰레드는 그 후부터 해당 계정으로 실행된 것처럼 동작합니다. 필요한 작업을 모두 완료한 후 쓰레드를 원래 계정으로 복원할 때는 RevertToSelf 함수를 호출합니다.

    C.       이제 Impersonate가 완료되었으니 바로 RegOpenKeyEx 함수에 HKEY_CURRENT_USER 상수를 전달하여 사용자 레지스트리에 접근해 볼까요? 그런데 여기서 또 실패가 나옵니다. HKEY_CURRENT_USER 상수는 세션 생성과 동시에 현재 로그온된 사용자 계정이 아닌, SYSTEM 계정의 HKCU 값으로 매핑되어 있기 때문입니다. 그래서 RegOpenCurrentUser 함수를 사용해서 현재 사용자의 HKCU Root 키를 구한 후 다시 구해진 Root 키를 RegOpenKeyEx의 인자로 전달하면 됩니다.

     

    12.   마치며

     

    핵심은 끝났고 몇 가지 알아두면 좋은 팁을 소개하겠습니다.

     

    A.        비스타 OS 인식하기

           GetVersionEx 함수로 구해진 OSVERSIONINFO 구조체의 dwMajorVersion 5이면 XP, 6이면 비스타입니다.

           출처: http://www.codeguru.com/cpp/w-p/system/systeminformation/article.php/c8973

    B.        프로그램 실행 시마다 UAC 팝업창이 항상 표시하도록 하기. (admin 권한이 꼭 필요하지만 서비스로 실행시키고 싶지 않은 경우)

           먼저 메모장을 열어 아래의 텍스트를 붙여 넣습니다.



       

         

            
     
              
     
           
     
         
     
      

           저장을 눌러 실행파명.exe.manifest 로 저장하되 UTF-8 포맷을 선택합니다

           폴더를 생성하고 그 내부에 mt.exe파일, 해당 실행파일, manifest 파일 이렇게 3가지를 넣습니다.

           명령 프롬프트를 열어서 해당 폴더로 경로를 변경한 후 아래 명령을 실행시킵니다.

    mt -manifest 실행파일.exe.manifest -outputresource:실행파일.exe;#1

           manifest가 리소스로 실행파일 안에 포함되었으므로 배포본에서는 manifest 파일을 제거하도록 합니다.

    C.       비스타 SDK 없이 비스타 전용 함수 호출하기

           아래와 같이 GetModuleHandle 또는 LoadLibrary 함수를 이용하여 라이브러리를 참조한 후 GetProcAddress 함수를 이용하여 필요한 함수 포인터를 구한 후 호출하면 됩니다. 이런 방법을 사용하지 않고 비스타 함수를 바로 호출하도록 코딩하여 실행시키면 XP 환경에서 오작동을 일으킬 수 있으므로 배포할 프로그램이 비스타 전용이 아닌 경우에는 반드시 이런 함수 호출 규칙을 따르도록 합니다.

    HMODULE shell32module = GetModuleHandle("shell32.dll");

    SHGetKnownFolderPathProc *SHGKFPfunction;

    SHGKFPfunction = (SHGetKnownFolderPathProc *)GetProcAddress(shell32module,

                   "SHGetKnownFolderPath");

    if (SHGKFPfunction)

    {

            hr = SHGKFPfunction(FOLDERID_LocalAppDataLow, KF_FLAG_CREATE, NULL,

                           &pwszCacheDir);

            if ( SUCCEEDED(hr) )

            {

                   strDir = CW2CT(pwszCacheDir);

                   strFilePath = strDir + _T("\\greenware.log");

                   CoTaskMemFree ( pwszCacheDir );

            }

    }

     

    D.        드디어 끝났습니다^^. 긴 글 읽느라 고생하셨구요, 조금이나마 도움이 되었으면 합니다. 수정할 부분이 있으면 바로 알려주세요. 감사합니다^^.

     

    경주때문에 왔는데, 좋은정보가 있네요
    담아가요
    잘 보고 갑니다~