Microsoft MVP성태의 닷넷 이야기
Windows: 209. Windows NT Service에서 UI를 다루는 방법 [링크 복사], [링크+제목 복사]
조회: 6880
글쓴 사람
정성태 (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)
13449정성태11/21/20232351개발 환경 구성: 688. Azure OpenAI 서비스 신청 방법
13448정성태11/20/20232628닷넷: 2163. .NET 8 - Dynamic PGO를 결합한 성능 향상파일 다운로드1
13447정성태11/16/20232487닷넷: 2162. ASP.NET Core 웹 사이트의 SSL 설정을 코드로 하는 방법
13446정성태11/16/20232419닷넷: 2161. .NET Conf 2023 - Day 1 Blazor 개요 정리
13445정성태11/15/20232700Linux: 62. 리눅스/WSL에서 CA 인증서를 저장하는 방법
13444정성태11/15/20232456닷넷: 2160. C# 12 - Experimental 특성 지원
13443정성태11/14/20232492개발 환경 구성: 687. OpenSSL로 생성한 사용자 인증서를 ASP.NET Core 웹 사이트에 적용하는 방법
13442정성태11/13/20232322개발 환경 구성: 686. 비주얼 스튜디오로 실행한 ASP.NET Core 사이트를 WSL 2 인스턴스에서 https로 접속하는 방법
13441정성태11/12/20232652닷넷: 2159. C# - ASP.NET Core 프로젝트에서 서버 Socket을 직접 생성하는 방법파일 다운로드1
13440정성태11/11/20232353Windows: 253. 소켓 Listen 시 방화벽의 Public/Private 제어 기능이 비활성화된 경우
13439정성태11/10/20232844닷넷: 2158. C# - 소켓 포트를 미리 시스템에 등록/예약해 사용하는 방법(Port Exclusion Ranges)파일 다운로드1
13438정성태11/9/20232462닷넷: 2157. C# - WinRT 기능을 이용해 윈도우에서 실행 중인 Media App 제어
13437정성태11/8/20232659닷넷: 2156. .NET 7 이상의 콘솔 프로그램을 (dockerfile 없이) 로컬 docker에 배포하는 방법
13436정성태11/7/20232894닷넷: 2155. C# - .NET 8 런타임부터 (Reflection 없이) 특성을 이용해 public이 아닌 멤버 호출 가능
13435정성태11/6/20232830닷넷: 2154. C# - 네이티브 자원을 포함한 관리 개체(예: 스레드)의 GC 정리
13434정성태11/1/20232631스크립트: 62. 파이썬 - class의 정적 함수를 동적으로 교체
13433정성태11/1/20232357스크립트: 61. 파이썬 - 함수 오버로딩 미지원
13432정성태10/31/20232407오류 유형: 878. 탐색기의 WSL 디렉터리 접근 시 "Attempt to access invalid address." 오류 발생
13431정성태10/31/20232722스크립트: 60. 파이썬 - 비동기 FastAPI 앱을 gunicorn으로 호스팅
13430정성태10/30/20232612닷넷: 2153. C# - 사용자가 빌드한 ICU dll 파일을 사용하는 방법
13429정성태10/27/20232871닷넷: 2152. Win32 Interop - C/C++ DLL로부터 이중 포인터 버퍼를 C#으로 받는 예제파일 다운로드1
13428정성태10/25/20232925닷넷: 2151. C# 12 - ref readonly 매개변수
13427정성태10/18/20233102닷넷: 2150. C# 12 - 정적 문맥에서 인스턴스 멤버에 대한 nameof 접근 허용(Allow nameof to always access instance members from static context)
13426정성태10/13/20233287스크립트: 59. 파이썬 - 비동기 호출 함수(run_until_complete, run_in_executor, create_task, run_in_threadpool)
13425정성태10/11/20233100닷넷: 2149. C# - PLinq의 Partitioner<T>를 이용한 사용자 정의 분할파일 다운로드1
13423정성태10/6/20233078스크립트: 58. 파이썬 - async/await 기본 사용법
1  2  3  4  5  6  [7]  8  9  10  11  12  13  14  15  ...