Содержание статьи
В нашей мини‑ферме запланировано три основных компонента:
- Скрипт на Python для обмена данными между компьютером и ESP32 через последовательный порт.
- ESP32 в роли оркестратора, который будет запускать задания на смартфонах с Android и возвращать полученные данные на компьютер.
- Бот для Android, который будет регистрировать аккаунты. Фармить аккаунты на реальных сервисах мы не станем — только в тестовой лабе и только для демонстрации. Таргетом послужит OWASP Juice Shop.
warning
Статья имеет ознакомительный характер и предназначена для специалистов по безопасности, проводящих тестирование в рамках контракта. Автор и редакция не несут ответственности за любой вред, причиненный с применением изложенной информации. Распространение вредоносных программ, нарушение работы систем и нарушение тайны переписки преследуются по закону.
Цель у такого занятия может быть вполне мирная: например, тестирование работы сервисов. Ну и, думаю, тебе будет интересно для разнообразия покопаться с реальным железом.
Идея в том, чтобы построить полностью автономную ферму, которая не занимает ресурсы компьютера. Сам скрипт небольшой, микросхему с контроллером можно запитать обычной зарядкой для телефона, полностью исключив ПК из схемы.

Это довольно бюджетный вариант: отладочная плата стоит 300–600 рублей и продается на любом маркетплейсе или в магазине радиодеталей. При этом она из коробки поддерживает Wi-Fi и Bluetooth. На плате обычно распаяны два порта USB Type-C: один — для заливки прошивки, второй — для подключения к компу или блоку питания.
Тестовая машина
Для автоматизации возьмем форму регистрации OWASP Juice Shop. Нам надо задать количество аккаунтов, а на выходе получаем список кред.
Установка и запуск:
git clone https://github.com/juice-shop/juice-shop.git --depth 1cd juice-shop
npm install
npm start
Переходим на http://localhost:3000, чтобы убедиться, что все прошло успешно и приложение доступно.
На моей хостовой машине установлена Windows. Если попытаться подключиться к сайту по IP, файрвол отбросит соединение. Надо добавить правило, разрешающее подключение к нужному порту.
В Windows открываем «Параметры» («Пуск» → значок шестеренки). В поиске вводим «Брандмауэр Защитника Windows». В левой части окна кликаем по пункту «Дополнительные параметры». Выбираем «Правила для входящих подключений» и создаем новое правило. Тип правила — «Для порта». Протокол — TCP, номер порта — 3000. Теперь Juice Shop доступен по адресу http://<. Узнать свой IP-адрес можно с помощью команды ipconfig в PowerShell.
Скетч для ESP32
Для разработки удобно использовать Arduino IDE. Скачиваем последнюю версию. После установки добавляем поддержку плат ESP. Но сначала подключаем плату к компу. Если у нее два порта USB Type-C, располагаем ее чипом к себе, а портами вниз. Обычно порт для прошивки находится справа.
Открываем меню Tools, затем Board и в подменю выбираем Boards Manager. Слева откроется панель. Находим и устанавливаем esp32 . Теперь в меню 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:: — ассоциативный массив с парами «ключ — значение»:
#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 отправит устройству:
// Должна совпадать с тем, что ожидает приложение Androidconst 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( и тем самым увеличиваем размер пакетов данных. По умолчанию их размер составляет 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();}
info
Учитываем, что в «плюсах», как и в чистом C, функция должна быть объявлена до использования. В файле скетча помещаем функции setup( и loop( в конец, а все остальные функции размещаем выше.
В функции запуска заданий обходим все известные устройства. Если есть задания и свободные устройства, запускаем выполнение.
Продолжение доступно только участникам
Материалы из последних выпусков становятся доступны по отдельности только через два месяца после публикации. Чтобы продолжить чтение, необходимо стать участником сообщества «Xakep.ru».
Присоединяйся к сообществу «Xakep.ru»!
Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее
