Объяснять сегодня, что такое бот и ботнет, вряд ли кому-нибудь нужно. Почти все информационные издания пишут об успехах антивирусных и правоохранительных органов в борьбе с этим, как им кажется, злом. Тем не менее бот — всего лишь программа, выполняющая автоматически какие-либо действия. Поэтому с тем же успехом к ботнету можно отнести, например, и 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());
}
Xakep #197. Социальная инженерия
Как два байта переслать
Используя текстовый протокол, мы будем отправлять боту 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"/>
Мы полностью рассмотрели реализацию клиента нашего проекта, теперь перейдем к серверной составляющей.
Бот
Серверную часть разнообразия ради мы напишем внезапно на паскале, а точнее, на его последней реинкарнации — Delphi XE. А почему бы и нет?
Так как пользователей нашего бота сравнительно немного, воспользуемся компонентом ServerSocket, установив свойство ServerType в stNonBlocking, то есть мы будем для простоты использовать так называемые неблокирующие сокеты (без создания многих потоков). Также закинем на форму RichEdit с именем Log для ведения журнала событий (см. рис. 2).
Для хранения хешей и паролей будем использовать класс TStringList, строки которого будут считываться (при создании формы) и записываться (при обнаружении пароля) в файл 'hash.txt'.
Вся логика работы бота через сокет заключена в процедуре ServerSocketClientRead:
procedure TForm1.ServerSocketClientRead(Sender: TObject; Socket: TCustomWinSocket);
var buf, pass : String;
begin
buf:=Socket.ReceiveText;
SetLength(buf, 32); // Убираем лишние символы
AddString(Format('%s > Получен MD5: %s', [DateTimeToStr(NOW), buf]));
pass:=getPass(buf);
if (pass <> '') then
begin
AddString(Format('%s > Пароль найден в словаре и отправлен клиенту: %s = %s', [DateTimeToStr(NOW), buf, getPass(buf)]));
Socket.SendText(pass);
end else
if bruteIsWorking then
begin
AddString(Format('%s > %s', [DateTimeToStr(NOW), 'Пароль в словаре не найден. Брут занят!']));
Socket.SendText('Dict : Password is NOT FOUND, Brute : BUSY');
end else
begin
startBruteThread(buf);
AddString(Format('%s > %s', [DateTimeToStr(NOW), 'Пароль в словаре не найден. Брут запущен...']));
Socket.SendText('Dict : Password is NOT FOUND, Brute : STARTED');
end;
Sleep(100);
Socket.Close;
end;
Функция getPass пытается найти пароль в словаре (итерация по строкам TStringList): в случае успеха результат сразу же отправляется клиенту Socket.SendText(pass), иначе за дело берется брут. Нужно сказать, что брут может быть занят в момент подключения клиента (отслеживается переменной bruteIsWorking), поэтому последнему стоит подключиться позднее. Как бы там ни было, клиенту отправляется соответствующее сообщение.
Запуск брута осуществляет процедура startBruteThread, принимающая в качестве параметра хеш-строку пароля:
procedure TForm1.startBruteThread(hash : String);
begin
if not bruteIsWorking then bruteThread:=TBruteThread.Create(hash);
end;
Сам брут работает в отдельном потоке в классе TThread:
TBruteThread = class(TThread)
private
hash : String;
pass : String;
procedure PasswordFound;
public
constructor Create(const hash_ : String);
destructor Destroy; override;
protected
procedure Execute; override;
end;
Конструктор и деструктор класса мы опустим (там все стандартно), а вот процедуру Execute рассмотрим:
procedure TBruteThread.Execute;
...
begin
FreeOnTerminate:=True;
pattern:='abcdefghijklmnopqrstuvwxyz0123456789';
len:=Length(pattern);
try
with TIdHashMessageDigest5.Create do
for i:=0 to len do
... // Еще циклы для j, k, l, m, n
begin
if Terminated then Exit;
if (i = 0) then a:='' else a:=pattern[i];
... // Аналогично для b, c, d, e, f
work:=HashStringAsHex(a + b + c + d + e + f);
if (work = hash) then
begin
pass:=a + b + c + d + e + f;
Synchronize(PasswordFound);
Exit;
end;
//Sleep(1);
end;
finally
DisposeOf;
end;
end;
Так как задача алгоритмической оптимизации у нас не стоит, осуществим перебор наиболее наглядно и просто, то есть «в лоб» с помощью циклов. Символы (на самом деле строки из одного символа) a, b, c, d, e и f постепенно перебирают все возможные символы строки шаблона pattern. Delphi из коробки поддерживает реализацию MD5 в модуле IdHashMessageDigest. Для начала необходимо создать объект TIdHashMessageDigest5, после чего функция HashStringAsHex вычислит MD5-хеш указанной строки. Полученный результат сравнивается с исходным хешем, и при равенстве считается, что пароль найден (строго говоря, это коллизия). Процедура с говорящим именем PasswordFound добавляет найденный пароль в словарь. Из-за того что мы находимся в отдельном потоке, для синхронизации с главным потоком приложения указываем явно метод Synchronize, то есть Synchronize(PasswordFound).
procedure TBruteThread.PasswordFound;
var st : String;
FName : String;
begin
st:=hash + ':' + pass;
FName:=ExtractFilePath(Paramstr(0)) + 'hash.txt';
EnterCriticalSection(CS);
try
Data.Add(st);
Data.SaveToFile(FName);
finally
LeaveCriticalSection(CS);
end;
Form1.AddString('');
Form1.AddString(Format('%s > Брут нашел пароль: %s = %s', [DateTimeToStr(NOW), hash, pass]));
end;
Так как к словарю (TStringList) доступ одновременно имеют и поток сокета соединения (при поиске по словарю), и поток брута (при добавлении нового пароля в словарь), нужно их синхронизировать. Самый простой способ этого добиться — использовать критическую секцию (CriticalSection). Критическая секция — участок кода, который в каждый момент времени может выполняться только одним из потоков. После того как первый поток вызовет EnterCriticalSection, всем другим потокам вход в этот код будет запрещен, и следующий поток, который дойдет до этой строки кода, будет остановлен до тех пор, пока первый поток не вызовет LeaveCriticalSection. Обрати внимание, мы используем критическую секцию как в функции getPass, так и в процедуре PasswordFound. Блок try-finally-end гарантирует выполнение LeaveCriticalSection. Инициализировать критическую секцию удобнее всего в методе FormCreate формы: InitializeCriticalSection(CS), а уничтожать — в FormDestroy: DeleteCriticalSection(CS).
На рис. 3 представлен результат отправки боту хеша 8A13DAB3F5EC9E22D0D1495C8C85E436. Видно, что в словаре пароль отсутствует и бот запустил перебор комбинаций (Brute : Started). Если мы попробуем снова подключится и указать тот же хеш (см. рис. 4), бот вежливо предложит подождать, сославшись на занятость (Brute : Busy), — в это время доступен только поиск пароля по словарю.
Подождав четверть часа, можно снова проверить бот и убедиться, что пароль он уже нашел — 12347 (см. рис. 5). На рис. 6 изображен этот же процесс, но уже со стороны бота.
В методе Execute есть одна закомментированная строка — Sleep(1). Процедура Sleep сообщает ОС о том, что текущий поток не нуждается в дополнительных циклах (квантах) процессора в течение миллисекунд, заданных параметром. Если раскомментировать эту строку, то после проверки очередного хеша Windows отдаст процессорные циклы другому потоку или процессу. В этом случае загрузка процессора упадет в несколько раз (у меня — до 1%), но время перебора хешей возрастет просто до неприличных значений. Ну здесь уж что главнее: незаметность или производительность — решать тебе.
Пароль длиною в жизнь
Если обратиться к Википедии по теме полного перебора методом грубой силы, то для использованного нами шаблона пароля можно найти любопытные цифры (скорость перебора — 100 000 паролей в секунду).
А какой длины пароли у тебя?
Послесловие
Сегодня мы «подружили» Windows и Андроид весьма своеобразным способом, рассмотрели использование сокетов и фоновых задач, познакомились с моделью, а также стряхнули античную пыль с Delphi, вспомнив о работе с потоками и хешами. И все это затем, чтобы ты наконец-то вспомнил свой забытый пароль. По-моему, уже настало время открывать брют...