Издательский дом ООО "Гейм Лэнд"СПЕЦВЫПУСК ЖУРНАЛА ХАКЕР #74, ЯНВАРЬ 2007 г.

робот-антимусорщик

_4EPEN (XDIMAN@MAIL.RU)

Хакер, номер #074, стр. 032


ПИШЕМ СПАМ-БОТА С УПРАВЛЕНИЕМ ЧЕРЕЗ IRC И POP3

РАССЫЛКОЙ СООБЩЕНИЙ ПО ЭЛЕКТРОННОЙ ПОЧТЕ ВРУЧНУЮ СЕЙЧАС НЕ ЗАНИМАЕТСЯ НИ ОДИН СПАМЕР. ДЛЯ ЭТОГО ИСПОЛЬЗУЮТСЯ ПРОГРАММЫ, КОТОРЫЕ ОБЫЧНО ПИШУТСЯ НА ЗАКАЗ И НЕ ДОСТУПНЫ ШИРОКОМУ КРУГУ ЮЗЕРОВ. Я РЕШИЛ СОЗДАТЬ СВОЕГО СПАМ-БОТА, УПРАВЛЯЕМОГО С ПОМОЩЬЮ IRC ИЛИ POP3, СПОСОБНОГО РАССЫЛАТЬ СООБЩЕНИЯ В МНОГОПОТОЧНОМ РЕЖИМЕ И РАБОТАТЬ ЧЕРЕЗ ПРОКСИ

[основные возможности.]

Сперва необходимо четко определить возможности и принцип работы программы. Спам-бот имеет два основных файла: файл со списком емейлов и файл с сообщением, которое необходимо рассылать. Каждое сообщение отправляется на определенный емейл в отдельном потоке. Для управления спам-ботом нам потребуется небольшой набор команд: добавление адреса электронной почты, запуск рассылки сообщений, остановка всех потоков и выход из программы. Для удобства я предлагаю два способа администрирования: с помощью специальных команд в IRC или POP3. В первом случае программа принимает сообщения в приват и обрабатывает их как команды, в последнем же случае через определенный интервал времени бот должен проверять отдельный почтовый ящик и просматривать сообщение на наличие команд. Естественно, должна быть предусмотрена аутентификация: я реализовал аутентификацию по одному паролю. Также (для анонимности) следует сделать возможной отправку сообщений через прокси, например через SOCKS5. Ну и напоследок, должны присутствовать атрибуты любой спамерской программы - специальные макросы в теле письма, которые при отправке будут автоматически заменены конкретными значениями, как то: текущая дата, адрес получателя, адрес отправителя и так далее. После того, как описаны возможности, следует определиться с опциями бота.

[общие опции спам-бота.]

Наш спам-бот будет иметь несколько обязательных опций: sender - e-mail адрес отправителя; send_threads_cnt - количество потоков для рассылки сообщений; password - пароль для управления через IRC или POP3; send_emails_file - файл со списком e-mail адресов, каждый адрес записан в отдельной строке; send_message_file - файл с сообщением для отправки; oper - тип администрирования программы – 0 (немедленный старт), 1 (управление через IRC), 2 (управление через POP3). Следующие опции зависят от типа управления и являются необязательными в общем случае.

[опции управления через IRC.]

В случае управления через IRC для начала необходимо указать сервер IRC-сети и порт. За это отвечают опции irc_server и irc_port :). Далее идут специфические для IRC параметры - irc_username, irc_nickname и irc_channel. Первые два необходимы для регистрации бота в сети, последний - канал, на котором бот будет ожидать выполнения команды. Программа заходит на канал для удобства отправки команд сразу всем ботам в том случае, если у тебя их несколько. Можно описать еще две опции, которые я не встроил в программу, но которые так же могут быть полезны - пароль на сервер и пароль на канал, чтобы никто другой не мог воспользоваться твоим спам-ботнетом :)

[опции управления через POP3.]

Идея управления ботом через POP3 заключается в следующем: через определенный интервал спам-бот проверяет заданный почтовый ящик на наличие новых писем, а в случае обнаружения считывает тело письма и просматривает его на наличие команд. Если команды обнаружены и пароль совпадает, бот выполняет их. После выполнения программа удаляет письмо. Опции pop_server, pop_username и pop_password отвечают за адрес мейл-сервера, логин и пароль соответственно. Для расширения функциональности еще можно задавать в опциях интервал проверки и порт, но для этого должны быть предусмотрены умолчания.

[опции прокси.]

В случае использования прокси нам потребуются три опции: версия SOCKS-протокола, адрес прокси-сервера и порт. Хочу заметить, что заставить почту работать через HTTP proxy несколько проблематично, вследствие этого благоразумно отправлять письма через SOCKS4- или SOCKS5-прокси. В данной статье я опишу только протокол SOCKS5, но ты с легкостью сможешь реализовать взаимодействие спам-бота и с 4 версией протокола. Для увеличения функциональности можно передавать опцией не сервер и порт, а файл со списком прокси, записанный в виде proxy:port.

[получение «длинных» опций]

Обычно, после описания опций программы, я приступаю к созданию либо парсера конфигурационного файла, либо парсера опций командной строки. В данном случае мы поступим вторым образом, а именно - будем использовать замечательную функцию getopt_long, позволяющую парсить «длинные опции». Прототип этой функции описан в файле getopt.h и имеет следующий вид:

int getopt_long(int, char *const[] , const char*, const struct option*, int*);

Первые две опции мы передаем из функции main() - это argc и argv, количество аргументов и сами аргументы командной строки соответственно. Следующую опцию мы пока пропустим, а я расскажу о предпоследнем параметре. Как ты уже заметил, этот параметр структурного типа struct option. Этот тип описан там же, в getopt.h, и имеет следующий формат:

struct option

{

const char *name;

int has_arg;

int *flag;

int val;

};

Поясню каждый элемент структуры. Первый элемент - const char* name - это сама «длинная» опция. Напомню, «длинные» опции, принимающие параметр, в командной строке указываются следующим образом: --parameter[=arg], а короткие - -p arg. Мы же должны описать массив типа данной структуры и первым элементом указать требуемую «длинную» опцию. Опции, помимо явного задания какого-либо параметра, могут включать-выключать какой-либо режим, например опция verbose включает режим подробного вывода сообщений в лог и не принимает аргумента. Чтобы определить, принимает опция аргумент или нет, предусмотрен следующий элемент структуры - has_arg. Он может принимать следующие значения - no_argument (опция не принимает аргумент), required_argument (аргумент обязателен), optional_argument (аргумент необязателен). Элемент flag - переменная для записи результата работы функции. Если она равна NULL, функция возвращает значение с помощью оператора return. И, наконец, последний элемент: в нашем случае это символ для «короткого» задания опции. Теперь самое время создать массив типа данной структуры и заполнить его нужными значениями для обработки всех опций. И еще один важдый момент - в последнем элементе массива все элементы структуры должны равняться нулю или NULL. Теперь настало время рассказать про третий аргумент функции getopt_long. Это строка, составленная из символов - коротких опций и двоеточий. Если после опции идет ее аргумент, то после этого символа ставится двоеточие, в противном случае - идет символ следующей короткой опции. Последний аргумент - option_index - нам не пригодится, о его назначении можешь прочитать, введя man getopt. Функция getopt_long будет возвращать один из символов - коротких опций или -1 в случае завершения парсинга командной строки. Для удобства обработки рекомендуется использовать оператор switch. Еще один важный момент - указатель на считанный аргумент для обработанной опции содержится в переменной optarg, а если обнаружена неизвестная опция, то функция возвращает знак вопроса. После обработки опций подобным образом обязательно должна быть проверка, определяющая, входят ли заданные целочисленные значения в определенный интервал, заданы ли значения для необходимых опций и т.д. На этом обработку опций можно завершить, и переходить к дальнейшей разработке.

[реализация многопоточности.]

Для ускорения работы я решил сделать наш спам-бот многопоточным. Для этого существует несколько решений. Первое из них - создавать потоки с помощью fork(). В данном случае дочерний поток является независимым процессом, продолжающим работать даже после завершения родителя. Память в таком случае не разделяется, что затрудняет сбор результатов от всех потоков. Для межпроцессного взаимодействия используется функция pipe(), но я считаю данный способ неподходящим к нашей задаче. Следующий способ, опробованный мной - системный вызов clone() с параметром CLONE_VM. Этот параметр отвечает за разделение памяти между родительским процессом и потомком, за исключением того, что необходимо самому выделить пространство для стека потока. Этот способ весьма удобен, но у меня по какой-то странной причине память не хотела разделяться, и я предпочел другой вариант :). Другой вариант, который рекомендуют использовать в большинстве многопоточных программ - это POSIX Threads. Это набор своеобразных функций-врапперов, скрывающих от тебя низкоуровневую реализацию с вызовом clone(). В нашем случае для удобства работы я определил для всех потоков структуру thread_info, имеющую следующий вид:

Структура информации о потоке

struct thread_info

{

int id; /* num of struct */

int result; /* result of exec */

int status; /* thread status */

pthread_t thread; /* thread struct */

char email[20]; /* email to send */

char *host; /* host to connect */

char *sender; /* message sender */

char *msg; /* message */

};

struct thread_info threads[MAX_THREADS]; /* all threads */

В i-ом элементе массива threads содержится информация для i-го потока. Расскажу подробнее об элементах данной структуры. Первый элемент - id - это номер текущего потока, в следующий - result - помечается результат выполнения. О назначении элемента status можно догадаться - он обозначает статус потока, THREAD_RUNNING или THREAD_STOPPED. Элемент thread - своеобразный pthread-дескриптор данного потока, а следующие элементы содержат задание, выполняемое потоком - адрес электронной почты, адрес мейл-сервера, адрес отправителя и само сообщение. В следующем фрагменте кода показано распределение заданий между потоками:

Распределение заданий между потоками

for(i=0;i<send_threads_cnt;i++)

{

if(i == send_threads_cnt - 1 &&

threads[i].status!=THREAD_STOPPED) { i = 0; continue; }

if(threads[i].status != THREAD_STOPPED) continue;

free_thread_info(i);

bzero(threads[i].email, 20);

fgets(threads[i].email, 20, fp);

if(!strcmp(threads[i].email, "")) break;

threads[i].host = mx_query(threads[i].email);

if(!threads[i].host) continue;

asprintf(&(threads[i].msg), "%s", message);

asprintf(&(threads[i].sender), "%s", sender);

threads[i].status = THREAD_RUNNING;

pthread_create(&(threads[i].thread), NULL, send_email, &threads[i]);

}

В данном цикле мы ищем те потоки, которые изменили свой статус на THREAD_STOPPED. Когда найден один такой поток, происходит освобождение памяти, занятой информацией о задании, и заполнение структуры новым заданием. Из файла считывается новый адрес электронной почты, определяется IP-адрес почтового сервера, записываются сообщение и адрес отправителя. После этого поток помечается как THREAD_RUNNING, и с помощью функции pthread_create происходит создание нового потока. Первый аргумент этой функции - структура-дескриптор потока, следующая - атрибуты (необязательна), далее - функция, которая будет исполняться в потоке, и, наконец, аргумент этой функции - мы передаем относящийся к данному потоку элемент массива threads. Поток в данном случае зависим от родителя, пока не вызвана функция pthread_detach, и ожидать его завершения все же лучше функцией pthread_join. После того, как считаются все адреса электронной почты, будет вызвана как раз эта функция для ожидания завершения работы всех потоков, чей статус не равен THREAD_RUNNING. Функция pthread_join имеет два аргумента - дескриптор потока и переменную, в которую будет записан результат возврата функции, работающей в потоке.

Следующая проблема на пути реализации многопоточного приложения - синхронизация. Дело в том, что в случае считывания или попытки изменения одной и той же области памяти разными потоками может возникнуть ошибка или могут быть записаны неправильные данные. Для этого в библиотеке POSIX Threads имеется специальный способ - мьютексы. Использование мьютексов дает гарантию, что данный код исполняется в единственном потоке и позволяет избежать подобных ошибок. Мьютексы имеют тип - pthread_mutex_t, и для работы с ними предусмотрено несколько функций: pthread_mutex_init - инициализация, pthread_mutex_lock - блокирование, pthread_mutex_unlock - разблокирование, pthread_mutex_destroy - уничтожение мьютекса. При изменении глобальных переменных в потоке рекомендуется использовать данный метод. А сейчас перейдем к следующей части - определение IP-адреса mail-сервера.

[DNS MX Query.]

Естественно, что трюк с отсеканием части e-mail'a после «собаки» и использованием данного адреса в качестве мейл-сервера не пройдет, равно как и не пройдет использование официального SMTP-сервера для отправки писем легальными клиентами. Для спама нам нужен адрес, который используется для пересылки писем между мейл-серверами. Данный адрес хранится в записи MX в DNS. Очевидно, что стандартные функции gethostbyname и gethostbyaddr не могут быть использованы по двум причинам: они считывают только записи типов A и PTR, и, к тому же, они имеют проблемы с работой в многопоточных приложениях. Для нашей задачи необходимо альтернативное решение - использование библиотеки resolver. Данная библиотека позволяет делать любые DNS-запросы, и, к тому же, не имеет проблем в работе с потоками. Нам предлагаются следующие функции:

int res_query(char *host, int class, int type, u_char *answer, size_t anslen);

int res_search(char *host, int class, int type, u_char *answer, size_t anslen);

int dn_expand(u_char *answer, u_char *endofanswer, u_char *ptr, char *buffer, int buflen);

Расскажу о них подробнее. Первые две функции делают запрос к DNS-серверу заданного класса и типа для имени host, и возвращают результат в answer. Их отличие состоит в том, что res_search обрабатывает опции, которые содержатся в элементе options структуры _res, определенной в файле arpa/nameser.h. Последняя функция «расширяет» сжатое имя и помещает результат в область памяти, указанную buffer. Чтобы сделать DNS-запрос для записи MX, нам необходимо указать C_IN в качестве класса и T_MX в качестве типа записи. Далее, с помощью dn_expand мы можем выделить значения MX-записей (к слову, их может быть несколько). В спам-боте я сделал основную функцию, которая считывает все записи, и оберточную, которая возвращает первую запись.

[протокол SMTP.]

Вот и подошли мы к самой ответственной части - отсылке сообщений по протоколу SMTP. После подключения к мейл-серверу спам-бот должен сообщить свой адрес с помощью команды EHLO:

asprintf(&buffer, "EHLO 127.0.0.1\r\n");

ret = send(sock, buffer, strlen(buffer), 0);

free(buffer);

if(ret == -1) goto error;

Далее мы дожидаемся ответа. Если первым в ответе идет число 250, значит, все нормально и можно переходить к следующей части. Хочу обратить внимание на то, что все команды, отправляемые серверу и приходящие от сервера, должны оканчиваться символами \r\n, или CR LF. Следующая команда указывает отправителя: MAIL FROM:<sender@sender.ru>. После получения положительного ответа указываем получателя: RCPT TO:<receiver@receiver.ru>. И, в конечном счете, команда DATA разрешает ввод данных. Следует заметить, что вводимое в дальнейшем сообщение завершает пара CR LF. Для завершения работы после отсылки сообщения используем команду QUIT, после чего можно разорвать соединение.

Опять же, для повышения быстродействия порекомендую тебе группировать адреса электронной почты по мейл-сервера и отправлять письма на несколько адресов в одном потоке.

[управление спам-ботом.]

Первое, что необходимо реализовать в управлении спам-ботом, будь то управление через POP3 или IRC - это аутентификация. Для простоты аутентифицироваться можно по одному паролю. Все команды, отправляемые спам-боту, имеют вид: password:command[:argument]. Пароль задается опцией командной строки при старте программы, а команды я решил сделать следующие: add добавляет адрес электронной почты в базу данных путем простого дописывания в конец файла, start запускает спам-бот на выполнение, а exit завершает работу программы. Для выделения отдельных частей в команде я применил замечательную функцию strtok(). Она имеет два аргумента - исходную строку и разделитель - и возвращает часть строки до разделителя. Если мы хотим получить часть строки от первого до второго разделителя, нам следует вызвать функцию повторно, но с NULL в качестве первого аргумента. Если же достигнут конец строки, функция strtok() возвращает NULL. Процедура admin() в моем спам-боте отвечает как раз за обработку команд. Специфические функции start_irc и start_pop с учетом взаимодействия со своим протоколом выделяют команды и передают их в процедуру admin(), которая работает так: первая часть до разделителя - символа двоеточия - сравнивается с паролем, в случае несовпадения происходит выход из процедуры; часть от первого двоеточия до второго или до конца строки сравнивается с существующими командами - в случае start выполняется процедура start(), в случае exit происходит выход из программы функцией exit(0), в случае add открывается файл со списком емейлов, считывается третий аргумент и добавляется в файл. В случае несовпадения ни с одной из перечисленных команд функция возвращает 0, в противном случае - 1. Естественно, при включенной опции verbose все изменения журналируются. Расширить функциональность ты можешь добавлением команд для изменения рассылаемого сообщения, для получения лога работы спам-бота, для остановки всех потоков без выхода из программы и так далее.

[управление с помощью IRC.]

Управление через IRC очень удобно, особенно в том случае, если у тебя не один спам-бот, а целый ботнет :). Можно отправлять команды всему ботнету одновременно, вдобавок к этому IRC имеет большую, по сравнению с POP3, скорость работы. Для защиты ботнета, как я уже говорил, можно ставить пароль на сервер или на канал. Но минусом является полная контролируемость сети IRC-опами, которым, как ни странно, бывает не все равно, что творится у них в сети. Короче говоря, приступим к реализации взаимодействия с протоколом, используя RFC 1459. Если сервер у нас запаролен, то первой командой будет команда PASS (напоминаю: в конце каждой команды здесь, по аналогии с SMTP, ставится \r\n):

PASS password

После этой довольно-таки незамысловатой аутентификации мы посылаем серверу ник нашего спам-бота:

NICK nick

Затем отправляем команду USER, которая имеет следующий формат - USER <username> <hostname> <servername> :<realname>. Параметры hostname и servername должны быть проигнорированы сервером и не имеют для нас никакого значения. Символ двоеточия перед realname означает, что в realname могут содержаться пробелы, но значение данного параметра также не принципиально. В принципе, подключение к серверу на этом завершено. Далее нам следует обработать два типа сообщений, приходящих от сервера. В случае неактивности клиента сервер периодически отправляет ему сообщения вида PING :<text>. Наша задача - ответить серверу командной PONG с той же строкой. Для этого во всех входящих сообщениях с помощью strtok() выделяем первую часть до пробела, сравниваем ее с PING и отправляем серверу PONG :). Если же команда PONG не приходит, сервер автоматически отключает клиента. Следующий тип команд - сообщения от других клиентов, приходящих на канал или присылаемых в приват боту. Из них-то мы и должны выделить команду, которая пойдет на вход функции admin(). Первым в таких сообщениях идет имя пользователя с предваряющим двоеточием, а за ним - команда и ее параметры: NICK, в случае если пользователь сменил ник, PRIVMSG - если пользователь отправил сообщение и так далее. Вот его-то нам и нужно обработать. После PRIVMSG идет имя пользователя или канал, на который отправлено сообщение, и уж затем, снова с двоеточием в начале, идет само сообщение. Обрабатывается все это так же функцией strtok, нужно только не забывать проверять ее результат на NULL, чтобы не вызвать ошибку сегментации. На этом я завершаю описание данного протокола. Добавлю только, что для выхода используется команда QUIT :<message>.

[управление через POP3.]

Идея управления через POP3 основана на следующем: через заданный интервал проверяется указанный почтовый ящик, считывается количество писем, считывается последнее письмо и из него выделяются необходимые команды, причем команд в одном письме может быть несколько. Авторизация в IRC очень проста - она делается двумя командами USER user и PASS pass, которые, как обычно, заканчиваются символами \r\n. Ответы сервера всегда начинаются со строки +OK или -ERR, что означает успешное выполнение команды или ошибку соответственно. Следующим шагом будет получение количества сообщений. Это делается командой STAT без параметров, и в результате сервер возвращает +OK, а через пробел количество сообщений и размер сообщений в октетах. Количество сообщений мы запоминаем как номер последнего сообщения и получаем его командой RETR N, где N - номер сообщения. Хочу заметить, что в конце сообщения стоит точка в одной строке. Далее стоит решить, где передавать команды. Передавать команды в теме письма неудобно, так как она кодируется особым алгоритмом большинством клиентов при отправке. Я реализовал это следующим образом - в письмо добавляются строчки вида spambot:command, что позволяет выделить команду при помощи все того же strtok. После обработки письма удалим его - DELE <номер письма> и завершим работу командой QUIT. Все - теперь можно ждать какое-то время функцией sleep() и повторять вышеуказанные команды заново.

[взаимодействие с SOCKS5-прокси.]

Прокси типа SOCKS очень удобен. Нам достаточно подключиться, обменяться некоторыми начальными данными и работать так же, как и напрямую с сервером. Для начала пошлем прокси буфер из трех байтов. Первый байт содержит цифру 5 - версию протокола, следующий байт - единицу (длину опций), и последний - 0, как опция, указывающая на отсутствие авторизации. После этого получаем двухбайтовый ответ и анализируем второй байт: если он не равен нулю, произошла ошибка, и мы выходим из процедуры. Далее отправляем десятибайтовую команду. Первый байт(5) - версия протокола, второй байт определяет команду - в нашем случае 1 (connect). Следующий байт зарезервирован, и мы оставляем его равным нулю, а в идущие затем четыре байта копируем IP-адрес. Ну и, наконец, в последние два байта запишем порт, соответственно 0 и 25. Далее получаем 10-байтовый ответ и аналогично анализируем второй байт. Если он равен нулю, то подключение установлено, и можно начинать рассылку спама. Данная возможность отсутствует в программе - предлагаю тебе реализовать ее самому.

[макросы в теле письма.]

Наиболее часто используемые макросы в теле письма - это адрес отправителя, адрес получателя и текущая дата. Обозначим их следующим образом - %sender, %receiver и зte. При передаче сообщения в поток для отправки его необходимо преобразовать, заменив данные макросы на реальные значения. Приведу фрагмент кода, делающего эту работу:

for(ptr=buffer;*ptr!='\0';ptr++)

{

if(!strncmp(ptr, "зte", 5)) { strcat(result, date()); ptr += 4; }

else if(!strncmp(ptr, "%sender", 7)) { strcat(result, sender); ptr += 6; }

else if(!strncmp(ptr, "%receiver", 9)) { strcat(result, email); ptr += 8; }

else strncat(result, ptr, 1);

}

Количество макросов очень легко расширяемо, и в такой реализации нет ничего сложного. Можно результат расширять динамически с помощью realloc(). Данную возможность я предоставлю тебе реализовать самому.

[заключение.]

Результатом моей работы явилась небольшая программа, предназначенная для массовой рассылки сообщений. Она обладает многими полезными функциями, такими как многопоточность, управление через IRC и POP3. Но все же она далеко не совершенна, и в этой статье я дал тебе много советов по поводу того, как улучшить функциональность бота. Удачного кодинга!

http://www.opennet.ru

большая подборка документации, в том числе и по кодингу

http://www.woodmann.com/fravia/DNS.htm

статья Lord Soth про получение MX-записи из DNS

http://www.rfcs.org

стандарты интернет RFC, здесь ты можешь найти подробную инфу о взаимодействии с протоколами POP3, SMTP и IRC

http://www.codenet.ru

подборка хорошей документации по кодингу, в том числе и по сетевому

WARNING!

ВСЯ ИНФОРМАЦИЯ ИЗ ЭТОЙ СТАТЬИ ПРИВЕДЕНА ИСКЛЮЧИТЕЛЬНО В ОБРАЗОВАТЕЛЬНЫХ ЦЕЛЯХ! ИСПОЛЬЗУЯ ЕЕ НА ПРАКТИКЕ, ТЫ НАРУШИШЬ ЗАКОН И ПОДВЕРГНЕШЬ СЕБЯ ГЛУБОКОМУ ДИЗРЕСПЕКТУ СО СТОРОНЫ ВСЕХ ИНТЕРНЕТЧИКОВ!

Содержание