如何利用Win32服务进程去创建一个GUI用户进程?

如何利用Win32服务进程去创建一个GUI用户进程

版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。

最近遇到一个小问题,我需要在一个服务进程中去启动一个用户GUI进程。按常理来说这很简单,通常情况下调用ShellExecute这个API即可。这种方法在XP的年代似乎是完美的,但由于现在大多已经到了Win7,这个方法似乎已经不好用了。追溯原因要谈到微软给XP以后的操作系统添加了Session隔离机制。网络上有很多介绍Windows Session机制的资料,这里就不再多余讨论,反正这个机制的出现让很多人为之头疼!

当我遇到这个问题时,我第一时间在网上搜索了资料,资料很多但也很乱,其中大部分资料都是乱拷贝粘贴的。里面充斥了很多错误,而这些发布者似乎从来没有去验证过他所拷贝的文章、代码是否正确!

出于以上的原因,在我解决这个问题后,我思考着将我的代码用一个简单的示例展示出来,用以帮助以后遇到该问题的朋友。我本着尽可能严谨的态度来书写这篇文章,但尽管这样,可能还是有所疏漏,如果您发现了,请告诉我,谢谢!

本文我将分两个部分介绍。首先,我们先创建一个足够简单的服务程序。接着,在此服务程序的基础上,实现用它来启动一个用户GUI程序。温馨提示,在本文我不会讲到什么是Session隔离,因为能力有限,我想我无法讲解清楚这个问题,但我会在本文末尾提供一些相关的链接供读者参考。另外,我会简单提及一下如何构建一个基本的Win32服务程序,以帮助没有任何服务程序开发经验的读者能更好的阅读,谢谢。

1.创建一个简单的服务进程
首先,我们搭建一个简单Win32服务进程框架,它的工作足够简单:在服务启动后,向指定文件输出一行”Hello Win32 Service”即可!
一个服务程序其实有它较为固定的框架,通常它包含一个main函数和两个回调函数。显而易见main函数还是唯一入口,另外两个回调函数会被自动调用而无需我们关心。因此,一个服务程序大概看起来就是这样的:

void WINAPI ServiceMain(DWORD argc, LPTSTR* argv);
void WINAPI Handler(DWORD fdwControl);

int main(int argc, char const *argv[])
{
return 0;
}

让我们先从main函数开始。先定义几个全局变量,因为即将用到它们:

//全局数据
TCHAR ServiceName[] = _TEXT(“ServiceDemo”); //服务名称
SERVICE_STATUS ServiceStatus; //服务状态
SERVICE_STATUS_HANDLE hStatus; //服务状态句柄

接着改写main函数:

int main(int argc, char const *argv[])
{
SERVICE_TABLE_ENTRY ServiceTable[] = {
{ServiceName, ServiceMain},
{NULL, NULL},
};
return StartServiceCtrlDispatcher(ServiceTable);
}

这里我们定义了一个SERVICE_TABLE_ENTRY类型的数组,通常称它为分派表。一个服务进程可以包含若干个子服务,每一个服务都必须在专门的分派表中注册。这个表的每一项都是一个 SERVICE_TABLE_ENTRY 结构对象。它有两个域:
  lpServiceName: 指向表示服务名称字符串的指针;当定义了多个服务时,这个域必须指定;
  lpServiceProc: 指向服务主函数的指针(服务入口点);
这里,我们传入了ServiceName和ServiceMain作为参数构造了第一个SERVICE_TABLE_ENTRY对象。无需构造太多,因为该服务进程只需一个服务就足够了。值得注意的是,分派表的最后一项必须是服务名和服务主函数的 NULL 指针,用以表示该数组的结束。

紧接着,调用StartServiceCtrlDispatcher方法,并传入了ServiceTable分派表作为其参数。服务控制管理器(SCM:Services Control Manager)是一个管理系统所有服务的进程。当 SCM 启动某个服务时,它等待该服务进程的主线程来调用 StartServiceCtrlDispatcher 函数,并将分派表传递给 StartServiceCtrlDispatcher。这一调用将把调用进程的主线程转换为控制分派器。该分派器启动一个新线程,该线程运行分派表中每个服务的 ServiceMain 函数。另外分派器还监视程序中所有服务的执行情况,然后分派器将控制请求从 SCM 传给各个服务,至于各个服务将如何响应这些控制请求,稍后再来关注。
另一个值得注意的是,如果 StartServiceCtrlDispatcher 函数30秒没有被调用,便会报错,为了避免这种情况,我们必须在 ServiceMain 函数中或在非主函数的单独线程中初始化服务分派表。而在本文所描述的服务中不需要防范这样的情况。

分派表中所有的服务执行完之后(例如,用户通过“服务”控制面板程序停止它们),或者发生错误时,StartServiceCtrlDispatcher 调用返回,然后服务主进程终止。

ServiceMain函数:

void WINAPI ServiceMain(DWORD argc, LPTSTR* argv)
{
ServiceStatus.dwServiceType = SERVICE_WIN32_OWN_PROCESS | SERVICE_INTERACTIVE_PROCESS;
ServiceStatus.dwCurrentState = SERVICE_START_PENDING;
ServiceStatus.dwControlsAccepted = SERVICE_ACCEPT_SHUTDOWN | SERVICE_ACCEPT_STOP;
ServiceStatus.dwWin32ExitCode = NO_ERROR;
ServiceStatus.dwServiceSpecificExitCode = NO_ERROR;
ServiceStatus.dwCheckPoint = 0;
ServiceStatus.dwWaitHint = 0;

hStatus = RegisterServiceCtrlHandler(ServiceName, Handler);
if(!hStatus)
{
    DWORD dwError = GetLastError();
    WriteErrorToLog(dwError, "RegisterServiceCtrlHandler");
    return ;
}

InitService();

//设置服务状态
ServiceStatus.dwCurrentState = SERVICE_RUNNING;
SetServiceStatus(hStatus, &ServiceStatus);

Run();

}
该函数是一个服务的入口点。ServiceMain 应该尽可能早的为服务注册控制处理器。这要通过调用 RegisterServiceCtrlHadler 函数来实现。你要将两个参数传递给此函数:服务名和指向 ControlHandlerfunction 的指针。控制处理器函数,它指示控制分派器调用 Handler 函数处理 SCM 控制请求。注册完控制处理器之后,获得状态句柄(hStatus)。通过调用 SetServiceStatus 函数,用 hStatus 向 SCM 报告服务的状态。
另外,在ServiceMain 函数中,还对ServiceStatus结构体的成员设置了一些值,关于这些值的含义可以查看MSDN给出的文档,这里有详细的介绍。
接着,调用RegisterServiceCtrlHandler函数给该服务注册了一个控制信号处理函数Handler。然后使用InitService函数来做一些初始化工作。完成后,设置服务的状态为正在运行中,并且调用Run函数开始真正的“服务”。

Handler函数:
这是最后一个要说明的函数了,它看上去是这样的:

void WINAPI Handler(DWORD fdwControl)
{
switch(fdwControl)
{
case SERVICE_CONTROL_STOP:
{
WriteMsgToLog(“ServiceDemo 服务停止…”);

        ServiceStatus.dwCurrentState = SERVICE_STOPPED;
        ServiceStatus.dwWin32ExitCode = 0;
        SetServiceStatus(hStatus, &ServiceStatus);
    }
    break;
case SERVICE_CONTROL_SHUTDOWN:
    {
        WriteMsgToLog("ServiceDemo 服务终止...");

        ServiceStatus.dwCurrentState = SERVICE_STOPPED;
        ServiceStatus.dwWin32ExitCode = 0;
        SetServiceStatus(hStatus, &ServiceStatus);
    }
    break;
default:
    break;
}

}
这里处理了SERVICE_CONTROL_STOP和SERVICE_CONTROL_SHUTDOWN消息,因为在ServiceMain函数的开头我们注册了这两个消息,表明我们希望自己处理这些消息。

到了这里,一个基本的服务程序就简单介绍完毕,稍后将给出整个服务程序的代码,你可以编译这份代码,并使用下面两条命令去安装和删除这个服务进程,安装成功后,可以到服务管理器中去启动该服务。

#安装服务 注意“binpath=”后有一个空格
sc create ServiceDemo binpath= C:/ServiceDemo.exe

#删除服务 删除前需先停止服务
sc delete ServiceDemo

如果不出什么意外,你能在代码定义的日志文件中看到一些输出。
完整代码:

#include <stdio.h>
#include <Windows.h>
#include <tchar.h>

#define LOGFILE “C:\Msg.log”

void WINAPI ServiceMain(DWORD argc, LPTSTR* argv);
void WINAPI Handler(DWORD fdwControl);

//日志记录的相关方法
void WriteMsgToLog(const char *Msg);
void WriteErrorToLog(DWORD dwErrorCode, const char *MsgInfo);

//服务相关方法
void InitService();
void Run();

//全局数据
TCHAR ServiceName[] = _TEXT(“ServiceDemo”); //服务名称
SERVICE_STATUS ServiceStatus; //服务状态
SERVICE_STATUS_HANDLE hStatus; //服务状态句柄

int main(int argc, char const *argv[])
{
SERVICE_TABLE_ENTRY ServiceTable[] = {
{ ServiceName, ServiceMain },
{ NULL, NULL },
};

return StartServiceCtrlDispatcher(ServiceTable);

}

void WINAPI ServiceMain(DWORD argc, LPTSTR* argv)
{
ServiceStatus.dwServiceType = SERVICE_WIN32_OWN_PROCESS | SERVICE_INTERACTIVE_PROCESS;
ServiceStatus.dwCurrentState = SERVICE_START_PENDING;
ServiceStatus.dwControlsAccepted = SERVICE_ACCEPT_SHUTDOWN | SERVICE_ACCEPT_STOP;
ServiceStatus.dwWin32ExitCode = NO_ERROR;
ServiceStatus.dwServiceSpecificExitCode = NO_ERROR;
ServiceStatus.dwCheckPoint = 0;
ServiceStatus.dwWaitHint = 0;

hStatus = RegisterServiceCtrlHandler(ServiceName, Handler);
if (!hStatus)
{
    DWORD dwError = GetLastError();
    WriteErrorToLog(dwError, "RegisterServiceCtrlHandler");
    return;
}

InitService();

//设置服务状态
ServiceStatus.dwCurrentState = SERVICE_RUNNING;
SetServiceStatus(hStatus, &ServiceStatus);

Run();

}

void WINAPI Handler(DWORD fdwControl)
{
switch (fdwControl)
{
case SERVICE_CONTROL_STOP:
{
WriteMsgToLog(“ServiceDemo 服务停止…”);

    ServiceStatus.dwCurrentState = SERVICE_STOPPED;
    ServiceStatus.dwWin32ExitCode = 0;
    SetServiceStatus(hStatus, &ServiceStatus);
}
break;
case SERVICE_CONTROL_SHUTDOWN:
{
    WriteMsgToLog("ServiceDemo 服务终止...");

    ServiceStatus.dwCurrentState = SERVICE_STOPPED;
    ServiceStatus.dwWin32ExitCode = 0;
    SetServiceStatus(hStatus, &ServiceStatus);
}
break;
default:
    break;
}

}

void InitService()
{
WriteMsgToLog(“ServiceDemo 服务启动…”);
}

void Run()
{
WriteMsgToLog(“Hello Win32 Service”);
}

void WriteMsgToLog(const char *Msg)
{
FILE *file = NULL;
fopen_s(&file, LOGFILE, “a+”);
if (!file || !Msg)
{
return;
}

fprintf_s(file, "%s\n", Msg);

fflush(file);
fclose(file);
file = NULL;

}

void WriteErrorToLog(DWORD dwErrorCode, const char *MsgInfo)
{
if (!MsgInfo) return;

char Msg[128] = { 0 };
sprintf_s(Msg, "%s Faild: %lu\n", MsgInfo, dwErrorCode);
WriteMsgToLog(Msg);

}
2.创建用户GUI进程

终于可以开始本文的重点了。首先,我们先试着启动一个notepad.exe,因此请在上面的源码中添加如下的定义:

TCHAR szApp[MAX_PATH] = _TEXT(“notepad.exe”)
接着,创建一个函数用以创建GUI进程。

void CreateMyProcess()
{
DWORD dwSessionID = WTSGetActiveConsoleSessionId();
HANDLE hToken = NULL;
HANDLE hTokenDup = NULL;
LPVOID pEnv = NULL;
STARTUPINFO si;
PROCESS_INFORMATION pi;

//获取当前处于活动状态用户的Token
if (!WTSQueryUserToken(dwSessionID, &hToken))
{
    DWORD nCode = GetLastError();
    WriteErrorToLog(nCode, "WriteErrorToLog");

    CloseHandle(hToken);
    return;
}

//复制新的Token
if (!DuplicateTokenEx(hToken, MAXIMUM_ALLOWED, NULL, SecurityIdentification, TokenPrimary, &hTokenDup))
{
    DWORD nCode = GetLastError();
    WriteErrorToLog(nCode, "DuplicateTokenEx");

    CloseHandle(hToken);
    return;
}

//创建环境信息
if (!CreateEnvironmentBlock(&pEnv, hTokenDup, FALSE))
{
    DWORD nCode = GetLastError();
    WriteErrorToLog(nCode, "CreateEnvironmentBlock");

    CloseHandle(hTokenDup);
    CloseHandle(hToken);
    return;
}

//设置启动参数
ZeroMemory(&si, sizeof(STARTUPINFO));
si.cb = sizeof(STARTUPINFO);
si.lpDesktop = _TEXT("winsta0\\default");

ZeroMemory(&pi, sizeof(PROCESS_INFORMATION));

//开始创建进程
DWORD dwCreateFlag = NORMAL_PRIORITY_CLASS | CREATE_NEW_CONSOLE | CREATE_UNICODE_ENVIRONMENT;
if (!CreateProcessAsUser(hTokenDup, szApp, NULL, NULL, NULL, FALSE, dwCreateFlag, pEnv, NULL, &si, &pi))
{
    DWORD nCode = GetLastError();
    WriteErrorToLog(nCode, "CreateProcessAsUser");

    DestroyEnvironmentBlock(pEnv);
    CloseHandle(hTokenDup);
    CloseHandle(hToken);
    return;
}

//附加操作,回收资源

//等待启动的进程结束
WaitForSingleObject(pi.hProcess, INFINITE);
WriteMsgToLog("子进程结束,回收资源...");

DestroyEnvironmentBlock(pEnv);
CloseHandle(hTokenDup);
CloseHandle(hToken);

}
看看CreateMyProcess()函数的部分细节。首先,我们调用WTSGetActiveConsoleSessionId() API尝试获取当前处于活动状态的用户的Session ID。为什么要这么做?因为我们需要以当前用户的权限去创建这个GUI进程,而要想这么做就得取到当前活动用户的权限令牌(Token),要取得权限令牌,首先就得找到是哪一个用户。
接着就可以使用WTSQueryUserToken() API来取得该用户的令牌了,我们将它存放到了hToken句柄中。紧接着用DuplicateTokenEx() API复制一个已经存在的令牌来产生一个新的令牌。值得注意的是,因为新的令牌将用于创建进程,即用于CreateProcessAsUser() API的参数,所以按照MSDN上面的说法,在复制令牌时,需要传入一些指定的参数,具体可以参看这里。
然后,使用CreateEnvironmentBlock() API获取了指定用户的环境变量,这个环境变量会作为参数传递给CreateProcessAsUser() API。最后,简单的对STARTUPINFO和PROCESS_INFORMATION结构体的一些成员赋予了一些特定的值,就开始调用CreateProcessAsUser() API,这个API完成了主要的工作。但它的一些参数需要特别注意,不过这些MSDN上都有详细说明!

最后,别忘了在服务程序的Run()函数中调用CreateMyProcess()函数,并添加一些必要的头文件和库,即可完成。

#include <UserEnv.h>
#include <WtsApi32.h>

#pragma comment(lib, “Wtsapi32.lib”)
#pragma comment(lib, “Userenv.lib”

试着编译并运行一下该服务吧,如果不出意外,会看到一个记事本程序被启动起来了,看样子一切顺利!但等等,真的一点问题都没有吗?不妨换一个notepad.exe进程之外的进程试试。但在这之前,先让我们准备一个用于测试的小程序,它很简单,仅用几行代码打印出它自己当前工作的目录。

#include
#include <windows.h>
#include <tchar.h>

using namespace std;

int main()
{
TCHAR g_strCurrentPath[MAX_PATH] = {0};
GetCurrentDirectory(MAX_PATH, g_strCurrentPath);

wcout << g_strCurrentPath << endl;

system("pause");
return 0;

}
————————————————
感谢原版主提供的示例,这边存在一些问题,做出了新的修改,以供后面大家学习:
将服务设置开机自启动是,使用CreateMyProcess函数,WTSQueryUserToken函数返回1008,在会话上获取用户令牌时出错,这边修改使用“winlogon.exe”创建token,在后面使用中可以正常创建;代码如下:
void CreateMyProcess()
{

bool result;		
HANDLE hToken = NULL;		
HANDLE hDupedToken = NULL;		
// HANDLE hUserToken = NULL;
PROCESS_INFORMATION pi;		
ZeroMemory(&pi, sizeof(PROCESS_INFORMATION));	
STARTUPINFO si;		
ZeroMemory(&si, sizeof(STARTUPINFO));		
si.cb = sizeof(STARTUPINFO);		
si.lpDesktop = _TEXT("winsta0\\default");
SECURITY_ATTRIBUTES sa;		
ZeroMemory(&sa, sizeof(SECURITY_ATTRIBUTES));	
sa.nLength = sizeof(SECURITY_ATTRIBUTES);		
DWORD dwSessionId, winlogonPid;		
// Loag the client on to the local computer		
dwSessionId = WTSGetActiveConsoleSessionId();		
/		
// Find the FSI.SnifferClient.UI.exe process		
/		
PROCESSENTRY32 procEntry;		
HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);		
if (hSnap == INVALID_HANDLE_VALUE) 
{			
	return ;		
}		
procEntry.dwSize = sizeof(PROCESSENTRY32);		
if (!Process32First(hSnap, &procEntry)) 
{			
	DWORD nCode = GetLastError();
	WriteErrorToLog(nCode, "Process32FirstError");
	return ;	
}		
do {		
	char output[260];	
	char* wc = procEntry.szExeFile;
	sprintf_s(output, "%s", wc);		
	if (_stricmp(output, "winlogon.exe") == 0) 
	{			
		// We found a winlogon process...
		// make sure it's running in the console session	
		DWORD fsiSnifferClientSessId = 0;			
		if (ProcessIdToSessionId(procEntry.th32ProcessID, &fsiSnifferClientSessId)&& fsiSnifferClientSessId == dwSessionId) 
		{					
			winlogonPid = procEntry.th32ProcessID;		
			break;			
		}		
	}	
} while (Process32Next(hSnap, &procEntry));	
HANDLE hProcess = OpenProcess(MAXIMUM_ALLOWED, FALSE, winlogonPid);		
if (!::OpenProcessToken(hProcess, TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY	| TOKEN_DUPLICATE | TOKEN_ASSIGN_PRIMARY | TOKEN_ADJUST_SESSIONID| TOKEN_READ | TOKEN_WRITE, &hToken))
{			
	DWORD nCode = GetLastError();
	WriteErrorToLog(nCode, "OpenProcessToken");
	return ;		
}		
TOKEN_PRIVILEGES tp;
LUID luid;	
if (!LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &luid)) 
{			
	DWORD nCode = GetLastError();
	WriteErrorToLog(nCode, "LookupPrivilegeValue");
	return ;		
}		
tp.PrivilegeCount = 1;		
tp.Privileges[0].Luid = luid;
tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;	
result = DuplicateTokenEx(hToken, MAXIMUM_ALLOWED, NULL, SecurityIdentification, TokenPrimary, &hDupedToken);	
SetTokenInformation(hDupedToken, TokenSessionId, (void*)dwSessionId, sizeof(DWORD));
if (!AdjustTokenPrivileges(hDupedToken, FALSE, &tp, sizeof(TOKEN_PRIVILEGES),	(PTOKEN_PRIVILEGES)NULL, NULL))
{	
	DWORD nCode = GetLastError();
	WriteErrorToLog(nCode, "AdjustTokenPrivileges");
	return ;		
}		
			
LPVOID pEnv = NULL;	
DWORD dwCreationFlags = NORMAL_PRIORITY_CLASS | CREATE_NEW_CONSOLE;	
if (CreateEnvironmentBlock(&pEnv, hDupedToken, TRUE))
{			
	dwCreationFlags |= CREATE_UNICODE_ENVIRONMENT;	
}
if (!CreateProcessAsUser(hDupedToken, szApp, NULL, NULL, NULL, FALSE, dwCreationFlags, pEnv, szCurrentDirectory, &si, &pi))
{
	DWORD nCode = GetLastError();
	WriteErrorToLog(nCode, "CreateProcessAsUser");

	DestroyEnvironmentBlock(pEnv);
	CloseHandle(hDupedToken);
	CloseHandle(hToken);
	return;
}

//等待启动的进程结束
WaitForSingleObject(pi.hProcess, INFINITE);
WriteMsgToLog("子进程结束,回收资源...");

DestroyEnvironmentBlock(pEnv);
CloseHandle(hProcess);		
CloseHandle(hDupedToken);		
CloseHandle(hToken);		

}
原文出处链接:https://blog.csdn.net/CMbug/article/details/52691180

将服务设置为开机自启:
win+R,打开运行,输入如下信息:
在这里插入图片描述
找到自己创建的服务,修改为自动:
在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值