屏幕监控是远控软件的基本功能之一。
现在很多远控程序的服务端通常为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这样的进程,要想做的通用还要再加些代码。