本章讲的是服务和应用程序在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;
}
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;
}
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);
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);
}
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);
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;
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();
应用程序获得该信号并继续执行。
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;
}