Продукты «Хакера»

  • Ежеквартальный «Хакер» #3

    Ежеквартальный «Хакер» #3

    Лучшие статьи квартала на бумаге

    2 000 ₽

    Заказать журнал
  • Годовая подписка на Xakep.ru

    Годовая подписка на Xakep.ru

    Полный доступ к статьям и архиву выпусков

    6 000 ₽ в год

    Подписаться
  • «Пентест WEB» для начинающих

    «Пентест WEB» для начинающих

    Совместный курс «Хакера» и лаборатории «Хаксет»

    7 000 ₽

    Купить курс
  • Спецвыпуск «Хакера» #4

    Спецвыпуск «Хакера» #4

    Лучшие статьи за 2021–2022 годы на бумаге

    2 000 ₽

    Заказать журнал
В этой статье раз­берем устрой­ство кро­шеч­ной фер­мы для мас­совых дей­ствий, нап­ример авто­мати­чес­кой регис­тра­ции акка­унтов. Мик­рокон­трол­лер ESP32 будет наз­начать задания смар­тфо­ну и забирать резуль­таты. Впро­чем, такая схе­ма подой­дет для самых раз­ных сце­нари­ев.

В нашей мини‑фер­ме зап­ланиро­вано три основных ком­понен­та:

  1. Скрипт на Python для обме­на дан­ными меж­ду компь­юте­ром и ESP32 через пос­ледова­тель­ный порт.
  2. ESP32 в роли оркес­тра­тора, который будет запус­кать задания на смар­тфо­нах с Android и воз­вра­щать получен­ные дан­ные на компь­ютер.
  3. Бот для Android, который будет регис­три­ровать акка­унты. Фар­мить акка­унты на реаль­ных сер­висах мы не ста­нем — толь­ко в тес­товой лабе и толь­ко для демонс­тра­ции. Тар­гетом пос­лужит OWASP Juice Shop.

warning

Статья име­ет озна­коми­тель­ный харак­тер и пред­назна­чена для спе­циалис­тов по безопас­ности, про­водя­щих тес­тирова­ние в рам­ках кон­трак­та. Автор и редак­ция не несут ответс­твен­ности за любой вред, при­чинен­ный с при­мене­нием изло­жен­ной информа­ции. Рас­простра­нение вре­донос­ных прог­рамм, наруше­ние работы сис­тем и наруше­ние тай­ны перепис­ки прес­леду­ются по закону.

Цель у такого занятия может быть впол­не мир­ная: нап­ример, тес­тирова­ние работы сер­висов. Ну и, думаю, тебе будет инте­рес­но для раз­нооб­разия покопать­ся с реаль­ным железом.

Идея в том, что­бы пос­тро­ить пол­ностью авто­ном­ную фер­му, которая не занима­ет ресур­сы компь­юте­ра. Сам скрипт неболь­шой, мик­росхе­му с кон­трол­лером мож­но запитать обыч­ной заряд­кой для телефо­на, пол­ностью исклю­чив ПК из схе­мы.

Плата разработки ESP32-S3
Пла­та раз­работ­ки ESP32-S3

Это доволь­но бюд­жетный вари­ант: отла­доч­ная пла­та сто­ит 300–600 руб­лей и про­дает­ся на любом мар­кет­плей­се или в магази­не ради­оде­талей. При этом она из короб­ки под­держи­вает Wi-Fi и Bluetooth. На пла­те обыч­но рас­паяны два пор­та USB Type-C: один — для залив­ки про­шив­ки, вто­рой — для под­клю­чения к ком­пу или бло­ку питания.

 

Тестовая машина

Для авто­мати­зации возь­мем фор­му регис­тра­ции OWASP Juice Shop. Нам надо задать количес­тво акка­унтов, а на выходе получа­ем спи­сок кред.

Ус­танов­ка и запуск:

git clone https://github.com/juice-shop/juice-shop.git --depth 1
cd juice-shop
npm install
npm start

Пе­рехо­дим на http://localhost:3000, что­бы убе­дить­ся, что все прош­ло успешно и при­ложе­ние дос­тупно.

На моей хос­товой машине уста­нов­лена Windows. Если попытать­ся под­клю­чить­ся к сай­ту по IP, фай­рвол отбро­сит соеди­нение. Надо добавить пра­вило, раз­реша­ющее под­клю­чение к нуж­ному пор­ту.

В Windows откры­ваем «Парамет­ры» («Пуск» → зна­чок шес­терен­ки). В поис­ке вво­дим «Бран­дма­уэр Защит­ника Windows». В левой час­ти окна кли­каем по пун­кту «Допол­нитель­ные парамет­ры». Выбира­ем «Пра­вила для вхо­дящих под­клю­чений» и соз­даем новое пра­вило. Тип пра­вила — «Для пор­та». Про­токол — TCP, номер пор­та — 3000. Теперь Juice Shop дос­тупен по адре­су http://<ip-хоста>:3000. Узнать свой IP-адрес мож­но с помощью коман­ды ipconfig в PowerShell.

info

Пол­ный код про­екта, вклю­чая скетч для ESP32 и про­ект Android-при­ложе­ния, ищи в моем ка­нале и на GitHub. Кода мно­го, в том чис­ле вто­рос­тепен­ного и метаре­сур­сов, поэто­му в статье я при­вожу толь­ко клю­чевые момен­ты.

 

Скетч для ESP32

Для раз­работ­ки удоб­но исполь­зовать Arduino IDE. Ска­чива­ем пос­леднюю вер­сию. Пос­ле уста­нов­ки добав­ляем под­дер­жку плат ESP. Но сна­чала под­клю­чаем пла­ту к ком­пу. Если у нее два пор­та USB Type-C, рас­полага­ем ее чипом к себе, а пор­тами вниз. Обыч­но порт для про­шив­ки находит­ся спра­ва.

От­кры­ваем меню Tools, затем Board и в под­меню выбира­ем Boards Manager. Сле­ва откро­ется панель. Находим и уста­нав­лива­ем esp32 by Espressif Systems. Теперь в меню Tools → Board появит­ся под­меню esp32. Прос­матри­ваем спи­сок и выбира­ем под­ходящую пла­ту. У меня пла­та на ESP32-S3, которая про­дава­лась как ESP32-S3-WROOM-2. Но соот­ветс­тву­ющий про­филь не подошел, зато зарабо­тал ESP32-S3-USB-OTG. Спо­рить не буду: Arduino IDE вид­нее, мне глав­ное, что пла­та про­шива­ется.

Ар­дуинис­ты называ­ют про­шив­ки скет­чами. Скет­чи пишут на уре­зан­ной вер­сии C++. Любой скетч в Arduino IDE дол­жен содер­жать две фун­кции:

  • setup() — фун­кция под­готов­ки чипа к работе. Она выпол­няет­ся один раз при запус­ке. В ней нас­тра­иваем все клю­чевые парамет­ры, соз­даем нуж­ные объ­екты и так далее. В нашем слу­чае ини­циали­зиру­ем Serial и BLE;
  • loop() — фун­кция с управля­ющей логикой, которая выпол­няет­ся цик­личес­ки, пока работа­ет прог­рамма. Мик­рокон­трол­лер выпол­няет весь код внут­ри loop(), а затем запус­кает фун­кцию сно­ва. Завер­шить этот цикл мож­но толь­ко перезаг­рузкой или отклю­чени­ем чипа.

Кон­нект со смар­тфо­нами реали­зуем через Bluetooth. Вер­нее, через его энер­госбе­рега­ющую раз­новид­ность — Bluetooth Low Energy. BLE боль­шую часть вре­мени спит и про­сыпа­ется толь­ко при необ­ходимос­ти, что­бы обме­нять­ся неболь­шими пакета­ми дан­ных. Это зна­чит, что соеди­нение с пос­тоян­ным потоком дан­ных не соз­дает­ся. Для нас это ско­рее плюс, пос­коль­ку сни­жают­ся нак­ладные рас­ходы при парал­лель­ной работе с нес­коль­кими устрой­ства­ми.

Нач­нем код с дирек­тивы преп­роцес­сора CONFIG_BT_NIMBLE_MAX_CONNECTIONS: она нуж­на, что­бы уве­личить мак­сималь­ное количес­тво одновре­мен­ных под­клю­чений. Без нее ESP32 будет под­держи­вать мак­симум три кон­некта по BLE. Такое огра­ниче­ние поз­воля­ет эко­номить ресур­сы. Но у нас неболь­шой скетч, поэто­му мож­но работать с девятью устрой­ства­ми. Девять — это уже аппа­рат­ный пре­дел для нашей схе­мы.

#define CONFIG_BT_NIMBLE_MAX_CONNECTIONS 9

Под­клю­чим биб­лиоте­ку NimBLE-Arduino — в ней есть все необ­ходимое для работы BLE-сер­вера:

#include <NimBLEDevice.h>

Для спис­ка устрой­ств удоб­но исполь­зовать стан­дар­тный кон­тей­нер плю­сов std::map — ассо­циатив­ный мас­сив с парами «ключ — зна­чение»:

#include <map>

Объ­явим три кон­стан­ты с уни­каль­ными иден­тифика­тора­ми, которые нуж­ны для двус­торон­него обме­на дан­ными по BLE:

// Идентификатор BLE-сервиса, по нему телефон ищет ESP32
#define SERVICE_UUID "40b32775-b0ee-43c7-8225-761d5b85c7b8"
// Нужен для отправки сообщения с ESP32 на телефон
#define TX_CHAR_UUID "37a1d50f-a8c5-4817-9555-9b47abf2be62"
// Нужен для отправки сообщения с телефона на ESP32
#define RX_CHAR_UUID "2fb2138a-bd2b-4c78-b75a-222cf2a956f1"

Иден­тифика­торы ран­домные. Это обыч­ные UUID, тебе не обя­затель­но исполь­зовать такие же. Более того, если у тебя работа­ет нес­коль­ко групп устрой­ств, их мож­но раз­делить по SERVICE_UUID. Осталь­ные айдиш­ники нуж­ны для обме­на сооб­щени­ями внут­ри груп­пы. Без это­го ты не смо­жешь изо­лиро­вать сиг­налы сво­их устрой­ств от сиг­налов дру­гих. Нап­ример, если у тебя бес­про­вод­ная кла­виату­ра и мышь, бла­года­ря раз­ным иден­тифика­торам их сиг­налы не пересе­кут­ся с сиг­налами телефо­нов и ESP32. Да и сиг­налы телефо­на могут идти к раз­ным BLE-устрой­ствам.

Объ­явля­ем тек­сто­вую коман­ду, которую ESP32 отпра­вит устрой­ству:

// Должна совпадать с тем, что ожидает приложение Android
const char* TASK_COMMAND = "BOT_START";

Три ука­зате­ля для управле­ния сер­вером BLE:

NimBLEServer* pServer = nullptr;
NimBLECharacteristic* pTxChar = nullptr;
NimBLECharacteristic* pRxChar = nullptr;

Пос­ле соз­дания сер­вера в setup() ука­зате­ли будут ссы­лать­ся на кон­крет­ные объ­екты.

Для хра­нения сос­тояний объ­ектов нуж­на струк­тура с MAC-адре­сом и приз­наком занятос­ти:

// Структура, описывающая устройство и его состояние
struct DeviceInfo {
String mac;
bool busy = false;
};
// Коллекция устройств
std::map<uint16_t, DeviceInfo> devices;
// Сколько выполнений еще нужно сделать
int remainingTasks = 0;
// Сколько заданий сейчас в работе
int busyCount = 0;

Еще одна перемен­ная, на этот раз для работы с пор­том Serial, — стро­ковый буфер:

String serialLineBuf;

Ког­да телефон кон­нектит­ся по Bluetooth, ему тре­бует­ся нем­ного вре­мени, что­бы уста­канить соеди­нение. Для механиз­ма отсроч­ки нуж­ны две перемен­ные:

// Переменная для задержки перед выдачей задания
uint32_t assignTasksAt = 0;
// Пауза две секунды
const uint32_t ASSIGN_DELAY_MS = 2000;

Ини­циали­зация устрой­ства:

void setup() {
// Инициализируем последовательный порт (подключение UART)
Serial.begin(115200);
// Даем небольшой тайм-аут, чтобы инициализация корректно завершилась
delay(300);
// Печатаем сообщение, которое придет в Python-скрипт и монитор Arduino IDE
Serial.println("READY. Ожидание количества заданий по Serial...");
// Инициализируем BLE-устройство. Передаем имя, которое увидят телефоны
NimBLEDevice::init("ESP32-Broadcast");
// Устанавливаем размер пакета данных
NimBLEDevice::setMTU(247);
// Создаем объект сервер BLE GATT и сохраняем указатель на него
pServer = NimBLEDevice::createServer();
// Привязываем обработчик событий подключения/отключения устройств
pServer->setCallbacks(new ServerCallbacks());
// Создаем сервис BLE GATT
NimBLEService* pService = pServer->createService(SERVICE_UUID);
// Создаем характеристику (канал передачи данных) внутри GATT-сервиса
pTxChar = pService->createCharacteristic(TX_CHAR_UUID, NIMBLE_PROPERTY::NOTIFY);
// Создаем характеристику (канал передачи данных)
pRxChar = pService->createCharacteristic(RX_CHAR_UUID, NIMBLE_PROPERTY::WRITE);
// Колбэк на сообщения от телефона к ESP32
pRxChar->setCallbacks(new RxCallbacks());
// Запускаем BLE-сервис
pService->start();
// Получаем ссылку на сервис рассылки приглашений BLE-сервиса
NimBLEAdvertising* pAdvertising = NimBLEDevice::getAdvertising();
// Добавляем ID сервиса в приглашения
pAdvertising->addServiceUUID(SERVICE_UUID);
// Запускаем рассылку, чтобы другие устройства могли увидеть ESP32
pAdvertising->start();
}

Сна­чала ини­циали­зиру­ем пос­ледова­тель­ный порт. Ско­рость 115 200 бит/с выб­рана не слу­чай­но: это рас­простра­нен­ное, прак­тичес­ки стан­дар­тное зна­чение для ESP32. Пос­ле ини­циали­зации даем драй­веру UART (universal asynchronous receiver-transmitter) вре­мя ста­били­зиро­вать­ся. В кон­це шлем сооб­щение об успешном стар­те.

Вся осталь­ная ини­циали­зация каса­ется толь­ко BLE. По сути, мы ини­циали­зиру­ем раз­ные уров­ни и прик­ручива­ем кол­бэки. При работе с Serial чте­ние и запись про­исхо­дят син­хрон­но, но с BLE мы не можем себе это­го поз­волить. Даже если наш бот выпол­нит все чет­ко по тай­мин­гу, воз­можны задер­жки из‑за интерва­лов соеди­нения или опе­раци­онной сис­темы. Поэто­му работа стро­ится на кол­бэках, которые могут быть выз­ваны в любой момент. При­ходит сооб­щение — вызыва­ется фун­кция‑обра­бот­чик.

Мо­жет показать­ся, что ини­циали­заций доволь­но мно­го. Здесь важ­но разоб­рать­ся с иерар­хией. Есть само устрой­ство — чип Bluetooth. На нем нуж­но запус­тить сер­вер, подоб­но веб‑сер­веру на компь­юте­ре. Так же как внут­ри веб‑сер­вера могут работать раз­ные сер­висы, на BLE-сер­вере мож­но запус­тить нес­коль­ко сер­висов. В нашем про­екте сер­вис один, но в теории их может быть нес­коль­ко, с раз­ными ID.

Внут­ри сер­виса с помощью createCharacteristic соз­дают­ся харак­терис­тики — что‑то вро­де име­нован­ных каналов дан­ных, толь­ко вмес­то имен у них кон­крет­ные ID. По этим каналам устрой­ства будут обме­нивать­ся сооб­щени­ями. Это сво­его рода мар­киров­ка, которая помога­ет сооб­щению попасть точ­но к адре­сату.

Ког­да все будет готово, сооб­щим окру­жающим устрой­ствам, что к нам мож­но под­клю­чить­ся. Для это­го исполь­зуют­ся «рек­ламные сооб­щения», или «приг­лашения». Смар­тфо­ны пой­мают инфу, под­тянут наз­вание устрой­ства BLE и отоб­разят его в спис­ке дос­тупных устрой­ств.

Сра­зу пос­ле соз­дания сер­вера вызыва­ем setMTU(247) и тем самым уве­личи­ваем раз­мер пакетов дан­ных. По умол­чанию их раз­мер сос­тавля­ет 23 бай­та, три из которых ухо­дят на слу­жеб­ные заголов­ки. Ука­зывая 247 байт, мы исполь­зуем мак­сималь­ный раз­мер в рам­ках стан­дарта Bluetooth 5. Сог­ласно стан­дарту, мак­симум сос­тавля­ет 257 байт, из которых 10 ухо­дят на заголов­ки: 2 бай­та — на заголо­вок пакета дан­ных, 4 бай­та — на заголо­вок L2CAP и еще 4 бай­та — на поле про­вер­ки целос­тнос­ти сооб­щения MIC (message integrity check).

Пос­коль­ку работа с BLE стро­ится на кол­бэках, в глав­ном цик­ле прог­раммы опра­шива­ем толь­ко пос­ледова­тель­ный порт:

void loop() {
// Читаем, пока что-то есть в потоке. Если там пусто, вернемся к этому через пару тактов
while (Serial.available()) {
// Читаем символ из потока
char c = Serial.read();
// Если это конец строки, передаем в функцию разбора строки
if (c == '\n') {
handleSerialLine(serialLineBuf);
// Очистка буфера
serialLineBuf = "";
// Если не конец строки, добавляем символ в буфер
} else if (c != '\r') {
serialLineBuf += c;
}
}
// Реализация механизма отсрочки выдачи заданий. Если прошло достаточно времени, раздаем задачи
if (assignTasksAt != 0 && (int32_t)(millis() - assignTasksAt) >= 0) {
assignTasksAt = 0;
Serial.println("ASSIGN delay done assignTasks()");
assignTasks();
}
// Небольшая задержка, чтобы не задалбывать компьютер чтением
delay(10);
}

Фун­кция handleSerialLine отве­чает за кор­рек­тный раз­бор стро­ки из пор­та Serial. Она про­веря­ет, что приш­ло чис­ло, и запус­кает задания на телефо­нах:

void handleSerialLine(const String& lineIn) {
// Копируем значение в переменную
String line = lineIn;
// Удаляем пробелы в начале и конце строки
line.trim();
// Проверяем, что переменная не пустая
if (line.length() == 0) return;
// Маркер цифр, если найдем буквы, отбросим значение
bool isNumber = true;
// Обходим каждый символ и проверяем, число это или нет. Если число, добавляем справа
for (size_t i = 0; i < line.length(); i++) {
if (!isDigit(line[i])) { isNumber = false; break; }
}
// Если не число игнорируем строку
if (!isNumber) return;
// Приводим к целому
remainingTasks = line.toInt();
// Печатаем в Serial-порт, что все прошло хорошо
Serial.printf("COUNT_SET total_remaining=%d\n", remainingTasks);
// Вспомогательная функция для назначения заданий телефонам
assignTasks();
}
Проверка исходного кода в Arduino IDE
Про­вер­ка исходно­го кода в Arduino IDE

info

Учи­тыва­ем, что в «плю­сах», как и в чис­том C, фун­кция дол­жна быть объ­явле­на до исполь­зования. В фай­ле скет­ча помеща­ем фун­кции setup() и loop() в конец, а все осталь­ные фун­кции раз­меща­ем выше.

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

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

Материалы из последних выпусков становятся доступны по отдельности только через два месяца после публикации. Чтобы продолжить чтение, необходимо стать участником сообщества «Xakep.ru».

Присоединяйся к сообществу «Xakep.ru»!

Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее

  • Подпишись на наc в Telegram!

    Только важные новости и лучшие статьи

    Подписаться

  • Подписаться
    Уведомить о
    1 Комментарий
    Старые
    Новые Популярные
    Межтекстовые Отзывы
    Посмотреть все комментарии