屏幕监控是远控软件的基本功能之一。
现在很多远控程序的服务端通常为DLL形式,通过远程线程注入等方法插入到services、svchost等SYSTEM权限的进程中去,而此时常规的屏幕监控就会失效(这是因为与SYSTEM权限进程关联的窗口站、桌面与普通进程不同)。
如何才能在SYSTEM权限下实现屏幕监控呢?
一、 屏幕监控的基本原理
屏幕监控简单说就是对进程的当前桌面进行截屏存成位图,然后将此位图数据传输到远程。
对桌面进行截图需要通过一系列Windows GDI API来完成的。
首先通过CreateDC,CreateCompatibleDC,CreateCompatibleBitmap,SelectObject等API将“DISPLAY”驱动器的设备上下文与位图句柄关联起来。
然后通过GetStockObject,GetDC,SelectPalette等API处理调色板。
最后在一个循环中通过GetDIBits将所有水平线像素数据存入到缓冲区中去。
这个缓冲区就是我们想要的位图数据,只要将这些数据组织一下,就可以当成位图显示出来了。通过连续传输位图,就可以实时对远程屏幕进行监控了。这个过程比较简单,就不浪费文字了。
二、窗口站与桌面
首先必须了解几个重要的概念:
窗口站(WindowsStation)和桌面(Desktop)是Windows操作系统底层暴露给Windows API的执行体对象(Windows内部有两种类型的对象:执行体对象和内核对象。执行体对象指由执行体的各种组件如进程管理器、内存管理器等等所实现的对象。内核对象是由Windows内核实现的一组更基本的对象)。
其中,窗口站对象包含了一个剪贴板、一组全局原子和一组桌面对象。桌面对象是一个被包含在窗口站内部的对象,桌面对象有一个逻辑显示器表面,其中包含了窗口、菜单和钩子。
0号窗口站(WinSta0)和默认的桌面对象(default desktop)是有Winlogon进程创建的。窗口站是会话(Session)的下一层组织结构。一个会话可以有多个窗口站,但同一时刻只能有一个窗口站可以与用户进行交互。每个窗口站有自己的剪贴板,可以有多个桌面。Winlogon进程调用NtUserCreateWindowsStation函数创建窗口站,再调用NtUserCreateDesktop来创建桌面。它首先会创建一个名为Winlogon的桌面供自己使用(Windows登录界面就属于属于这个桌面),然后再创建一个名为Default的桌面给应用程序使用。创建完桌面后,Winlogon调用SetActiveDesktop函数将Winlogon桌面设置为当前的活动桌面。
之后,Winlogon会创建用于管理系统服务的服务管理器(Service.exe)和本地安全认证子系统(LSASS.exe)。用户登陆信息被验证后,Winlogon会将应用程序桌面激活,启动UserInit程序,UserInit会运行注册表中定义的登录脚本,然后启动操作系统外壳程序(Shell-默认是explorer.exe)。这是SYSTEM权限进程和普通用户进程逻辑显示器桌面分离的开始。在以后进程创建CreateProcess的过程中,如果没有指定桌面,那么进程就会与调用者的当前桌面关联在一起。
在实际测试中,发现services、svchost这些进程似乎没有关联任何桌面(截的屏都是黑屏)。普通的进程都是Default桌面,登录界面是Winlogon桌面。所以,当dll插入到service.exe等进程中的时候,要想实现截屏必须将进程与Default桌面关联,用户注销、离开或未登录时就要将进程与Winlogon桌面关联。
Windows给我们提供的一些API允许我们干这些事。
首先可以通过OpenWindowStation打开一个窗口站对象,然后通过SetProcessWindowStation将进程与窗口站关联,通过OpenDesktop打开一个桌面对象,再通过SetThreadDesktop将线程与这个桌面关联。这样service.exe就可以实现截屏了。但如何才能知道当前用户在哪个桌面呢?可以通过下列函数实现:
OpenInputDesktop(DF_ALLOWOTHERACCOUNTHOOK, FALSE, MAXIMUM_ALLOWED);//打开输入桌面
GetUserObjectInformation(hActiveDesktop, UOI_NAME, pvInfo, sizeof(pvInfo), &dwLen); //获取指定桌面对象的信息,一般情况和屏保状态为default,登陆界面为winlogon
pvInfo缓冲区包含的就是当前桌面。这样就可以放心的调用OpenDesktop打开它了。
完整代码如下:
BOOL OpenDesktop(LPCWSTR szName)
{
WCHAR pvInfo[128] = {0};
WCHAR tmp[1024] = {0};
if(szName != NULL)
lstrcpy(pvInfo, szName);
else
{
HDESK hActiveDesktop;
DWORD dwLen;
hActiveDesktop = OpenInputDesktop(DF_ALLOWOTHERACCOUNTHOOK, FALSE, MAXIMUM_ALLOWED);
if(!hActiveDesktop)//打开失败
{
return FALSE;
}
//获取指定桌面对象的信息,一般情况和屏保状态为default,登陆界面为winlogon
GetUserObjectInformation(hActiveDesktop, UOI_NAME, pvInfo, sizeof(pvInfo), &dwLen);
if(dwLen==0)//获取失败
{
return FALSE;
}
CloseDesktop(hActiveDesktop);
//打开winsta0
m_hwinsta = OpenWindowStation(_T("winsta0"), FALSE,
WINSTA_ACCESSCLIPBOARD |
WINSTA_ACCESSGLOBALATOMS |
WINSTA_CREATEDESKTOP |
WINSTA_ENUMDESKTOPS |
WINSTA_ENUMERATE |
WINSTA_EXITWINDOWS |
WINSTA_READATTRIBUTES |
WINSTA_READSCREEN |
WINSTA_WRITEATTRIBUTES);
if (m_hwinsta == NULL){
return FALSE;
}
if (!SetProcessWindowStation(m_hwinsta)){
return FALSE;
}
//打开desktop
m_hdesk = OpenDesktop(pvInfo, 0, FALSE,
DESKTOP_CREATEMENU |
DESKTOP_CREATEWINDOW |
DESKTOP_ENUMERATE |
DESKTOP_HOOKCONTROL |
DESKTOP_JOURNALPLAYBACK |
DESKTOP_JOURNALRECORD |
DESKTOP_READOBJECTS |
DESKTOP_SWITCHDESKTOP |
DESKTOP_WRITEOBJECTS);
if (m_hdesk == NULL){
return FALSE;
}
SetThreadDesktop(m_hdesk);
return TRUE;
}
代码有点乱,将就一下!
三、后记
上面的代码只是针对service.exe这样的进程,要想做的通用还要再加些代码。
http://hi.baidu.com/iprowq/blog/item/9ba8d655c13d3eccb745aeac.html