Осторожно: в данной серии статей я рассказываю о реверс-инжиниринге и хакинге простых кнопочных звонилок. Цель простая: расширить скудный функционал телефонов ценой до 1 000 рублей и сделать их привлекательной моддинг-платформой для самых разных гиков. Если вы когда-нибудь слышали про эльфы и патчи, и вам интересно узнать, как происходит процесс взлома и изучения прошивок, а также написания новых программ для кнопочников — приглашаю вас под кат!

❯ Предыстория

Недавно я познакомился с Ilya_ZX, человеком-легендой в моддинг сцене телефонов из нулевых. Илья рассказал мне забавную историю: ещё будучи студентом, он увидел как одногруппник играет на своём LG G1800 в легендарную мобильную игру нулевых — ‭«Змейку‭». Его тогдашний Siemens A60 не умел ничего кроме игрушки Stack Attack, даже Java-игры не поддерживались, а молодому парню очень хотелось сыграть в Змейку на скучных парах!

Казалось бы, на дворе 2005 год, можно просто пойти на рынок и купить уже изрядно подешевевшую Б/У 3310 и поиграть в ‭«трушную‭» змейку именно там. Но Илья был не просто студентом технаря, он с юности интересовался программированием, реверс-инжинирингом и телефонами! И он решил поспорить с одногруппником — сможет ли он реализовать Змейку на своём A60? Всего за один месяц он умудрился исследовать прошивку телефона на диковинной процессорной архитектуре, найти необходимые функции для работы с дисплеем, вводом и окнами и написать ту самую змейку. Попробуйте теперь представить лицо его одногруппника, который проиграл спор молодому реверсеру :)

Сначала Илья написал игру на Паскале для самопального ‭«симулятора‭» A60, а затем переписал её на ассемблере для C166s!
Сначала Илья написал игру на Паскале для самопального ‭«симулятора‭» A60, а затем переписал её на ассемблере для C166s!

На момент написания статьи мне 23 года, я лишь чуточку старше тогдашнего Ильи. После рассказанной истории, я подумал ‭«А чем я хуже?‭» и принялся реверсить прошивку бюджетного кнопочника 10-й давности - Explay B240. В прошлой статье, мы с вами проделали первые шаги по хакингу телефона: загрузка прошивки в IDA Pro и поиск системных функций, хакинг файлового менеджера для запуска программ с MicroSD-флэшки, разработка загрузчика исполняемых файлов и организация таблицы функций. В целом, это весьма неплохая поучительная статья для новичков в реверс-инжиниринге.

Однако итоговый результат в виде заливки экрана желтым цветом может показаться незначительным. Поэтому в сегодняшней статье мы с вами напишем первую действительно полезную программу!

❯ «Змейка»

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

Наглядная демонстрация работы
Наглядная демонстрация работы

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

__attribute__((section(".main")))
int WindowProc(LoaderContext* context, int window, int msgId, int dparam)
{
	switch(msgId)
    {
      case MSG_CREATE:
        state = (CState*)malloc(sizeof(CState));
        break;
      case MSG_CLOSE:
          free(state);
          break;
    }
	
	return 1;
}

Для реализации змейки, нам необходимо уметь обрабатывать кнопки и рисовать что-то на дисплей. С кнопками проблем никаких не возникает: система шлёт сообщения типа MSG_KEYDOWN_KEY и MSG_KEYUP_KEY на каждое событие с клавиатурой. А вот с графикой чуточку сложнее: поскольку встроенные в прошивку функции завязаны на работу с вшитыми ресурсами, мы напишем нужные функции сами.

UI-подсистема телефона реализована по принципу ‭«грязных зон‭». Вместо перерисовки всего экрана, система хранит массив с координатами прямоугольников, где что-то изменилось с момента прошлой перерисовки: это позволяет сэкономить такты процессора на тяжелых операциях по типу альфа-блендинга.

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

void Paint(LoaderContext* context)
{
	LcdId lcd = { 0, 0 };
	uint16* fb = ((uint16*(*)(LcdId* id)) LcdGetFrameBuffer)(&lcd); // Get framebuffer for primary screen

	uint16 startEnd[4] = { 0, 0, 240, 320 }; // Rect
	((void(*)(LcdId* lcdId, uint32 start, uint32 end, uint16 col)) LcdDrawRectPtr)(&lcd, ((uint32*)&startEnd[0])[0], ((uint32*)&startEnd[0])[1], 0x0); // Draw fullscreen rectangle
	
	((void(*)()) LcdUpdateRect)(); // Update rect
}

Далее я реализовал функцию для отрисовки текста на экране. Шрифты самые примитивные — 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; // Out of screen

			x += FONT_WIDTH;
		}
	}
}

Наверняка вы заметили страшный костыль в локальной переменной glyph с арифметикой над указателями. Дело в том, что на момент написания статьи, программа представляет из себя сырую склейку секций .text, .data, .bss и .rodata, поэтому на данный момент в ней нет релокаций, которые помогли бы сделать программу перемещаемой в памяти. В arm-none-eabi все вызовы функций без явного указателя — относительные, но при этом обращения к глобальным переменным и константам (например, строковым литералам) — абсолютные. Если попытаться напрямую использовать глобальную переменную по адресу 0x18 — программа будет пытаться читать или портить память в таблице векторов прерываний, что неизбежно приведет к HardFault. Поэтому для получения настоящего адреса переменной, к ней необходимо прибавить базовый адрес загрузки программы:

#define GLOBAL(ctx) (uint32)(*ctx->WindowFunc)

unsigned char* str = (unsigned char*)(GLOBAL(context) + "Hello world!");

Этот костыль можно избежать, если в конец программы дописать сведения о релокациях, которые можно вытянуть путем парсинга промежуточного эльфа, а при особом желании — можно сделать так, что программа сама себя будет патчить ‭«на лету‭»!

Далее мы рисуем нашу строку с текстом:

LcdDrawString(context, fb, SCONST(context, "Ya lyublu AvtoVAZ"), 0, 0, 0xFFFFFF);

И получаем следующий результат:

Для змейки, если она не ASCII, этого всё равно мало. Поэтому нам нужна функция для вывода картинок на дисплей. Написать загрузчик 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;

		// Slow debug version
		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-миллисекунд вызывается один игровой тик, который двигает игрока в текущем выбранном направлении. Если в момент тика нажата одна из кнопок-стрелок — направление движения меняется — тут всё очевидно:

    switch(msgId)
	{
		case MSG_KEYDOWN_LEFT:
			gameState->Movement = MOVEMENT_LEFT;
			
			break;
		case MSG_KEYDOWN_RIGHT:
			gameState->Movement = MOVEMENT_RIGHT;
			
			break;
		case MSG_KEYDOWN_UP:
			gameState->Movement = MOVEMENT_UP;
			
			break;
		case MSG_KEYDOWN_DOWN:
			gameState->Movement = MOVEMENT_DOWN;
			
			break;
	}

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

typedef struct {
  uint32 X;
  uint32 Y;
} CSnakeSegment;

...

#define MAX_SEGMENTS 32
CSnakeSegment Segments[MAX_SEGMENTS];
int SegmentLength;

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

    for(int i = state->SegmentLength - 1; i > 0; i--)
	{
		state->Segments[i].X = state->Segments[i - 1].X;
		state->Segments[i].Y = state->Segments[i - 1].Y;
	}
	
	if(state->Movement == MOVEMENT_UP)
		state->Segments[SEGMENT_HEAD].Y--;
	
	if(state->Movement == MOVEMENT_LEFT)
		state->Segments[SEGMENT_HEAD].X--;
	
	if(state->Movement == MOVEMENT_RIGHT)
		state->Segments[SEGMENT_HEAD].X++;
	
	if(state->Movement == MOVEMENT_DOWN)
		state->Segments[SEGMENT_HEAD].Y++;

Для того, чтобы проверить скушали ли мы яблочко — достаточно сравнить координаты головы и объекта. Если они идентичны, то прибавляем очко и переносим яблоко на другую позицию:

  if(state->Segments[SEGMENT_HEAD].X == state->AppleX && state->Segments[SEGMENT_HEADER].Y == state->AppleY)
  {
    state->Score++;
    MoveApple(state);
  }

Для респавна яблочка можно использовать два подхода: параметрические таблицы с заранее прописанными координатами объекта (эдакая псевдослучайность) и обычный PRNG-генератор, который путем реверса можно найти в прошивке.

Благодаря дипсику удалось определить, что в прошивке используется LCG. Вообще, нейронки очень сильно помогают при реверсе и могут легко анализировать стандартные алгоритмы: я тестировал от простых по типу memcpy, до относительно сложных как например программное деление по модулю и программная растеризация треугольника по Scanline-алгоритму.

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;
}
void MoveApple(CGameState* state)
{
	state->AppleX = RandMax(LCD_WIDTH / (GRID_SIZE - 1));
	state->AppleY = RandMax(LCD_HEIGHT / (GRID_SIZE - 1));
}

Если же голова оказывается в одном из сегментов или же за полем — игра окончена. Полный вес собранного приложения - 5 килобайт 644 байта! А ниже - демонстрация его работы:

❯ Заключение

Вот такой интересный моддинг бюджетного кнопочника у нас с вами сегодня получился. От слов к делу мы с вами написали первые действительно рабочие программы, которые может быть и не самые полезные, но поверьте, для гика важен сам процесс и объём получаемого от этого эндорфина...

Это приносит невероятное моральное наслаждение
Это приносит невероятное моральное наслаждение

А что ещё нужно парню в 23 года? Правильно: чтобы мотор бодро тянул любимую десятку и чтобы реверсилось всё легко и понятно! Исходный код и все что необходимо для установки бинлоадера есть в на моем гите.

А если вам интересна тематика ремонта, моддинга и программирования для гаджетов прошлых лет — подписывайтесь на мой Telegram-канал ‭«Клуб фанатов балдежа‭», куда я выкладываю бэкстейджи статей, ссылки на новые статьи и видео, а также иногда выкладываю полезные посты и щитпостю. А ролики (не всегда дублирующие статью) можно найти на моём YouTube канале.

Важно: друзья! Я уверен, что статью будут читать выходцы с форумов моддеров и возможно даже ребята, связанные с прошивочными боксами. Если у вас есть исходный код или объектные файлы для телефонов 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-канале 

Опробовать ↩

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

Комментарии (18)


  1. bodyawm Автор
    07.06.2025 14:17

    Ну что друзья, интересен ли вам контент на тему реверса и диковинного программирования? Если да, то будем развивать рубрику в сторону портирования загрузчика любой современный кнопочный телефон до 2к


    1. bodyawm Автор
      07.06.2025 14:17

      Я интереса ради спросил у фплюс на почте не хотели бы они выпустить телефончик с загрузчиком программ, который я для них абсолютно бесплатно готов разработать (ради гиковского коммьюнити и поддержания интереса к моддингу совсем бюджетных кнопочников). Вот уж не знаю, был бы резонанс если бы вышел кнопарь за 1200 с сдк?


      1. bodyawm Автор
        07.06.2025 14:17

        А контент план идет по плану. На следующей неделе опубликую статью об интересном телефоне, который тоже поддается моддингу.


      1. MaFrance351
        07.06.2025 14:17

        Не уверен, заинтересуется ли кто-то кроме нескольких гиков вроде нас, но за 1200 я бы купил...


        1. bodyawm Автор
          07.06.2025 14:17

          Я б в продвижение несколько статей написал бы) Уверен, что они набрали бы под 200+


          1. MaFrance351
            07.06.2025 14:17

            Вот интересно: а девайсы на чипсете как в статье ещё продаются? Понятно, что штука древняя, но нынешние бюджетные кнопочники тоже как будто навеки застряли в начале десятых, так что, может, остались какие-то старые запасы.


            1. bodyawm Автор
              07.06.2025 14:17

              На 6500 и 6530 нет. На 6531 - да и прошивка почти идентичная, только сжата. Так что пропатчить ее не составит труда)


      1. NutsUnderline
        07.06.2025 14:17

        в свое время, как раз мне было примерно 23-25 обращались мне за тем чтобы пореверсить прошивку для видеореристраторов, руссифицировать, а я все что тут написано не умел и не хотел.

        а хотел и немного уже умел я к той поре чтобы у navitel был сервис обмена координат между контактами, на что получил ответ "а вам зачем" и нафиг не надо. Прошло чуть меньше 20 лет и стало ой как надо

        полагаю что на идеи все сами умные, и заимствовать сильно любят (у apple) ищут только реализаторов на идеи эти, любят чтобы уже готовое... разве что колуарно договариватьсяб да и то я прям не верю чтобы кто из fplus еще и китайцев попросил опуьликовать что то из nda, выгода вообще непонятная.

        собственно конопарь для разработчика нужен очень ограниченному числу людей, я знаю только один пример, который не взлетел - xiadow phone (у меня есть этот набор), причем сделали не плохо, и спокойно забыли


        1. bodyawm Автор
          07.06.2025 14:17

          У F+ возможно есть исходный код прошивок для кастомизации)


        1. Wok_u3_cBuHuHbl
          07.06.2025 14:17

          собственно конопарь для разработчика нужен очень ограниченному числу людей, я знаю только один пример

          А как же та кулстори, где парень в ВК спрашивал по поводу J2ME, и по итогу оказалось, что он в тюрьме на кнопочном телефоне прогал?


          1. NutsUnderline
            07.06.2025 14:17

            очень ограниченному числу людей

            тут он тоже немного .. ограничен


  1. TaskForce141
    07.06.2025 14:17

    После прочтения заголовка первая мысль была про Snakes для Symbian.


    1. bodyawm Автор
      07.06.2025 14:17

      Очень годная и интересная пол капотом игра. Это тест софтоварной реализации OpenGLES, которую в те годы только только добавили в Symbian. До этого все писали свои софтрендеры


    1. ivantgam
      07.06.2025 14:17

      Спасибо, разблокировали старое воспоминание :)


  1. bolk
    07.06.2025 14:17

    «Хабр» — торт, продолжай, пожалуйста! Каждый раз читаю с огромным удовольствием!


    1. bodyawm Автор
      07.06.2025 14:17

      Спасибо большое) очень стараюсь)


  1. tormozedison
    07.06.2025 14:17

    А вы лазили в караоке-систему Leadsinger на картриджах? Просто внезапно возникло подозрение, что там каким-нибудь боком применены те или иные наработки от NES или Sega.


    1. bodyawm Автор
      07.06.2025 14:17

      Неа)