Microsoft MVP성태의 닷넷 이야기
Windows: 209. Windows NT Service에서 UI를 다루는 방법 [링크 복사], [링크+제목 복사],
조회: 7235
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
 
(연관된 글이 1개 있습니다.)

Windows NT Service에서 UI를 다루는 방법

Windows의 Service는 처음 윈도우 프로그래밍을 배우는 분들에게는 다소 어려운 주제입니다. 또한 윈도우 버전마다 서비스를 다루는 방식이 달라지는 바람에 예전에, 가령 XP 시절에 Service를 많이 다뤄보셨다고 해도 근래에 다시 접하면 어려울 수 있습니다.

일단, 마이크로소프트가 제시한 원칙은 간단합니다.

NT Service 내에 작성한 코드에서는 UI 이외의 코드를 작성하고,
UI가 필요하다면 그 부분만 일반적인 데스크톱 프로그램에서 담당한다.

바로 저 원칙을 잘 따르는 프로그램이 "Microsoft SQL Server"입니다. SQL Server는 RDB 서비스를 sqlserver.exe 프로세스를 통해 제공하는데요, 그 프로세스 내에 있는 코드는 UI를 전혀 제공하지 않습니다.

물론, 데이터베이스를 만드는 등의 작업들은 쉽게 UI로 하고 싶을 텐데요, 그래서 마이크로소프트는 NT Service로 구동 중인 sqlsevrer.exe에서 제공하는 기능을 이용한 SQL Server Management Studio (SSMS) 도구를 별도로 제공합니다.

즉, UI는 SSMS가, 핵심 DB 코드는 NT Service가 담당하는 것입니다.




그런데, 만약... SQL Server처럼 별도의 UI 프로그램을 제공하지 않고 굳이 NT Service가 UI를 접근하고 싶다면 어떻게 해야 할까요? 이에 대한 방법을 아래의 글에서 설명하고 있습니다.

[.NET] Windows Service 에서 UI 사용하기
; https://blog.naver.com/vactorman/222847763183

그런데, 설명이 좀 아쉽군요. ^^

우선, "Windows Service"는 커널 모드로 실행되는 프로세스가 아닙니다. 그것 역시 User Mode이고, 오직 device driver로만 작성한 프로그램만이 커널 모드의 권한으로 실행이 됩니다. 따라서, 당연히 NT 서비스는 프로세스 별로 가상화된 User 메모리를 접근하고, 절대 커널 메모리에는 접근할 수 없습니다.

그렇다면, 왜? NT 서비스에서는 UI 생성을 하지 못하는 걸까요? 사실 NT 서비스도 UI를 생성할 수 있습니다. 단지, 그렇게 생성된 UI를 우리가 사용하는 화면에서는 보이지 않는 것뿐입니다. 왜냐하면, 서로 간에 세션 자체가 달라 UI가 활성화되는 Desktop이 분리돼 있기 때문입니다.

아래의 작업 관리자 화면을 보면,

nt_service_is_1.png

위에서 DtsApo4Service.exe는 0번 세션에서 활성 중인 NT 서비스입니다. Windows Vista 이후로 모든 NT 서비스는 이렇게 0번 세션에서 활성화됩니다. 반면, 우리가 사용하는 explorer.exe는 1번 세션에 묶여 있습니다. 일반적으로는, 물리 PC에 직접 로그인한 사용자가 1번 세션입니다.

앞서 설명한 대로, NT 서비스가 활성화되는 0번 세션을 우리는 볼 수 없습니다. 하지만 과거에는 공식적으로 이것이 가능하도록 "Interactive Services Detection" 서비스를 제공했으므로 저도 아래의 글에서,

윈도우 8 - UI가 있는 프로그램을 Local SYSTEM 권한의 세션 0 데스크톱에서 실행하는 방법
; https://www.sysnet.pe.kr/2/0/1584

psexec.exe를 활용해 계산기 프로세스(calc.exe)를 세션 0에 띄운 후 그 UI를 볼 수 있었습니다. 하지만, Windows 10 Build 1803과 Windows Server 2016/2019부터는 아예 "Interactive Services Detection"까지 없애 버렸습니다. (없어지긴 했지만, 그 프로그램이 사용하던 API가 없어지진 않았을 것이므로 별도로 구현하면 방법이 있을 것입니다. ^^)

세션이 나누어진 것을 낯설게 느낄 수도 있는데, 흔한 예로 RDP 접속을 하면 새로운 세션이 만들어지는 것이므로 여러분은 알게 모르게 자신만의 세션을 사용했던 것입니다. 일례로, 물리 컴퓨터 앞에 앉아 로그인하고 있는 사용자가 1번 세션을 사용하고, 그 컴퓨터에 RDP 접속을 한 사용자는 또 다른 세션을 사용하게 되는데, 그 사용자들 간에 UI를 간섭할 수 없다는 것을 여러분들은 바로 수긍할 수 있을 것입니다.

참고로, 세션과 데스크톱, 윈도우 스테이션에 관해서는 다음의 글에서 잘 설명하고 있습니다.

Sessions, Desktops and Windows Stations
; https://techcommunity.microsoft.com/t5/ask-the-performance-team/sessions-desktops-and-windows-stations/ba-p/372473

위의 글에 실린 아래의 그림이 너무 잘 표현해주고 있어서 더 이상의 설명은 생략합니다. ^^

nt_service_is_2.png




위의 사실을 인지하고 "[.NET] Windows Service 에서 UI 사용하기" 글에 실린 소스 코드를 보면 이제 이해가 될 것입니다.

public static bool StartProcessAndBypassUAC(string applicationName, out PROCESS_INFORMATION procInfo)
{
    uint winlogonPid = 0;
    IntPtr hUserTokenDup = IntPtr.Zero, hPToken = IntPtr.Zero, hProcess = IntPtr.Zero;
    procInfo = new PROCESS_INFORMATION();

    // Active Console Session을 찾는 거니까, 결국 사용자가 로그인한 세션의 아이디이고,
    uint dwSessionId = WTSGetActiveConsoleSessionId();

    // 그리고 그 세션에 속한 winlogon.exe 프로세스의 id를 가져온 다음,
    Process[] processes = Process.GetProcessesByName("winlogon");
    foreach (Process p in processes)
    {
        if ((uint)p.SessionId == dwSessionId)
        {
            winlogonPid = (uint)p.Id;
        }
    }

    // obtain a handle to the winlogon process
    hProcess = OpenProcess(MAXIMUM_ALLOWED, false, winlogonPid);

    // obtain a handle to the access token of the winlogon process
    if (!OpenProcessToken(hProcess, TOKEN_DUPLICATE, ref hPToken))
    {
        CloseHandle(hProcess);
        return false;
    }

    // ... 그 프로세스를 실행한 사용자의 Access Token을 가져오고...
    // 세션 내의 첫 번째 Window Station에 활성화되도록 설정한 프로세스를 CreateProcessAsUser Win32 API를 이용해 실행
    // ...

    return result; // return the result
}

그리고 저 코드가 항상 실행되는 것은 아니라는 것도 알 수 있습니다. WTSGetActiveConsoleSessionId 함수는,

WTSGetActiveConsoleSessionId function (winbase.h)
; https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-wtsgetactiveconsolesessionid

using System.Runtime.InteropServices;

namespace ConsoleApp1;

internal class Program
{
    [DllImport("kernel32.dll")]
    internal static extern int WTSGetActiveConsoleSessionId();

    static void Main(string[] args)
    {
        Console.WriteLine(WTSGetActiveConsoleSessionId()); // 출력 결과: 1 (RDP 환경에서도!)
    }
}

물리적으로 콘솔에 로그인한 사용자가 있는 경우에 한해 세션 ID를 가져오므로, Windows Server 제품군처럼 RDP로 접속해 작업하는 식이라면 저 값은 -1을 반환할 수 있습니다. 직접 테스트해봐야 하는데... 아마 그럴 것입니다. ^^




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]






[최초 등록일: ]
[최종 수정일: 9/15/2023]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 



2023-09-08 05시49분
게으른 지식에 깨우침 주셔서 감사합니다.
잘못된 내용을 당당하게 제가 포스팅을 해뒀군요 =ㅅ=;;
가르침 감사드립니다.
SeongHwan Lee
2023-09-11 11시17분
@SeongHwan Lee 님, 쓰신 글("https://blog.naver.com/vactorman/223206122756") 보고 RDP 환경에서 WTSGetActiveConsoleSessionId를 테스트했더니 저도 1이 나왔습니다. 작업 관리자를 보면, 사용자가 "콘솔"에서 로그인하지 않아도 세션 1의 프로세스가 5개(csrss.exe, dwm.exe, fontdrvhost.exe, LogonUI.exe, winlogon.exe)가 기본적으로 떠 있는데 그래서 1이 반환되는 듯합니다.

그리고, "Administrator"라고 단독으로 있는 것은 Built-in 계정입니다. 근래에는 보안 강화로 기본적으로 disabled 상태로 돼 있습니다. ("Computer Management" MMC에서 "System Tools" / "Local Users and Groups" / "Users"에서 보이는 "Administrator"가 그것입니다.

중간에 추가한 "...\Administrator"는 혹시 Active Directory 계정인가요? 아마도 Built-in 관리자 계정을 쓰지 않았을 것이므로 어찌되었든 다른 계정일 것입니다.

마지막으로, "특권(Privileges)"은 사실 보안상 기본적으로 필요한 만큼만 활성화되는 것이 보통입니다. 반면 NT 서비스를 "Local SYSTEM" 등의 계정으로 활성화시키면 해당 계정은 보다 많은 특권을 활성화시키므로 CreateProcessAsUser 실행에 문제가 없습니다.

-----------------------------

참고로, 계정의 "특권"이 어떤 것들이 활성화되었는지 런타임에 확인하려면 "로컬 보안 정책" MMC보다는, "Process Explorer"를 이용("https://www.sysnet.pe.kr/2/0/1806#privileges")하는 것이 더 편할 것입니다. ^^
정성태
2023-09-12 09시27분
넵. 맞슴다.
사내 장비들은 AD 로 관리 중이고 로컬 PC 로그온도 AD 계정으로 하고 있습니다. 로그온한 계정이 당연히 built-in admin 이었을 거라고 착각하는 바람에 문제 확인하는 데에 애를 좀 먹었네요.
privilege 확인하는 건 좋은 정보 하나 더 배웠습니다. 감사합니다.
SeongHwan Lee

... 31  32  33  34  35  36  37  [38]  39  40  41  42  43  44  45  ...
NoWriterDateCnt.TitleFile(s)
12694정성태7/2/202110295Java: 24. Azure - Spring Boot 앱을 Java SE(Embedded Web Server)로 호스팅 시 로그 파일 남기는 방법 [1]
12693정성태6/30/20217997오류 유형: 731. Azure Web App Site Extension - Failed to install web app extension [...]. {1}
12692정성태6/30/20217915디버깅 기술: 180. Azure - Web App의 비정상 종료 시 남겨지는 로그 확인
12691정성태6/30/20218700개발 환경 구성: 573. 테스트 용도이지만 테스트에 적합하지 않은 Azure D1 공유(shared) 요금제
12690정성태6/28/20219531Java: 23. Azure - 자바(Java)로 만드는 Web App Service - Tomcat 호스팅
12689정성태6/25/202110132오류 유형: 730. Windows Forms 디자이너 - The class Form1 can be designed, but is not the first class in the file. [1]
12688정성태6/24/20219799.NET Framework: 1073. C# - JSON 역/직렬화 시 리플렉션 손실을 없애는 JsonSrcGen [2]파일 다운로드1
12687정성태6/22/20217745오류 유형: 729. Invalid data: Invalid artifact, java se app service only supports .jar artifact
12686정성태6/21/202110234Java: 22. Azure - 자바(Java)로 만드는 Web App Service - Java SE (Embedded Web Server) 호스팅
12685정성태6/21/202110472Java: 21. Azure Web App Service에 배포된 Java 프로세스의 메모리 및 힙(Heap) 덤프 뜨는 방법
12684정성태6/19/20218899오류 유형: 728. Visual Studio 2022부터 DTE.get_Properties 속성 접근 시 System.MissingMethodException 예외 발생
12683정성태6/18/202110393VS.NET IDE: 166. Visual Studio 2022 - Windows Forms 프로젝트의 x86 DLL 컨트롤이 Designer에서 오류가 발생하는 문제 [1]파일 다운로드1
12682정성태6/18/20218036VS.NET IDE: 165. Visual Studio 2022를 위한 Extension 마이그레이션
12681정성태6/18/20217376오류 유형: 727. .NET 2.0 ~ 3.5 + x64 환경에서 System.EnterpriseServices 참조 시 CS8012 경고
12680정성태6/18/20218515오류 유형: 726. python2.7.exe 실행 시 0xc000007b 오류
12679정성태6/18/20219013COM 개체 관련: 23. CoInitializeSecurity의 전역 설정을 재정의하는 CoSetProxyBlanket 함수 사용법파일 다운로드1
12678정성태6/17/20218236.NET Framework: 1072. C# - CoCreateInstance 관련 Inteop 오류 정리파일 다운로드1
12677정성태6/17/20219841VC++: 144. 역공학을 통한 lxssmanager.dll의 ILxssSession 사용법 분석파일 다운로드1
12676정성태6/16/20219758VC++: 143. ionescu007/lxss github repo에 공개된 lxssmanager.dll의 CLSID_LxssUserSession/IID_ILxssSession 사용법파일 다운로드1
12675정성태6/16/20217862Java: 20. maven package 명령어 결과물로 (war가 아닌) jar 생성 방법
12674정성태6/15/20218715VC++: 142. DEFINE_GUID 사용법
12673정성태6/15/20219836Java: 19. IntelliJ - 자바(Java)로 만드는 Web App을 Tomcat에서 실행하는 방법
12672정성태6/15/202110964오류 유형: 725. IntelliJ에서 Java webapp 실행 시 "Address localhost:1099 is already in use" 오류
12671정성태6/15/202117739오류 유형: 724. Tomcat 실행 시 Failed to initialize connector [Connector[HTTP/1.1-8080]] 오류
12670정성태6/13/20219196.NET Framework: 1071. DLL Surrogate를 이용한 Out-of-process COM 개체에서의 CoInitializeSecurity 문제파일 다운로드1
12669정성태6/11/20219231.NET Framework: 1070. 사용자 정의 GetHashCode 메서드 구현은 C# 9.0의 record 또는 리팩터링에 맡기세요.
... 31  32  33  34  35  36  37  [38]  39  40  41  42  43  44  45  ...