这个项目已经结束快两年了,由于公司需要,所以又来做个总结。
当时需要做屏幕截图,但是在winlogon桌面(ctrl+alt+del 或 登录界面 或 UAC)和screensave桌面(屏保下)截图都是黑屏,所以就引来了这个问题。
解决这个问题涉及到的东西比较多,包括session,window station,service,desktop什么的。关于他们的前世今生的问题今天我们就不讨论了,有兴趣的同胞们可以去查一查,这里有一个简单的概括http://blog.sina.com.cn/s/blog_42a88fc10100o6ez.html
总之我们记住本机windows的所有用户交互操作都在seesion0下的winsta0下的几个桌面中(default,winlogon,screensaver)。
在这里我们需要记住三件事:
1.同一时刻只能有一个桌面与用户进行交互(能接受鼠标,键盘输入),我们称之为InputDesktop
2.我们的操作(即截图)只能在InputDesktop中才能生效。
3.想在InputDesktop中进行有效操作的process需要一定的用户权限,普通进程无法进行有效操作。
基于以上三点,我们基本就能解决问题了。
首先,我们需要获得inputDesktop,这个可以使用windowsApi OpenInputDesktop()获得。(具体使用方法请查MSDN)。
注:其实最先还应该将我们的进程附到winsta0,OpenWinstation(winsta0,SetProcessWinstation(winsta0),由于我们的进程基本只能在winsta0中运行,所以一般不需这一步。
其次,是将我们的操作附到InputDesktop中,当然这个也是使用windows的API SetThreadDesktop(),这个函数会将我们当前线程切换到InputDesktop。
注意:在切换前我们应该调用 GetThreadDesktop将当前桌面保存起来,以便在特定操作(本文指截图)完成后再将线程切换回原来的desktop。
最后,我们需要给我们的进程赋予足够的权限。本文中,我们从服务继承SYSTEM权限(其他方式还未研究),主要有以下几个步骤:
a.写一个windows 服务,在服务中调用OpenProcessToken,取得服务的token。
b.调用DuplicateTokenEx复制服务的token到tokenDup。
c.然后调用SetTokenInfomation()对tokenDup进行一些其他设置。
d.最后调用CreateProcessAsUser(),以tokenDup为参数启动我们的程序,这样我们的程序就具有了SYSTEM权限了。
注:这里使用的是两个程序,我们的逻辑处理程序单独写,设为A,然后通过写一个服务B来启动程序A以便使A具有system权限。
虽然写起来没几个字,但是不得不说当时也是痛苦的调查了好几天。在此希望能给大家带来帮助,让后来者少走弯路。
为了方便理解,这个地方贴两段代码:
1.windows服务启动功能进程的代码
BOOL StartProcess(const TCHAR *appNameAndPath, const TCHAR* param, PROCESS_INFORMATION *procInfo)
{
HANDLE hTokenThis = NULL;
HANDLE hTokenDup = NULL;
HANDLE hThisProcess = GetCurrentProcess();
BOOL bResult = FALSE;
bResult = OpenProcessToken(hThisProcess, TOKEN_ALL_ACCESS, &hTokenThis);
if(!bResult)
{
return FALSE;
}
bResult = DuplicateTokenEx(hTokenThis, MAXIMUM_ALLOWED, NULL, SecurityIdentification, TokenPrimary, &hTokenDup);
if(!bResult)
{
return FALSE;
}
DWORD dwSessionId = WTSGetActiveConsoleSessionId();
bResult = SetTokenInformation(hTokenDup, TokenSessionId, &dwSessionId, sizeof(DWORD));
DWORD UIAccess = 1;
SetTokenInformation(hTokenDup, TokenUIAccess, &UIAccess, sizeof(DWORD));
if(!bResult)
{
return FALSE;
}
STARTUPINFO si;
ZeroMemory(&si, sizeof(STARTUPINFO));
si.cb = sizeof(STARTUPINFO);
si.lpDesktop = TEXT("WinSta0\\default");
LPVOID pEnv = NULL;
DWORD dwCreationFlag = NORMAL_PRIORITY_CLASS | CREATE_NEW_CONSOLE | CREATE_UNICODE_ENVIRONMENT;
bResult = CreateEnvironmentBlock(&pEnv, hTokenDup, FALSE);
if(!bResult)
{
return FALSE;
}
bResult = CreateProcessAsUser(hTokenDup, appNameAndPath, (LPWSTR)param, NULL, NULL, FALSE, dwCreationFlag, pEnv, NULL, &si, procInfo);
if(!bResult)
{
return FALSE;
}
if(hTokenDup)
{
CloseHandle(hTokenDup);
}
return TRUE;
}
二,功能进程切换desktop并截图的代码(其中GetDesktopBitmap(picNum)是截图函数,picNum没有意义,这是当初调研的时候写的,因为懒直接截图就存硬盘了,picnum是为了防止存的文件重名):
BOOL GetInputDesktop()
{
HDESK currentDesk = NULL;
HDESK inputDesk = NULL;
currentDesk = GetThreadDesktop(GetCurrentThreadId());
if(!currentDesk)
{
return FALSE;
}
inputDesk = OpenInputDesktop(DF_ALLOWOTHERACCOUNTHOOK, FALSE, GENERIC_ALL);
if (inputDesk == NULL)
{
DWORD tempError = GetLastError();
return FALSE;
}
if(!SetThreadDesktop(inputDesk))
{
DWORD tempError = GetLastError();
CloseDesktop(inputDesk);
return FALSE;
}
GetDesktopBitmap(picNum);
if(!SetThreadDesktop(currentDesk))
{
DWORD tempError = GetLastError();
return FALSE;
}
if(!CloseDesktop(inputDesk))
{
return FALSE;
}
return TRUE;
}
由于项目代码不能泄露,所以以上代码是当初调研的时候写的测试代码,所以可能有很多神奇的地方和随意的写法,望勿见怪。但是这两个代码肯定能用,刚写文章之前我还测试了一遍。如果大家要试验的话,拷过去修改一下应该就可以了。