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

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

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

Одним из таких энтузиастов является @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 по всей прошивке, я обнаружил, что производитель просто не удосужился выгрузить с телефона отладочную версию прошивки, что довольно редко встречается в мире мобильников...

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

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

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

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

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

Самый опасный момент — когда заливаешь прошивку в телефон, в течение 280 секунд всего процесса жадно изучаешь ассемблерный листинг патча в прошивке...
Обратите внимание: Кнопочный телефон с большим экраном, камерой, батареей больше чем у некоторых смартфонов..
только чтобы обнаружить, что где-то забыл прибавить единицу к адресу функции, потому что используется Thumb (инструкции BX/BLX меняют режим процессора с ARM на Thumb, если первый бит адреса функции равен 1, а при переходах они устанавливают первый бит в ноль, давая правильный адрес), и телефон внезапно уходит в ребут :)
Вот оно! После пары перепрошивок всё заработало как надо! Я так счастлив! Далее я немного усложнил патч и вызвал функции для работы с файловой системой, чтобы понять, какие буквы дисков используются. Всё прошло отлично, и я получил файл «Privet5.txt»!

Первый патч!
Так как патчить прошивку вручную неудобно, а в Vi_Klay нет скриптов, я написал свой собственный редактор патчей с поддержкой C#. Скрипт автоматизирует множество рутинных задач: поиск функций по паттерну, формирование таблиц функций и скриптов линкера, а также полную прошивку флеш-памяти.
Используйте систему;
Используйте system.io;
Используйте MonoPatcher.Scripting;
Публичный статический класс Script
{
публичный статический void run()
{
string baseDir = "D:/windows-arm-none-eabi-master/bin/fasolim/";
byte прошивка = File.ReadAllBytes(baseDir + "firmware.bin");
Patcher.CopyFile(baseDir + "firmware.bin", baseDir + "patched.bin");
Если (!Файл.Exists(baseDir + "bin/binloader.bin"))
{
Patcher.Log.WriteLine("binloader.bin не существует");
возвращаться;
}
byte binloader = File.ReadAllBytes(baseDir + "bin/binloader.bin");
Использование (FileStream strm = File.OpenWrite(baseDir + "patched.bin"))
{
Patcher.Log.WriteLine("Упаковка функции обработчика игрового окна...");
int handlerOffset = FindWindowHandlerFunction(прошивка);
int fmOffset = FindFileManagerFunction(прошивка);
если (handlerOffset == -1)
{
Patcher.Log.WriteError("Функция обработки окна не найдена");
возвращаться;
}
если (fmOffset == -1)
{
Patcher.Log.WriteError("Функция FileManager не найдена");
возвращаться;
}
//Patcher.Log.WriteLine(string.Format("P{0:X}", handlerOffset));
длинный конец прошивки = strm.length;
//Patcher.Append(strm,binloader);
если (FirmwareEnd%4!=0)
{
Patcher.Log.WriteLine("Пожалуйста, выровняйте полную вспышку по границе 4");
возвращаться;
}
//Применить патч, чтобы пропустить анимацию запуска
//byte skipAnim = File.ReadAllBytes(baseDir + "patches/nopoweronanim.bin");
Patcher.Patch(strm, 0x252FE8, baseDir + "bin/nopoweronanim.bin");
Patcher.InsertNOP(strm, 0x9DC3C4); // Альянс
Патчер.Патч(strm, 0x9DC3C4, baseDir + "bin/fmpatch.bin");
АссоциацияФайлаПатча(strm, firmware);
Patcher.InsertNOP(strm, handlerOffset-2); // Альянс
Patcher.Log.WriteLine("Адрес функции: {0:X}", handlerOffset);
Patcher.Patch(strm, handlerOffset, binloader);
//Patcher.HookFunction(strm, handlerOffset, (int)firmwareEnd | 1, true); // ПОМНИ БОЛЬШОЙ ПАЛЕЦ!
}
}
}
❯ Разбираемся в подсистемах телефона
До сих пор мы видели, как загружать «эльфы» с флешки в оперативную память и передавать им управление. Однако мне очень хотелось иметь возможность запускать программы прямо из Проводника, без использования внешнего загрузчика, а значит, пришло время подключить файловый менеджер!
Функция MMIAPIFMM_OpenFile отвечает за открытие файла; она получает внутренний числовой тип из расширения. Сначала я думал, что у файлового менеджера есть расширения, привязанные к MIME-типам, и ассоциативный массив с различными обработчиками типов файлов, но оказалось, что там большой switch case, что плохо с точки зрения эстетики кода и производительности, но хорошо для реверс-инжиниринга (есть прямые ссылки на функции).
Public void MMIAPIFMM_OpenFile(wchar *full_path_name_ptr)
{
uint16 длина полного имени пути = 0;
uint16 suffix_len = MMIFMM_FILENAME_LEN;
wchar *suffix_wstr_ptr = PNULL;
Тип файла MMIFMM_FILE_TYPE_E = MMIFMM_FILE_TYPE_NORMAL;
Информация о файле MMIFILE_FILE_INFO_T = {0};
full_path_name_len = MMIAPICOM_Wstrlen(full_path_name_ptr);
Дело MMIFMM_FILE_TYPE_EBOOK:
{
MMIFMM_ShowTxtContent(fullpathname_ptr);
}
В телефоне есть читалка электронных книг, но она не очень полезна — поддерживает очень мало кодеков, да и внешний вид непривлекательный. При желании можно написать своего собственного «эльфа», который будет читать книги в формате .txt. Разработчики прошивки вполне успешно написали функцию MMIFMM_ShowTxtContent, куда передаётся указатель на полный путь к нашему файлу, а это значит, что именно её мы и будем перехватывать... но сначала давайте изменим ассоциацию файла с .txt на .app:
/* Описание патча: Заменить ассоциацию файлов с .txt на .app, чтобы подключить электронную книгу к нашему коду */
publicstaticvoid PatchFileAssociation(FileStream strm, byte прошивка)
{
int offset = Patcher.PatternSearch(Firmware, "01 00 00 00 74 78 74 00");
байт расширенный = { (байт)'a', (байт)'p', (байт)'p' };
если (смещение == -1)
{
Patcher.Log.WriteError("Не удалось применить исправление расширения файла");
возвращаться;
}
Patcher.Patch(strm, offset+4, ext);
}
Суть хука проста: мы «крадём» глобальную переменную (массив символов) из другой, в данный момент неактивной программы и сохраняем в ней строку, содержащую путь к нашему «эльфу», после чего запускаем окно привязанной игры. Запускается elfloader, получает указатель на программу и загружает её, перенаправляя все события на неё. Вот простой хук:

При отмене функционала открытия окна в прошивке я обнаружил скрытое меню... В прошивке была дополнительная игра, но она не была доступна стандартными методами!

Затем я настроил binloader по-новому и, наконец, у меня всё заработало...
...но мне пришлось не спать до 5 утра в течение двух дней, прежде чем я получил результаты ;)
uint32 readByte = 0;
дескриптор uint32;
void** loadAddr = (void**)LOAD_ADDRESS_VARIABLE; // Файловый менеджер также помещает сюда абсолютный путь к двоичному файлу
беззнаковое целое* stateVariable = (беззнаковое целое*)STATE_VARIABLE;
если (msgId == MSG_CLOSE_WINDOW || msgId == MSG_KEYDOWN_CANCEL || msgId == MSG_CTL_CLOSE)
{
MMKCloseWin(окно);
*переменная статуса = 0;
}
// Умещается в 294 байта!!!
если (*переменная_состояния != НОМЕР_СОСТОЯНИЯ)
{
// Состояние инициализации: загрузить среду выполнения из E:/rt.so в память и сохранить ее адрес в глобальной переменной.
wchar_t* str = (wchar_t*)loadAddr;
Дескриптор = FileOpen(str, 0x31, 0, 0);
если (!процесс)
Перейти к ошибке;
размер uint32 = 0;
FileGetSize(дескриптор,&размер);
*loadAddr = Alloc(размер, "m", 1);
если (!(*loadAddr))
Перейти к ошибке;
FileRead(handle, *loadAddr, size, &readBytes);
если (readBytes == 0)
Перейти к ошибке;
*переменная состояния = номер состояния;
}
Другой
{
// Состояние программы: MMI продолжает отправлять события нашей функции-ловушки, мы передаем их напрямую загруженной программе.
// Программа также может передать выполнение другой программе, обменяв WindowFunc с указателем на загруженную программу.
контекст загрузчика ctx = {
__api_table,
Загрузить адрес
};
Функция WindowFunc = (WindowFunc)(*loadAddr + 1); // Будьте осторожны с THUMB
func(&ctx, window, msgId, dparam);
}
возврат 1;
Ошибки:
create_debug_file(u"D:/E");
возврат 1;
Затем я решил попробовать что-нибудь вывести на экран и начал реверсить функционал для работы с дисплеем. Графическая подсистема в телефоне привязана к ресурсам, зашитым в ПЗУ и прошивке, поэтому я решил найти функции, которые получают указатель на фреймбуфер, чтобы можно было рисовать произвольную графику и обновлять так называемые «грязные» области (чтобы не перерисовывать весь экран, а только то, что обновилось). Здесь пришлось заняться реверсом других игр и программ, так как в исходниках прошивки следов этих функций не было, а графическая подсистема сильно отличалась, но методом дедукции я нашёл эти две функции за несколько часов.

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


Поскольку адреса функций в разных телефонах различаются, необходимо реализовать таблицу функций для унификации программ на разных аппаратах. Саму таблицу можно составить по образцу одной из изученных пожертвованных прошивок и автоматически искать по ней между разными телефонами на одном процессоре. Для этого я написал ещё один скрипт, который экспортирует специальный заголовочный файл, содержащий таблицу функций и макросы для их вызова:
Публичные статические ImportedFunction функции = new ImportedFunction{
// Файловый ввод и вывод
New ImportedFunction("Alloc", "B5 F7 1C 07 25 00 37 19 B0 82", "void*", "размер беззнакового целого числа, char* где, беззнаковое целое число lineNumber"),
Новая импортированная функция("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 и FileWrite похожи, поскольку имеют одинаковые параметры
Новая 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, буфер void*, uint32 байтов для записи, uint32* байтов для записи"),
Новая 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"),
Новая 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"),
Новая ImportedFunction("AllowTurnOffBacklight", "B5 F1 B0 92 24 00 94 11", "void", "uint32 value"),
Новая импортированная функция("SetKeypadBacklight","B5 10 1C 04 1C 01 A0 F4","void","uint32 value"),
Новая импортированная функция("AllowBacklight","B5 B0 1C 04 1C 02 48 BF 4D A5","void","uint32 value")*/
};
Саму таблицу функций можно поместить в конец прошивки или в тело какого-нибудь BMP-изображения... Например, для телефонов Motorola делают так:

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