[+]-----------------------------------------------[+]
| Я тебя вижу |
| |
| Создаем компактный видеошпион с использованием |
| GDI+ и WINSOCK2 API |
| |
| Автор: xh4ck |
| (vol_e@mail.ru) |
| _ _ _ _ _ __ _ _ _ |
| | || |___| | | |/ /_ _ (_)__ _| |_| |_ ___ |
| | __ / -_) | | ' <| ' \| / _` | ' \ _(_-< |
| |_||_\___|_|_|_|\_\_||_|_\__, |_||_\__/__/ |
| |___/ |
| |
[+]-----------------------------------------------[+]


[Вступление]

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

У меня есть знакомый, который коллекционирует у себя на компьютере вирусы (да, вот такое тупое занятие) и любит делать
гадость окружающим, но сейчас речь не об этом и я не буду объяснять, что такое хорошо, а что такое плохо. Однажды этот
знакомый узнал, что я занимаюсь хмм... программированием и спросил, могу ли я написать троян, основной функцией которого
было бы шпионить за происходящим на экране. Я сначала решил, что нефиг мне заниматься ерундой и тратить свое время, но
мой знакомый очень уж убедил меня и пообещал поставить n-ное количество пива за это творение ;) А почему бы и нет?
Я давно хотел разобраться с этой темой, да и пиво тоже никогда не бывает лишним. В общем, понеслась...

[Скальпель, зажим, еще зажим, огурец ;)]

Писать совершенно новый троян я не захотел (даа, лень - двигатель прогресса), а вот доработать уже ранее написанный было
бы неплохо, чем я и занялся. Писать я решил на С++ с использованием лишь АПИ (никаких вам TBitmap, TJPEGImage, TidSMTP и
прочих чисто дельфийских штучек, так что отдыхайте ;)). Весь троян уже был написан на этом языке, да и при таком раскладе
не было соблазна зафигачить какой-нибудь TJPEGImage и не заморачиваться, скажем, на сжатии картинки ;) Я добавил к списку
поддерживаемых команд еще одну и при получении этой команды из сети шпион снимал скриншот десктопа, сжимал его в JPEG/PNG
и отправлял обратно в сеть. (Прим. автора: хоть я здесь и описывал, как я расширял функциональность трояна, но сейчас мы
сделаем отдельный самостоятельный шпион)
Графического интерфейса у шпиона не будет (кто тут сказал, что форму можно сделать скрытой? выйти из зала!), а из
заголовочных файлов остаются только winsock2.h (сеть) и ole2.h (для одной единственной функции создания потока данных),
 а также gdiplus.h (здесь используется лишь для сжатия изображения). О последнем файле речь пойдет ниже.
Краткий алгоритм работы шпиона:
1. Слушаем порт.
2. К нам подключились, принимаем команду.
3. Снимаем скриншот средствами GDI
4. При помощи GDI+ упаковываем скриншот в нужный тебе формат.
5. Отправляем пожатое изображение и переходим к п.1
Каждый шаг подробно описан в данной статье, а сама статья логически разбита на две части: снимок экрана и его упаковка,
 работа с сетью. Начнем с теории о сжатии картинки.
<У C++ плюсы уже есть, а как же с GDI?>
Трезво поразмыслив и оценив ситуацию, я пришел к выводу, что мне придется использовать какую-нибудь внешнюю графическую
библиотеку. В micro$oft уже позаботились о нас и включили в Windows XP и .NET Server библиотеку GDI+, призванную
заменить существующий уже больше 14 лет интерфейс GDI, являющийся графическим ядром предыдущих версий Windows.
Официальная документация скромно называет ее Class-based API, этакая оболочка, состоящая из набора классов над
библиотекой GdiPlus.dll. Основные направления GDI+: векторная графика (многочисленные векторные примитивы), растровая
графика (с поддержкой прозрачности и альфа-канала) и работа со шрифтами. GDI+ поддерживает популярные графические форматы,
как, например, JPEG или PNG. В общем, ребята из microsoft неплохо постарались (ну это, конечно, если не учитывать очень
низкую производительность и кучу багов, которые они добавили, чтобы жизнь совсем уж медом не казалась). Перечислять все
возможности GDI+ не хватит целой статьи (а они поистине безграничны), поэтому здесь я опишу только те, которые могут нам
 пригодиться в нашем нелегком деле создания шпиона.

[Война форматов или переливание из пустого в порожнее]

Кстати, если хочешь полноразмерных скриншотов с максимальным качеством и быстродействием, то тебе лучше не сжимать
картинку в JPEG/PNG, а использовать внешнюю библиотеку для упаковки данных без потери информации (LZMA, Zlib). Но у этого
способа есть и свои минусы, они хорошо упаковывают изображения окошек, но совершенно не пригодны для упаковки битмапа со
сложным изображением. Если, например юзер сидит в фотошопе или, скажем, смотрит фильм, то токда лучше использовать сжатие
с потерями (JPEG), иначе файлы будут гигантских размеров. PNG в этом случае тоже лучше не использовать, все дело в том,
что PNG - грубо говоря, ни что иное, как сжатый без потерь данных битмап (если не учитывать, что максимальная глубина
цвета у этого формата 24 бита). Но, к сожалению GDI+ будет работать все-равно медленнее, чем Zlib (ха, даже с Zlib более
5-10 скринов в секунду у тебя не получится, притом загрузка проца будет на все 100, тут нужен DirectShow, о нем,
как-нибудь в следующий раз). Итак, почему же мой выбор пал на GDI+? Да только потому, что он входит в систему Windows XP
и .Net Server (Что? Вы все еще сидите на этом дереволазе и ходячем глюкодроме - win98? Тогда мы идем к вам!) и является
стандартным средством и нам не придется таскать с собой многокилограммовые библиотеки. А формат, скорее всего, лучше
выбрать JPEG, так как размер картинки JPEG не так сильно зависит от сложности рисунка, да и привычнее как-то уже ;)
В любом случае GDI+ поддерживает множество форматов (BMP, GIF, TIFF, JPEG, Exif, ICON, PNG, WMF, EMF) и ты полностью
свободен в выборе.

Интерфейса у шпиона никакого не будет, а из заголовочных файлов остаются только winsock2.h и ole2.h, а также gdiplus.h,
если последнего у тебя нет, то поищи в нете или скачай свежую версию Platform SDK (скачать PSDK - звучит как издевка
над модемщиками;)). Для того, чтобы воспользоваться функциями GDI+, мало подключить заголовочный файл, тебе еще придется
подрубить к проекту соответствующий lib файл, который ты можешь изготовить самостоятельно из gdiplus.dll, используя
утилиту implib (или подобную, просто под моей горячей рукой оказалась именно она ;)).
#include
using namespace Gdiplus; /* как хочешь, но мне не в кайф постоянно писать Gdiplus:: */
#pragma comment(lib, "GdiPlus.lib") /* наш многострадальный lib-файл */

Работа с GDI+ начинается с вызова функции GdiplusStartup и заканчивается вызовом GdiplusShutdown.
После вызова GdiplusShutdown освобождаются все ресурсы, занятые GDI+, таким образом, значительно снижается
вероятность утечки графических ресурсов. Весь код, так или иначе связанный с GDI+ должен располагаться именно между этими
двумя функциями.

GdiplusStartupInput gdiplusStartupInput;
ULONG_PTR gdiplusToken;
GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL);

В структуре gdiplusStartupInput можно задать указатель на функцию, которая будет вызываться при возникновении ошибок,
но так как нам это не надо, а конструктор по умолчанию выполняет достаточную инициализацию, то оставляем все как есть.
При этом третий параметр можно оставить NULL. Значение, которое помещается в gdiplusToken необходимо сохранить до конца,
скормив его функции GdiplusShutdown.

GdiplusShutdown(gdiplusToken);

Теперь мы знаем, как сделать минимальное, ничего не делающее, GDI+ приложение, можно идти дальше.

[GpBitmap]

Получить скриншот в формате HBITMAP не составляет труда. Для этого используется просто GDI. int Width, Height;
HDC scrdc, memdc;
HBITMAP membit;
// Получаем HDC рабочего стола // Параметр HWND для рабочего стола всегда равен нулю.
scrdc = GetDC(0);
// Определяем разрешение экрана
Height = GetSystemMetrics(SM_CYSCREEN);
Width = GetSystemMetrics(SM_CXSCREEN);
// Создаем новый DC, идентичный десктоповскому и битмап размером с экран.
memdc = CreateCompatibleDC(scrdc);
membit = CreateCompatibleBitmap(scrdc, Width, Height);
SelectObject(memdc, membit);
// Улыбаемся... Снято!
BitBlt(memdc, 0, 0, Width, Height, scrdc, 0, 0, SRCCOPY);
Теперь у нас есть скриншот, преобразовываем его во внутренний формат GDI+ - Bitmap. Создадим этот объект.
Конструктор класса Bitmap многократно перегружен и предоставляет большое количество вариантов инициализации класса.
В моем случае лучше всего подходит вариант Bitmap(IN HBITMAP hbm, IN HPALETTE hpal). Естественно, не забываем, что мы
спользуем область видимости Gdiplus (у любителей Borland'a могут быть проблемы с перекрытием объекта GDI+ и одноименного
объекта из VCL).
Bitmap * bmp = new Bitmap(membit, (HPALETTE *) NULL);
Если вам не хочется смотреть чужие скриншоты, с разрешением 1600х1200 (а то и больше), можно получить уменьшенную копию
скрина (называется эскизом), для этого в классе Bitmap есть метод GetThumbnailImage, в параметрах которого необходимо
указать ширину и высоту уменьшенной картинки. Там, правда, можно указать еще два необязательных параметра, но в нашем
случае они не нужны, хотя для любознательных я поясню, что это за параметры. Параметры callback и callbackData используют
ся, если нужна возможность прерывания процесса создания эскиза. Т.е. в параметре callback указываем указатель на функцию,
а в callbackData данные, которые будут приходить в эту callback функцию. По умолчанию они равны NULL, и эта возможность
игнорируется.

Image * img = bmp->GetThumbnailImage(720, 540);

Все готово для сжатия битмапа и сохранения его в файл/поток. Для сохранения битмапа в классах Bitmap и Image
предусмотрен метод Save, с его помощью можно сохранить изображение в файл, либо в поток. Т.к. сохранять в файл
немного некрасиво (а, грубо говоря, это вообще по-ламерски), выбираем сохранение в поток IStream. Дабы не забивать
тебе голову премудростями COM (для работы с IStream), я использовал самый минимум COM функций (всего одну ;)) Она
создает поток на основе HGLOBAL, который является просто указателем на область памяти, выделенную функциями GlobalAlloc,
 LocalAlloc. Вся фича состоит в том, что можно не указывать этот HGLOBAL, а передать этой функции NULL и она все
сделает за нас, тогда размер выделенной памяти можно будет узнать с помощью IStream::Stat

// Создаём стрём
CreateStreamOnHGlobal(NULL, true, &stream); Прототип метода Save имеет следующий вид:
Status Save( IN IStream* stream, IN const CLSID* clsidEncoder, IN const EncoderParameters *encoderParams)
Если с первым параметром все понятно - указатель на поток, то со вторым и третьим могут возникнуть трудности.
Второй параметр - CLSID нужного нам кодека (Jpeg, PNG и т.д.), его можно получить с помощью функции GetEncoderClsid,
ее нет в API, эту функцию можно написать самому, а можно взять с Platform SDK (в microsoft уже многое за нас сделали ;)).
// Взято From Microsoft Platform SDK ;)
// Функция для получения нужного нам CLSID
// Использование: GetEncoderClsid("image/jpeg", &imgClsid);
// Список возможных значений параметра format можно получить
// С помощью функции GetImageEncoders (читай матчасть по ее использованию)
int GetEncoderClsid(const WCHAR* format, CLSID* pClsid)
{
UINT num = 0; // number of image encoders
UINT size = 0; // size of the image encoder array in bytes
ImageCodecInfo* pImageCodecInfo;
GetImageEncodersSize(&num, &size);
if(size == 0) return -1; // Failure
pImageCodecInfo = (ImageCodecInfo*) LocalAlloc(LPTR, size);
if(pImageCodecInfo == NULL) return -1; // Failure
GetImageEncoders(num, size, pImageCodecInfo);
for(UINT j = 0; j < num; ++j)
{
if(wcscmpi(pImageCodecInfo[j].MimeType, format) == 0 )
{
*pClsid = pImageCodecInfo[j].Clsid;
LocalFree(pImageCodecInfo);
return j; // Success
}
}
LocalFree(pImageCodecInfo);
return -1; // Failure
}
Третий параметр - указатель на структуру EncoderParameters, в которой нужно указать степень сжатия. Каждый формат
имеет определенные свойства, узнать какие свойства поддерживаются в GDI+ для определенного формата можно,
воспользовавшись функцией

Status GetEncoderParameterList (
IN const CLSID* clsidEncoder,
IN UINT size,
OUT EncoderParameters* buffer
)

Первым параметром у нее идет уже известный нам CLSID нужного кодека, для получения второго параметра достаточно вызвать

UINT GetEncoderParameterListSize (
IN const CLSID* clsidEncoder
)

В которой опять же используется CLSID. Эти оби функции являются членами класса Bitmap.
Приведу краткий пример использования этих двух функций.
UINT size = bitmap.GetEncoderParameterListSize(&pInfo->Clsid);
if(!size) return;
EncoderParameters* params=(EncoderParameters*)malloc(size);
bitmap.GetEncoderParameterList(&Clsid, size, params);
Также отличная программа для отображения информации по кодекам GDI+ есть на rsdn.ru, поставляемая
с исходными тестами, что поможет тебе лучше познать работу с кодеками ;)
// Настройка качества (уровня сжатия),
// у каждого формата свой перечень изменяемых свойств
// Вся настройка сводится к заполнению полей структуры EncoderParameters
EncoderParameters encoderParams;
int quality = 65;
encoderParams.Count = 1;
encoderParams.Parameter[0].Guid = EncoderQuality;
encoderParams.Parameter[0].Type = 4;
encoderParams.Parameter[0].NumberOfValues = 1;
encoderParams.Parameter[0].Value = &quality;
<Связь с внешним миром>
Раз уж решено все писать на чистом АПИ, то и сетевая часть тоже будет выполнена на АПИ - Winsock2 API. Использовать
Winsock2 API не труднее, чем кидать баттоны на форму ;) Хотя и там есть своя туева хуча особенностей.
В начало нашего шпиона добавляем следующие строчки:
#include
#pragma comment(lib, "ws2_32.lib")
Т.е. мы подключаем заголовочный файл WinSockets2 и соответствующий lib-файл. Если у тебя по каким-то причинам нет
файла ws2_32.lib, то ты также можешь изготовить его самостоятельно из ws2_32.dll. Как и в случае с GDI+ перед началом
работы с WINSOCK нужно вызвать функцию инициализации, а по окончании - функцию очистки.

WSADATA wdata;
WSAStartup(MAKEWORD(2,2), &wdata);
/* Тут работаем с сетью, организовываем прием/передачу и прочее */
WSACleanup();
Наш шпион будет выступать в роли сервера, будет прослушивать определенный порт (listen) и принимать входящие подключения
(accept). Для начала нам нужно создать сокет, сокеты бывают двух типов: блокирующие и неблокирующие. Блокирующие сокеты
полностью блокируют поток, в котором они работают, скажем, вызвал ты функцию listen и пока кто-нибудь не попытается
подключиться - ты ничего не сможешь сделать в этом потоке. В неблокирующих сокетах ты можешь поставить прослушку порта
и недожидаясь подключения продолжить работу, а узнать о том, что к серверу подключились, можно получив от системы
сообщение, либо, обработав Event. Какой тип сокетов выбрать - дело вкуса и обстоятельств. Выбор абсолютно никак не
скажется на надежности и скорости работы (это при условии, что руки растут из того места, из которого нужно). Сразу скажу,
что блокирующие сокеты использовать легче, поэтому, дабы не загромождать код, я буду использовать именно их.

// TCP-Сокет
SOCKET sck = socket(AF_INET, SOCK_STREAM, 0);
Приняв входящее подключение функцией accept, можно приступать к приему команды. Для приема данных вызываем функцию recv
и ждем прихода ;). Главное тут не наступить на очень больно бьющие грабли, связанные с тем, что ожидание прерывается,
если придет хоть один байт, а не столько, сколько ты запросил в recv. Поэтому прием лучше организовать в виде цикла,
принимая команду до контрольного символа (обычно нуль-символ).
// Принимаем текстовую информацию
// И помещаем ее в динамический строковый буфер
char * buff = (char *) LocalAlloc(LPTR, 1);
for (int i = 0;;)
{
if((recv(sck, &buff[i], 1, 0) == SOCKET_ERROR) || buff[i] == 0)
{
buff[i] = '\0';
break;
}
buff = (char *) LocalReAlloc(buff, ++i, LMEM_MOVEABLE);
}

Далее можно забабахать проверку имени/пароля/ключевого слова, а можно сразу читать команду и определять есть ли она в
списке поддерживаемых, тут работает твоя фантазия. В моем же коде делается одна единственная проверка на совпадение
принятой строки с командой и выполняется отправка клиенту скриншота. Отправив, клиент готовится к приему следующей
команды. Чтобы не усложнять жизнь клиенту, перед отправкой самого скриншота логично отправить его размер в виде
четырехбайтового числа. Клиент, приняв это число, будет точно знать, сколько данных ему предстоит принять.
Принцип работы клиента я тут приводить не буду, да и не нужно это. Создание клиента я оставляю за тобой. Думаю, что после
прочтения этой статьи у тебя хоть что-нибудь прояснится в голове, и ты станешь писать маленькие программки на чистом АПИ,
а не плодить многомегабайтных монстров, когда этого можно избежать. Если хочешь делать программки формата "микро", то
очень действенным способом является отключение RTL (или CRT, где как называется), но это уже тема отдельной статьи,
скажу лишь одно - проблем будет много, никаких стандартных функций языка, никаких new, delete и иже с ними, только АПИ,
зато полученный размер стоит затраченных на него средств - в BCB6 от 4кб, в VC6 от 3кб. Здесь я постарался дать общее
представление о GDI+ и WINSOCK API, конечно этой статьи слишком мало, чтобы действительно освоить их, но, тем не менее,
с чего-то же ведь нужно было начать? Глядишь, сегодня ты еще не разбираешься в параметрах функции setsockopt, а уже
завтра напишешь собственный Apache ;) По любым вопросам, касающимся данной темы, ты запросто можешь обратиться ко мне - я
постараюсь помочь.

[А как же с трояном]

Троян я благополучно дописал, получив при этом обещанное пивко. Кстати и по сей день, этот старичок крутится в
определенных кругах и по прежнему не ловится антивирусами ;) Дело в том, что написал я его больше года назад, и только
сейчас у меня появилась идея поведать о той интересной фиче миру. Конечно, идея уже стара как мир, но не каждый знает
такое её нестандартное воплощение. ЗЫ. Если ты всерьез заинтересовался GDI+, то для начала предлагаю проштудировать
www.rsdn.ru - там есть цикл статей, посвященных GDI+.

[Файлы к статье]

Скачать исходняки к статье

(c) Hell Knights Crew
Автор: xh4ck