Microsoft MVP성태의 닷넷 이야기
Windows: 209. Windows NT Service에서 UI를 다루는 방법 [링크 복사], [링크+제목 복사]
조회: 6955
글쓴 사람
정성태 (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)
13507정성태1/2/20242766닷넷: 2191. C# - IPGlobalProperties를 이용해 netstat처럼 사용 중인 Socket 목록 구하는 방법파일 다운로드1
13506정성태12/29/20232338닷넷: 2190. C# - 닷넷 코어/5+에서 달라지는 System.Text.Encoding 지원
13505정성태12/27/20232884닷넷: 2189. C# - WebSocket 클라이언트를 닷넷으로 구현하는 예제 (System.Net.WebSockets)파일 다운로드1
13504정성태12/27/20232459닷넷: 2188. C# - ASP.NET Core SignalR로 구현하는 채팅 서비스 예제파일 다운로드1
13503정성태12/27/20232319Linux: 67. WSL 환경 + mlocate(locate) 도구의 /mnt 디렉터리 검색 문제
13502정성태12/26/20232419닷넷: 2187. C# - 다른 프로세스의 환경변수 읽는 예제파일 다운로드1
13501정성태12/25/20232238개발 환경 구성: 700. WSL + uwsgi - IPv6로 바인딩하는 방법
13500정성태12/24/20232289디버깅 기술: 194. Windbg - x64 가상 주소를 물리 주소로 변환
13498정성태12/23/20232988닷넷: 2186. 한국투자증권 KIS Developers OpenAPI의 C# 래퍼 버전 - eFriendOpenAPI NuGet 패키지
13497정성태12/22/20232409오류 유형: 885. Visual Studiio - error : Could not connect to the remote system. Please verify your connection settings, and that your machine is on the network and reachable.
13496정성태12/21/20232394Linux: 66. 리눅스 - 실행 중인 프로세스 내부의 환경변수 설정을 구하는 방법 (gdb)
13495정성태12/20/20232368Linux: 65. clang++로 공유 라이브러리의 -static 옵션 빌드가 가능할까요?
13494정성태12/20/20232522Linux: 64. Linux 응용 프로그램의 (C++) so 의존성 줄이기(ReleaseMinDependency) - 두 번째 이야기
13493정성태12/19/20232615닷넷: 2185. C# - object를 QueryString으로 직렬화하는 방법
13492정성태12/19/20232303개발 환경 구성: 699. WSL에 nopCommerce 예제 구성
13491정성태12/19/20232239Linux: 63. 리눅스 - 다중 그룹 또는 사용자를 리소스에 권한 부여
13490정성태12/19/20232361개발 환경 구성: 698. Golang - GLIBC 의존을 없애는 정적 빌드 방법
13489정성태12/19/20232145개발 환경 구성: 697. GoLand에서 ldflags 지정 방법
13488정성태12/18/20232077오류 유형: 884. HTTP 500.0 - 명령행에서 실행한 ASP.NET Core 응용 프로그램을 실행하는 방법
13487정성태12/16/20232393개발 환경 구성: 696. C# - 리눅스용 AOT 빌드를 docker에서 수행 [1]
13486정성태12/15/20232209개발 환경 구성: 695. Nuget config 파일에 값 설정/삭제 방법
13485정성태12/15/20232093오류 유형: 883. dotnet build/restore - error : Root element is missing
13484정성태12/14/20232169개발 환경 구성: 694. Windows 디렉터리 경로를 WSL의 /mnt 포맷으로 구하는 방법
13483정성태12/14/20232309닷넷: 2184. C# - 하나의 resource 파일을 여러 프로그램에서 (AOT 시에도) 사용하는 방법파일 다운로드1
13482정성태12/13/20232893닷넷: 2183. C# - eFriend Expert OCX 예제를 .NET Core/5+ Console App에서 사용하는 방법 [2]파일 다운로드1
13481정성태12/13/20232279개발 환경 구성: 693. msbuild - .NET Core/5+ 프로젝트에서 resgen을 이용한 리소스 파일 생성 방법파일 다운로드1
1  2  3  4  [5]  6  7  8  9  10  11  12  13  14  15  ...