- PVSM.RU - https://www.pvsm.ru -
Внимание: Данный материал создан исключительно в образовательных и исследовательских целях. Все манипуляции проводились на личном оборудовании автора. Несанкционированный доступ к чужим системам и сетям преследуется по закону.
Современные IoT-устройства, несмотря на компактные размеры и ограниченные ресурсы, представляют собой полноценные embedded-системы. Под пластиковым корпусом бюджетного гаджета часто скрывается стандартный Linux-компьютер с собственным ядром, драйверами, файловыми системами и сетевыми службами.
Статья посвящена проведению полного цикла реверс-инжиниринга типовой IP-камеры: от аппаратного анализа печатной платы до закрепления в операционной системе с root-правами.
Разберём путь от подключения к внутренним отладочным интерфейсам устройства до реализации устойчивого удаленного root-доступа по Wi-Fi. На примере IP-камеры Aceline AIP-O4 наглядно рассмотрим типовую архитектуру бюджетного IoT-девайса и классические ошибки, допускаемые вендорами при проектировании безопасности.
Было проведено реверс-исследование IP-камеры Aceline AIP-O4.
Через UART был получен доступ к загрузчику U-Boot, после чего параметр init был изменён на /bin/sh и получен доступ в root-консоль.
Далее была обнаружена возможность выполнения произвольных shell-скриптов через /var/syscfg/test.sh в read-write разделе файловой системы.
Результат:
получен полный root доступ через telnet shell по Wi-Fi
внедрён бэкдор устойчивый к перезагрузкам
найдена уязвимость path traversal в веб-сервере
сохранена штатная работоспособность устройства
Первым делом нужно определиться с подопытным, поэтому я выделил вот такие критерии для выбора устройства:
Цена 1000-1500 руб., чтобы было не так жалко, если вдруг случайно сделаю "кирпич".
Не очень высокая предполагаемая степень защиты. Если будет чрезмерно сложно, интересный эксперимент превратится в пытку и тяжёлую нудную работу, что убьёт весь энтузиазм и любопытство.
Богатый функционал и разнообразие периферии. Это даст более широкий простор для экспериментов.
Выбор пал на IP-камеру Aceline AIP-O4, и вот что у неё на борту:
Камера
Микрофон
Динамик
ИК-подсветка
Слот для SD-карты
Wi-Fi (как точка доступа, так и подключение к другой сети)
На борту урезанная версия Linux - маленький, слабенький, но всё-таки настоящий компьютер с настоящей ОС.
Камера была куплена 07.03.2026 за 950 руб. в магазине DNS.
Для дальнейшей работы мне понадобились следующие инструменты:
Отвертки, пластиковая лопатка для ремонта электроники (чтобы аккуратно поддеть детали корпуса), 3 шт. Dupont проводов, гелевая клейкая лента (не проводит ток, клеится легко, очень плотно прижимает провода к UART контактам - пайка не нужна)
Arduino UNO (как UART-мост) или отдельный USB-TTL адаптер на FT232BL/RL
Программатор CH341A с прищепкой SOP8
MicroSD карта любого объёма
Софт: PuTTY, NeoProgrammer, binwalk
Любой hardware-анализ начинается с отвертки. Пластиковой лопаткой или медиатором поддеваем тонированную панель на передней части камеры, выкручиваем два винта и аккуратно извлекаем внутренности. Важно не повредить тонкие шлейфы, идущие к микрофону и Wi-Fi антенне.
Отсоединив периферию, мы получаем доступ к основной плате. Нас интересует "Святая Троица" IoT-устройств: процессор (SoC), SPI Flash память и отладочный интерфейс UART. На фото отлично видно всех троих: самый большой чип - это SoC, рядом с ним SPI Flash с 8 ножками, а на обратной стороне платы расположились 4 медных контакта UART.
Идентификация компонентов:
SoC: Ingenic XBurst T31. Архитектура MIPS32. Популярный и дешевый чип для видеоаналитики.
Flash-память: 8-пиновая микросхема GD25Q64CSIG (GigaDevice). Это SPI NOR Flash объемом 64 Мегабита (8 Мегабайт). Питание логики - 3.3V. Здесь хранится загрузчик, ядро и вся файловая система.
UART: На плате любезно оставлена нераспаянная гребёнка из четырех контактов, подписанная как GND, TX, RX, 3V3. Бинго.
Прежде чем что-то менять, нужно понять, что вообще происходит под капотом при старте устройства.
Пока ждал доставку нормального программатора, я собрал колхозный UART-мост из Arduino UNO для первичного "прослушивания" логов загрузки. Сначала замыкаем RST и GND на Arduino, чтобы отключить микроконтроллер от шины UART и сделать USB-UART мост от USB порта компьютера до UART на плате камеры. Dupont провода подсоединяем к контактам UART на плате - TX (Transmit) камеры к TX Arduino, ну и GND (Ground) --> GND, разумеется.
Обратите внимание на то, как UART устройства подключаются к пинам Arduino в роли UART-моста: не TX к RX (Receive), а именно TX к TX. Замыкание RST на GND отключает основной микроконтроллер (ATmega) от шины, и мы начинаем напрямую общаться с чипом USB-TTL, встроенным в плату Arduino. Именно поэтому пин TX на Arduino (который физически ведет к RX USB-чипа) должен подключаться к TX целевого устройства.
В обычных UART соединениях используется TX→RX.
Подключение к UART позволило увидеть логи загрузчика в реальном времени, и понять, что мы на верном пути.
Важно: Arduino оперирует 5-вольтовой логикой, а SoC камеры работает на 3.3V. Если бы мы соединили TX Arduino с RX камеры для отправки команд в терминал, мы бы с высокой вероятностью сожгли процессор. Поэтому при помощи Arduino (без использования резисторов) мы можем только посмотреть логи.
Позже я перешел на классический USB-to-TTL адаптер (FT232BL). Намного удобнее, стабильнее и компактнее.
Подключение: GND к GND, TX к RX, RX к TX - тут уже всё стандартно. Питание подается штатно от блока питания камеры.
В настройках терминала (я использовал PuTTY) указываем COM-порт USB-to-TTL адаптера (можно посмотреть в диспетчере устройств), Connection type - serial, baudrate - 115200. Включаем питание камеры от штатного блока питания, и... в консоль посыпался лог загрузки!
Некоторые интересные моменты, которые удалось сразу же понять из логов:
systemCommonConfigRead,1722] tip_led_switch: 1 telnetd_switch: 0 alarm_interval_sec: 60 sd_capacity_multiply: 1 сообщает нам о том, что telnetd не вырезан из прошивки, но программно отключён. Что ж, это очень хорошая новость.
00:00:17.267 11 CONET> ra0APStart,1387] is going... (ap0, gwIpv4=172.50.4.1) даёт информацию о наличии Wi-Fi интерфейса, который является точкой доступа и статическим адресом шлюза назначается IP 172.50.4.1. Отлично, это нам тоже пригодится.
Но самая важная строка в нем выглядела так:
Hit any key to stop autoboot: 1 0
Уязвимость №1: Загрузчик U-Boot не заблокирован. У нас есть ровно 1 секунда, чтобы нажать Enter и прервать загрузку ОС, получив консоль isvp_t31#. Просто спамим любую клавишу, прерываем процесс загрузки, и получаем промпт командной строки. Если не успели - просто перезагружаем камеру через отключение и включения питания.
Из консоли U-Boot мы можем изменить параметры загрузки ядра (bootargs). По умолчанию система монтирует ФС и просит логин/пароль, но командой printenv посмотрим, какие переменные загрузки сейчас установлены. Среди них нас интересует параметр, который сейчас выглядит как init=/linuxrc.
Если мы поменяем этот параметр на init=/bin/sh, ядро после инициализации сразу выбросит нас в root-оболочку, минуя авторизацию и стандартные загрузочные скрипты.
Заменяя
init=linuxrcнаinit=/bin/sh, мы даём команду ядру Linux вместо штатной программы инициализации (которая запускает службы и процесс login, требующий пароль) просто запустить оболочку командной строки с правами суперпользователя.
Копируем bootargs из вывода printenv и меняем там параметр init, после чего применяем новый конфиг через setenv.
# Команда в консоли U-Boot для перехвата загрузки
# Устанавливаем переменную init=/bin/sh
setenv bootargs 'tf=0 console=ttyS1,115200n8 mem=43520K@0x0 rmem=22016K@0x2A80000 init=/bin/sh rootfstype=squashfs root=/dev/mtdblock2 rw mtdparts=jz_sfc:256k(boot),1632k(kernel),2752k(rootfs),3136k(app),384k(syscfg),32k(flag),8M@0(all)'
# Запускаем процесс загрузки
boot
Важно отметить, что команда setenv сохраняет значения переменных только в рамках текущей сессии, и после перезагрузки изменения пропадут (для чистоты эксперимента - в самый раз).
После загрузки мы получили root (/ #). Но система в состоянии комы: ничего не смонтировано, драйверов нет, сервисы мертвы. Ну... значит попробуем оживить её вручную.
Прежде чем мы начнем ковырять файлы инициализации, необходимо сделать полный дамп прошивки. Если камера "окирпичится", это будет наш единственный билет обратно.
Важно не забыть предварительно обесточить плату, отключив штатный шлейф питания. Программатор сам подаёт напряжение на чип, и может работать в двух режимах 5V и 3.3V - не забываем проверить информацию о рабочем напряжении памяти, и переключить режим перемычкой на плате программатора, прежде чем подключаться к памяти.
(Часто бывает, что память работает на 1.8V, и для таких случаев нужен понижающий адаптер. Благо, что для CH341A это довольно типовой "обвес", поэтому его купить можно на любом маркетплейсе.)
Считываем все 8 Мб памяти с помощью NeoProgrammer (или аналогичной программы) и обязательно делаем проверку целостности получившегося дампа кнопкой Verify IC - программатор ещё раз прочитает данные с чипа, а затем побитно сравнит исходный и контрольный дамп. Если при чтении и при проверке получили "Success" - сохраняем дамп. Если видим ошибки, значит контакт был нестабильным и данные записались некорректно. Обычно, в таких случаях нужно установить прищепку получше и попробовать ещё раз. На это может уйти не одна попытка, но на мой взгляд это всё равно в разы удобнее и быстрее, чем выпаивать микросхему, снимать дамп, и потом припаивать обратно.
Дамп сохранен. Теперь можно действовать смелее.
В загрузчике U-Boot переменная bootargs заботливо хранила подробную карту разделов в памяти: mtdparts=jz_sfc:256k(boot),1632k(kernel),2752k(rootfs),3136k(app),384k(syscfg),32k(flag),8M@0(all). На основе этих данных и более детального вывода binwalk мне удалось составить точную карту разделов.
|
Смещение (HEX) |
Размер (KB) |
Раздел MTD |
Содержимое по данным Binwalk |
Описание |
|---|---|---|---|---|
|
|
256 |
|
U-Boot / SPL (сигнатуры JBOOT, LZO) |
Загрузчик и инициализация железа |
|
|
1632 |
|
uImage (Linux Kernel 3.10.14) |
Сжатое ядро ОС Linux |
|
|
2752 |
|
SquashFS (RootFS) |
Корневая система (Read-only) |
|
|
3136 |
|
SquashFS (App) |
Бинарники вендора (в т.ч. |
|
|
384 |
|
JFFS2 (SysCfg) |
RW раздел. Хранит в основном конфиги |
|
|
32 |
|
Raw Data (Flag) |
Системные флаги и сброс настроек |
|
|
8192 |
|
Full Dump |
Весь образ целиком |
Оказавшись в "голой" ОС через эксплойт U-Boot, нужно было найти способ штатно запустить систему с монтированием всех разделов, запуском служб и т.д., но при этом сохранить за собой права root.
Немного покопавшись в системе из-под root (всё ещё через UART-консоль и подцепленные к плате провода) мне удалось обнаружить скрипт инициализации /etc/init.d/rcS. Этот скрипт выполняется при старте системы, о чём нам заботливо подсказывает /etc/inittab:
# Фрагмент /etc/inittab
# now run any rc scripts
::sysinit:/etc/init.d/rcS
Более подробный взгляд на содержимое /etc/init.d/rcS выявил Уязвимость №2 (Логический бэкдор прямо от китайских разработчиков):
# Фрагмент /etc/init.d/rcS
# mount /var/syscfg
mkdir -p /var/syscfg
mount -t jffs2 /dev/mtdblock4 /var/syscfg
# start test.sh
if [ -f /var/syscfg/test.sh ];then
sh /var/syscfg/test.sh &
else # start boot.sh
sh /bin/boot.sh &
fi
Разработчики оставили прекрасный хук! Если в разделе /var/syscfg (который, в отличие от read only корневой системы, смонтирован в JFFS2 и доступен для записи) существует файл test.sh, система выполнит его ВМЕСТО штатного boot.sh. Эти строчки в /etc/init.d/rcS буквально кричат "ЗАНОСИТЕ СЮДА ВАШ БЭКДОР!"
Но наша система пока всё ещё в коматозном состоянии, и в ней ничего не происходит. Поэтому, руководствуясь порядком команд из /etc/inittab, я вручную поднял систему, чтобы получить живое окружение:
/sbin/swapoff -a
mount -t tmpfs tmpfs /dev
mkdir -p /dev/pts
mkdir -p /dev/shm
mount -a
hostname -F /etc/hostname
/bin/sh /etc/init.d/rcS
Система загрузилась штатно. Запустились все сервисы (веб-сервер, трансляция RTSP, облачные демоны и т.д.). Это уже большой прогресс! С каждым шагом становится понятнее как тут вообще всё устроено, и "магия" становится просто набором скриптов и команд.
В UART тут же полетел спам логов от всевозможных сервисов. Из-за этого стремительного потока невероятно полезной информации, команды приходилось отправлять "вслепую" - пока вводишь команду, она визуально уже "улетела" куда-то вверх вместе с логами. На это нужно просто забить, и допечатав команду (или вставив команду из буфера) смело нажимать Enter - команда выполнится.
Чтобы комфортнее взаимодействовать с подопытным, я подключился к Wi-Fi точке доступа, которую раздаёт камера, после чего поднял telnetd на самой камере:
telnetd -l /bin/sh -p 23 &
Кроме того, выяснилась неприятная деталь: при штатной инициализации камера поднимает незапароленную точку доступа для первоначальной настройки со смартфона. Оставлять её открытой - значит потенциально пустить кого угодно в свою root-консоль.
Меня такой расклад не устроил, и я нашёл решение - на лету дописываем настройки WPA2 в конфигурацию hostapd и перезапускаем демон точки доступа:
echo "wpa=2" >> /var/hostapd.conf
echo "wpa_passphrase=MySecretPassword123" >> /var/hostapd.conf
echo "wpa_key_mgmt=WPA-PSK" >> /var/hostapd.conf
echo "wpa_pairwise=TKIP CCMP" >> /var/hostapd.conf
echo "rsn_pairwise=CCMP" >> /var/hostapd.conf
killall hostapd && hostapd -B /var/hostapd.conf
(Опционально: на этом этапе, когда вся файловая система уже смонтирована, мы можем сделать бэкапы разделов на вставленную SD-карту с помощью cat /dev/mtdX > /mnt/mmc/mtdX.bin, где X - номер нужного раздела).
После активации режима точки доступа (AP Mode) камера поднимает DHCP-сервер и назначает себе статический IP-адрес шлюза точки доступа. В логах мы уже видели вот такая строку, где можно этот самый адрес и увидеть:
00:00:03.336 08 CONET> loadNetIni,2844] net support:eth0=1 ra0=1 bStaticIpFlag=0 gs_cAp0GwIpv4=172.50.4.1
Поэтому всё - подключаемся к Wi-Fi с паролем, и залетаем в root shell по telnet 172.50.4.1 словно к себе домой.
Теперь, когда концепт доказан, мы напишем скрипты, которые камера будет исполнять при каждом включении сама. Для гибкости и удобства мы разделим логику на загрузчик (сохраняется во внутреннюю память и остаётся там даже после перезагрузки) и полезную нагрузку (хранится на SD-карте для удобства редактирования).
1. Создаем загрузчик test.sh:
Чуть позже мы поместим этот загрузчик в /var/syscfg/test.sh. Скрипт первым делом запускает штатную загрузку камеры (/bin/boot.sh), а затем раз в секунду в течение 30 секунд "пингует" наличие смонтированной SD-карты и скрипта с бэкдором на ней. Если находит - выполняет скрипт (с root правами), а если не находит (не вставлена SD-карта или на ней нет скрипта) - просто ничего не делает, чтобы всё работало как обычно.
#!/bin/sh
# Запускаем штатную загрузку камеры
sh /bin/boot.sh &
# Проверяем наличие на SD-карте файла с командами
# Если файл найден - выполняем
timeout=30
while [ $timeout -gt 0 ]; do
if [ -f /mnt/mmc/backdoor.sh ]; then
sh /mnt/mmc/backdoor.sh &
break
fi
timeout=$((timeout-1))
sleep 1
done
2. Создаем Payload backdoor.sh (на SD-карте):
Этот скрипт открывает Telnet-порт 23, отдавая нам root shell, и защищает заводскую точку доступа Wi-Fi паролем.
#!/bin/sh
# Поднимаем root-консоль
telnetd -l /bin/sh -p 23 &
# Ждем формирования конфига точки доступа
timeout=60
while [ $timeout -gt 0 ]; do
if [ -f /var/hostapd.conf ]; then
break
fi
timeout=$((timeout-1))
sleep 1
done
sleep 1
# Перезапускаем точку доступа,
# предварительно установив на неё пароль
killall hostapd
{
echo "wpa=2"
echo "wpa_passphrase=MySecretPassword123"
echo "wpa_key_mgmt=WPA-PSK"
echo "wpa_pairwise=TKIP CCMP"
echo "rsn_pairwise=CCMP"
} >> /var/hostapd.conf
sleep 1
hostapd -B /var/hostapd.conf &
Инжект:
Используя SD-карту и наш временный root-доступ через UART, копируем скрипт test.sh в системный раздел смонтированный в read-write режиме:
cp /mnt/mmc/test.sh /var/syscfg/test.sh
chmod +x /var/syscfg/test.sh
sync
sync
Перезагружаем устройство с установленной "волшебной" SD-картой, и идём открывать шампанское (и telnet).
Подключаемся к (уже запароленной) Wi-Fi точке доступа, открываем терминал (в моём случае Powershell), подключаемся к камере по telnet 172.50.4.1 и получаем root-shell:
Всё, теперь можно отключаться от UART и собирать камеру обратно в корпус. Дальнейшее исследование системы куда удобнее продолжать через беспроводной root shell, чем через проводной UART.
Для конечного пользователя типовое IoT-устройство обычно выглядит как абстрактный "чёрный ящик", внутренняя логика которого скрыта за удобным мобильным приложением и облачными сервисами. Это нередко создает ложное ощущение закрытости и надежности системы. Однако аппаратный аудит может быстро разрушить эту иллюзию.
Как показала практика, за фасадом обычного бытового гаджета скрывается старый добрый Linux и классический стек технологий, изобилующий стандартными точками входа и логическими ошибками:
Нераспаянный, но активный UART-интерфейс на плате
Незаблокированный загрузчик U-Boot с возможностью изменения bootargs
Отсутствие верификации загрузочных скриптов (оставленный разработчиками хук test.sh в RW-разделе)
Использование устаревшего ПО с банальными уязвимостями конфигурации (веб-сервер с открытым корнем SD-карты и Path Traversal)
Громкие истории про "взлом сети через умный чайник" перестают казаться хакерской магией, когда понимаешь, что в их основе могут лежать вот такие архитектурные недочеты, кривые конфиги и оставленные на производстве отладочные бэкдоры.
Напоследок приведу схему, демонстрирующую пройденные этапы и общую последовательность загрузки устройства.
Camera
│
├─ UART → U-Boot
│ │
│ └─ bootargs → init=/bin/sh
│
├─ root shell
│
├─ анализ скрипта rcS
│
└─ /var/syscfg/test.sh
│
└─ backdoor
SPI flash
│
└── U-Boot
│
└── Kernel
│
└── /etc/inittab
│
└── /etc/init.d/rcS
│
└── /var/syscfg/test.sh
│
└── /mnt/mmc/backdoor.sh
│
└── /bin/boot.sh
Упомянутые в статье скрипты, конфиги, дамп прошивки и логи в исходном виде можно посмотреть здесь:
Ссылка на GitHub [9]
В процессе изучения конфига веб-сервера boa.conf я заметил вот такой параметр - DocumentRoot /mnt/mmc, который говорит нам о том, что корневая директория веб-сервера это... корневая директория SD-карты! Вероятно, файлы с SD-карты раздаются веб-сервером через простые GET запросы.
Уязвимость №3: после короткого теста через Postman была найдена и подтверждена классическая уязвимость Path Traversal.
Уязвимость возникает из-за того, что веб-сервер раздаёт файлы из директории /mnt/mmc, но не фильтрует последовательности ../ в URL.
URL адрес запроса, составленный определённым образом, позволяет выйти за пределы DocumentRoot, и веб-сервер (который в данном случае имеет root-права) любезно отдаст вам совершенно любой файл из системы.
Даже без физического вмешательства и разборки корпуса камеры, потенциальный злоумышленник, находясь в одной Wi-Fi сети с устройством, может скачать любые файлы, которые лежат в основной файловой системе ОС камеры или на подключённой к ней SD-карте (записи видео, логи, фото, хеши паролей из /etc/shadow, и т.д.).
В качестве демонстрации получим /etc/shadow через GET запрос на адрес http://172.50.4.1/../../../../etc/shadow :
Автор: x0ttab
Источник [10]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/hardware/446854
Ссылки в тексте:
[1] Этап 0: Выбор устройства: #0
[2] Этап 1: Разведка и препарирование пациента: #1
[3] Этап 2: Слушаем эфир и ловим U-Boot: #2
[4] Этап 3: Художественный фильм - "Сдампили": #3
[5] Этап 4: Эволюция окружения - от "комы" к живой системе: #4
[6] Этап 5: Автоматизация и закрепление бэкдора: #5
[7] Итоги: #%D0%B8%D1%82%D0%BE%D0%B3%D0%B8
[8] Бонус: #%D0%B1%D0%BE%D0%BD%D1%83%D1%81
[9] Ссылка на GitHub: https://github.com/h0ttab/aceline-AIP-O4-reverse
[10] Источник: https://habr.com/ru/articles/1011094/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1011094
Нажмите здесь для печати.