Реверс plz для Motorolla ACE3600

Jun 20, 26

IEC104MS License Key Generator — Reverse Engineering Report

Первичный этап

Через binwalk расшифровываем поток zlib.

└─$ binwalk -e exa001.plz

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
64            0x40            Zlib compressed data, best compression

А полученный файл прогоняем ещё раз - это у нас ELF файл для линукса.

└─$ binwalk exa001.pl    

DECIMAL       HEXADECIMAL     DESCRIPTION
--------------------------------------------------------------------------------
0             0x0             ELF, 32-bit MSB relocatable, PowerPC or cisco 4500, version 1 (SYSV)

На тестовых данных погоняли, теперь пробуем с нашим файлом:

binwalk-1

binwalk-2

Что у нас есть

Библиотека: ace104ms.plz (архив) → ELF-файл “40” (PowerPC big-endian, 32-bit)

Декомпилятор: Hex-Rays (40.c, 44579 строк)

Драйвер: IEC104MS API Driver ver.4.2 (Apr 28 2017)

Процессор: Motorola PowerPC (RTU ACE3600)

1. Общая схема лицензирования


При инициализации драйвера вызывается Vira_IEC104MS_Ini() (40.c:2771), которая, среди прочего, вызывает license_check() (40.c:8121).

Последовательность проверки:

  1. Читается CPU ID из аппаратного регистра 0x400FC014 (серийный номер)
  2. Читается 16-байтный ключ из конфигурационной таблицы PLC (M870iniTable + смещение 0xA0) — так называемый “Ladder key”
  3. Если существует файл “iec104lk.29” — ключ читается из него и ПЕРЕОПРЕДЕЛЯЕТ ключ из Ladder (приоритет файла выше)
  4. Ключ (32-символьная HEX-строка) проверяется функцией key_valid()
  5. key_valid() перебирает три типа лицензии:
    • IEC104MASTER4.2 → режим 1 (Master)
    • IEC104SLAVE_4.2 → режим 2 (Slave)
    • IEC104PEER__4.2 → режим 3 (Master+Slave)
  6. Если ни один не подошёл → режим 0 (DEMO, ограниченное время)

2. Детальный разбор функций


2.1 license_check() — 40.c:8121

1. strncpy(ace_cpu_serial_number_string, 0x400FC014, 20) — читаем CPU ID
2. memcpy(buf, M870iniTable + 0xA0, 16)                  — ключ из конфигурации
3. Конвертация 16 байт → 32 символа HEX (sprintf "%02X")
4. key_valid(hex_string, &result_ladder)                  — проверка Ladder
5. key_in_File(hex_string)                                — ищем файл iec104lk.29
   └─ если найден: key_valid(hex_string, &result_file)   — проверка File
6. Итог: result = result_file | result_ladder

2.2 key_in_File() — 40.c:8022

  • Открывает файл “iec104lk.29” через mffs_OpenFile
  • Ищет подстроку “IEC104License” в содержимом
  • Парсит формат: IEC104License = <32-char-hex-key>
  • Копирует ключ в выходной буфер

2.3 key_valid() — 40.c:8081

Формирует строки для проверки:

sprintf(buf, "%s%d.%d", "IEC104MASTER", 4, 2);  // "IEC104MASTER4.2"
sprintf(buf, "%s%d.%d", "IEC104SLAVE_", 4, 2);  // "IEC104SLAVE_4.2"
sprintf(buf, "%s%d.%d", "IEC104PEER__", 4, 2);  // "IEC104PEER__4.2"

Для каждой вызывает key_check(key, label, 0, 0). Если key_check возвращает 0 — лицензия валидна для этого типа.

2.4 key_check() — 40.c:44412 (КЛЮЧЕВАЯ ФУНКЦИЯ)

Алгоритм проверки:

  1. Если a3 == 0, CPU ID читается из 0x400FC014 (аппаратный регистр)
  2. Формируется буфер v31[12] (48 байт):
    memset(v31, 0, 41);                      // очистка 41 байта
    memcpy(v31, cpu_id, strlen(cpu_id)+1);    // копируем CPU ID + '\0'
    memcpy(v31 + strlen(cpu_id), label, strlen(label)+1);
    // ВНИМАНИЕ: второй memcpy пишет ПОВЕРХ '\0' от первого!
    // Результат: ПРЯМАЯ конкатенация без разделителя:
    //   "085SLW00TV" + "IEC104SLAVE_4.2" + '\0'
    
  3. Замена нулевых байт в первых 40 байтах на ‘*’ (0x2A):
    "085SLW00TVIEC104SLAVE_4.2\0\0\0..." →
    "085SLW00TVIEC104SLAVE_4.2*********..."
    
  4. Принудительный ‘\0’ в байте 40 → strlen возвращает 40
  5. Вызов Hash_128(v31, 40, &hash_output)
  6. Преобразование 16-байтного хеша в 32-символьную HEX-строку
  7. Посимвольное сравнение с переданным ключом

3. Алгоритм Hash_128 — 40.c:44327


Это 128-битная хеш-функция на основе TEA (Tiny Encryption Algorithm), работающая в режиме, похожем на CBC-MAC.

Параметры:

  • Вход: 40 байт (5 блоков по 8 байт, каждый блок = 2 × uint32 BE)
  • Состояние: (v21, v22, v23, v24) — 4 × uint32 = 128 бит
  • Константа: delta = 0x61C88647 (отрицательное значение TEA delta)
  • Раундов: 32 на каждую фазу

Для каждого 8-байтного блока (w0, w1):

Фаза 1 — TEA-encipher (v23, v24) с ключом (v21, v22, w0, w1): for i in 0..31: sum -= delta v23 += ((v24«4)+v21) ^ (v24+sum) ^ ((v24»5)+v22) v24 += ((v23«4)+w0) ^ (v23+sum) ^ ((v23»5)+w1) state[2] ^= v23; state[3] ^= v24

Фаза 2 — TEA-encipher (v21, v22) с ключом (w0, w1, v23_new, v24_new): for i in 0..31: sum -= delta v21 += ((v22«4)+w0) ^ (v22+sum) ^ ((v22»5)+w1) v22 += ((v21«4)+v23) ^ (v21+sum) ^ ((v21»5)+v24) state[0] ^= v21; state[1] ^= v22

Результат: (v21, v22, v23, v24) — 16 байт → 32 символа HEX.

4. Верификация на образцах


У меня были примеры эталоных файлов, на них я проверил генератор - он выдаёт точные совпадения.

5. Ключевая находка (баг/особенность)


При построении входного буфера в key_check():

memcpy(v31, cpu_id, strlen(cpu_id)+1);            // "085SLW00TV\0"
memcpy(v31 + strlen(cpu_id), label, strlen(label)+1); // поверх '\0'!

Второй memcpy пишет ПОВЕРХ нуль-терминатора CPU ID. В результате строка label приклеивается к CPU ID напрямую, без какого-либо разделителя:

“085SLW00TV” + “IEC104SLAVE_4.2” (без ‘\0’ между ними!)

Если бы разделитель сохранялся (распространённая ошибка при анализе), хеш был бы совершенно другим. Именно это дало неверный результат с первой попытки.

6. Применение


Генератор: license/keygen.py

python license\keygen.py [ ...]

Без аргументов — самопроверка на эталонных образцах. С аргументом — генерация всех трёх типов лицензий.

Формат вывода готов для копирования в файл iec104lk.29:

; CPU ID: 085SLW00TV ; Driver version 4.2

IEC104License = полученный_хэш ; Slave mode

Файл размещается в файловой системе RTU. При загрузке драйвер читает его и активирует соответствующий режим лицензии.

Ссылка на репозиторий с расшифровкой - ACE3600-iec104-plz.