
Любительские радиостанции — интересный способ знакомства с работой радиоспектра; что ещё более важно, это встроенные устройства, на которых могут быть установлены странные чипы/прошивки! Мне стало любопытно, насколько просто взломать мою Yaesu FT-70D, поэтому я приступил к расследованию. Единственный ресурс по радиостанциям Yaesu — это пост на Reddit о кастомной прошивке для Yaesu FT1DR.
Пользователь Reddit написал, что если выполнить процесс обновления прошивки через USB, то радиостанция раскрывает микроконтроллер Renesas H8SX, флэш-память которого можно изменить при помощи Renesas SDK. Отличное многообещающее начало, но SDK было не так легко настроить, а я не был даже уверен, сможет ли он сдампить прошивку... поэтому долгое время не брался за него.
Другие пути
На сайте Yaesu есть приложение для Windows, при помощи которого можно обновлять прошивку радиостанции через USB:

В zip содержатся следующие файлы:
1.2 MB Wed Nov 8 14:34:38 2017 FT-70D_ver111(USA).exe
682 KB Tue Nov 14 00:00:00 2017 FT-70DR_DE_Firmware_Update_Information_ENG_1711-B.pdf
8 MB Mon Apr 23 00:00:00 2018 FT-70DR_DE_MAIN_Firmware_Ver_Up_Manual_ENG_1804-B.pdf
3.2 MB Fri Jan 6 17:54:44 2012 HMSEUSBDRIVER.exe
160 KB Sat Sep 17 15:14:16 2011 RComms.dll
61 KB Tue Oct 23 17:02:08 2012 RFP_USB_VB.dll
1.7 MB Fri Mar 29 11:54:02 2013 vcredist_x86.exe
Предположу, что файл, относящийся к FT-70D, FT-70D_ver111(USA).exe, скорее всего, содержит образ прошивки. Файл PE (.exe) может содержать в разделе .rsrc двоичные ресурсы, так что давайте при помощи XPEViewer проверим, что же находится в файле:

Ресурсы относятся к одному из множества различных типов ресурсов, но образ прошивки, скорее всего, будет находиться в специализированном типе. Что это за последний элемент, 23? Развернув этот узел, можно найти пару интересных пунктов:

RES_START_DIALOG — это строка, отображаемая программой обновления при подготовке к обновлению, то есть мы попали в нужное место!

RES_UPDATE_INFO похоже на двоичные данные. Возможно, это и есть наш образ прошивки? К сожалению, изучив вкладку Strings в XPEViewer и обработав эти данные утилитой strings, я не нашёл ничего читаемого. Вероятно, образ прошивки зашифрован.
Реверс-инжиниринг двоичного файла
Давайте загрузим утилиту обновления в дизассемблер, чтобы разобраться, как зашифрованы данные. Я буду пользоваться IDA Pro, но у него есть и отличные альтернативы: Ghidra (бесплатная!), radare2 (бесплатная!) и Binary Ninja. По возможности я постараюсь демонстрировать свой переписанный на C код, потому что он ближе к выводу декомпилятора и машинного кода.
Будет разумно начать со строки, которую мы видели: RES_UPDATE_INFO. Приложения Windows загружают ресурсы, вызывая один из API FindResource*. FindResourceA имеет следующие параметры:
HMODULE— дескриптор модуля, в котором нужно искать ресурс.lpName— имя ресурса.lpType— тип ресурса.
В дизассемблере можно найти ссылки на строку RES_UPDATE_INFO и поискать вызовы FindResourceA с этой строкой в качестве аргумента в позиции lpName.

Найдено соответствие в функции, которая находит/загружает все эти специальные ресурсы типа 23.

Мы знаем, куда приложение загружает данные, так что теперь нужно разобраться, как они используются. На этом этапе выполнение статического анализа не оправдает себя, если данные обрабатываются не напрямую. Чтобы ускорить работу, я воспользуюсь отладчиком. При помощи инструмента Time Travel Debugging WinDbg я записал трассировку исполнения программы обновления при обновлении прошивки моей радиостанции. TTD — бесценный инструмент, и я крайне рекомендую по возможности его использовать. Если вы работаете не в Windows, то альтернативой будет rr.
Вывод декомпилятора показывает, что эта функция копирует ресурс RES_UPDATE_INFO в динамически распределяемый буфер. qmemcpy() встроена и представлена в дизассемблере командой rep movsd, поэтому нам нужно прервать исполнение на этой команде и изучить регистр edi (адрес назначения). Я установил контрольную точку, введя в окно команд bp 0x406968, продолжил исполнение приложения, а при прекращении её работы значение регистра edi оказалось равным 0x2be5020. Теперь мы можем установить контрольную точку доступа к памяти по этому адресу при помощи ba r4 0x2be5020, чтобы прерывать исполнение при чтении этих данных.
Контрольная точка сработала в 0x4047DC — возвращаемся к дизассемблеру. В IDA можно нажать G и ввести адрес, чтобы перейти к нему. Наконец-то мы добрались до чего-то, напоминающего функцию обработки данных:

Исполнение было прервано на разыменовании v2, и IDA автоматически назвала переменную Time. Переменная Time передаётся в другую функцию, форматирующую её, в виде строки с помощью %Y%m%d%H%M%S. Давайте подчистим переменные, чтобы подумать над тем, что нам известно:
bool __thiscall sub_4047B0(char *this)
{
char *encrypted_data; // esi
BOOL v3; // ebx
char *v4; // eax
char *time_string; // [esp+Ch] [ebp-320h] BYREF
int v7; // [esp+10h] [ebp-31Ch] BYREF
__time64_t Time; // [esp+14h] [ebp-318h] BYREF
int (__thiscall **v9)(void *, char); // [esp+1Ch] [ebp-310h]
int v10; // [esp+328h] [ebp-4h]
// переименуем v2 в encrypted_data
encrypted_data = *(char **)(*((_DWORD *)AfxGetModuleState() + 1) + 160);
Time = *(int *)encrypted_data;
// переименуем эту функцию и её второй параметр
format_timestamp(&Time, (int)&time_string, "%Y%m%d%H%M%S");
v10 = 1;
v7 = 0;
v9 = off_4244A0;
sub_4082C0(time_string);
v3 = sub_408350(encrypted_data + 4, 0x100000, this + 92, 0x100000, &v7) == 0;
v4 = time_string - 16;
v9 = off_4244A0;
v10 = -1;
if ( _InterlockedDecrement((volatile signed __int32 *)time_string - 1) <= 0 )
(*(void (__stdcall **)(char *))(**(_DWORD **)v4 + 4))(v4);
return v3;
}
В строке 20 строка временной метки передаётся sub_4082c0, а в строке 21 остальная часть образа обновления передаётся sub_408350. Я буду изучать sub_408350 , потому что пока меня интересуют только данные прошивки, а на основании имени этой функции предположу, что её сигнатура выглядит примерно так:
status_t sub_408350(uint8_t *input, size_t input_len, uint8_t *output, output_len, size_t *out_data_processed);
Давайте посмотрим, что она делает:
int __stdcall sub_408350(char *a1, int a2, int a3, int a4, _DWORD *a5)
{
int v5; // edx
int v7; // ebp
int v8; // esi
unsigned int i; // ecx
char v10; // al
char *v11; // eax
int v13; // [esp+10h] [ebp-54h]
char v14[64]; // [esp+20h] [ebp-44h] BYREF
v5 = a2;
v7 = 0;
memset(v14, 0, sizeof(v14));
if ( a2 <= 0 )
{
LABEL_13:
*a5 = v7;
return 0;
}
else
{
while ( 1 )
{
v8 = v5;
if ( v5 >= 8 )
v8 = 8;
v13 = v5 - v8;
for ( i = 0; i < 0x40; i += 8 )
{
v10 = *a1;
v14[i] = (unsigned __int8)*a1 >> 7;
v14[i + 1] = (v10 & 0x40) != 0;
v14[i + 2] = (v10 & 0x20) != 0;
v14[i + 3] = (v10 & 0x10) != 0;
v14[i + 4] = (v10 & 8) != 0;
v14[i + 5] = (v10 & 4) != 0;
v14[i + 6] = (v10 & 2) != 0;
v14[i + 7] = v10 & 1;
++a1;
}
sub_407980(v14, 0);
if ( v8 )
break;
LABEL_12:
if ( v13 <= 0 )
goto LABEL_13;
v5 = v13;
}
v11 = &v14[1];
while ( 1 )
{
--v8;
if ( v7 >= a4 )
return -101;
*(_BYTE *)(a3 + v7++) = v11[6] | (2
* (v11[5] | (2
* (v11[4] | (2
* (v11[3] | (2
* (v11[2] | (2
* (v11[1] | (2
* (*v11 | (2 * *(v11 - 1))))))))))))));
v11 += 8;
if ( !v8 )
goto LABEL_12;
}
}
}
Вот, как выглядят данные до:

А вот, как они выглядят после обхода вызова функции:

Можно сдампить эти данные в файл при помощи следующей команды:
.writemem C:\users\lander\documents\maybe_deobfuscated.bin 0x2d7507c L100000
В 010 Editor есть встроенная строковая утилита (Search > Find Strings...), и есть проскроллить немного результаты, можно найти настоящие строки, которые отображаются у меня в радиостанции!

Пока нас интересовало лишь получение незашифрованной прошивки, чтобы не заморачиваться с двоичным файлом и загрузить прошивку в IDA Pro... но мне захотелось узнать, как работает шифрование.
Подробности шифрования
Повторим то, к чему мы пришли в предыдущем разделе:
Мы обнаружили подпрограмму обработки данных (давайте называть эту функцию
decrypt_update_info).Мы знаем, что первые четыре байта данных обновления — это временная метка Unix, отформатированная в виде строки и имеющая неизвестное предназначение.
Мы знаем, какая функция начинает расшифровку образа прошивки.
Расшифровка данных
Давайте взглянем подпрограмму расшифровки образа прошивки с переименованными переменными:
int __thiscall decrypt_data(
void *this,
char *encrypted_data,
int encrypted_data_len,
char *output_data,
int output_data_len,
_DWORD *bytes_written)
{
int data_len; // edx
int output_index; // ebp
int block_size; // esi
unsigned int i; // ecx
char encrypted_byte; // al
char *idata; // eax
int remaining_data; // [esp+10h] [ebp-54h]
char inflated_data[64]; // [esp+20h] [ebp-44h] BYREF
data_len = encrypted_data_len;
output_index = 0;
memset(inflated_data, 0, sizeof(inflated_data));
if ( encrypted_data_len <= 0 )
{
LABEL_13:
*bytes_written = output_index;
return 0;
}
else
{
while ( 1 )
{
block_size = data_len;
if ( data_len >= 8 )
block_size = 8;
remaining_data = data_len - block_size;
// Разворачиваем 1 байт входных данных до 8 бит его битового представления
for ( i = 0; i < 0x40; i += 8 )
{
encrypted_byte = *encrypted_data;
inflated_data[i] = (unsigned __int8)*encrypted_data >> 7;
inflated_data[i + 1] = (encrypted_byte & 0x40) != 0;
inflated_data[i + 2] = (encrypted_byte & 0x20) != 0;
inflated_data[i + 3] = (encrypted_byte & 0x10) != 0;
inflated_data[i + 4] = (encrypted_byte & 8) != 0;
inflated_data[i + 5] = (encrypted_byte & 4) != 0;
inflated_data[i + 6] = (encrypted_byte & 2) != 0;
inflated_data[i + 7] = encrypted_byte & 1;
++encrypted_data;
}
// Делаем что-то с развёрнутыми данными
sub_407980(this, inflated_data, 0);
if ( block_size )
break;
LABEL_12:
if ( remaining_data <= 0 )
goto LABEL_13;
data_len = remaining_data;
}
// Снова сворачиваем данные в байты
idata = &inflated_data[1];
while ( 1 )
{
--block_size;
if ( output_index >= output_data_len )
return -101;
output_data[output_index++] = idata[6] | (2
* (idata[5] | (2
* (idata[4] | (2
* (idata[3] | (2
* (idata[2] | (2
* (idata[1] | (2 * (*idata | (2 * *(idata - 1))))))))))))));
idata += 8;
if ( !block_size )
goto LABEL_12;
}
}
}
На высоком уровне эта подпрограмма:
Распределяет временный 64-байтный буфер
Проверяет, есть ли какие-нибудь данные для обработки. Если нет, присваивает переменной вывода
out_data_processedколичество обработанных данных и возвращает 0x0 (STATUS_SUCCESS)Обходит в цикле входные данные блоками по 8 байт и разворачивает каждый байт в его битовое представление.
После разворачивания 8-байтового блока вызывает
sub_407980, использующую в качестве аргументов временный буфер и0.Обходит в цикле временный буфер и пересобирает 8 последовательных битов в 1 байт, а затем устанавливает байт в качестве соответствующего индекса в буфере вывода.
Здесь происходит множество операций, но давайте приглядимся к третьему этапу. Если взять байты 0xAA и 0x77, имеющие битовые представления 0b1010_1010 и 0b0111_1111, и развернуть их в 16-байтовый массив показанным выше алгоритмом, то у нас получится следующее:
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | | 8 | 9 | A | B | C | D | E | F |
|---|---|---|---|---|---|---|---|----|---|---|---|---|---|---|---|---|
| 1 | 0 | 1 | 0 | 1 | 0 | 1 | 0 | | 0 | 1 | 1 | 1 | 0 | 1 | 1 | 1 |
Эта подпрограмма выполняет процесс группами по 8 байтов и полностью заполняет 64-байтный буфер нулями и единицами, как показано выше.
Теперь перейдём к четвёртому этапу и посмотрим, что происходит в sub_407980:
_BYTE *__thiscall sub_407980(void *this, _BYTE *a2, int a3)
{
// длинный список переменных стека, убранных, чтобы не запутывать вас
v3 = (int)this;
v4 = 15;
v5 = a3;
v32[0] = (int)this;
v28 = 0;
v31 = 15;
do
{
for ( i = 0; i < 48; *((_BYTE *)&v33 + i + 3) = v18 )
{
v7 = v28;
if ( !v5 )
v7 = v4;
v8 = *(_BYTE *)(i + 48 * v7 + v3 + 4) ^ a2[(unsigned __int8)byte_424E50[i] + 31];
v9 = v28;
*(&v34 + i) = v8;
if ( !v5 )
v9 = v4;
v10 = *(_BYTE *)(i + 48 * v9 + v3 + 5) ^ a2[(unsigned __int8)byte_424E51[i] + 31];
v11 = v28;
*(&v35 + i) = v10;
if ( !v5 )
v11 = v4;
v12 = *(_BYTE *)(i + 48 * v11 + v3 + 6) ^ a2[(unsigned __int8)byte_424E52[i] + 31];
v13 = v28;
*(&v36 + i) = v12;
if ( !v5 )
v13 = v4;
v14 = *(_BYTE *)(i + 48 * v13 + v3 + 7) ^ a2[(unsigned __int8)byte_424E53[i] + 31];
v15 = v28;
v38[i - 1] = v14;
if ( !v5 )
v15 = v4;
v16 = *(_BYTE *)(i + 48 * v15 + v3 + 8) ^ a2[(unsigned __int8)byte_424E54[i] + 31];
v17 = v28;
v38[i] = v16;
if ( !v5 )
v17 = v4;
v18 = *(_BYTE *)(i + 48 * v17 + v3 + 9) ^ a2[(unsigned __int8)byte_424E55[i] + 31];
i += 6;
}
v32[1] = *(int *)((char *)&dword_424E80
+ (((unsigned __int8)v38[0] + 2) | (32 * v34 + 2) | (16 * (unsigned __int8)v38[1] + 2) | (8 * v35 + 2) | (4 * v36 + 2) | (2 * v37 + 2)));
v32[2] = *(int *)((char *)&dword_424F80
+ (((unsigned __int8)v38[6] + 2) | (32 * (unsigned __int8)v38[2] + 2) | (16
* (unsigned __int8)v38[7]
+ 2) | (8
* (unsigned __int8)v38[3]
+ 2) | (4 * (unsigned __int8)v38[4] + 2) | (2 * (unsigned __int8)v38[5] + 2)));
v32[3] = *(int *)((char *)&dword_425080
+ (((unsigned __int8)v38[12] + 2) | (32 * (unsigned __int8)v38[8] + 2) | (16
* (unsigned __int8)v38[13]
+ 2) | (8 * (unsigned __int8)v38[9]
+ 2) | (4 * (unsigned __int8)v38[10] + 2) | (2 * (unsigned __int8)v38[11] + 2)));
v32[4] = *(int *)((char *)&dword_425180
+ (((unsigned __int8)v38[18] + 2) | (32 * (unsigned __int8)v38[14] + 2) | (16
* (unsigned __int8)v38[19]
+ 2) | (8 * (unsigned __int8)v38[15] + 2) | (4 * (unsigned __int8)v38[16] + 2) | (2 * (unsigned __int8)v38[17] + 2)));
v32[5] = *(int *)((char *)&dword_425280
+ (((unsigned __int8)v38[24] + 2) | (32 * (unsigned __int8)v38[20] + 2) | (16
* (unsigned __int8)v38[25]
+ 2) | (8 * (unsigned __int8)v38[21] + 2) | (4 * (unsigned __int8)v38[22] + 2) | (2 * (unsigned __int8)v38[23] + 2)));
v32[6] = *(int *)((char *)&dword_425380
+ (((unsigned __int8)v38[30] + 2) | (32 * (unsigned __int8)v38[26] + 2) | (16
* (unsigned __int8)v38[31]
+ 2) | (8 * (unsigned __int8)v38[27] + 2) | (4 * (unsigned __int8)v38[28] + 2) | (2 * (unsigned __int8)v38[29] + 2)));
v32[7] = *(int *)((char *)&dword_425480
+ (((unsigned __int8)v38[36] + 2) | (32 * (unsigned __int8)v38[32] + 2) | (16
* (unsigned __int8)v38[37]
+ 2) | (8 * (unsigned __int8)v38[33] + 2) | (4 * (unsigned __int8)v38[34] + 2) | (2 * (unsigned __int8)v38[35] + 2)));
v19 = (char *)(&unk_425681 - (_UNKNOWN *)a2);
v20 = &unk_425680 - (_UNKNOWN *)a2;
v33 = *(int *)((char *)&dword_425580
+ (((unsigned __int8)v38[42] + 2) | (32 * (unsigned __int8)v38[38] + 2) | (16
* (unsigned __int8)v38[43]
+ 2) | (8
* (unsigned __int8)v38[39]
+ 2) | (4 * (unsigned __int8)v38[40] + 2) | (2 * (unsigned __int8)v38[41] + 2)));
result = a2;
if ( v4 <= 0 )
{
v30 = 8;
do
{
*result ^= *((_BYTE *)v32 + (unsigned __int8)result[v20] + 3);
result[1] ^= *((_BYTE *)v32 + (unsigned __int8)v19[(_DWORD)result] + 3);
result[2] ^= *((_BYTE *)v32 + (unsigned __int8)result[&unk_425682 - (_UNKNOWN *)a2] + 3);
result[3] ^= *((_BYTE *)v32 + (unsigned __int8)result[byte_425683 - a2] + 3);
result += 4;
--v30;
}
while ( v30 );
}
else
{
v29 = 8;
do
{
v24 = result[32];
v22 = *result ^ *((_BYTE *)v32 + (unsigned __int8)result[v20] + 3);
result += 4;
result[28] = v22;
*(result - 4) = v24;
v25 = result[29];
result[29] = *(result - 3) ^ *((_BYTE *)v32 + (unsigned __int8)result[(_DWORD)v19 - 4] + 3);
*(result - 3) = v25;
v26 = result[30];
result[30] = *(result - 2) ^ *((_BYTE *)v32 + (unsigned __int8)result[&unk_425682 - (_UNKNOWN *)a2 - 4] + 3);
*(result - 2) = v26;
v27 = result[31];
result[31] = *(result - 1) ^ *((_BYTE *)v32 + (unsigned __int8)result[byte_425683 - a2 - 4] + 3);
*(result - 1) = v27;
--v29;
}
while ( v29 );
}
v5 = a3;
v3 = v32[0];
v4 = v31 - 1;
v23 = v31 - 1 <= -1;
++v28;
--v31;
}
while ( !v23 );
return result;
}
Уф, это намного сложнее, но похоже на основную часть алгоритма расшифровки. В дальнейшем эту функцию sub_407980 мы будем называть decrypt_data. Сразу видно, в чём может возникнуть проблема: эта функция получает указатель C++ this (строка 5) и выполняет побитовые операции с одним из его членов (строка 18, 23 и так далее). Пока назовём этот член класса key и вернёмся к нему позже.
Эта функция — прекрасный пример того, как из-за оптимизаций и изменения порядка кода компиляторами декомпиляторы генерируют далеко не идеальный код. На мой взгляд, TTD был просто необходим для отслеживания потока данных в этой функции. Чтобы разобраться, мне понадобилось несколько часов борьбы с IDA и WinDbg, однако в конечном итоге я выяснил, что эту функцию можно разбить на три высокоуровневых этапа:
1. Создание 48-байтного буфера, содержащего материал ключа, подвергнутого XOR с данными из статической таблицы.
int v33;
unsigned __int8 v34; // [esp+44h] [ebp-34h]
unsigned __int8 v35; // [esp+45h] [ebp-33h]
unsigned __int8 v36; // [esp+46h] [ebp-32h]
unsigned __int8 v37; // [esp+47h] [ebp-31h]
char v38[44]; // [esp+48h] [ebp-30h]
v3 = (int)this;
v4 = 15;
v5 = a3;
v32[0] = (int)this;
v28 = 0;
v31 = 15;
do
{
// Последний оператор этого цикла странный -- он записывает куда-то байт?
// Вернёмся к этому позже
for ( i = 0; i < 48; *((_BYTE *)&v33 + i + 3) = v18 )
{
// Изначально v28 равна 0, но на каждой итерации внешнего цикла while увеличивается на 1
v7 = v28;
// v5 - это последний аргумент, который был равен 0
if ( !v5 )
// Перезаписываем v7 значением v4, которое изначально равно 15,
// но уменьшается на 1 в каждой итерации внешнего цикла while
v7 = v4;
// Левая половина xor, *(_BYTE *)(i + 48 * v7 + v3 + 4)
// v3 в этом контексте - наш указатель this + 4, что даёт нам *(_BYTE *)(i + (48 * v7) + this->maybe_key),
// поэтому левая половина xor, скорее всего, индексируется в нашем материале ключа:
// this->maybe_key[i + 48 * loop_multiplier]
//
// Правая половина xor, a2[(unsigned __int8)byte_424E50[i] + 31]
// a2 - это входные зашифрованные данные, а byte_424E50 - некие статические данные
//
// это полное выражение можно переписать так:
// v8 = this->maybe_key[i + 48 * loop_multiplier] ^ encrypted_data[byte_424E50[i] + 31]
v8 = *(_BYTE *)(i + 48 * v7 + v3 + 4) ^ a2[(unsigned __int8)byte_424E50[i] + 31];
v9 = v28;
// Записываем результат key_data ^ input_data во временный буфер (v34)
// v34, похоже, объявляется неверного типа. На самом деле, v33 - это 52-байтный буфер
*(&v34 + i) = v8;
// Повторяем приведённые выше операции ещё пять раз
if ( !v5 )
v9 = v4;
v10 = *(_BYTE *)(i + 48 * v9 + v3 + 5) ^ a2[(unsigned __int8)byte_424E51[i] + 31];
v11 = v28;
*(&v35 + i) = v10;
// вырезано
// В конце цикла v18 записывается во временный буфер...
v18 = *(_BYTE *)(i + 48 * v17 + v3 + 9) ^ a2[(unsigned __int8)byte_424E55[i] + 31];
// наверно, это была *настоящая* последняя конструкция цикла for,
// то есть for (int i = 0; i < 48; i += 6)
i += 6;
}
2. Создание 32-байтного буфера, содержащего данные из 0x800-байтной статической таблицы, индексы которой берутся из индексов, построенных из буфера на этапе 1. Соединяем этот 32-байтный буфер с 48-байтным буфером из первого этапа.
// dword_424E80 -- какие-то статические данные
// (unsigned __int8)v38[0] + 2) -- в исходном выводе декомпилятора здесь ошибка.
// v33 должна быть 52-байтным буфером, потребляющим v38, поэтому v38 - это на самом деле
// данные, настраиваемые в цикле выше.
// (32 * v34 + 2) -- v34 тоже должна быть какими-то данными из цикла выше.
// Это похоже на оптимизацию двоичного сдвига,
// повторяемую с разными множителями...
//
// Это можно упростить так:
// size_t index = ((v34 << 5) + 2)
// | ((v37[1] << 4) + 2)
// | ((v35 << 3) + 2)
// | ((v36 << 2) + 2)
// | ((v37 << 1) + 2)
// | v38[0]
// v32[1] = *(int*)(((char*)&dword_424e80)[index])
v32[1] = *(int *)((char *)&dword_424E80
+ (((unsigned __int8)v38[0] + 2) | (32 * v34 + 2) | (16 * (unsigned __int8)v38[1] + 2) | (8 * v35 + 2) | (4 * v36 + 2) | (2 * v37 + 2)));
// Повторяется семь раз. Каждый раз ссылка на dword_424e80 сдвигается вперёд на 0x100.
// Примечание: если if you do the math, the next line uses dword_424e80[64]. We shift by 0x100 instead of
// 64 because is misleading because dword_424e80 is declared as an int array -- not a char array.
3. Итеративно обходим следующие 8 байт буфера вывода. Для каждого байтового индекса буфера вывода выполняем индексацию в ещё одном статическом 32-байтном буфере и используем его, как индекс в таблице из второго этапа. Выполняем XOR этого значения со значением текущего индекса буфера вывода.
// Не совсем понимаю, почему эти вычисления выполняются так. В результате при использовании
// они просто оказываются адресом unk_425681.
v19 = (char *)(&unk_425681 - (_UNKNOWN *)a2);
v20 = &unk_425680 - (_UNKNOWN *)a2;
// v4 - это число, декремент которого происходит на каждой итерации. Возможно, это количество оставшихся байт?
if ( v4 <= 0 )
{
// Обходим в цикле 8 байт
v30 = 8;
do
{
// Начинаем выполнять XOR байтов вывода с частью данных, сгенерированных на втором этапе.
//
// Здесь я немного сжульничал, не став объяснять всё, но если обратить внимание на то,
// как используются unk_425680 (v20), unk_425681 (v19), unk_425682 и byte_425683, то можно увидеть,
// что декомпилятор сгенерировал неоптимальный код. Можно упростить его до относительности к unk_425680
//
// *result ^= step2_bytes[unk_425680[output_index] - 1]
*result ^= *((_BYTE *)v32 + (unsigned __int8)result[v20] + 3);
// result[1] ^= step2_bytes[unk_425680[output_index] + 1]
result[1] ^= *((_BYTE *)v32 + (unsigned __int8)v19[(_DWORD)result] + 3);
// result[2] ^= step2_bytes[unk_425680[output_index] + 2]
result[2] ^= *((_BYTE *)v32 + (unsigned __int8)result[&unk_425682 - (_UNKNOWN *)a2] + 3);
// result[3] ^= step2_bytes[unk_425680[output_index] + 3]
result[3] ^= *((_BYTE *)v32 + (unsigned __int8)result[byte_425683 - a2] + 3);
// Перемещаем наш указатель на буфер вывода вперёд на 4 байта
result += 4;
--v30;
}
while ( v30 );
}
else
{
// Обходим в цикле 8 байт
v29 = 8;
do
{
// Берём байт в 0x20, позже мы его заменим
v24 = result[32];
// v22 = *result ^ step2_bytes[unk_425680[output_index] - 1]
v22 = *result ^ *((_BYTE *)v32 + (unsigned __int8)result[v20] + 3);
// Не понимаю, почему здесь выполняется инкремент указателя на буфер вывода,
// но это делает код уродливым
result += 4;
// Записываем сгенерированный выше байт по смещению 0x1c
result[28] = v22;
// Записываем байт в 0x20 в смещение 0
*(result - 4) = v24;
// Повторяем, каждый раз с немного отличающимися смещениями...
v25 = result[29];
result[29] = *(result - 3) ^ *((_BYTE *)v32 + (unsigned __int8)result[(_DWORD)v19 - 4] + 3);
*(result - 3) = v25;
v26 = result[30];
result[30] = *(result - 2) ^ *((_BYTE *)v32 + (unsigned __int8)result[&unk_425682 - (_UNKNOWN *)a2 - 4] + 3);
*(result - 2) = v26;
v27 = result[31];
result[31] = *(result - 1) ^ *((_BYTE *)v32 + (unsigned __int8)result[byte_425683 - a2 - 4] + 3);
*(result - 1) = v27;
--v29;
}
while ( v29 );
}
Мне кажется, внутренний цикл в ветви else уродлив, поэтому приведу его повторную реализацию на Rust:
for _ in 0..8 {
// Заменяем индекс first на second
for (first, second) in (0x1c..=0x1f).zip(0..4) {
let original_byte_idx = first + output_offset + 4;
let original_byte = outbuf[original_byte_idx];
let constant = unk_425680[output_offset + second] as usize;
let new_byte = outbuf[output_offset + second] ^ generated_bytes_from_step2[constant - 1];
let new_idx = original_byte_idx;
outbuf[new_idx] = new_byte;
outbuf[output_offset + second] = original_byte;
}
output_offset += 4;
}
Подготовка ключа
Теперь нам нужно разобраться, как подготавливается ключ к использованию в функции decrypt_data. Я решил установить контрольную точку на первой команде, использующей данные ключа в decrypt_data; это оказалась xor bl, [ecx + esi + 4] по адресу 0x4079d3. Я знаю, что мы должны прекратить исполнение здесь, потому что в выводе декомпилятора левая половина операции XOR (материал ключа) будет вторым операндом в команде xor. Напомню, что декомпилятор выводит XOR так:
v8 = *(_BYTE *)(i + 48 * v7 + v3 + 4) ^ a2[(unsigned __int8)byte_424E50[i] + 31];
При срабатывании контрольной точки адрес загрузки равен 0x19f5c4. Теперь мы можем при помощи TTD попробовать разобраться, откуда эти данные загружались в последний раз. Установим контрольную точку 1-байтной операции записи в память по этому адресу при помощи ba w1 0x19f5c4 и нажмём кнопку Go Back. На случай, если вы никогда не пользовались TTD, объясню, что это работает точно так же, как Go, только в обратном направлении в трассировке программы. В этом случае будет выполняться исполнение в обратном порядке, пока или мы не дойдём до контрольной точки, или не будет сгенерировано прерывание, или мы не достигнем начала программы.
Контрольная точка записи в память срабатывает в 0x4078fb, эту функцию мы пока ещё не встречали. Стек вызовов показывает, что она вызывается не так уж далеко от подпрограммы decrypt_update_info!
set_key(мы находимся здесь — функция изначально называласьsub_407850)sub_4082c0decrypt_update_info
Что это за sub_4082c0?

Здесь нет ничего особо примечательного, кроме того, что одна и та же функция вызывается четыре раза, изначально с аргументом строки временной метки в позиции 0, 64-байтным буфером и кучей вызовов функций, использующих в качестве входных данных возвращаемое значение предыдущей. Функция, в которую вошёл отладчик, получает только один аргумент — 64-байтный буфер, используемый во всех этих вызовах. Что же происходит в sub_407e80?

Побитовые операции, выглядящие подозрительно похожими на развёртывание из байта в биты, которое мы видели выше с данными прошивки. Переименовав функции и переменные, а также развернув некоторые циклы, мы получим следующее:
1// sub_407850
2int inflate_timestamp(void *this, char *timestamp_str, char *output, uint8_t *key) {
3 for (size_t output_idx = 0; output_idx < 8; output_idx++) {
4 uint8_t ts_byte = *timestamp_str;
5 if (ts_byte) {
6 timestamp_str += 1;
7 }
8
9 for (int bit_idx = 0; bit_idx < 8; bit_idx++) {
10 uint8_t bit_value = (ts_byte >> (7 - bit_idx)) & 1;
11 output[(output_idx * 8) + bit_idx] ^= bit_value;
12 }
13 }
14
15 set_key(this, key);
16 decrypt_data(this, output, 1);
17
18 return timestamp_str;
19}
20
21// sub_4082c0
22int set_key_to_timestamp(void *this, char *timestamp_str) {
23 uint8_t key_buf[64];
24 memset(&key_buf, 0, sizeof(key_buf));
25
26 char *str_ptr = inflate_timestamp(this, timestamp_str, &key_buf, &static_key_1);
27 str_ptr = inflate_timestamp(this, str_ptr, &key_buf, &static_key_2);
28 str_ptr = inflate_timestamp(this, str_ptr, &key_buf, &static_key_3);
29 inflate_timestamp(this, str_ptr, &key_buf, &static_key_4);
30
31 set_key(this, &key_buf);
32}
Единственной загадкой осталась подпрограмма set_key:
1int __thiscall set_key(char *this, const void *a2)
2{
3 _DWORD *v2; // ebp
4 char *v3; // edx
5 char v4; // al
6 char v5; // al
7 char v6; // al
8 char v7; // al
9 int result; // eax
10 char v10[56]; // [esp+Ch] [ebp-3Ch] BYREF
11
12 qmemcpy(v10, a2, sizeof(v10));
13 v2 = &unk_424DE0;
14 v3 = this + 5;
15 do
16 {
17 v4 = v10[0];
18 qmemcpy(v10, &v10[1], 0x1Bu);
19 v10[27] = v4;
20 v5 = v10[28];
21 qmemcpy(&v10[28], &v10[29], 0x1Bu);
22 v10[55] = v5;
23 if ( *v2 == 2 )
24 {
25 v6 = v10[0];
26 qmemcpy(v10, &v10[1], 0x1Bu);
27 v10[27] = v6;
28 v7 = v10[28];
29 qmemcpy(&v10[28], &v10[29], 0x1Bu);
30 v10[55] = v7;
31 }
32 for ( result = 0; result < 48; result += 6 )
33 {
34 v3[result - 1] = v10[(unsigned __int8)byte_424E20[result] - 1];
35 v3[result] = v10[(unsigned __int8)byte_424E21[result] - 1];
36 v3[result + 1] = v10[(unsigned __int8)byte_424E22[result] - 1];
37 v3[result + 2] = v10[(unsigned __int8)byte_424E23[result] - 1];
38 v3[result + 3] = v10[(unsigned __int8)byte_424E24[result] - 1];
39 v3[result + 4] = v10[(unsigned __int8)byte_424E25[result] - 1];
40 }
41 ++v2;
42 v3 += 48;
43 }
44 while ( (int)v2 < (int)byte_424E20 );
45 return result;
46}
Эту функцию реализовать чуть проще:
1void set_key(void *this, uint8_t *key) {
2 uint8_t scrambled_key[56];
3 memcpy(&scrambled_key, key, sizeof(scrambled_key));
4
5 for (size_t i = 0; i < 16; i++) {
6 size_t swap_rounds = 1;
7 if (((uint32_t*)GLOBAL_KEY_ROUNDS_CONFIG)[i] == 2) {
8 swap_rounds = 2;
9 }
10
11 for (int i = 0; i < swap_rounds; i++) {
12 uint8_t temp = scrambled_key[0];
13 memcpy(&scrambled_key, &scrambled_key[1], 27);
14 scrambled_key[27] = temp;
15
16 temp = scrambled_key[28];
17 memcpy(&scrambled_key[28], &scrambled_key[29], 27);
18 scrambled_key[55] = temp;
19 }
20
21 for (size_t swap_idx = 0; swap_idx < 48; swap_idx++) {
22 size_t scrambled_key_idx = GLOBAL_KEY_SWAP_TABLE[swap_idx] - 1;
23
24 size_t persistent_key_idx = swap_idx + (i * 48);
25 this->key[persistent_key_idx] = scrambled_key[scrambled_key_idx];
26 }
27 }
28}
Соединяем всё вместе
Данные обновления считываются из ресурсов.
Первые четыре байта данных обновления — это временная метка Unix.
Метка форматируется в строку, каждый байт разворачивается в его битовое представление и расшифровывается при помощи статического материала ключа. Это повторяется четыре раза, и вывод предыдущего прогона используется, как входные данные следующего.
Получившиеся на третьем этапе данные используются в качестве ключа для расшифровки данных.
Оставшаяся часть образа обновления прошивки разворачивается в её битовое представление по 8 байт за раз и при помощи динамического ключа и трёх других уникальных статических таблиц поиска преобразует развёрнутые входные данные.
Результат пятого этапа сворачивается в его байтовое представление.
Мою утилиту расшифровки, которая полностью воссоздаёт эту магию на Rust, можно найти здесь.
Загрузка прошивки в IDA Pro
К счастью, IDA поддерживает дизассемблирование архитектуры Hitachi/Rensas H8SX. Если загрузить прошивку в IDA и выбрать тип процессора «Hitachi H8SX advanced», использовать опции по умолчанию в диалоговом окне «Disassembly memory organization», а затем, наконец выбрать в диалоговом окне «Choose the device name» опцию «H8S/2215R», то...

Ничего не получится. Я не специалист по встроенным системам, но мой друг сказал, что первые несколько DWORD выглядят так, как будто относятся к таблице векторов. Если нажать правой клавишей мыши на адрес 0 и выбрать «Double word 0x142A», то можно нажать на новую переменную unk_142A, чтобы перейти к её местоположению. Нажмём C в этом месте, чтобы определить его, как код, а затем нажмём P, чтобы создать функцию по этому адресу:

Теперь можно заняться реверс-инжинирингом прошивки!