Осторожно: помните ли вы, как в вашем телефоне Siemens, Motorola и Sony поселились маленькие программы - «эльфы»? В рамках этой статьи мы во всех деталях исследуем прошивку бюджетного кнопочника, разберемся в её архитектуре, хакнем и напишем загрузчик тех самых эльфов с MicroSD-флэшки. При этом я постараюсь объяснить всё максимально простым и доступным языком!
Недавно я познакомился с легендой форума allsiemens.ru — Ilya_ZX, который известен своим огромным вкладом в тему реверса и моддинга телефонов на платформе E-Gold и S-Gold. Илья поведал мне интересную историю о том, как в начале нулевых, будучи студентом, поспорил с одногруппником, сможет ли он добавить «змейку» в свой Siemens A60. И спор он этот выиграл, путем бессонных ночей ковыряния прошивки в IDA Pro! Я подумал — «а чем я хуже?». Взял в руки кнопочный телефон на платформе Spreadtrum, сдампил прошивку и загрузил в дизассемблер...
Если вам интересен подробный процесс реверса различных модулей прошивки, как они взаимодействуют между собой, как я написал программу для применения патчей к фуллфлэшу и, собственно, бинлоадер с первой программой — жду вас под катом!
❯ Предисловие
В прошлой статье я рассказал краткую предысторию того, как энтузиасты из нулевых годов превращали простые кнопочные телефоны в почти полноценные смартфоны с вытесняющей многозадачностью и поддержкой нативных программ. Казалось бы, на пути было множество преград: отсутствие исходного кода прошивок и какой либо документации, заблокированные загрузчики без возможности разблокировки и общая сложность такого процесса, как реверс-инжиниринг.

Но общими усилиями сообщество моддеров делало невозможное. Люди искали уязвимости и патчили загрузчики на телефонах Motorola для обхода проверки подписи RSA, резали тест-поинты и разбирались в BootROM'е процессоров S-Gold в телефонах Siemens, чтобы написать генератор BootKEY для возможности прошивки кастомов и немного ковыряли телефоны Samsung, где не только не было никакого секьюрбута, но ещё и прошивки утекали со всей информацией о символах (файлы .lst).

Но что самое интересное — эльф-сцена до сих пор живая и в профильных чатах уже повзрослевшие ребята до сих пор обсуждают то, как им ускорить порт эмулятора NES путем перемещения кода в IRAM, разогнать процессор Freescale Argon LV и... кто бы мог подумать — написать эмулятор периферии S-Gold для запуска прошивки Benq-Siemens E71 в QEMU. И хотя форумы по Siemens'ам уже давно не работают, MotoFan всё ещё жив и хранит кладезь полезной информации для моддеров и реверсеров!

Одним из таких парней был и @ILYA_ZX, который сделал большой вклад в моддинг-сцену телефонов Siemens. Например, именно благодаря его исследованиям звуковой подсистемы, в S65 и M65 добавили полноценную поддержку MP3, чего не смогла сделать даже сама Siemens. Когда я услышал его историю о том, как он на спор с одногруппником отреверсил и написал «змейку» для своего Siemens A60, я сразу же вдохновился и понял... что нужно обязательно что-нибудь пореверсить и повторить успехи Ильи на какой-нибудь неисследованной платформе, причём желательно достаточно свежей, которую можно встретить в новых бюджетных кнопочниках...
❯ Первые шаги...
Я начал с поиска в своей коллекции телефонов цели для будущего моддинга. Одними из главных критериев были: относительно быстрая флэш-память для того, чтобы не поседеть в процессе заливки софта и поддержка прошивки через обычный USB без необходимости покупки отдельных кабелей и прошивочных боксов. У меня нашлись несколько телефонов на разных процессорах: MediaTek, Spreadtrum, а также Coolsand (похож на MTK и Spreadtrum, но MIPS, а не ARM). Я поочередно вычитывал с них прошивки с помощью специального сервисного софта, а затем ковырял их в дизассемблере и подбирал подходящую модель.

Моё внимание привлёк телефон Explay B240, который за пару дней до начала процесса мне подарил подписчик Павел, за что ему огромное спасибо.

После загрузки прошивки в IDA Pro, я сразу же полез смотреть строки и искать самые первые необходимые функции: printf, аллокатор динамической памяти, функции libc для работы со строками, а также ABI-функции по типу деления (в ARM аппаратного деления нет).
Большинство функций libc найти очень просто, если иметь представление об их оригинальных сигнатурах. Например, у аллокатора первый аргумент — всегда размер выделяемой памяти. Смотрим, что предполагаемой функции поступает на вход, если похоже на константный размер структуры от оператора sizeof — значит это скорее всего то, что нам нужно.

И вот тут я приметил кое-что очень интересное, что опытный читатель уже мог заметить на скриншоте выше: огромное количество дебажных строк, трейсы почти на каждый чих, а в ассертах есть названия функций и строки, позволяющие легко определить список аргументов. Поискав строчку DEBUG в фуллфлэше, я обнаружил, что производитель вообще не парился и просто заливал в телефон отладочную версию прошивки, что в мире телефонов можно встретить достаточно редко...

Изначально код после загрузки в дизассемблер выглядел странно: и тут и там указатели на несуществующую память, побитые данные и очень малое количество прямых референсов на ROM. И тут Илья взял фулл, поковырял его и сказал — «да это ж Big-Endian, я нутром чую!». И как оказалось — действительно, телефон использовал BE порядок байт.
Что такое BE и LE?
Для тех, кто не в курсе — в мире компьютеров есть два варианта представления одного и того же многобайтового числа (то есть полуслова, слова, длинного слова): Big Endian и Little Endian. В Little Endian байты числа исчисляются от младшего к старшему, а в Big Endian — от старшего к младшему. Таким образом, если программа, например, захочет загрузить BMP-картинку, ей необходимо будет перевернуть порядок байт в каждом машинном слове заголовка, чтобы получить правильные данные о размере изображения.
Существует также сетевой порядок байт — это Big Endian, он нужен для унификации протокола общения между компьютерами разной архитектуры!
ARM умеет работать в обеих режимах, но до ARMv6 BE и LE аппаратно переключался в процессорном ядре на этапе синтеза или, например, внешним пином. Как раз таки из-за этого перед дизассемблирование ARM-прошивки необходимо сначала узнать её Endianness: например вместо LE-инструкции FE B5 в Thumb у нас будет B5 FE.
Для меня это стало неожиданностью, поскольку из примеров BE на телефонах я знал только легендарные Motorola. После этого я выяснил, что телефон работает на чипсете Spreadtrum SC6500L 2010 года выпуска, который представляет из себя:
Одно ядро ARM9EJ-S на частоте 208МГц в паре с DSP для обработки GSM-радиоканала.
4МБ интегрированной оперативной памяти типа PSRAM и 4МБ NOR-памяти.
Контроллеры LCD-дисплеев и различной периферии, включая SPI, I2C, I2S и GPIO.
Встроенный контроллер питания вместе с модулем чарджера.
И это очень достойные для простого телефона характеристики. Такой чипсет может потянуть не только змейку или Java-игры, но даже эмуляторы ретро-консолей! И 4Мб оперативной памяти для этого хватит с головой. Перспективы дальнейшего моддинга уж точно были!
В реверс-инжиниринге полезно всё: промежуточные elf'ы с прошивкой (axf), таблица символов и тем более исходный код. В поисках слитых исходников прошивки, я наткнулся на архив для гораздо более свежего чипсета — SC6531 (который до сих пор используется в 90% кнопочных телефонов, которые сейчас можно купить в условном DNS до 2.000 рублей), и для общего понимания архитектуры принялся его изучать.

Поскольку кодовая база Spreadtrum тянется из нулевых, прошивка написана по «эмбеддерски» олдскульно: весь код на Plain-C, практически везде глобальные переменные (для экономии RAM, т. к. флэшка поддерживает XIP и маппится в адресное пространство процессора напрямую, а также во избежание фрагментации), UI-построен на модели сообщений как в Windows — то есть, огромные свич-кейсы. Вкратце, архитектуру можно описать так:
В основе лежит RTOS ThreadX, которая также называется Nucleus. В задачи ОСРВ входит реализация вытесняющей многозадачности, включая примитивы синхронизации — мьютексы, семафоры, системного аллокатора, обработчика аппаратных исключений и некоторых других низкоуровневых подсистем.
Nucleus также использовался в телефонах на процессорах MediaTek, Coolsand/RDA, Infineon (Siemens, Panasonic), Freescale (Motorola) и многих других.Над RTOS реализованы драйверы для работы с железом. Дисплей, звук, коммуникация с DSP, клавиатура — всё это тоже низкоуровневые подсистемы.
Далее идёт UI-подсистема MMI — Man Machine Interface. MMI представляет из себя менеджер окон, служб (например воспроизведение музыки в фоне), GUI-фреймворк для построения интерфейса и апплетов — встроенных в телефон приложений, а также менеджер ресурсов. При этом MMI оперирует жестко-прописанными в прошивке набором окон, где каждая структура содержит строковой идентификатор и указатель на функцию-обработчик событий, что заметно упрощает реверс-инжиниринг этой части прошивки.
❯ «Угоняем» окно MMI
Для того, чтобы запустить код с внешнего носителя, необходимо сначала найти функции для работы с файловой системой и пропатчить уже существующую часть прошивки, дабы она могла вызывать наш код в ответ на какое либо действие. С функциями для работы с ФС проблем не возникло. Поскольку трейсов много, я практически сразу нашел необходимый минимум — SFS_OpenFile
, SFS_GetFileSize
, SFS_SetFilePointer/GetFilePointer
, SFS_ReadFile/SFS_WriteFile
и SFS_CloseFile
.
На вход SFS_OpenFile
принимает указатель wchar_t
на строку с полным путем к файлу с учетом диска (C:/ — системное хранилище, D:/ — внутренняя память, E:/ — MicroSD), числовой идентификатор с режимом открытия файла и два дополнительных параметра для атрибутов.
Имейте ввиду, что на некоторых мобильных ОС работа с файлами только асинхронная и на коллбэках!

Как я уже говорил ранее, у каждого окна в системе есть функция для обработки сообщений — концепция такая же, как и WndProc на Windows. Если эту самую функцию пропатчить, можно сразу «угнать» контекст MMI и использовать для того, чтобы писать свои собственные GUI-приложения. В качестве вектора атаки я решил использовать какое-нибудь не сильно нужное в повседневной жизни приложение. Например, встроенную игру «Сокобан».

Поскольку реализация игры на моей версии прошивки значительно отличалась от той, что есть в исходном коде, то пришлось искать функцию «по наитию». Сначала нашел функции для управления таймером подсветки, затем от неё несколько WndProc и анализировав одну из функций (в частности то, что она вызывает функции для движения персонажа, а вектор задается значениями -1 и 1 для X и Y), понял, что это скорее всего то, что мне нужно.

Далее я написал небольшой Makefile, ld-скрипт и первый патч, который должен приостанавливать отключение подсветки по таймеру при запуске игры...

Момент заливки прошивки в телефон — самый рисковый, когда все 280 секунд процесса ты лихорадочно изучаешь листинг ассемблера патча в прошивке... чтобы обнаружить, что ты где-то упустил прибавление единицы к адресу функции, поскольку у тебя Thumb (инструкция BX/BLX изменяет режим процессора с ARM на Thumb, если в первом бите адреса функции единица, а в момент прыжка — устанавливает первый бит на ноль и получает таким образом корректный адрес), и получаешь ребут на ровном месте :)

И вот! Спустя несколько перепрошивок всё запустилось! Моей радости просто не было предела! Далее я немного усложнил патч и вызывал функции для работы с файловой системой, дабы понять, какие буквы «диска» используются. Всё заработало и я получил файл "Privet5.txt"!

Поскольку вручную патчить прошивку неудобно, а Vi_Klay не хватает скриптинга, я написал свой редактор патчей с поддержкой С#. Скрипты автоматизируют выполнение многих руинных задач: ищут функции по паттернам, формируют таблицу функций и скрипт линкера, а также патчат фуллфлэш.
using System;
using System.IO;
using MonoPatcher.Scripting;
public static class Script
{
public static int FindWindowHandlerFunction(byte[] firmware)
{
int offset = Patcher.PatternSearch(firmware, "B5 FE 1C 04 20 00 4B C2 25 01 33 A0", 0);
return offset + 2;
}
/* Patch description: Replace file association from .txt to .app to make possible hooking EBook with our code */
public static void PatchFileAssociation(FileStream strm, byte[] firmware)
{
int offset = Patcher.PatternSearch(firmware, "01 00 00 00 74 78 74 00");
byte[] ext = { (byte)'a', (byte)'p', (byte)'p' };
if(offset == -1)
{
Patcher.Log.WriteError("Failed to apply file-extension patch");
return;
}
Patcher.Patch(strm, offset + 4, ext);
}
/* Patch description: Hook file manager */
public static int FindFileManagerFunction(byte[] firmware)
{
int offset = Patcher.PatternSearch(firmware, "B5 7F 1c 15 AA 08 1C 0C", 0);
return offset;
}
public static void Run()
{
string baseDir = "D:/windows-arm-none-eabi-master/bin/fasolim/";
byte[] firmware = File.ReadAllBytes(baseDir + "firmware.bin");
Patcher.CopyFile(baseDir + "firmware.bin", baseDir + "patched.bin");
if(!File.Exists(baseDir + "bin/binloader.bin"))
{
Patcher.Log.WriteLine("binloader.bin does not exist");
return;
}
byte[] binloader = File.ReadAllBytes(baseDir + "bin/binloader.bin");
using(FileStream strm = File.OpenWrite(baseDir + "patched.bin"))
{
Patcher.Log.WriteLine("Patching game window handler function...");
int handlerOffset = FindWindowHandlerFunction(firmware);
int fmOffset = FindFileManagerFunction(firmware);
if(handlerOffset == -1)
{
Patcher.Log.WriteError("Window handler function not found");
return;
}
if(fmOffset == -1)
{
Patcher.Log.WriteError("FileManager function not found");
return;
}
//Patcher.Log.WriteLine(string.Format("P{0:X}", handlerOffset));
long firmwareEnd = strm.Length;
//Patcher.Append(strm, binloader);
if(firmwareEnd % 4 != 0)
{
Patcher.Log.WriteLine("Please align fullflash to border of 4");
return;
}
// Apply skip boot animation patch
//byte[] skipAnim = File.ReadAllBytes(baseDir + "patches/nopoweronanim.bin");
Patcher.Patch(strm, 0x252FE8, baseDir + "bin/nopoweronanim.bin");
Patcher.InsertNOP(strm, 0x9DC3C4); // Alignment
Patcher.Patch(strm, 0x9DC3C4, baseDir + "bin/fmpatch.bin");
PatchFileAssociation(strm, firmware);
Patcher.InsertNOP(strm, handlerOffset - 2); // Alignment
Patcher.Log.WriteLine("Function address: {0:X}", handlerOffset);
Patcher.Patch(strm, handlerOffset, binloader);
//Patcher.HookFunction(strm, handlerOffset, (int)firmwareEnd | 1, true); // Remember about THUMB!
}
}
}
❯ Разбираемся в подсистемах телефона
На данный момент мы уже умеем загружать эльфы с флэшки в ОЗУ и передавать им управление. Однако очень хотелось бы иметь возможность запускать программы из проводника без использования внешнего загрузчика, а значит, пришло время хукать файловый менеджер!
За открытие файлов отвечает функция MMIAPIFMM_OpenFile
, которая получает из расширения его внутренний числовой тип. Сначала я думал что у менеджера файлов есть ассоциация расширений с MIME-типами и ассоциативный массив с обработчиками для разных типов файлов, но как оказалось, здесь у нас был большой свич-кейс, что одновременно и плохо с точки зрения красоты и производительности кода, и хорошо для реверс-инжиниринга (есть прямые референсы на функции).
PUBLIC void MMIAPIFMM_OpenFile(wchar *full_path_name_ptr)
{
uint16 full_path_name_len = 0;
uint16 suffix_len = MMIFMM_FILENAME_LEN;
wchar *suffix_wstr_ptr = PNULL;
MMIFMM_FILE_TYPE_E file_type = MMIFMM_FILE_TYPE_NORMAL;
MMIFILE_FILE_INFO_T file_info = {0};
full_path_name_len = MMIAPICOM_Wstrlen(full_path_name_ptr);
//SCI_TRACE_LOW:"MMIAPIFMM_OpenFile Enter"
SCI_TRACE_ID(TRACE_TOOL_CONVERT,MMIFMM_WINTAB_13453_112_2_18_2_21_3_559,(uint8*)"");
if (0 == full_path_name_len)
{
//SCI_TRACE_LOW:"MMIAPIFMM_OpenFile, file name is null"
SCI_TRACE_ID(TRACE_TOOL_CONVERT,MMIFMM_WINTAB_13457_112_2_18_2_21_3_560,(uint8*)"");
return;
}
MMIAPICOM_WstrTraceOut(full_path_name_ptr, full_path_name_len * sizeof(wchar));
if (MMIAPIUDISK_UdiskIsRun()) //U盘使用中
{
MMIPUB_OpenAlertWarningWin(TXT_COMMON_UDISK_USING);
return;
}
if (MMIAPIFMM_GetFileInfoFormFullPath(full_path_name_ptr, full_path_name_len, &file_info))
{
suffix_wstr_ptr = SCI_ALLOCA((MMIFMM_FILENAME_LEN + 1) * sizeof(wchar));
if (PNULL == suffix_wstr_ptr)
{
//SCI_TRACE_LOW:"MMIAPIFMM_OpenFile Fail, no memory"
SCI_TRACE_ID(TRACE_TOOL_CONVERT,MMIFMM_WINTAB_13474_112_2_18_2_21_3_561,(uint8*)"");
return;
}
SCI_MEMSET(suffix_wstr_ptr, 0x00, (MMIFMM_FILENAME_LEN + 1) * sizeof(wchar));
MMIAPIFMM_SplitFileName(file_info.file_name, file_info.file_name_len, PNULL, PNULL, suffix_wstr_ptr, &suffix_len);
file_type = MMIAPIFMM_ConvertFileType(suffix_wstr_ptr, suffix_len);
#if defined(DRM_SUPPORT)
// 如果是DRM文件,进一步分析是何种媒体文件
{
DRM_PERMISSION_MODE_E drm_permission = DRM_PERMISSION_NONE;
DRMFILE_PRE_CHECK_STATUS_E pre_check_drmfile_status = DRMFILE_PRE_CHECK_NORMAL;
if (MMIFMM_FILE_TYPE_DRM == file_type)
{
// FmmCombineFullFileName(&s_fmm_list_data,&s_fmm_current_path,index,s_full_file_name,MMIFMM_FULL_FILENAME_LEN);
file_type = MMIAPIDRM_GetMediaFileType(SFS_INVALID_HANDLE, full_path_name_ptr);
switch(file_type)
{
case MMIFMM_FILE_TYPE_PICTURE:
case MMIFMM_FILE_TYPE_EBOOK:
drm_permission = DRM_PERMISSION_DISPLAY;
break;
case MMIFMM_FILE_TYPE_MUSIC:
case MMIFMM_FILE_TYPE_MOVIE:
drm_permission = DRM_PERMISSION_PLAY;
break;
case MMIFMM_FILE_TYPE_JAVA:
drm_permission = DRM_PERMISSION_EXECUTE;
break;
default:
break;
}
if (DRM_PERMISSION_NONE == drm_permission)
{
MMIPUB_OpenAlertWarningWin(TXT_COMMON_NO_SUPPORT);
return;
}
pre_check_drmfile_status = MMIAPIDRM_PreCheckFileStatus(full_path_name_ptr, drm_permission);
if (DRMFILE_PRE_CHECK_NORMAL != pre_check_drmfile_status)
{
if (DRMFILE_PRE_CHECK_NO_RIGHTS == pre_check_drmfile_status)
{
//如果是无效的,则需要提示guilist去刷新本行
// MMIPUB_OpenAlertWarningWin(TXT_DRM_COPYRIGHTS_PROTECTION_NOT_OPERATE);
// MMIAPIFMM_UpdateListIconData(ctrl_id, index, list_data_ptr->pathname, s_full_file_name);
}
return;
}
}
}
#endif
switch(file_type)
{
case MMIFMM_FILE_TYPE_PICTURE:
{
MMIAPIFMM_PreviewPicture(full_path_name_ptr);
}
break;
#ifdef MMI_AUDIO_PLAYER_SUPPORT
case MMIFMM_FILE_TYPE_MUSIC:
{
MMIAPIMP3_PlayFile(full_path_name_ptr, (const uint32)full_path_name_len);
}
break;
#endif
#ifdef VIDEO_PLAYER_SUPPORT
case MMIFMM_FILE_TYPE_MOVIE:
{
#ifdef MMI_VIDEOPLAYER_MINI_FUNCTION
MMIAPIVP_MiniFunction_PlayVideo(full_path_name_ptr,full_path_name_len);
#else
MMIAPIFMM_PreviewVideo(full_path_name_ptr);
#endif
}
break;
#endif
#ifdef EBOOK_SUPPORT
case MMIFMM_FILE_TYPE_EBOOK:
{
MMIFMM_ShowTxtContent(full_path_name_ptr);
}
break;
#endif
#if defined MMI_VCARD_SUPPORT
case MMIFMM_FILE_TYPE_VCARD:
{
//MMIPB_ReadVCardFile(full_path_name_ptr);
MMIFMM_ShowTxtContent(full_path_name_ptr);
}
break;
#endif
case MMIFMM_FILE_TYPE_JAVA:
{
#ifdef JAVA_SUPPORT_IA
MMIAPIJAVA_InstallFromFilesystem(full_path_name_ptr, full_path_name_len);
#elif defined (JAVA_SUPPORT_MYRIAD)
MMIAPIJAVA_Install(full_path_name_ptr, full_path_name_len);
#endif
}
break;
default:
{
MMIPUB_OpenAlertWarningWin(TXT_COMMON_NO_SUPPORT);
}
break;
}
SCI_FREE(suffix_wstr_ptr);
}
else
{
MMIPUB_OpenAlertWarningWin(TXT_COM_FILE_NO_EXIST);
}
}
В телефоне есть читалка электронных книг, но пользы от неё немного — кодировок поддерживает мало, выглядит невзрачно. Если захотим, то сами напишем эльф для чтения книг в .txt. Разработчики прошивки очень удачно написали функцию MMIFMM_ShowTxtContent
, куда передается указатель на полный путь к нашему файлу, а значит, именно её мы и будем с вами хукать... но сначала сменим ассоциацию файлов с txt на app:
/* Patch description: Replace file association from .txt to .app to make possible hooking EBook with our code */
public static void PatchFileAssociation(FileStream strm, byte[] firmware)
{
int offset = Patcher.PatternSearch(firmware, "01 00 00 00 74 78 74 00");
byte[] ext = { (byte)'a', (byte)'p', (byte)'p' };
if(offset == -1)
{
Patcher.Log.WriteError("Failed to apply file-extension patch");
return;
}
Patcher.Patch(strm, offset + 4, ext);
}
Суть хука простая: мы «воруем» глобальную переменную (массив символов) у какой-то другой, неактивной в данный момент программы и храним в ней строку с путём к нашему эльфу, а затем запускаем окно хукнутой игры. Эльфлоадер стартует, берёт указатель на программу и загружает её, перенаправляя все события ей. Такой вот незамысловатый хук:
#include <api.h>
#define PATH_VARIABLE 0x46F9224 //0x46C37F0
// TODO: Make this patch more portable. Now it require manual porting for new platforms.
#define RUN_BOXMAN_GAME_PTR 0x6EA9E4
#define PATH_MAX 255
#define HEAP_BASE 0x212431C7
#define _Alloc(...) ((void*(*)( unsigned int size, unsigned int heap, char* where, unsigned int lineNumber )) 0x0043E2BC + 1)(__VA_ARGS__)
void MMIAPIEBOOK_ManagerWinMsg_4Fmm(uint8 file_dev, wchar_t* name_ptr, uint32 name_length, uint32 file_size, wchar_t* full_name_ptr, uint32 full_name_length)
{
wchar_t* filePath = (wchar_t*)PATH_VARIABLE;
uint32 written;
int len = wstrlen(full_name_ptr) + 1;
for(int i = 0; i < len; i++)
filePath[i] = full_name_ptr[i];
((void(*)()) RUN_BOXMAN_GAME_PTR + 1)(); // TODO: Pattern search of this function (it's thunk at this moment).
}
В процессе реверса функций для открытия окон в прошивке я нашел скрытое меню... с дополнительной игрой, которая есть в прошивке, но штатными способами до неё невозможно добраться!

Далее я адаптировал бинлоадер на новый лад и по итогу у меня всё заработало...
...но до получения результата мне пришлось два дня подряд не спать всю ночь и сидеть до 5 утра ;)
/*
*
* Spreadtrum binloader
*
* ©2025 Bogdan Nikolaev. All rights reserved.
*
* Special thanks to Ilya_ZX
*/
// Binloader uses reduced, statically linked with binary function table
#include <api.h>
#define DISK_SYSTEM u"D:/"
#define DISK_CARD u"E:/"
// We hijack global variable from web browser.
#define LOAD_ADDRESS_VARIABLE 0x46F9224 // 0x46C37F0
#define STATE_VARIABLE 0x46C37F4
#define STATE_NUMBER 0xCAFEBABE
#define CreateDebugFile(str) FileClose(FileOpen(str, FILE_CREATE, 0, 0));
int HandleBoxmanWinMsgHook(uint32 window, uint32 msgId, uint32 dparam)
{
uint32 readBytes = 0;
uint32 handle;
void** loadAddr = (void**)LOAD_ADDRESS_VARIABLE; // Also filemanager put absolute path to binary here
unsigned int* stateVariable = (unsigned int*)STATE_VARIABLE;
if(msgId == MSG_CLOSE_WINDOW || msgId == MSG_KEYDOWN_CANCEL || msgId == MSG_CTL_CLOSE)
{
MMKCloseWin(window);
*stateVariable = 0;
}
// FIT IN 294 BYTES!!!
if(*stateVariable != STATE_NUMBER)
{
// Initialization state: Load runtime from E:/rt.so to memory, store it's address into some global variable.
wchar_t* str = (wchar_t*)loadAddr;
handle = FileOpen(str, 0x31, 0, 0);
if(!handle)
goto err;
uint32 size = 0;
FileGetSize(handle, &size);
*loadAddr = Alloc(size, "m", 1);
if(!(*loadAddr))
goto err;
FileRead(handle, *loadAddr, size, &readBytes);
if(readBytes == 0)
goto err;
*stateVariable = STATE_NUMBER;
}
else
{
// Program state: MMI keep sending our hooked function events, we pass them directly to loaded program.
// The program can also pass execution to another program by swapping WindowFunc with pointer to loaded program.
LoaderContext ctx = {
__api_table,
loadAddr
};
WindowFunc func = (WindowFunc)(*loadAddr + 1); // Beware of THUMB
func(&ctx, window, msgId, dparam);
}
return 1;
err:
CreateDebugFile(u"D:/E");
return 1;
}
Далее я решил попробовать вывести что-нибудь на экран и начал реверсить функции для работы с дисплеем. Подсистема графики в телефоне завязана на ROM и вшитые с прошивкой ресурсы, поэтому решил найти функции для получения указателя на фреймбуфер, чтобы иметь возможность отрисовывать произвольную графику и для обновления так называемых «грязных» зон (дабы перерисовывать не весь экран, а только то, что обновилось). Тут пришлось пореверсить и другие игры и программы, поскольку трейсов в этих функциях не было, а в исходниках прошивки графическая подсистема очень сильно отличалась, однако пользуясь дедукцией, я за пару часов нашёл обе функции.

И залил экран желтым цветом:

#include <api.h>
void LcdClear()
{
LcdId lcd = { 0, 0 };
Rect rct = { 0, 0, 240, 320 };
uint16* fb = ((uint16*(*)(LcdId* id)) 0x321DEA + 1)(&lcd);
uint16 startEnd[4] = { 0, 0, 240, 320 };
((void(*)(LcdId* lcdId, uint32 start, uint32 end, uint16 col)) 0x9701C4 + 1)(&lcd, ((uint32*)&startEnd[0])[0], ((uint32*)&startEnd[0])[1], 0xFFFF);
for(int i = 0; i < 240 * 320; i++)
fb[i] = 0xFF00;
((void(*)()) 0x966378 + 1)(); // Update rect
//((void(*)(LcdId* lcdId, Rect* rct, void* res)) 0x9662F2 + 1)(&lcd, &rct, 0); // Store update rect
}
__attribute__((section(".main")))
int WindowProc(LoaderContext* context, int window, int msgId, int dparam)
{
LcdClear();
// Send MSG
((void(*)(uint32 window, uint32 msg, uint32 res)) 0x36A3CA + 1)(window, MSG_FULL_PAINT, 0);
return 1;
}
Поскольку в разных телефонах функции расположены по разным адресам, для унификации программ между ними необходимо реализовать таблицу функций. Саму таблицу можно составить по паттернам одной из уже изученных донорских прошивок и автоматизировать их поиск среди разных телефонов на одном процессоре. Для этого я написал другой скрипт, который экспортирует специальный заголовочный файл с таблицей функцией и макросами для их вызова:
public static ImportedFunction[] Functions = new ImportedFunction[]{
// File IO
new ImportedFunction("Alloc", "B5 F7 1C 07 25 00 37 19 B0 82", "void*", "unsigned int size, char* where, unsigned int lineNumber"),
new ImportedFunction("wstrlen", "1C 01 D1 00 47 70 88 0A", "uint32", "wchar_t* str"),
new ImportedFunction("FileOpen", "B5 FE 1C 05 09 08", "uint32", "wchar_t* fileName, uint32 accessMode, uint32 shareMode, uint32 fileAttributes"),
new ImportedFunction("FileRead", "B5 FF 1C 06 1C 17 1C 1D B0 85 9C 0E 21 00 A0 86 F7 FF F8 2F 1C 23", "uint32", "uint32 handle, void* buffer, uint32 bytesToRead, uint32* bytesRead"), // FileRead as well as FileWrite are similiar due to identical arguments
new ImportedFunction("FileWrite", "B5 FF 1C 06 1C 17 1C 1D B0 85 9C 0E 21 00 A0 8D F7 FF F8 09 1C 23", "uint32", "uint32 handle, void* buffer, uint32 bytesToWrite, uint32* bytesWritten"),
new ImportedFunction("FileClose", "B5 10 1C 04 A0 8A 21", "uint32", "uint32 fileHandle"),
new ImportedFunction("FileGetSize", "B5 B0 1C 05 1C 0C 21 00", "uint32", "uint32 fileHandle, uint32* fileSize"),
new ImportedFunction("MMKCloseWin", "B5 70 25 00 F1 A7", "uint32", "uint32 windowHandle"),
/*new ImportedFunction("TurnOffBacklight", "49 1D B5 10 20 02 60 C8", "void", "uint32 value"),
new ImportedFunction("AllowTurnOffBacklight", "B5 F1 B0 92 24 00 94 11", "void", "uint32 value"),
new ImportedFunction("SetKeypadBacklight", "B5 10 1C 04 1C 01 A0 F4", "void", "uint32 value"),
new ImportedFunction("AllowBacklight", "B5 B0 1C 04 1C 02 48 BF 4D A5", "void", "uint32 value")*/
};
#ifdef LOADER
__attribute__((section(".text")))
void* __api_table[] = {
(void*)(0x90FEF8 | 1), // Alloc
(void*)(0x92DEAC | 1), // wstrlen
(void*)(0x9D41AE | 1), // FileOpen
(void*)(0x9D4CD2 | 1), // FileRead
(void*)(0x9D4D1E | 1), // FileWrite
(void*)(0x9D4CA4 | 1), // FileClose
(void*)(0x9D509C | 1), // FileGetSize
(void*)(0x981242 | 1), // MMKCloseWin
};
#endif
// _addr defines needed only for patches to make them portable
#define Alloc_addr 0x90FEF8
#define Allocsig unsigned int size, char* where, unsigned int lineNumber
#ifndef PATCH
#define Alloc(...) ((void*(*)( Allocsig )) __api_table[0])(__VA_ARGS__)
#else
#define Alloc(...) ((void*(*)( Allocsig )) 0x90FEF8 + 1)(__VA_ARGS__)
#endif
Саму таблицу функций можно расположить в конце прошивки или в теле какой-нибудь BMP-картинки... Например так делали с телефонами Motorola:

❯ Заключение
Вот такой программный моддинг у нас с вами получился. Надеюсь, вам было интересно! На самом деле ничего сложного в такой модификации нет: у меня были на руках не совсем актуальные, но всё же исходники, а прошивка была собрана в дебаге и в ней было достаточно строк для подробного анализа. Более опытные реверсеры умудряются раскапывать прошивки с куда меньшим количеством документации и без дебаггера, а это значит, что есть куда расти!

И хотя в рамках сегодняшней статьи мы не успели с вами написать реальную программу, задел уже есть и через недельку-другую выйдет вторая часть статьи со змейкой и возможно с эмулятором какой-нибудь ретро-консоли :)
А если вам интересна тематика ремонта, моддинга и программирования для гаджетов прошлых лет — подписывайтесь на мой Telegram-канал «Клуб фанатов балдежа», куда я выкладываю бэкстейджи статей, ссылки на новые статьи и видео, а также иногда выкладываю полезные посты и щитпостю. А ролики (не всегда дублирующие статью) можно найти на моём YouTube канале.
Отдельное спасибо: @ILYA_ZX и @Andy51 за мотивацию, @Azq2и @EXL за советы, а также авторам IDA Pro и Ghidra за крутые инструменты! Без вас этой статьи бы не вышло.
Важно: друзья! Я уверен, что статью будут читать выходцы с форумов моддеров и возможно даже ребята, связанные с прошивочными боксами. Если у вас есть исходный код или объектные файлы для телефонов Siemens (S-Gold или E-Gold — не имеет значения) и вы хотели бы помочь общему моддерскому делу — напишите пожалуйста мне в Telegram. Несмотря на то, что этот код уже давно никому не нужен и E-Gold/S-Gold уже более 15 лет снят с производства, гарантирую полную анонимность и крутой контент :)
Очень важно! Разыскиваются девайсы для будущих статей!
Друзья! Если вам понравилась сегодняшняя статья про разработку эльфов, то спешу объявить: для подготовки будущих материалов с разработкой самопальных игрушек под необычные устройства, объявляется розыск телефонов и консолей! В 2000-х годах, китайцы часто делали дешевые телефоны с игровым уклоном — обычно у них было подобие геймпада (джойстика) или хотя бы две кнопки с верхней части устройства, выполняющие функцию A/B, а также предустановлены эмуляторы NES/Sega. Фишка в том, что на таких телефонах можно выполнять нативный код и портировать на них новые эмуляторы, чем я сейчас занимаюсь, а затем написать об этом подробную статью и записать видео! Если у вас есть телефон подобного формата и вы готовы его задонатить или продать, пожалуйста напишите мне в Telegram (@monobogdan) или в комментарии. Также интересуют смартфоны-консоли на Android (на рынке РФ точно была Func Much-01), там будет контент чуточку другого формата :)

А также я ищу старые (2010-2014) подделки на брендовые смартфоны Samsung, Apple и т. п. Они зачастую работают на весьма интересных чипсетах и поддаются хорошему моддингу, парочку статей уже вышло, но у меня ещё есть идеи по их моддингу! Также может у кого-то остались самые первые смартфоны Xiaomi (серии Mi), Meizu (ещё на Exynos) или телефоны на Linux (например Motorola EM30, RAZR V8, ROKR Z6, ROKR E2, ROKR E6, ZINE ZN5 и т. п., о них я хотел бы подготовить специальную статью и видео т. к. на самом деле они работали на очень мощных для своих лет процессорах, поддавались серьезному моддингу и были способны запустить даже Quake!). Всем большое спасибо за донаты!


А ещё я держу все свои мобилы в одной корзине при себе (в смысле, все проекты у одного облачного провайдера) — Timeweb. Потому нагло рекомендую то, чем пользуюсь сам — вэлкам:
Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud — в нашем Telegram-канале ↩

Перед оплатой в разделе «Бонусы и промокоды» в панели управления активируйте промокод и получите кэшбэк на баланс.