Объяснять сегодня, что такое бот и ботнет, вряд ли кому-нибудь нужно. Почти все информационные издания пишут об успехах антивирусных и правоохранительных органов в борьбе с этим, как им кажется, злом. Тем не менее бот — всего лишь программа, выполняющая автоматически какие-либо действия. Поэтому с тем же успехом к ботнету можно отнести, например, и SETI — распределенный проект по поиску внеземных цивилизаций. Сегодня мы поддержим светлую сторону силы и соорудим небольшой бот и центр управления им на смартфоне. Наш бот будет помогать вполне земным людям восстанавливать забытые пароли, ведь как оно часто бывает: хеш есть, а пароля с ним нет :).

WARNING

Вся информация предоставлена исключительно в ознакомительных целях. Ни редакция, ни автор не несут ответственности за любой возможный вред, причиненный материалами данной статьи.
 

Клиент-сервер

С точки зрения механизма работы бот — это серверное приложение, принимающее команды, центр управления — клиент, периодически подключающийся к нему. Если бот находится за маршрутизатором (NAT), то роли, как правило, меняются и уже сам бот подключается к командному центру для обмена информацией. Условимся, что будем рассматривать бот (ботнет), работающий в одной локальной сети с командным центром, — мы же вспоминаем свои пароли, верно? Итак, наш бот в силу трудоемкости вычислений будет работать на компьютере под управлением Windows (сервер), а мы будем его нагружать и контролировать из командного центра в лагере Андроида (клиент). Для протокола обмена информацией выберем обычный текст — посылаем боту хеши, а он в ответ отбивается паролями.

 

Фрагмент

Начнем издалека: Андроид очень любит убивать активности и создавать их заново — стоит хотя бы изменить ориентацию экрана, как твоя активность отправится в мир иной, а на ее месте будет создана новая. Что интересно, все поля для ввода текста автоматически восстановят введенную информацию, тогда как большинство других компонентов окажутся девственно чисты. То есть активность обязана быть готовой в любой момент восстановить состояние своих компонентов. Это вызывает определенные трудности у осваивающих азы программирования под данную ОС, хотя, случается, и популярные приложения не совсем адекватно реагируют на поворот экрана.

В Android 3 появился новый компонент — фрагмент (Fragment), позволяющий разделить активности на независимые компоненты, каждый из которых имеет свой жизненный цикл и пользовательский интерфейс. Такая «фрагментация» активности существенно упрощает разработку GUI под разные форм-факторы устройств, но нас все это интересует по другой причине. С помощью метода setRetainInstance мы можем сделать так, чтобы фрагмент сохранял свой экземпляр между перезапусками родительской активности. Кроме того, фрагмент не обязан иметь визуальные компоненты. Все это делает его идеальным местом для хранения модели взаимодействия центра с ботом в фоновом режиме. Создадим такой класс-фрагмент (ModelFragment.java):

public class ModelFragment extends Fragment {
    private final Model mModel;
    public ModelFragment() { mModel = new Model(); }
    @Override
    public void onCreate(final Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setRetainInstance(true);
    }
    public Model getModel() { return mModel; }
}

Данный код создает фрагмент, в котором мы будем хранить модель работы с ботом (обрати внимание, Model создается в конструкторе фрагмента): соединение по сети, отправка и прием данных, обработка ошибок и прочее. Как видишь, фрагмент не зависит от активности, иначе говоря, бизнес-логика отделена от пользовательского интерфейса.

 

Витрина

Главная активность нашего проекта (Main.java) уже по доброй традиции будет состоять из проверенных временем хакерских компонентов — полей ввода, кнопки и анимированного прогресс-бара (см. рис. 1). В методе onCreate нам необходимо найти наш фрагмент (а если он не существует — создать новый) и получить из него ссылку на модель:

private static final String TAG_MODEL_FRAGMENT = "TAG_MODEL_FRAGMENT";
private Model mModel;
...
final ModelFragment retainedModelFragment = (ModelFragment) getFragmentManager().findFragmentByTag(TAG_MODEL_FRAGMENT);
if (retainedModelFragment != null) mModel = retainedModelFragment.getModel();
else {
    final ModelFragment tempFragment = new ModelFragment();
    getFragmentManager().beginTransaction()
        .add(tempFragment, TAG_MODEL_FRAGMENT)
        .commit();
    mModel = tempFragment.getModel();
}
mModel.registerObserver(this);

Теперь, сколько бы раз ни была пересоздана активность, mModel будет всегда содержать ссылку на одну и ту же модель (метод getModel). Метод registerObserver подписывает нашу активность на обработку различных событий модели. Например, когда происходит нажатие на кнопку отправки хеша и стартует новая задача, модель уведомляет подписчика (собственно, их может быть и несколько) об этом, вызывая зарегистрированный метод onTaskStarted:

@Override
public void onTaskStarted(Model mModel) {
    ed2.setText("");
    setupGUI(false);
}

Здесь setupGUI манипулирует с доступностью (Enabled) полей ввода и кнопки, а также видимостью прогресс-бара. Таким образом, если во время работы с ботом активность будет уничтожена (не путать с приложением!), новая активность оперативно получит состояние модели и с помощью setupGUI правильно настроит свои компоненты. Стоит отметить, что для регистрации в качестве подписчика активность должна реализовывать интерфейс Observer, описанный далее в модели:

public class Main extends ActionBarActivity implements Model.Observer {...}

Мы подробнее поговорим об этом, когда будем рассматривать модель. Упомянутый обработчик нажатия кнопки выглядит неприлично просто:

public void bSend_click(View v){
    mModel.startTask(SERVER_IP, SERVER_PORT, ed1.getText().toString());
}
Рис. 1. Рендер интерфейса в Android Studio
Рис. 1. Рендер интерфейса в Android Studio
 

Как два байта переслать

Используя текстовый протокол, мы будем отправлять боту MD5-хеши паролей для расшифровки. Этот процесс можно разделить на два: подбор пароля по словарю и перебор пароля методом грубой силы, то есть brute force. Наш бот будет использовать словарь в виде текстового файла, каждая строка которого представляет собой сочетание хеша и пароля после двоеточия:

MD5_HASH:PASSWORD

Так что, если у тебя уже есть боевые словари, можешь их без проблем использовать в работе. Если пароля в словаре нет, бот запустит брут, или, выражаясь литературно, удаленная программа инициирует перебор всех возможных сочетаний вариантов парольной строки по шаблону. Брут будет работать во вторичном потоке, что позволит подключаться к боту в любой момент, но в случае активного перебора только для поиска пароля по словарю. Забегая вперед, скажу, что один брут-поток использует порядка 13% процессорного времени на четырехъядерном камне Intel (в конце мы сможем уменьшить этот показатель). Конечно, можно создать несколько потоков, но тогда это будет уже не бот, а какой-то баян. Когда (если вообще) пароль будет найден, бот бережно запишет его в словарь и освободится для следующего перебора. В качестве шаблона пароля выберем незамысловатую строку:

pattern:='abcdefghijklmnopqrstuvwxyz0123456789'

а сам пароль ограничим шестью символами (полный перебор займет максимум пару-тройку часов).

 

Модель же!

Каркас модели в сокращенном виде представлен ниже:

public class Model {
    private boolean mIsWorking = false; // Задача запущена?
    private Task mTask; // Ссылка на задачу
    private String mResponse; // Ответ сервера
    public String getResponse() { return mResponse; }
    public void startTask(String SERVER_IP, int SERVER_PORT, String DATA_TO_SEND) {
        if (mIsWorking) return;
        mObservable.notifyTaskStarted();
        mIsWorking = true;
        mResponse = "";
        mTask = new Task(SERVER_IP, SERVER_PORT, DATA_TO_SEND);
        mTask.execute();
    }
    class Task extends AsyncTask<Void, Void, String> {
        @Override
        protected String doInBackground(final Void... params) { }
        @Override
        protected void onPostExecute(final String res) {
            mIsWorking = false;
        }
    }
}

StartTask запускает новую асинхронную задачу (класс Task), передавая IP-адрес сервера (SERVER_IP), порт для подключения (SERVER_PORT) и хеш для расшифровки (DATA_TO_SEND). Используемый AsyncTask хорошо подходит для непродолжительных фоновых операций, результаты которых должны быть отражены в пользовательском интерфейсе. Однако при пересоздании активности эти операции не сохраняются (задача отменяется) — вот еще одна причина, по которой мы вынесли весь функционал в отделенную от активности модель.

Апгрейд модели

Рассматриваемая «Модель» (частная реализация шаблона MVC) всем хороша и удобна, но может возникнуть ситуация, когда ты захочешь, например, вместо сокетов использовать HTTP, FTP или даже IMAP. В этом случае без существенной модификации кода модели не обойтись, что сильно повышает риск возникновения ошибок в уже отлаженном коде, да и просто огорчает блюстителей перфекционизма (ведь листинг должен быть красивым, не правда ли?). Поэтому код сетевого взаимодействия с ботом желательно вынести в отдельный класс для каждого из используемых протоколов. Это твое новое домашнее задание.

Для реализации AsyncTask необходимо указать по порядку: тип входных данных (мы передаем данные через конструктор, поэтому указываем Void), тип данных для отображения хода выполнения операции (не используем, Void), тип итоговых значений (ответ сервера, тип String). Обработчик doInBackground выполняется в фоновом потоке, и именно здесь мы разместим код работы с сервером. Когда doInBackground завершит работу, конечный результат, то есть ответ бота, вернется в качестве параметра для обработчика onPostExecute. Кстати, последний при вызове синхронизируется с потоком GUI, поэтому внутри него можно безопасно работать с элементами интерфейса (в нашем варианте модель оповестит подписчика (активность Main) соответствующим событием, а он уже сам обновит компоненты GUI).

В основе сетевой поддержки Java лежит концепция сокета (Socket), идентифицирующего конечную точку сети. В нашем случае такой точкой (сервером) будет бот, «слушающий» определенный порт (скажем, номер 7001) до тех пор, пока клиент не соединится с ним. Для создания сокета на стороне клиента напишем функцию:

private Socket socket = null; // Ссылка на сокет
...
private void openSocket() {
    try {
        InetAddress serverAddr = InetAddress.getByName(IP);
        socket = new Socket(serverAddr, PORT);
    } catch (IOException e) {
        e.printStackTrace();
    }
}

Класс InetAddress используется для инкапсуляции как числового IP-адреса сервера, так и его доменного имени. Для локальной сети проще использовать IP. Парная функция закрытия сокета:

private void closeSocket(){
    if (socket != null)
    try {
        socket.close();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

Теперь рассмотрим реализацию doInBackground:

 @Override
 protected String doInBackground(final Void... params) {
     String res = null; // Ответ сервера
     openSocket(); // Открываем Socket
     if (socket == null || !socket.isConnected()) {
         closeSocket();
         return CONNECTION_FAILED;
     }
     // Отправляем запрос
     try {
         PrintWriter out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())), true);
         out.println(DATA);
     } catch (Exception e) {
         e.printStackTrace();
         closeSocket();
         return SENDING_FAILED;
     }
     // Ждем ответа
     try {
         BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
         res = in.readLine();
     } catch (IOException e) {
         e.printStackTrace();
         closeSocket();
         return RECEIVING_FAILED;
     }
     closeSocket(); // Закрываем Socket
     return res;
 }

Для отправки строки используется символьный класс PrintWriter, определяющий выходной поток (в этом смысле запись строки в сокет аналогична выводу System.out). В качестве первого параметра используется абстрактный класс выходного байтового потока — OutputStream, в качестве второго — булево значение, управляющее сбросом буфера при вызове метода println. Для реализации OutputStream возьмем класс BufferedWriter, который буферизует вывод, тем самым увеличивая производительность работы. Метод socket.getOutputStream возвращает выходной байтовый поток сокета, но так как мы передаем строку, то перед отправкой в сокет ее необходимо преобразовывать в байтовый массив, чем и занимается объект OutputStreamWriter. Данная «матрешка» объектов в итоге позволит нам отправить строку в сокет простым вызовом метода println.

Прием строки аналогичен отправке, только вместо выходных потоков используются входные, а результат после readLine помещается в переменную res.

Нетрудно заметить, что данный обработчик вернет либо ответ бота (res), либо одну из ошибок — строковую константу (CONNECTION_FAILED, SENDING_FAILED или RECEIVING_FAILED).

После получения ответа или ошибки, как уже отмечалось, вызывается обработчик onPostExecute:

@Override
protected void onPostExecute(final String res) {
    mIsWorking = false;
    if (res != null)
        if (res.equalsIgnoreCase(CONNECTION_FAILED)) mObservable.notifyConnectionFailed();
    else
    if (res.equalsIgnoreCase(SENDING_FAILED)) mObservable.notifySendingFailed();
    else
    if (res.equalsIgnoreCase(RECEIVING_FAILED)) mObservable.notifyReceivingFailed();
    else {
        mResponse = res;
        mObservable.notifyTaskCompleted();
    }
}

Цель этого кода — оповестить подписчиков модели об итогах запроса к боту, а в случае обнаружения пароля или иного ответа сервера — записать его во внутреннее поле mResponse. Так, вызов метода mObservable.notifyConnectionFailed() в модели приведет к срабатыванию обработчика onConnectionFailed(Model mModel) в активности, mObservable.notifyTaskCompleted() соответствует onTaskCompleted(Model mModel) и так далее. Чтобы все это работало, необходимо описать интерфейс взаимодействия и класс для его реализации:

public interface Observer {
    void onTaskStarted(Model mModel);
    void onTaskCompleted(Model mModel);
    ....
}
private class ModelObservable extends Observable<Observer> {
    public void notifyTaskStarted() {
        for (final Observer observer : mObservers) observer.onTaskStarted(Model.this);
    }
    public void notifyTaskCompleted() {
        for (final Observer observer : mObservers) observer.onTaskCompleted(Model.this);
    }
    ...
}

Все обработчики уведомлений модели регистрируются абсолютно одинаково, поэтому часть из них опущена. Разумеется, готовый исходник всегда к твоим услугам. Цикл в нотификаторах предусматривает наличие нескольких подписчиков у модели.

Упомянутый ранее метод registerObserver выглядит следующим образом:

private final ModelObservable mObservable = new ModelObservable();
...
public void registerObserver(final Observer observer) {
    mObservable.registerObserver(observer);
    if (mIsWorking) mObservable.notifyTaskStarted();
}

После регистрации подписчика мы сразу же сообщаем ему о запущенной задаче, если такая имеется, то есть активность всегда будет в курсе того, что делает модель в данный момент.

Нам осталось только упомянуть о реализации обработчика сообщения от бота onTaskCompleted:

@Override
public void onTaskCompleted(Model mModel) {
    ed2.setText(mModel.getResponse());
    setupGUI(true);
}

Здесь геттер модели getResponse() отображается в строке с результатом, после чего setupGUI подготавливает интерфейс активности для следующего запроса.

LAN

Для работы в локальной сети необходимо запросить в манифесте приложения (AndroidManifest.xml) те же разрешения, что и для доступа в интернет:

<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>

Мы полностью рассмотрели реализацию клиента нашего проекта, теперь перейдем к серверной составляющей.

 

Бот

Продолжение статьи доступно только подписчикам

Вариант 1. Оформи подписку на «Хакер», чтобы читать все статьи на сайте

Подписка позволит тебе в течение указанного срока читать ВСЕ платные материалы сайта, включая эту статью. Мы принимаем оплату банковскими картами, электронными деньгами и переводами со счетов мобильных операторов. Подробнее о подписке

Вариант 2. Купи одну статью

Заинтересовала статья, но нет возможности оплатить подписку? Тогда этот вариант для тебя! Обрати внимание: этот способ покупки доступен только для статей, опубликованных более двух месяцев назад.


Комментарии

Подпишитесь на ][, чтобы участвовать в обсуждении

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

Check Also

Realm: к концу 2018 года Kotlin потеснит Java и станет основным языком для разработки Android-приложений

Специалисты компании Realm подсчитали, что в скором будущем молодой язык программирования …