Всем привет!

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

Итак, имеем видеорегистратор FineVu LX5000 power. В поисках русской или английской прошивки перерыл некоторое количество сайтов, но все безуспешно.

На сайте производителя мне попадалась информация что для продуктов для рынка Кореи, прошивки с другими языками не предоставляются.

Скачал с офф сайта Корейскую прошивку. Потом заметил что у офф сайта есть US версия, подумал что там тоже могут быть подобные видеорегистраторы. Нашел видеорегистратор который выглядит примерно так же как мой, это был GX5000. Скачал прошивку от него и начал изучать.

Прошивка с расширением файла .bin легко открылась архиватором 7-zip, внутри была структура папок Linux системы.

Структура папок в прошивке
Структура папок в прошивке

Начал я искать папку с программой, которая рулит видеорегистратором. Нашел в /mnt/blackbox. В этой директории мне попался файл git-revision в котором был указан автор коммитов, попытался написать ему, но ответа нет по сей день. Ладно, продолжаем, в этой же директории нашел картинки, очень много картинок, прошивка занимает 80Мб, и львиная часть этого объема это картинки и звуки. Я так понял что инженеры компании производителя вообще не парились, и весь интерфейс нарезали картинками, даже информационные сообщения в картинках, вот пример:

Как вы помните, до этого я же скачал английскую прошивку от видеорегистратора GX5000, в этой прошивке оказались почти такие же картинки, но уже с англ. надписями. Вытащил картинки и звуки, из каждого файла, и начал сравнивать, оказалось что большая часть картинок переведена, однако были и особенные пункты меню и кнопки, которые остались не переведенные, с этим помог переводчик. С помощью других сервисов определил что шрифт на картинках очень поход на Roboto Medium. Благодаря этому удалось сделать наиболее похожие к родным картинки с кнопками и пр.

Дальше встал вопрос, как собрать прошивку с обновленными файлами, пробовал разные варианты с форматом iso, но все безуспешно. Решил что обратно все соберу просто заменой файлов в исходном файле. Попросил ChatGPT написать мне простенький скрипт на Python, который ищет последовательность байт в исходном файле, и заменяет нужной последовательностью байт. Если проще, то ищет в.bin файле картинку и заменяет ее английской версией картинки. Скрипт работал плохо, и пришлось его немного допилить. Тут же я увидел что некоторые картинки получились больше весом, поэтому они перетирают байты следующих файлов. Сначала сделал так: если файл с англ. текстом в байтах больше, чем файл с родным, корейским текстом, то просто сдвину байты в.bin файле. Но потом отказался от этой идеи, из за того что в прошивке есть симлинки, и даже если для симлинков это не страшно, есть еще причины почему лучше было по-максимуму сохранить структуру файла, но об этом позже. Как обходной вариант, придумал что могу ужать png и wav а разницу заменить нулями. Так и сделал. Пришлось повозиться конечно чтобы все заменяемые файлы были меньше, сложнее всего было с wav, пришлось немного порезать продолжительность дорожки.

import os

def replace_and_adjust_bytes(file1_path, file2_path, file3_path):
    try:
        # Read bytes from file1
        with open(file1_path, 'rb') as file1:
            file1_bytes = file1.read()

        # Read bytes from file2
        with open(file2_path, 'rb') as file2:
            file2_bytes = file2.read()

        # Read bytes from file3
        with open(file3_path, 'rb') as file3:
            file3_bytes = bytearray(file3.read())

        # Find the starting index of the file1 bytes in file3
        start_index = file3_bytes.find(file1_bytes)

        if start_index == -1:
            print(f"Error: File 1 bytes not found in {file3_path}.")
            return

        # If file2 is larger than file1, we need to shift the bytes in file3
        if len(file2_bytes) > len(file1_bytes):
            extra_bytes = len(file2_bytes) - len(file1_bytes)
            # Shift bytes in file3 after the found sequence to accommodate extra bytes from file2
            file3_bytes = file3_bytes[:start_index] + file2_bytes + file3_bytes[start_index + len(file1_bytes):]
            print(f"WARNING! file2 {file2_path} more than file1 {file1_path} make shift!")
        else:
            # If file2 is smaller, replace and pad the difference with zeros
            padding = len(file1_bytes) - len(file2_bytes)
            file3_bytes[start_index:start_index + len(file1_bytes)] = file2_bytes + b'\x00' * padding
            #print(f"File2 {file2_path} less than file1 {file1_path} make zeroes!")

        # Write the modified content back to file3
        with open(file3_path, 'wb') as file3:
            file3.write(file3_bytes)

        #print(f"Successfully replaced bytes of {file1_path} with {file2_path} in {file3_path}")

    except Exception as e:
        print(f"An error occurred while processing {file1_path}: {e}")

def recursive_file_process(folder1, folder2, file3):
    for dirpath1, _, filenames1 in os.walk(folder1):
        # Generate corresponding folder2 path
        relative_path = os.path.relpath(dirpath1, folder1)
        dirpath2 = os.path.join(folder2, relative_path)

        for filename in filenames1:
            # Full paths for file1 and file2
            file1_path = os.path.join(dirpath1, filename)
            file2_path = os.path.join(dirpath2, filename)

            # Only proceed if the file exists in both folder1 and folder2
            if os.path.exists(file2_path):
                replace_and_adjust_bytes(file1_path, file2_path, file3)
            else:
                print(f"Skipping {filename}: File not found in {dirpath2}.")

# Example usage
folder1 = 'C:\\temp\\blackbox lx5000 orig' #Директория с родными Корейскими картинками
folder2 = 'C:\\temp\\blackbox lx5000' #Директория с англ. картинками
file3 = 'LX5000PWR_FW.bin'  # This is the single file in the folder with the script

recursive_file_process(folder1, folder2, file3)

Дальше нужно было понять что в файле с прошивкой отвечает за версию и есть ли подписи, CRC или что-то такое. Открыл .bin файл в hex-editor и обнаружил в первых байтах название регистратора, и версию прошивки, сразу поднял версию прошивки.

Начало файла оригинальной прошивки
Начало файла оригинальной прошивки

Попробовал закинуть прошивку в видеорегистратор, он отказался принимать прошивку. Предположил что скорее всего какая то контрольная сумма поменялась, или я в процессе замены картинок что то еще зацепил. Продолжил изучение дальше, ушел в конец файла, там были 10 байт чем то заняты, возможно подпись или CRC подумал я. Как оно вычисляется я тогда не знал. Но файлы внутри прошивки то мне видны, начал искать что-то вроде updater или upgrader. Нашел несколько файлов sh и исполняемых, по sh вышел на файл bbupgrade, закинул его в ida, декомпилировал в псевдокод, и начал смотреть.

Методы которые представляли интерес
Методы которые представляли интерес

Увидел там и проверку подписи и проверку файла.

Благодаря выводам в консоль, намного проще разобраться что происходит на каждом этапе
Благодаря выводам в консоль, намного проще разобраться что происходит на каждом этапе

В очередной раз обратился к ChatGPT, попросил переписать этот псевдокод в c++, он переписал, но программа не работала. Как и сказал сам ChatGPT, псевдокод был для POSIX систем - там были методы которые есть только в linux. Не стал я искать чем заменить эти вызовы, просто перешел в linux, запустил код и подсунул ему оригинальный файл с прошивкой, код не сработал, потому что ChatGPT не учел некоторые касты, ну и вообще перемудрил. В общем немного посидел с напильником, код успешно запустился и выдал сообщение, что файл валидный.

#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <cerrno>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <regex.h>
#include <string.h>
using namespace std;

int FWUpgradeFileAnalyze(const char *a1, const char *a2)
{
    int fd;
    ssize_t bytesRead;
    struct stat buf;
    unsigned char *ptr = nullptr;
    int versionNew;
    int s2[] = {-953237502, 1437226410, 44717482};
    __int16_t v22 = 11968;
    char s[128] = {0};
    char name[128] = {0};
    int currentVersion = 100000;
    unsigned char checksum;

    // Create the file path

    // Check if the file exists
    if (stat(a1, &buf) >= 0)
    {
        printf("[Firmware Check] %s - %ld\n", a1, buf.st_size);

        // Allocate memory for the file content
        ptr = (unsigned char *)malloc(buf.st_size);
        if (ptr)
        {
            memset(ptr, 0, buf.st_size);

            // Open the file
            fd = open(a1, O_RDONLY);
            if (fd >= 0)
            {
                // Read the file
                bytesRead = read(fd, ptr, buf.st_size);
                if (bytesRead == buf.st_size)
                {
                    close(fd);

                    // Validate the file content
                    if (strlen((const char *)ptr) == 9 && strcmp((const char *)ptr, "LX5000PWR") == 0)
                    {
                        checksum = 255;
                        for (int i = 0; i < buf.st_size - 1; ++i)
                        {
                            checksum += ptr[i];
                        }

                        printf("[Firmware Check] checksum=%d checkF=%d\n", (unsigned char)~checksum, ptr[buf.st_size - 1]);

                        if ((unsigned char)~checksum == ptr[buf.st_size - 1])
                        {
                            // Calculate the new version from the file content
                            versionNew = 10 * (10 * (10 * (10 * (10 * (ptr[16] - '0') + ptr[18] - '0') + ptr[19] - '0') + ptr[21] - '0') + ptr[22] - '0') + ptr[23] - '0';

                            printf("[Firmware Check] versionNew=%02x\n", versionNew);

                            if (versionNew >= currentVersion)
                            {
                                printf("[Firmware Check] OK(%s, %s(new:%d, curr:%d), %02x)\n",
                                       (const char *)ptr, (const char *)ptr + 16, versionNew, currentVersion,
                                       ptr[buf.st_blksize - 1]);
                                free(ptr);
                                return 1;
                            }
                            else
                            {
                                printf("[Firmware Check] Fail(%s, %s(new:%d, curr:%d), %02x)\n",
                                       (const char *)ptr, (const char *)ptr + 16, versionNew, currentVersion,
                                       ptr[buf.st_blksize - 1]);
                            }
                        }
                        else
                        {
                            printf("[Firmware Check] Checksum Fail(%s, %s, %02x)\n",
                                   (const char *)ptr, (const char *)ptr + 16, ptr[buf.st_blksize - 1]);
                        }
                    }
                    else
                    {
                        printf("[Firmware Check] Sig Fail:");
                        for (int j = 0; j <= 14; ++j)
                            printf("%02x:%02x ", ((unsigned char *)s2)[j], ptr[buf.st_size - 16 + j]);
                        putchar('\n');
                    }
                    free(ptr);

                    puts("[Firmware Check] Fail.");
                    sync();
                    return 0;
                }
                else
                {
                    int err = errno;
                    printf("[Firmware Check] read fail:%s, error:%d\n", s, err);
                    free(ptr);
                    close(fd);
                    return 0;
                }
            }
            else
            {
                printf("[Firmware Check] open fail:%s\n", s);
                free(ptr);
                return 0;
            }
        }
        else
        {
            int err = errno;
            printf("[Firmware Check] malloc fail:%d\n", err);
            return 0;
        }
    }
    else
    {
        int err = errno;
        printf("[Firmware Check] stat fail:%s, error:%d\n", s, err);
        return 0;
    }
}

int main(int argc, char *argv[])
{

    FWUpgradeFileAnalyze("/opt/fw/LX5000PWR_FW_my.bin", "");
    return 1;
}

int FWUpgradeFileCheck(int a1)
{
    return -1;
}

int sub_10FA4(char *a1, char *a2)
{
    size_t v3;
    char *s;
    char *pattern;
    size_t v7;
    regmatch_t pmatch;
    regex_t preg;

    pattern = a1;
    s = a2;
    v7 = strlen(a2);

    if (regcomp(&preg, pattern, REG_EXTENDED) != 0) // Using REG_EXTENDED for flag '3' in regcomp
        return 0;

    v3 = strlen(s);

    if (regexec(&preg, s, 1, &pmatch, 0) != 0) // Match single occurrence
    {
        regfree(&preg);
        return 0;
    }
    else
    {
        regfree(&preg);
        return 1;
    }
}

Отлично, подумал я, уже финиш в одном метре от меня. Подсунул модифицированный файл с прошивкой, внутри которого были английские картинки, программа выдала мне что файл не валидный, и отобразила какая контрольная сумма должна быть в последнем байте, поменял в прошивке байт на указанный, запустил снова с++ код, программа выдала сообщение, что файл валидный. Супер.

Довольный я принялся к установке прошивки в видеорегистратор. Закинул файл на флешку, запустил регистратор, и начал ждать... Первым появилось сообщение о наличии новой прошивки(если не совпадает сумма, подпись, да даже название файла, регистратор просто удаляет прошивку, и никаких сообщений не появляется), это был хороший знак, через несколько секунд начинается процесс установки прошивки. 2%...4%...6%...8% и за секунду счетчик с 8% перепрыгивает на 100% и видеорегистратор перезагружается...

Процесс обновления прошивки
Процесс обновления прошивки

Через несколько секунд он включается, и.... Все осталось по-корейски. Не получилось.

Предупреждение при запуске видеорегистратора
Предупреждение при запуске видеорегистратора

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

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

Разница в оригинальной и модифицированной прошивке
Разница в оригинальной и модифицированной прошивке

Тут я уже понимал что 99% что проблема в этом сдвиге. Исправляю эту нелепую ошибку, закидываю прошивку на флэшку и включаю видеорегистратор. Начался процесс прошивки 2%...4%...6%...8%...10%...12%.... доходит до 100%, видеорегистратор перезагружается, иии...

Предупреждение при запуске видеорегистратора на англ. языке
Предупреждение при запуске видеорегистратора на англ. языке

Все получилось, теперь у видеорегистратора интерфейс англофицирован.

Меню на английском языке
Меню на английском языке

Спасибо за внимание!

P.S. Никого не призываю к реверс инжинирингу, и проделыванию подобных манипуляций с техникой. Все что вы делаете, вы делаете на свой страх и риск.

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


  1. arx3889
    01.11.2024 21:34

    7zip не просто так открывает этот bin-фай -- он там находит образ ФС ext (это видно, если открыть Файл-Свойства)

    В среде Linux. Берем binwalk:

    $ binwalk GX5000_FW_3.bin 
    
    DECIMAL       HEXADECIMAL     DESCRIPTION
    --------------------------------------------------------------------------------
    96            0x60            JPEG image data, JFIF standard 1.01
    126           0x7E            TIFF image data, big-endian, offset of first image directory: 8
    497572        0x797A4         device tree image (dtb)
    685320        0xA7508         CRC32 polynomial table, little endian
    709583        0xAD3CF         Android bootimg, kernel size: 2037543936 bytes, kernel addr: 0x206F7420, ramdisk size: 1684104562 bytes, ramdisk addr: 0x72617020, product name: "ash read :offset %x, %d bytes %s"
    711052        0xAD98C         Copyright string: "Copyright (C) 2010 Charles Cazabon."
    988256        0xF1460         device tree image (dtb)
    1097824       0x10C060        Android bootimg, kernel size: 4251328 bytes, kernel addr: 0x40008000, ramdisk size: 22 bytes, ramdisk addr: 0x41000000, product name: "sun8i_arm"
    1099872       0x10C860        Linux kernel ARM boot executable zImage (little-endian)
    1106104       0x10E0B8        LZO compressed data
    1106488       0x10E238        LZO compressed data
    1107092       0x10E494        device tree image (dtb)
    3483003       0x35257B        Certificate in DER format (x509 v3), header length: 4, sequence length: 4736
    3829095       0x3A6D67        MySQL MISAM index file Version 1
    4949254       0x4B8506        mcrypt 2.5 encrypted data, algorithm: "", keysize: 20720 bytes, mode: "E",
    5353568       0x51B060        Linux EXT filesystem, blocks count: 18944, image size: 19398656, rev 2.0, ext4 filesystem data, UUID=57f8f4bc-abf4-655f-bf67-946fc0f9c0f9
    ...

    Смотрим смещение, по которому начинается ФС. Далее, монтируем:

    $ sudo mount -oloop,offset=5353568 GX5000_FW_3.bin /mnt

    И можем модифицировать содержимое.

    Это должно сильно упростить моддинг. Ваш шаг с корректировкой контрольных сумм, скорее всего, так же потребуется.


    1. whind Автор
      01.11.2024 21:34

      Спасибо, буду иметь в виду


      1. arx3889
        01.11.2024 21:34

        На самом деле проблему здесь я вижу не сколько в сложности всей этой возни (чатГПТ до кучи -- просто вишенка), а в том, что так патчить образы файловых систем нельзя. Вам повезло, что файлы маленькие а файловая -- ext4, которая "не фрагментируется" (нет). Вообще говоря, гарантий, что весь перетираемый файл записан последовательно, нет. Да и добивание нулями в конце тоже не всегда может быть безопасно, если это не png или wav, как в вашем случае.