Microsoft MVP성태의 닷넷 이야기
Windows: 209. Windows NT Service에서 UI를 다루는 방법 [링크 복사], [링크+제목 복사],
조회: 8663
글쓴 사람
정성태 (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

[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
13688정성태7/21/2024317닷넷: 2279. C# - Lock / Wait 상태에서도 일부 Win32 메시지 처리파일 다운로드1
13687정성태7/19/2024270닷넷: 2279. C# - PostThreadMessage로 보낸 메시지를 Windows Forms에서 수신하는 방법파일 다운로드1
13686정성태7/19/2024262오류 유형: 918. Visual Studio - ATL Simple Object 추가 시 error C2065: 'IDR_...': undeclared identifier
13685정성태7/19/2024296스크립트: 66. Windows 디렉터리 경로를 WSL의 /mnt 포맷으로 구하는 방법 - 두 번째 이야기
13684정성태7/19/2024315닷넷: 2278. C# - 문자열 보간식 사례
13683정성태7/18/2024285오류 유형: 917. ClrMD - Linux 환경의 .NET 5 덤프 분석 시 hang 현상
13682정성태7/18/2024369닷넷: 2277. WPF - 스레드에 종속되는 DependencyObject파일 다운로드1
13681정성태7/17/2024425닷넷: 2276. C# 13 - (2) 메서드 그룹의 자연 타입 개선 (메서드 추론 개선)파일 다운로드1
13680정성태7/16/2024522닷넷: 2275. C# - Method Group, Natural Type, function_type파일 다운로드1
13679정성태7/16/2024651Linux: 75. Linux - C++ (getaddrinfo 등을 담고 있는) libnss 정적 링크
13678정성태7/15/2024542VS.NET IDE: 191. Visual Studio 2022 - .NET 5 프로젝트를 Docker Support로 실행했을 때 오류
13677정성태7/15/2024573오류 유형: 916. MSBuild - CheckEolTargetFramework (warning NETSDK1138)
13676정성태7/14/2024560Linux: 75. gdb에서 glibc의 함수에 Breakpoint 걸기
13675정성태7/13/2024811C/C++: 166. C/C++ - DLL에서 template 함수를 export하는 방법파일 다운로드1
13674정성태7/13/2024932오류 유형: 915. Unhandled Exception: Microsoft.Diagnostics.NETCore.Client.ServerNotAvailableException: Unable to connect to Process
13673정성태7/11/2024980닷넷: 2274. C# 13 - (1) 신규 이스케이프 시퀀스 '\e'파일 다운로드1
13672정성태7/10/2024991닷넷: 2273. IIS - (프로세스 종료 없는) AppDomain Recycle
13671정성태7/10/2024845오류 유형: 914. Package ca-certificates is not installed.
13669정성태7/9/2024988오류 유형: 913. C# - AOT StaticExecutable 정적 링킹 시 빌드 오류
13668정성태7/8/2024959개발 환경 구성: 716. Hyper-V - Ubuntu 22.04 Generation 2 유형의 VM 설치
13667정성태7/7/2024873닷넷: 2272. C# - 리눅스 환경에서의 Hyper-V Socket 연동 (AF_VSOCK)파일 다운로드1
13666정성태7/7/20241115Linux: 74. C++ - Vsock 예제 (Hyper-V Socket 연동)파일 다운로드1
13665정성태7/6/20241148Linux: 73. Linux 측의 socat을 이용한 Hyper-V 호스트와의 vsock 테스트파일 다운로드1
13663정성태7/5/20241131닷넷: 2271. C# - Hyper-V Socket 통신(AF_HYPERV, AF_VSOCK)의 VMID Wildcards 유형파일 다운로드1
13662정성태7/4/20241392닷넷: 2270. C# - WSL 2 VM의 VM ID를 알아내는 방법 - Host Compute System API파일 다운로드1
[1]  2  3  4  5  6  7  8  9  10  11  12  13  14  15  ...