
Внимание: в этой серии материалов я делюсь опытом по реверс-инжинирингу и модификации простых кнопочных телефонов. Основная цель — расширить ограниченный функционал бюджетных устройств (до 1000 рублей) и превратить их в интересную платформу для моддинга, привлекательную для технических энтузиастов. Если вам знакомы такие понятия, как «эльфы» и патчи, и вы хотите узнать, как происходит взлом, изучение прошивок и создание новых программ для кнопочных телефонов, — добро пожаловать под кат!
📜 Предыстория и вдохновение
Не так давно я познакомился с Ильей (Ilya_ZX), настоящей легендой в среде моддеров телефонов 2000-х годов. Он поделился забавной историей из студенческих лет: увидев, как одногруппник играет в культовую «Змейку» на LG G1800, Илья загорелся идеей запустить эту игру на своём Siemens A60, который из коробки поддерживал лишь Stack Attack и не имел Java. Купить Nokia 3310 для этой цели казалось простым решением, но Илья, увлекавшийся программированием и реверс-инжинирингом, решил пойти другим путём и поспорил, что сможет реализовать игру на своём телефоне самостоятельно.

Всего за месяц он исследовал прошивку на специфичной архитектуре, нашёл функции для работы с дисплеем и вводом и написал свою «Змейку». Сначала на Паскале для симулятора, а затем на ассемблере C166s. Представьте удивление его одногруппника, проигравшего спор! Эта история вдохновила меня, 23-летнего энтузиаста, на собственный эксперимент. Я взялся за реверс-инжиниринг бюджетного кнопочника Explay B240 десятилетней давности.

В предыдущей статье мы сделали первые шаги: загрузили прошивку в IDA Pro, нашли системные функции, модифицировали файловый менеджер для запуска программ с карты памяти, разработали загрузчик исполняемых файлов и организовали таблицу функций. Это была отличная вводная для новичков в реверсе.

Однако итоговый демо-код, просто заливавший экран жёлтым цветом, выглядел не слишком впечатляюще. Поэтому в этой статье мы создадим первую по-настоящему полезную и функциональную программу!
🐍 Разработка «Змейки»: от загрузчика до графики
Напомню принцип работы нашего загрузчика внешних программ. Мы нашли в дизассемблере функцию обработки сообщений окна встроенной игры и перехватили её. Теперь при открытии этого окна загрузчик считывает программу с карты MicroSD в оперативную память и передаёт ей управление. Загрузчик интегрирован в проводник: при запуске файла с расширением .app патч помещает полный путь к нему в одну из «захваченных» глобальных переменных, открывает перехваченное окно игры, после чего наш бутлоадер перенаправляет все системные сообщения загруженной программе.

Наглядная демонстрация работы загрузчика.
Такой подход значительно упрощает жизненный цикл приложений по сравнению с «эльфами» для Motorola или Siemens. По сути, нам нужно лишь инициализировать состояние программы при получении сообщения MSG_CREATE и освободить динамическую память при MSG_CLOSE. Тем, кто хоть раз писал под Windows, это покажется очень знакомым!

Для реализации «Змейки» нам нужно обрабатывать нажатия кнопок и рисовать на экране. С клавиатурой проблем нет: система отправляет сообщения MSG_KEYDOWN_KEY и MSG_KEYUP_KEY. А вот с графикой пришлось повозиться. Встроенные функции прошивки завязаны на внутренние ресурсы, поэтому необходимые инструменты для рисования пришлось писать с нуля.
🖼️ Работа с дисплеем и «грязными» зонами
UI-подсистема телефона использует принцип «грязных зон» (dirty rectangles). Вместо постоянной перерисовки всего экрана система хранит массив координат прямоугольников, которые изменились с момента последнего обновления. Это экономит процессорное время на ресурсоёмких операциях вроде альфа-смешивания.

Поэтому для рисования сначала нужно получить указатель на буфер кадра, затем «загрязнить» весь экран, нарисовав на нём полноэкранный прямоугольник, и уже поверх него выводить графику игры. Исходный код может показаться сложным, но после небольшого рефакторинга он становится похож на типичный код для встраиваемых систем.
void Paint(LoaderContext* context) {
LcdId lcd = { 0, 0 };
uint16* fb = ((uint16*(*)(LcdId* id)) LcdGetFrameBuffer)(&lcd); // Получаем указатель на буфер кадра для основного экрана
uint16 startEnd[4] = { 0, 0, 240, 320 }; // Координаты прямоугольника
((void(*)(LcdId* lcdId, uint32 start, uint32 end, uint16 col)) LcdDrawRectPtr)(&lcd, ((uint32*)&startEnd[0])[0], ((uint32*)&startEnd[0])[1], 0x0); // Рисуем полноэкранный прямоугольник
((void(*)()) LcdUpdateRect)(); // Обновляем область
}
🔤 Отрисовка текста и шрифтов
Далее я реализовал функцию вывода текста. Используются простейшие растровые шрифты 8x8, похожие на знакогенератор оригинального IBM PC. Каждый символ (глиф) хранится в виде 8 байт, где каждый бит представляет пиксель по оси Y. Нулевой бит означает прозрачный пиксель, единичный — пиксель, который нужно закрасить заданным цветом.
Алгоритм отрисовки символа:
int LcdDrawChar(LoaderContext* context, uint16* frameBuffer, char chr, uint32 x, uint32 y, uint16 color) {
if(x >= 0 && y >= 0 && x + FONT_WIDTH < LCD_WIDTH && y + FONT_HEIGHT < LCD_HEIGHT) {
int i, j;
unsigned char* glyph = (unsigned char*)(GLOBAL(context) + &embedded_font[chr * 8]);
for(i = 0; i < FONT_HEIGHT; i++) {
short* fb = &((short*)frameBuffer)[(y + i) * LCD_WIDTH + x];
for(j = 0; j < FONT_WIDTH; j++) {
if((*glyph >> (FONT_WIDTH - j)) & 0x1)
*fb = color;
fb++;
}
glyph++;
}
return true;
}
return false;
}
void LcdDrawString(LoaderContext* context, uint16* frameBuffer, char* str, uint32 x, uint32 y, uint16 color) {
if(x >= 0 && y >= 0) {
unsigned int i;
for(i = 0; i < strlen(str); i++) {
if(!LcdDrawChar(context, frameBuffer, str[i], x, y, color))
return; // Выход за границы экрана
x += FONT_WIDTH;
}
}
}
Вы могли заметить неочевидную операцию с указателем `glyph`. Поскольку наша программа представляет собой сырую склейку секций без релокаций, все обращения к глобальным переменным и константам — абсолютные. Прямое обращение к адресу 0x18 привело бы к чтению таблицы векторов прерываний и краху (HardFault). Поэтому для получения реального адреса переменной к нему прибавляется базовый адрес загрузки программы. Этот костыль можно обойти, добавив в конец программы информацию о релокациях, извлечённую из промежуточного ELF-файла, или даже реализовав самопатчинг «на лету».

Теперь мы можем вывести строку на экран:
LcdDrawString(context, fb, SCONST(context, "Ya lyublu AvtoVAZ"), 0, 0, 0xFFFFFF);
И получить результат:

🖼️ Отрисовка растровых изображений (битмапов)
Для «Змейки» текста недостаточно, нужна графика. Написать загрузчик для TGA или BMP несложно, но хотелось, чтобы программа была самодостаточной и содержала все ресурсы внутри себя. Для конвертации изображений я использовал специальный инструмент, преобразующий картинку в 16-битный формат 565 и затем в C-массив.
Процесс отрисовки изображения называется блиттингом — это копирование пикселей с одной поверхности (изображения) на другую (буфер кадра).
Обратите внимание: Кнопочный телефон с большим экраном, камерой, батареей больше чем у некоторых смартфонов..
К пикселям могут применяться дополнительные операции: поворот, масштабирование, альфа-смешивание и цветокоррекция.void LcdDrawBitmap(uint16* frameBuffer, short* bitmap, uint32 width, uint32 height, uint32 x, uint32 y) {
if(bitmap) {
int i, j;
short* bmp = bitmap;
// Медленная отладочная версия
for(i = 0; i < height; i++) {
for(j = 0; j < width; j++) {
LCD_PLOT_565(clamp(x + j, 0, LCD_WIDTH), clamp(y + i, 0, LCD_HEIGHT), bmp[i * width + j]);
}
}
}
}
Отрисовка изображения:
LcdDrawBitmap(fb, (short*)(GLOBAL(context) + (uint32)&lada_bmp), LADA_WIDTH, LADA_HEIGHT, 0, 0);
Почему бы не использовать структуру-дескриптор для изображения? Потому что это потребовало бы двойного разрешения указателей и усложнило бы код. И вот результат — тестовое изображение (да, я фанат АвтоВАЗа):

Помощь
🎮 Реализация игровой логики «Змейки»
«Змейка» — игра с простой логикой. Каждый уровень представляет собой сетку. Игровой цикл: каждые n миллисекунд происходит «тик», который перемещает игрока в текущем направлении. Если в момент тика нажата одна из кнопок-стрелок, направление меняется.

Змея представлена массивом сегментов, каждый из которых хранит свою позицию в сетке уровня.

Для движения змейки массив сегментов обходится в обратном порядке: каждый сегмент занимает позицию предыдущего. Позиция головы обновляется в соответствии с выбранным направлением.

Проверка «съедения» яблока: если координаты головы и яблока совпадают, увеличиваем счёт и перемещаем яблоко в новую позицию.
if(state->Segments[SEGMENT_HEAD].X == state->AppleX && state->Segments[SEGMENT_HEADER].Y == state->AppleY) {
state->Score++;
MoveApple(state);
}
🎲 Генерация случайных чисел для яблока
Для респавна яблока можно использовать либо заранее заданную таблицу координат (псевдослучайность), либо генератор псевдослучайных чисел (PRNG), который можно найти в прошивке.

С помощью дизассемблера удалось определить, что в прошивке используется линейный конгруэнтный генератор (LCG). Нейросетевые инструменты сильно помогают в реверсе, легко распознавая стандартные алгоритмы — от memcpy до программной растеризации треугольника.
unsigned int rand() {
int v0; // r3
int v1; // r4
int v2; // r1
v0 = MEMORY[0x4710E80] - 1;
v1 = *(_DWORD *)(4 * MEMORY[0x4710E84] + 0x4710E88) + *(_DWORD *)(4 * MEMORY[0x4710E80] + 0x4710E88);
*(_DWORD *)(4 * MEMORY[0x4710E84] + 0x4710E88) = v1;
MEMORY[0x4710E80] = v0;
v2 = MEMORY[0x4710E84] - 1;
if ( v0 >= 0 ) {
--MEMORY[0x4710E84];
if ( v2 < 0 )
MEMORY[0x4710E84] = 54;
} else {
--MEMORY[0x4710E84];
MEMORY[0x4710E80] = 54;
}
return (unsigned int)(2 * v1) >> 1;
}
Игра заканчивается, если голова сталкивается с телом или выходит за границы поля. Итоговый размер собранного приложения — всего 5 КБ 644 байта! Демонстрация работы:

🏁 Заключение и призыв к сообществу
Вот такой интересный проект по моддингу бюджетного кнопочника у нас получился. Мы перешли от теории к практике, создав первые рабочие программы. Возможно, они не самые полезные в быту, но для технического энтузиаста важен сам процесс и то удовлетворение, которое он приносит.

Это приносит невероятное моральное наслаждение.
А что ещё нужно в 23 года? Чтобы «мотор тянул», а код реверсился легко! Исходный код и всё необходимое для установки загрузчика доступны в моём репозитории на GitHub.
Если вам интересны ремонт, моддинг и программирование для гаджетов прошлых лет — подписывайтесь на мой Telegram-канал «Клуб фанатов балдежа», где я публикую бэкстейджи статей, анонсы и полезные материалы. Видеоверсии некоторых проектов есть на моём YouTube-канале.
Важное обращение к сообществу! Я уверен, что статью читают выходцы с форумов моддеров и, возможно, люди, связанные с прошивочными боксами. Если у вас есть исходный код или объектные файлы для телефонов Siemens (платформы S-Gold или E-Gold) и вы готовы помочь общему делу сохранения знаний — пожалуйста, напишите мне в Telegram. Гарантирую полную анонимность и интересный контент на основе этих материалов.
Очень важно! Разыскиваются устройства для будущих статей!
Друзья! Если вам понравилась эта статья, объявляю розыск телефонов и консолей для будущих материалов! В 2000-х китайцы часто выпускали дешёвые телефоны с игровым уклоном — с подобием джойстика или кнопками A/B сверху, иногда с предустановленными эмуляторами NES/Sega. На таких устройствах можно выполнять нативный код и портировать новые эмуляторы, о чём я планирую написать. Если у вас есть подобный телефон и вы готовы его передать — пишите в Telegram (@monobogdan) или в комментарии. Также интересуют смартфоны-консоли на Android (например, Func Much-01) и старые подделки под брендовые смартфоны (2010-2014 гг.), которые часто работают на интересных чипсетах. Ищу первые смартфоны Xiaomi, Meizu на Exynos и телефоны на Linux (Motorola EM30, RAZR V8 и др.), которые были весьма мощными для своего времени и способны, например, запустить Quake! Всем спасибо за поддержку!

А ещё я храню все свои проекты у одного облачного провайдера — Timeweb. Поэтому смело рекомендую то, чем пользуюсь сам.
Больше интересных статей здесь: Гаджеты.
Источник статьи: Самая сложная «Змейка»: Как я отреверсил и хакнул кнопочный телефон, чтобы написать для него классическую игру.