[Из песочницы] Промышленный реверс-инжиниринг
Рассказ о процессе заимствования при разработке электроники на наглядном примере.
Запись лога работы лифта самодельным сниффером
Однажды мне понадобилось скопировать довольно простое устройство. Компания-производитель перестала существовать, но по всей стране ещё был спрос на замену сломанных или отработавших свой ресурс девайсов.
Само устройство — кнопка вызова лифта на фото слева. Для опытов мне дали два экземпляра, один из которых можно было полностью разобрать.
Общий план работы выглядел примерно так:
- Изучение схемы подключения платы;
- Изучение элементной базы самой платы;
- Срисовывание её электрической схемы;
- Попытка считывания файла прошивки из микроконтроллера;
- Дизассемблирование прошивки;
- Извлечение алгоритма работы;
- Разработка новой платы;
- Написание новой прошивки.
При неудаче пункта 4, дальнейший план выглядел бы сложнее, но мне повезло.
Изучаем подопытного
Основной микроконтроллер
Кусок электросхемы лифта, на которой наши платы обведены красным
Плата собрана на микроконтроллере 1997 года выпуска AT89C2051, в основе которого лежит архитектура Intel MCS-51. В 2020 году она празднует свой 40-летний юбилей на рынке встраиваемых систем.
Небольшое пояснение: микроконтроллер — это такая микросхема, содержащая вычислительное ядро и набор периферии для управления внешними устройствами. Например, в современной стиральной машине микроконтроллер опрашивает кнопки управления, датчики, выводит информацию на экран и управляет насосами, нагревателем, клапанами и приводом барабана. Для большинства перечисленных функций ему не требуются промежуточные устройства, только набор пассивных электронных компонентов.
Разбираем плату для срисовывания электросхемы
Срисовывание оригинальной электросхемы платы в будущем поможет узнать назначение пинов микроконтроллера, которое необходимо для разбора кода прошивки.
Оригинальное устройство разработано китайской компанией, а потому его схема крайне запутана и со множеством лишних компонентов. Например, включение реле производилось через тройной каскад из биполярного транзистора, оптопары и полевика (именно в таком порядке).
Знакомый, работающий с китайскими производствами, рассказал мне, что китайцы занимаются подобным усложнением схем для увеличения стоимости разработки и производства, если то и другое делают одни люди. После такого я склонен ему верить:
Одно и то же место на китайской двухслойной плате с обеих сторон. Три огромных резистора ни к чему не подключены. Я даже просвечивал плату мощным фонариком, чтобы убедиться.
Схема срисована, загадочные места смоделированы в мультисиме, берёмся за прошивку.
Пытаемся считать прошивку
Мне очень повезло, что на обоих платах в контроллерах не включена защита от чтения, поэтому я успешно слил два варианта прошивки подобной порнографией:
Фото из личного блога американского энтузиаста
Дизассемблирование прошивки
Следующим этапом нам нужно преобразовать этот машинный код во что-то более читаемое:
Берём известный инструмент IDA Pro, в котором уже есть наш контроллер со всеми регистрами периферии, и открываем HEX файл прошивки:
Обработка принимаемых платой данных на языке ассемблера
После этого идёт довольно нудный процесс изучения набора команд нашего вычислительного ядра, комментирование и расшифровка ассемблерного кода.
По адресам таблицы векторов прерываний нашлись сами обработчики прерываний, записи в регистры периферии дали информацию о конфигурации интерфейса связи. Шаг за шагом безымянный ассемблерный код превратился в то, что можно читать.
Извлечение алгоритма работы
Так как мне нужно было разработать новое устройство на другой элементной базе, из кода необходимо было извлечь алгоритм. Некоторое время спустя родился такой псевдокод:
void UartISR (void) {
counter500ms = 0;
//ClearFlag(isrFlags, ISR_FLAG_3);
ProcessUart(recievedByte);
}
void ProcessUart(uint8_t recievedData) {
static uint8_t uartPacketsToRxLeft, uartRecievedCmd, uartCurrPacketCRC;
static uint8_t i, carryFlag;
static uint16_t uartIsrPointer;
static uint8_t uartBuffer1[8], uartBuffer2[6];
static uint8_t uartBuffer1Pos, uartBuffer2Pos;
// 0 -
// 1 -
// 2 -
// 3 - led state, 0x0F
// 4 -
// 5 -
// 6 -
// 7 -
// 8 - buttons time
static uint8_t dataRegisters[9]; // RAM:0050
uint8_t tmpVal, i;
uint8_t dataToSend;
if (GetFlag(UartISRFlags, UART_RECIEVED_FLAG)) {
ClearFlag(UartISRFlags, UART_RECIEVED_FLAG);
if (recieved9thBit) {
switch (recievedData) {
case 0xC1:
uartPacketsToRxLeft = 8;
uartRecievedCmd = 1;
uartBuffer1Pos = 0;
uartBuffer1[uartBuffer1Pos] = recievedData;
//uartIsrPointer = 0x0037;
//tmpVal_0037 = recievedData;
uartCurrPacketCRC = recievedData;
UartRxOn();
return;
break;
case 0xC2:
uartPacketsToRxLeft = 3;
uartRecievedCmd = 2;
Та же самая обработка принимаемых данных на языке Си
Кому интересен протокол передачи:
Станция управления лифтом общалась с платами кнопок вызовов по полнодуплексному 24-вольтовому интерфейсу. В обычном режиме платы кнопок слушали линию, ожидая 9-битный пакет данных. Если в этом пакете приходил адрес нашей платы (задавался DIP-переключателем на плате), то плата переключалась на 8-битный режим приёма, и все последующие пакеты аппаратно игнорировались остальными платами.Первым после адреса шёл пакет с кодом команды управления. Конкретно эта плата принимала всего 3 команды:
- Запись в регистры данных. Например частота и длительность мигания кнопки при вызове;
- Включение подсветки кнопок;
- Запрос состояния кнопок (нажаты или нет).
Последним байтом шла контрольная сумма, которая представляет собой простой XOR всех байт после адреса.
После контрольной суммы плата опять переходила в режим ожидания своего адреса.
Разработка новой платы
Для этапа разработки новой электросхемы и печатной платы у меня нет картинок, но всё было примерно так:
Составление электросхемы и разводка платы делались в Altium Designer. Изготовление печатной платы заказывалось в зеленоградском «Резоните».
Написание новой прошивки
Пока наша новая плата на изготовлении, едем на объект, где установлены такие кнопки вызова, и проверяем правильность разобранного протокола передачи с помощью собранного на ардуине сниффера:
Кусок схемы передатчика, электрически эквивалентный оригиналу. На приёмнике просто оптопара.
//UART1 initialize
// desired baud rate:19200
// actual baud rate:19231 (0,2%)
// char size: 9 bit
// parity: Disabled
void uart1_init(void)
{
UCSR1B = 0x00; //disable while setting baud rate
UCSR1A = 0x00;
UCSR1C = 0x06;
UBRR1L = 0x33; //set baud rate lo
UBRR1H = 0x00; //set baud rate hi
UCSR1B = 0x94;
}
#pragma interrupt_handler uart1_rx_isr:iv_USART1_RXC
void uart1_rx_isr(void) {
unsigned char tmp;
unsigned int rcv = 0;
if (UCSR1B & 0x02) {
rcv = 0x100;
}
rcv |= UDR1;
tmp = (rcv >> 4) & 0x0F;
if (rcv & 0x100) {
tmp |= 0xC0;
}
else {
tmp |= 0x80;
}
txBuf12 = (rcv & 0x0F);
txBuf11 = tmp;
txState1 = 0;
TX_ON();
msCounter0 = 5000;
}
Говнокодим в ICC AVR наш сниффер
Дальше нужно было действовать крайне аккуратно, чтобы не спалить ничего в лифте и не допустить его остановки.
Лезем в кнопку вызова. Толстые жёлтые провода — питание платы и интерфейс передачи. Белые на 4-пиновом разъёме — подключение кнопки и её подсветки.
Проверяем, что всё работает как надо, исправляем косяки и пишем новую прошивку под наше устройство:
//ICC-AVR application builder : 11.02.2015 12:25:51
// Target : M328p
// Crystal: 16.000Mhz
#include
#include
#include
#include "types.h"
#include "gpio.h"
#define TX_OFF() UCSR0B &= 0b11011111;
#define TX_ON() UCSR0B |= 0b00100000;
#define TX_STATE() (UCSR0B & 0b00100000)
#define MAX_TIMEOUT 3000
//#define SNIFFER_MODE 1
//#define MASTER_MODE 1
// #pragma avr_fuse (fuses0, fuses1, fuses2, fuses3, fuses4, fuses5)
#pragma avr_fuse (0xFF, 0xD1, 0xFC)
#pragma avr_lockbits (0xFC)
// AVR signature is always three bytes. Signature0 is always the Atmel
// manufacturer code of 0x1E. The other two bytes are device dependent.
#pragma avr_signature (0x1E, 0x95, 0x0F) // atmega32
static GPIOx errorLed, rcvLed, butUp, butDn, ledUp, ledDn, butLedUp, butLedDn, ledButUp, ledButDn;
static uint8_t msFlag = 0;
static uint8_t ledState = 0, buttonsState = 0;
static uint16_t rcvLedLitTime = 0, butMaskCalcTime = 0, timeoutTimer = 0;
typedef struct {
uint16_t buffer[10];
uint8_t dataLength;
} UartPacket;
static UartPacket txPacket, rxPacket;
#ifdef SNIFFER_MODE
static uint8_t txBuffer[64], txBufferLength = 0, bufferMutex = 0;
#endif
static uint8_t GetPacketCRC(UartPacket* packet);
static void SendLedState(void);
uint8_t GetAddress(void) {
return (PINC & 0x3F);
}
Код на Си для новой платы на основе микроконтроллера AVR ATmega328P
Простоту устройства и прошивки можно оценить по объёму кода, он содержит всего лишь около 600 строк на языке Си.
Процесс сборки выглядел так:
Плата другая, но принцип тот же
Фотографию готового устройства приложить не могу, просто поверьте, что оно до сих пор производится и продаётся.
Лирическое заключение
По поводу кнопок лифта «вверх» и «вниз» на этаже. Я заметил, что многие люди совершенно не понимают их назначение и жмут обе сразу.
Отсюда
У лифта есть два набора кнопок: в кабине — панель приказов, и на этаже — панель вызова. Уже по названию можно догадаться, что панель приказов имеет более высокий приоритет управления.
Все лифты, имеющие панели вызовов с кнопками «вверх» и «вниз», работают с каким-то из вариантов алгоритма оптимизации поездок, целью которых является перевозка максимального числа пассажиров за минимальное время и отдельное условие максимального времени ожидания на этаже (регулируется госстандартом).
Такой алгоритм обычно предполагает подбор пассажиров на этажах, если они едут в том же направлении, которое указывают нажатием кнопки вызова «вверх» или «вниз».
Представим ситуацию, что лифт с пассажирами едет вниз и по пути получает с этажа ниже вызов «вниз». Лифт остановится для подбора пассажира (да, есть ещё учёт загрузки кабины по весовому датчику, но его мы опустим).
Лифт едет дальше и получает с этажа ниже вызов «вверх». Логично, что лифт не остановится для подбора пассажира, так как не будет менять направление движения (это тоже регулируется стандартом), а подбирать пассажира, чтобы поехать вниз, а затем вверх — бесполезный расход энергии и места в лифте.
Лифт едет дальше и получает с этажа ниже сразу два вызова «вверх и «вниз», которые нажал какой-то нетерпеливый пассажир, которому нужно ехать вверх. Логично, что лифт остановится на этом этаже, но пассажир в него не войдёт, зато потратит время людей в кабине на замедление и остановку лифта, открытие дверей, ожидание, закрытие дверей и разгон до номинальной скорости.
Если у лифта только одна кнопка на этаже, то в 99% случаев он работает по алгоритму «собирательный вниз», и при наличии приказов в кабине останавливается только при движении вниз.
Если у вас есть навыки программирования на JS, то можете попробовать реализовать подобный алгоритм управления в онлайновой игре Elevator Saga. В ней есть все аспекты оптимизации поездок без углубления в хардкор вроде работы цепей безопасности лифта.
В своём телеграм-канале я выкладываю подобные материалы. Прямо сейчас там можно следить за разработкой очередного устройства.