服务和应用程序间在用户级的交互(Windows Vista及其后)

本章讲的是服务和应用程序在Windows Vista及其后系统上的工作问题。特别提到的是,会给出如何从服务启动一个交互用户级别应用程序的方法,且在服务和应用间如何进行数据交互。给出了C++和C#的代码。

在Vista系统之前,在Windows系列操作系统中服务和用户应用程序都能够共同在Session0下使用。这中情况下会很容易直接从服务中,在当前用户的桌面上打开一个窗口,而且也可以非常方便地通过窗口消息就能够在服务和应用程序间交换数据。但是,这带来了很严重的安全问题,被服务打开的窗口获得访问服务的权限使得整个系统遭到攻击。而在Vista及之后的系统上使用了抵制这种攻击的机制。

在Vista上,所以的用户登入登出都是在Session0以外的session中进行。通过服务在用户桌面打开窗口的可能性被严格限制,且如果试图从服务启动一个应用程序,它是在session0中启动的。相应地,若应用程序是需要交互的,则必须切换到session0的桌面。通过使用窗口消息来进行数据交互变的相当困难。

这种安全策略是应该得到大家认可的。但是如何一定要从服务在用户桌面启动一个可交互的应用程序呢?本章介绍了一种对该问题可能的解决方案。

只要服务和当前用户的桌面存在于不同的session中,服务就不得不去模拟用户来启动可交互程序。为了达到目的,必须知道对应的登录账号和密码或者获得LocalSystem账号。后者更为常见,所以会介绍这种方式。

因此,就来创建一个具备LocalSystem账号的服务,首先,要获取当前用户的token。为了达到目的,需要做以下事情:

1.或者当前所有终端session的列表

2.选择激活的session

3.获取登入到激活session的用户token

4.拷贝得到的token

对应的C++代码如下:


PHANDLE GetCurrentUserToken()
{
    PHANDLE currentToken = 0;
    PHANDLE primaryToken = 0;

    int dwSessionId = 0;
    PHANDLE hUserToken = 0;
    PHANDLE hTokenDup = 0;

    PWTS_SESSION_INFO pSessionInfo = 0;
    DWORD dwCount = 0;

    // Get the list of all terminal sessions 
    WTSEnumerateSessions(WTS_CURRENT_SERVER_HANDLE, 0, 1, 
                         &pSessionInfo, &dwCount);

    int dataSize = sizeof(WTS_SESSION_INFO);

    // look over obtained list in search of the active session
    for (DWORD i = 0; i < dwCount; ++i)
    {
        WTS_SESSION_INFO si = pSessionInfo[i];
        if (WTSActive == si.State)
        { 
            // If the current session is active – store its ID
            dwSessionId = si.SessionId;
            break;
        }
    } 	
	
	    WTSFreeMemory(pSessionInfo);

    // Get token of the logged in user by the active session ID
    BOOL bRet = WTSQueryUserToken(dwSessionId, currentToken);
    if (bRet == false)
    {
        return 0;
    }

    bRet = DuplicateTokenEx(currentToken, 
             TOKEN_ASSIGN_PRIMARY | TOKEN_ALL_ACCESS,
             0, SecurityImpersonation, TokenPrimary, primaryToken);
    if (bRet == false)
    {
        return 0;
    }

    return primaryToken;
}


其实还可以使用WTSGetActiveConsoleSessionId()函数而不是遍历整个列表来获取激活的Session ID,该函数会直接返回激活session的ID。但是当在某些任务中使用该函数时,发现它有时不起作用,但是遍历整个session的方法能够保证总是得到正确的结果。若当前session没有登入用户,则函数WTSQueryUserToken()返回FALSE,且错误码为ERROR_NO_TOKEN。下面的代码是使用得到的token来模拟当前用户环境启动应用程序。确保应用程序的权限和当前用户账户权限对应并且不是LocalSystem账户。代码如下:

BOOL Run(const std::string& processPath, const std::string& arguments)
{
    // Get token of the current user
    PHANDLE primaryToken = GetCurrentUserToken();
    if (primaryToken == 0)
    {
        return FALSE;
    }
    STARTUPINFO StartupInfo;
    PROCESS_INFORMATION processInfo;
    StartupInfo.cb = sizeof(STARTUPINFO);

    SECURITY_ATTRIBUTES Security1;
    SECURITY_ATTRIBUTES Security2;

    std::string command = "\"" + 
         processPath + "\"";
    if (arguments.length() != 0)
    {
        command += " " + arguments;
    }

    void* lpEnvironment = NULL; 

    // Get all necessary environment variables of logged in user
    // to pass them to the process
    BOOL resultEnv = CreateEnvironmentBlock(&lpEnvironment, 
                                            primaryToken, FALSE);
    if (resultEnv == 0)
    { 
        long nError = GetLastError(); 
    }

    // Start the process on behalf of the current user 
    BOOL result = CreateProcessAsUser(primaryToken, 0, 
                  (LPSTR)(command.c_str()), &Security1, 
    &Security2, FALSE, CREATE_NO_WINDOW | NORMAL_PRIORITY_CLASS | 
                    CREATE_UNICODE_ENVIRONMENT, lpEnvironment, 0, 
                    &StartupInfo, &processInfo);
	   DestroyEnvironmentBlock(lpEnvironment);
    CloseHandle(primaryToken);
    return result;
}


若开发的软件只会在Vista及其后的操作系统上使用,则可使用函数CreateProcessWithTokenW()而不是CreateProcessAsUser()。它可以以下面的方式被调用:

BOOL result = CreateProcessWithTokenW(primaryToken, LOGON_WITH_PROFILE, 
              0, (LPSTR)(command.c_str()), 
              CREATE_NO_WINDOW | NORMAL_PRIORITY_CLASS | 
              CREATE_UNICODE_ENVIRONMENT, lpEnvironment, 0, 
              &StartupInfo, &processInfo);



C#代码如下:

下面是以C#方式实现该功能,创建类ProcessStarter且会在随后的例子中也使用到:

public static IntPtr GetCurrentUserToken()
{
    IntPtr currentToken = IntPtr.Zero;
    IntPtr primaryToken = IntPtr.Zero;
    IntPtr WTS_CURRENT_SERVER_HANDLE = IntPtr.Zero;

    int dwSessionId = 0;
    IntPtr hUserToken = IntPtr.Zero;
    IntPtr hTokenDup = IntPtr.Zero;

    IntPtr pSessionInfo = IntPtr.Zero;
    int dwCount = 0;

    WTSEnumerateSessions(WTS_CURRENT_SERVER_HANDLE, 0, 1, 
                         ref pSessionInfo, ref dwCount);

    Int32 dataSize = Marshal.SizeOf(typeof(WTS_SESSION_INFO));

    Int32 current = (int)pSessionInfo;
    for (int i = 0; i < dwCount; i++)
    {
        WTS_SESSION_INFO si = (WTS_SESSION_INFO)Marshal.PtrToStructure(
            (System.IntPtr)current, typeof(WTS_SESSION_INFO));
        if (WTS_CONNECTSTATE_CLASS.WTSActive == si.State)
        {
            dwSessionId = si.SessionID;
            break;
        }

        current += dataSize;
    }
	
	   WTSFreeMemory(pSessionInfo);

    bool bRet = WTSQueryUserToken(dwSessionId, out currentToken);
    if (bRet == false)
    {
        return IntPtr.Zero;
    }

    bRet = DuplicateTokenEx(currentToken, 
           TOKEN_ASSIGN_PRIMARY | TOKEN_ALL_ACCESS,
           IntPtr.Zero, SECURITY_IMPERSONATION_LEVEL.SecurityImpersonation,
           TOKEN_TYPE.TokenPrimary, out primaryToken);
    if (bRet == false)
    {
        return IntPtr.Zero;
    }

    return primaryToken;
}

public void Run()
{
    IntPtr primaryToken = GetCurrentUserToken();
    if (primaryToken == IntPtr.Zero)
    {
        return;
    }
    STARTUPINFO StartupInfo = new STARTUPINFO();
    processInfo_ = new PROCESS_INFORMATION();
    StartupInfo.cb = Marshal.SizeOf(StartupInfo);

    SECURITY_ATTRIBUTES Security1 = new SECURITY_ATTRIBUTES();
    SECURITY_ATTRIBUTES Security2 = new SECURITY_ATTRIBUTES();

    string command = "\"" + processPath_ + "\"";
    if ((arguments_ != null) && (arguments_.Length != 0))
    {
        command += " " + arguments_;
    }

    IntPtr lpEnvironment = IntPtr.Zero;
    bool resultEnv = CreateEnvironmentBlock(out lpEnvironment, 
                     primaryToken, false);
    if (resultEnv != true)
    {
        int nError = GetLastError();
    }

    CreateProcessAsUser(primaryToken, null, command, ref Security1, 
                        ref Security2, false, 
                        CREATE_NO_WINDOW | NORMAL_PRIORITY_CLASS | 
                        CREATE_UNICODE_ENVIRONMENT,
                        lpEnvironment, null, ref StartupInfo, 
                        out processInfo_);
    DestroyEnvironmentBlock(lpEnvironment);
    CloseHandle(primaryToken); 
}


服务和应用程序间的数据交互:
可使用socket,命名内存映射,RPC以及COM方式。这儿使用最简单的方式:文本文件,事件(C#)和命名管道(C++)

文本文件:
最简单的方法是文本文件了。当说到基于C#开发时,最自然的方式就是使用XML文件了。
假设,必须从用户级应用程序传一些数据字符串到服务中。开始,必须选择中间文件创建的位置。这个位置必须能够被应用程序和服务同时访问到。
若应用程序是以当前登录用户权限启动的,一个比较好的位置就是放在My Documents目录中。这种情况下,就对两边都不会有访问上的权限问题了(然而LocalSystem服务具备访问任何位置的权限)。
因此,就先在My Documents目录下创建一个名为sample.xml的xml文件吧。代码如下:

using System.Xml;

XmlWriterSettings xmlWriterSettings = new XmlWriterSettings();
// provide the XML declaration
xmlWriterSettings.OmitXmlDeclaration = false;
// write attributes on the new line
xmlWriterSettings.NewLineOnAttributes = true;
// indent elements
xmlWriterSettings.Indent = true;
// get "My Documents" folder path
String myDocumentsPath = 
  Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
String sampleXmlFilePath = Path.Combine(myDocumentsPath,"sample.xml"); 
// create the XML file "sample.xml"
sampleXmlWriter = XmlWriter.Create(sampleXmlFilePath, xmlWriterSettings);


现在,再创建一个SampleElement的元素,用于传输一些有用的数据:

sampleXmlWriter.WriteStartElement("SampleElement");
sampleXmlWriter.WriteElementString("Data", "Hello");

完成文件的创建:
sampleXmlWriter.WriteEndElement();
sampleXmlWriter.Flush();
sampleXmlWriter.Close();

现在,服务要打开那个文件了。为了能够访问到它,服务必须首先获取当前用户的My Documents目录路径。为了达到目的,就需要通过上面获取到的token来做一个模拟:
// Get token of the current user 
IntPtr currentUserToken = ProcessStarter.GetCurrentUserToken();
// Get user ID by the token
WindowsIdentity currentUserId = new WindowsIdentity(currentUserToken);
// Perform impersonation 
WindowsImpersonationContext impersonatedUser = currentUserId.Impersonate();
// Get path to the "My Documents" 
String myDocumentsPath = 
  Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
// Make everything as it was 
impersonatedUser.Undo();

现在,服务可以从sample.xml文件读取数据了:
String sampleXmlFilePath = Path.Combine(myDocumentsPath,"sample.xml");
XmlDocument oXmlDocument = new XmlDocument();
oXmlDocument.Load(sampleXmlFilePath);

XPathNavigator oPathNavigator = oXmlDocument.CreateNavigator();
XPathNodeIterator oNodeIterator = 
  oPathNavigator.Select("/SampleElement/Data");
oNodeIterator.MoveNext();

String receivedData = oNodeIterator.Current.Value;


通过文本文件的形式进行数据交互非常容易实现,但是它有许多的劣势。可能没有足够多的硬盘空间,用户能直接干涉数据记录过程等等。因此,考虑其他方式。
事件:
在不是大量数据交互的情况下,也就是当需要传输的仅仅是yes/no类型的数据情况下(对话窗口上的问题的回答,服务要停止还是不停止的信息等等),可以使用事件。这里有一个例子,应用程序sample的功能在某个点要暂停,一直暂停到服务给出一个继续的命令为止。
sample应用程序通过类ProcessStarter的方式从服务启动:

ProcessStarter sampleProcess = new ProcessStarter();
sampleProcess.ProcessName = "sample";
sampleProcess.ProcessPath = @"C:\Base\sample.exe"; 
sampleProcess.Run();

现在,在sample应用程序要停止的那个点创建全局事件SampleEvent,并等到来自服务的命令。停止线程一直到信号的到来:
using System.Threading;

EventWaitHandle sampleEventHandle = 
     new EventWaitHandle(false, EventResetMode.AutoReset, 
     "Global\\SampleEvent");
bool result = sampleEventHandle.WaitOne();

在服务的哪个点打开全局事件SampleEvent,在那儿必须发送命令到应用程序。设置改时间到信号模式:
EventWaitHandle handle = 
  EventWaitHandle.OpenExisting("Global\\SampleEvent");
bool setResult = handle.Set();

应用程序获得该信号并继续执行。
命名管道:
若涉及到大量的数据交互过程,在技术上可使用命名管道。以下代码是C++形式,因为C#中涉及到命名管道的类只出现在.NET Framework3.5之后。
现在假设一个应用程序要周期性的发送一些无符号整型到服务。
在这种情况下,可在服务端打开命名管道,随后在一个单独的线程中控制它的状态来读取并处理数据。因此,可以在服务代码中创建DataPipe的命名管道:
HANDLE CreatePipe()
{
    SECURITY_ATTRIBUTES sa;
    sa.lpSecurityDescriptor = 
      (PSECURITY_DESCRIPTOR)malloc(SECURITY_DESCRIPTOR_MIN_LENGTH);
    if (!InitializeSecurityDescriptor(sa.lpSecurityDescriptor, 
         SECURITY_DESCRIPTOR_REVISION))
    {
        DWORD er = ::GetLastError();
    }
    if (!SetSecurityDescriptorDacl(sa.lpSecurityDescriptor, 
                                   TRUE, (PACL)0, FALSE))
    {
        DWORD er = ::GetLastError();
    }
    sa.nLength = sizeof sa;
    sa.bInheritHandle = TRUE;

    // To know the maximal size of the received data
    // for reading from the pipe buffer

    union maxSize
    {
        UINT _1;
    };

    HANDLE hPipe = ::CreateNamedPipe((LPSTR)"\\\\.\\pipe\\DataPipe",
                    PIPE_ACCESS_INBOUND, PIPE_TYPE_MESSAGE | 
                    PIPE_READMODE_MESSAGE | PIPE_WAIT, 
                    PIPE_UNLIMITED_INSTANCES, sizeof maxSize, 
                    sizeof maxSize, NMPWAIT_USE_DEFAULT_WAIT, &sa);

    if (hPipe == INVALID_HANDLE_VALUE)
    {
        DWORD dwError = ::GetLastError();
    }
    return hPipe;
}

还要创建函数来检测线程状态并读取数据:

unsigned int __stdcall ThreadFunction(HANDLE& hPipe)
{
    while (true)
    {
        BOOL bResult = ::ConnectNamedPipe(hPipe, 0);
        DWORD dwError = GetLastError();

        if (bResult || dwError == ERROR_PIPE_CONNECTED)
        {
            BYTE buffer[sizeof UINT] = {0}; 
            DWORD read = 0;

            UINT uMessage = 0;

            if (!(::ReadFile(hPipe, &buffer, sizeof UINT, &read, 0)))
            {
                unsigned int error = GetLastError(); 
            } 
            else
            {
                uMessage = *((UINT*)&buffer[0]);
                // The processing of the received data
            }
            ::DisconnectNamedPipe(hPipe);
        }
        else
        { 
        }
        ::Sleep(0);
    }
}

最后,使用函数ThreadFunction()启动独立线程:
unsigned int id = 0; 
HANDLE pipeHandle = CreatePipe(); 
::CloseHandle((HANDLE)::_beginthreadex(0, 0, ThreadFunction, 
              (void*)pipeHandle, 0, &id));


现在,来看客户端,通过命名管道发送数据到服务:
SendDataToService(UINT message)
{
    HANDLE hPipe = INVALID_HANDLE_VALUE;
    DWORD dwError = 0;
    while (true) 
    { 
        hPipe = ::CreateFile((LPSTR)"\\\\.\\pipe\\DataPipe", 
                GENERIC_WRITE, 0, 0, OPEN_EXISTING, 0, 0);
        dwError = GetLastError();
        if (hPipe != INVALID_HANDLE_VALUE)
        {
            break;
        }

        // If any error except the ERROR_PIPE_BUSY has occurred,
        // we should return FALSE. 
        if (dwError != ERROR_PIPE_BUSY) 
        {
            return FALSE;
        }
        // The named pipe is busy. Let’s wait for 20 seconds. 
        if (!WaitNamedPipe((LPSTR)"\\\\.\\pipe\\DataPipe", 20000)) 
        { 
            dwError = GetLastError();
            return FALSE;
        } 
    } 
    DWORD dwRead = 0;
    if (!(WriteFile(hPipe, (LPVOID)&message, sizeof UINT, &dwRead, 0)))
    {
        CloseHandle(hPipe);
        return FALSE;
    }
    CloseHandle(hPipe);
    ::Sleep(0);
    return TRUE;
}


总结:
在Windows Vistar上关于服务和应用程序交互的问题,不只仅仅一个正确的解决方案。还有很多机制,且要根据对应问题选择最恰当的方式。


参考 网址


  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值