Войти
ФлеймФорумПрограммирование

Дизассемблер IDA Pro 7.5 для восстановления исходного кода игры (C/C++) (7 стр)

Advanced: Тема повышенной сложности или важная.

Страницы: 16 7 8 913 Следующая »
#90
18:29, 30 июля 2021

Настало время, когда я избавился от всех функций игры, заменив их на отреверсенные. Кроме этих (CLIB):

4 | Дизассемблер IDA Pro 7.5 для восстановления исходного кода игры (C/C++)

Было принято решение собрать чистый проект с нуля, из полученных исходников игры. Игра пошла!

Только был один баг, связанный с хардкодом. Hex-Rays в цикле вместо указателя на память сделал константу:

hardcode | Дизассемблер IDA Pro 7.5 для восстановления исходного кода игры (C/C++)

Делается цикл, где итератор - есть адрес, с помощью которого идёт обращения к ячейкам памяти (подчёркнуто красным).

Это привело к артефактам в игре :  если сместить сегмент данных на другой адрес, то вершины объектов в игре были некорректными, текстуры отсутствовали. Всё распадалось на чёрные треугольники.

Переводим 2602400 в 16-ричное и получаем 0x27B5A0.  Смотрим MAP-файл что там:

2 | Дизассемблер IDA Pro 7.5 для восстановления исходного кода игры (C/C++)

Смотрим ещё в IDA  - это массив :

3 | Дизассемблер IDA Pro 7.5 для восстановления исходного кода игры (C/C++)

Я сразу понял в чём дело. С помощью Notepad++ и поиска по регулярному выражению нашёл в коде программе все "семизнаки":

5 | Дизассемблер IDA Pro 7.5 для восстановления исходного кода игры (C/C++)

Часть из них я ранее исправил, но остался этот один  незамеченым. 

Тут только автоматизировать поиск-замену:  выхлоп вышел более 20 000 строчек на Си. И это только то, что нужно.  На самом деле,  он был больше.

Теперь, отпочковавшись окончательно от бинарника игры, нужно прикручивать музыку-звук на свой исходник.


#91
(Правка: 3:56) 3:52, 1 авг. 2021

Написал свои функции для воспроизведения музыки и звуков в игре:

void MY_OpenMusic(unsigned int a4);
void MY_FreeMusic(void);
void MY_StartMusic(__int16 a1, unsigned __int8 a2);
void MY_StopMusic(void);

void MY_OpenSound(void);
void MY_FreeSound(void);
char MY_PlaySample(unsigned a1, unsigned a2, unsigned a3, unsigned a4, unsigned a5, unsigned a6, unsigned a7);
void MY_FadeOutSample(short a1, short a2, short a3, short a4);
void MY_FadeInSample(short a1, short a2, short a3, short a4);
void MY_SetSamplePitch(short a1, short a2, short a3);
void MY_SetSamplePan(short a1, short a2, unsigned short a3);

Музыка играется рутиной плеера HMP, о которой я уже писал в этом посте:
https://gamedev.ru/flame/forum/?id=262348&page=6&m=5425328#m89

Со звуками пришлось повозиться.  Сделал своё микширование каналов, поддерживающее изменение тона, громкости; зацикливание. Движок игры уникальный, пришлось реализовывать поведение функций точно, как в игре.

Завёл всё это на SoundBlaster16. Благо, наработанного кода со времён DOS'а у меня много :)

Игра вышла 1-в-1, как оригинал.

Остался всего один шаг, перед портированием на винду/линукс (SDL) - преобразовать сегменты данных:  они сейчас на ассемблере и описаны как байты-полуслова и слова.  И линкуется всё в один блоб:

1 | Дизассемблер IDA Pro 7.5 для восстановления исходного кода игры (C/C++)

В принципе, можно оставить как есть, но тогда под каждый компилятор придётся править АСМ в синтаксис того тулчейна, под который идёт сборка.

Причём, в некоторых случаях некоторые байты расчитываются при ассемблировании, формируя нужные смещения данных в памяти:

1 | Дизассемблер IDA Pro 7.5 для восстановления исходного кода игры (C/C++)

Объекты сегмента данных с точки зрения Си-компилятора могут трактоваться, как:

- переменная
- массив

extern "C" int _STACKLOW;
extern "C" char polypoints[];

Необходимо сохранить сишную трактовку объектов и их взаимное расположение в памяти.  Базовый адрес может быть любой.

Каким образом можно решить эту задачу?  (сегмент данных на ассемблере преобразовать в си)?

Hex-rays разрывает взаимное расположение объектов, превращая сегмент данных в негодность. Тут только  с АСМа ваять.

#92
(Правка: 14:24) 14:13, 2 авг. 2021

Gradius
> Каким образом можно решить эту задачу? (сегмент данных на ассемблере
> преобразовать в си)?

Чё, все закисли что-ли?  Ни у кого мыслей никаких нет?

Проблему решил другим способом: https://gamedev.ru/code/forum/?id=262694&m=5426315#m2

Принялся переносить игру на винду через SDL. Первое с чем столкнулся - нестандартный аллокатор памяти в игре через dos_allocate().  Игра запускает цикл по отхапыванию мем-пулов через функцию ДОС'а. 

Оттрассировал весь цикл - глянул что за пулы и как успешно они аллоцируются:


am | Дизассемблер IDA Pro 7.5 для восстановления исходного кода игры (C/C++)


Итого, успешно аллоцировано 8 пулов: семь с размером 0x10000 байт и один пулл с размером 0xF400 байт.  Справа число - младшее слово - сегмент, старшее - селектор.

Сейчас попробую эти пулы растащить в память по физическим адресам  выше 4 МБ напрямую  (там ранее сидел код моего инжектора).  И если получится добиться идентичности в поведении игры, то сымитирую данный аллокатор маллоками.  Ещё учесть, что  блоки памяти таким макаром должны быть вырoвнены на границу параграфа - тоесть на 16 байт.

P.S.  У ДОС'овского ваткома malloc не может быть за раз слишком большим. Поэтому вот так извращаются.

Хотя странно.  32-битный плоский режим, а маллок не смогли обновить.  В том же TMT Pascal я делал огромные маллоки и всё было успешно...

#93
(Правка: 16:53) 16:51, 2 авг. 2021

Оказалось всё не так просто.  Аллокатор в игре оказался трёх-ступенчатым.

Первая ступень:

    v6 = 0x10000;
    v7 = 0;

    while ( v6 >= 1024 )
    {
      v8 = CPP_dos_alloc(v6);
      if ( v8 )
      {

printf("%x \n",v6);
getch();

        v9 = (unsigned __int16)v8;
        v10 = HIWORD(v8);
        v11 = 3 * v7;
        dword_2B3F68[v11] = v6;
        ++v7;
        dword_2B3F64[v11] = v10;
        v6 += 1024;
        MemoryBlocks[v11] = 16 * v9;
      }
      v6 -= 1024;
    }

Аллоцируются 8 блоков как писал ранее:
7 раз по 0x10000
1 раз по 0xF400

Вторая ступень:

    for ( j = 0x1000000; j >= 4096; j -= 4096 )
    {
      v13 = (int)malloc(j); //CLIB
      MemoryBlocks[3 * v7] = v13;
      if ( v13 )
      {

printf("%x \n",j);
getch();

        for ( k = 4096; k > 0; k -= 16 )
        {
          v27 = j + k;
          if ( _expand((void*)MemoryBlocks[3 * v7], j + k) ) //CLIB
          {

printf("! %x \n",v27);
getch();

            dword_2B3F68[3 * v7] = v27;
            dword_2B3F64[3 * v7] = 0;
            break;
          }
        }
        j += 4096;
        ++v7;
      }
    }

Пара блоков с размерами:
0x561000
0x4000

Затем делается удлинение (функция _expand()):
0x561FC0
0x43F0

И наконец третья ступень:

for ( l = 4096; l >= 16; l -= 16 )
    {
      v15 = (int)malloc(l);  //CLIB
      v16 = 12 * v7;
      MemoryBlocks[3 * v7] = v15;
      if ( v15 )
      {

printf("! %x \n",l);
getch();

        ++v7;
          *(_DWORD *)(v16 + ((int)dword_2B3F68)) = l;

        l += 4096;
          *(_DWORD *)(v16 + ((int)dword_2B3F64)) = 0;
      }
    }

Эта ступень дала три блока размерами:
0xE70
0x590
0x350

Затем ещё сортировка выделенных блоков:

    qsort((void*)MemoryBlocks, v7, 12, CPP_compare); //CLIB

Это просто п**д*ц....
Хотел бы я в глаза заглянуть тому разработчику, кто такой аллокатор писал :)

am | Дизассемблер IDA Pro 7.5 для восстановления исходного кода игры (C/C++)
#94
18:23, 2 авг. 2021

Ещё раз убедился в том, что первоначальный реверс нужно делать в той среде, в которой работает игра. А не в виндах или ещё где-нибудь как тут писал кое-кто.

Иначе ускользнёт специфика некоторых вещей, которые делают необходимые условия для нормальной работы игры.
Конкретно - использование dos_alloc() в динамическом аллокаторе игры.

Если просто взять и заменить dos_aloc() на malloc(), то ничего не выйдет. Нарушится  последовательность успешного выделения блоков в цикле.

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

dword_2B3F68[3*0]=0x350;
dword_2B3F64[3*0]=0x400000;
MemoryBlocks[3*0]=0x400000;

dword_2B3F68[3*1]=0x590;
dword_2B3F64[3*1]=MemoryBlocks[3*0]+0x350;
MemoryBlocks[3*1]=MemoryBlocks[3*0]+0x350;

dword_2B3F68[3*2]=0xE70;
dword_2B3F64[3*2]=MemoryBlocks[3*1]+0x590;
MemoryBlocks[3*2]=MemoryBlocks[3*1]+0x590;

dword_2B3F68[3*3]=0x43F0;
dword_2B3F64[3*3]=MemoryBlocks[3*2]+0xE70;
MemoryBlocks[3*3]=MemoryBlocks[3*2]+0xE70;

dword_2B3F68[3*4]=0xF400;
dword_2B3F64[3*4]=MemoryBlocks[3*3]+0x43F0;
MemoryBlocks[3*4]=MemoryBlocks[3*3]+0x43F0;

dword_2B3F68[3*5]=0x10000;
dword_2B3F64[3*5]=MemoryBlocks[3*4]+0xF400;
MemoryBlocks[3*5]=MemoryBlocks[3*4]+0xF400;

dword_2B3F68[3*6]=0x10000;
dword_2B3F64[3*6]=MemoryBlocks[3*5]+0x10000;
MemoryBlocks[3*6]=MemoryBlocks[3*5]+0x10000;

dword_2B3F68[3*7]=0x10000;
dword_2B3F64[3*7]=MemoryBlocks[3*6]+0x10000;
MemoryBlocks[3*7]=MemoryBlocks[3*6]+0x10000;

dword_2B3F68[3*8]=0x10000;
dword_2B3F64[3*8]=MemoryBlocks[3*7]+0x10000;
MemoryBlocks[3*8]=MemoryBlocks[3*7]+0x10000;

dword_2B3F68[3*9]=0x10000;
dword_2B3F64[3*9]=MemoryBlocks[3*8]+0x10000;
MemoryBlocks[3*9]=MemoryBlocks[3*8]+0x10000;

dword_2B3F68[3*0xA]=0x10000;
dword_2B3F64[3*0xA]=MemoryBlocks[3*9]+0x10000;
MemoryBlocks[3*0xA]=MemoryBlocks[3*9]+0x10000;

dword_2B3F68[3*0xB]=0x10000;
dword_2B3F64[3*0xB]=MemoryBlocks[3*0xA]+0x10000;
MemoryBlocks[3*0xB]=MemoryBlocks[3*0xA]+0x10000;

dword_2B3F68[3*0xC]=0x561FC0;
dword_2B3F64[3*0xC]=MemoryBlocks[3*0xB]+0x10000;
MemoryBlocks[3*0xC]=MemoryBlocks[3*0xB]+0x10000;

v7=0xD; //13 кусочков

С такой раскладкой работает.  Это - временное решение. Тут  я по-пиратски  выделил блоки памяти, начиная с 4 МБ, точно зная, что там никого нет.

Есть подозрения, что игра использует только последний большой блок с размером 0x561FC0.  При его отключении - пишет ошибку выделения памяти при чтении из файла данных.

Где ещё, кроме как в DOS это исследовать? :)))

#95
18:47, 2 авг. 2021

Gradius
> Есть подозрения, что игра использует только последний большой блок с размером
> 0x561FC0. При его отключении - пишет ошибку выделения памяти при чтении из
> файла данных.

Гипотеза подтвердилась. Достаточно одного этого блока.  Но он будет расщепляться при выделении маленьких размеров. А это не очень хорошо.  Поэтому было принято решение - оставить все 13 блоков.

Сделал снимок памяти с 4 МБ до 8МБ.  Игра берёт менее 2 МБ для динамических данных.
Видно, что идёт аллокация в блоки разного размера.  Значит так нужно:

memdump | Дизассемблер IDA Pro 7.5 для восстановления исходного кода игры (C/C++)
#96
(Правка: 18:56) 18:56, 2 авг. 2021

А вот распределение в памяти, в случае одного большого блока:

memdump2 | Дизассемблер IDA Pro 7.5 для восстановления исходного кода игры (C/C++)
#97
(Правка: 18:18) 18:14, 3 авг. 2021

Переношу код игры на винду.  С аллокатором разобрался.

Новые проблемы, которые решились:

1) В игре используются устаревшие функции для работы с файлами (1994 г. всё-таки) - creat, sopen, read, close, write.
При открытии файла  функцией sopen возникала ошибка - файл не открывался.
Оказалось, что второй аргумент функции в ваткоме и mingw - различается!  Разными дефайнами сделан.
Нет совместимости.
Пофиксил агрумент, а позже заменил на более современный fopen().

2) Игра постоянно вылетала в сегфолт - при попадании в противника. Сразу же при касании.
Перешёл к отладке под Linix.  Использую отладчик GDB.

Раскрутил сегфолт.  Стек вызовов:

1 | Дизассемблер IDA Pro 7.5 для восстановления исходного кода игры (C/C++)

Проблема в функции рисования треугольника - той самой, которая на 10 000 строк кода.
Глянул что там на 5960-й строке:

2 | Дизассемблер IDA Pro 7.5 для восстановления исходного кода игры (C/C++)

Млять,  опять хардкод )))  Как-то упустил из виду этот модуль.  Код игры очистил от хардкодов, а про треугольник забыл )))

Вот и заходит оно на  эту ветку при попадании патронов, вызывая сегфолт.  ДОС  конечно же сегфолты не чухает )))

3) Дополнительно в программе просто море обращений к невыровненным данным - для микроконтроллеров придётся всё фиксить.  На ПК конечно же пойдёт без проблем, так как x86 допускают прозрачный доступ к невыровненным данным.

4) В Линуксе пришлось поизвращаться с путями и именами файлов - учесть регистр и развернуть слэш.

Написал для этого такой хелпер:

void fixfilename(char *d,const char *s)
{
 memset(d,0,strlen(s)+1);
 for(u32 i=0;i<strlen(s);i++)d[i]=toupper(s[i]);
 for(u32 i=0;i<strlen(d);i++)if(d[i]=='\\')d[i]='/';
}

5) GCC в Линуксе не понял мой объектник с данными, сделанный из ассемблера.    Как оказалось, дело в нижнем подчёрке в именах переменных - убрал его , переконвертировав с нужным флагом.

В настоящее время в винде и Линуксе работают:

1) алгоритм игры
2) графика
3) управление

Осталось портануть звук и музыку. Подчистить код.

#98
4:27, 5 авг. 2021

Я чешу репу от недоумения...

В Linux проект собрался и блестяще работает. Даже все баги вычистил что были (в основном - переполнение буфера и обращения к левым ячейкам памяти - сегфолты из-за хардкодов).

В винде под Mingw32 программа работает слегка неверно:

1) Часть полигонов у модели не отрисовано
2) Портится одна вершина или заходит лишняя - её координаты уходят в (0,0), что приводит к появлению "иголки".

Причём выставил те же самые ключи компиляции/оптимизации.
Выключал оптимизацию.

Выхлоп вышел чувствителен к таким флагам:

эти надо выключать - они вредят:

strict-aliasing 
tree-pre 
schedule-insns 
schedule-insns2 

а это включить:

signedchar

тогда хоть на -Ofast можно собирать.

Выходит Open Watcom и Linux GCC дают рабочую программу, а Mingw32 - нет.

Интересно, что родит MSVC ?

#99
7:04, 5 авг. 2021

Gradius
> Выходит Open Watcom и Linux GCC дают рабочую программу, а Mingw32 - нет.

Все три дают рабочую программу.  Вопрос решён: https://gamedev.ru/code/forum/?id=262794

Проблема была в нарушении работы парсера файлов 3д-моделей из-за устаревших функций для работы с файлами. Заменил на новые.

#100
(Правка: 16:27) 16:10, 7 авг. 2021

Отдебажил программу с помощью UBSAN: https://clang.llvm.org/docs/UndefinedBehaviorSanitizer.html

Отпрепарировал код игры, чтобы он пошёл на архитектурах, чувствительным к невыровненным данным.

Для этого завёл типы для невыровненных указателей:

typedef int          __attribute__((may_alias,aligned(1)))    intNA;
__attribute__((may_alias, aligned(1))), to tell the compiler the type is under-aligned as well as possibly aliasing.
example: typedef int __attribute__((may_alias, aligned(1))) unaligned_int;

Все обращения к невыровненным данным выловил с помощью UBSAN

#101
(Правка: 4:16) 4:06, 8 авг. 2021

Время, затраченное на дебаг уже становится соизмеримым со временем на реверс! )))
Зато теперь код собирается на максимальной оптимизации и без ограничений (проверено: в DOS, Windows, Linux).

Коллекция ошибок во время трассировки приложения.  Часть из них смертельны, другие - нет.

1) Крэш санитайзера во время компиляции.  Писать фидбек им конечно же я не стал, но нашёл выход - разбил код на более простые части:

1san_crash | Дизассемблер IDA Pro 7.5 для восстановления исходного кода игры (C/C++)

2) Обращение к невыровненным данным.  Очень принципиально, если код готовится для микроконтроллеров. Все выловлены и помечены как unaligned:

2misalign | Дизассемблер IDA Pro 7.5 для восстановления исходного кода игры (C/C++)

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

Действительно,  (0xF0000002 + 0x30000002) - 0x70000000 = 0xB0000004.  В скобках будет переполнение, но итоговый результат будет верным, за счёт ограничения разрядности до 32 бит.

Или типкаст в long long, если "кольцевая арифметика" отсутствует.

3overflow | Дизассемблер IDA Pro 7.5 для восстановления исходного кода игры (C/C++)

4) Знаковое переполнение из-за сдвига влево.  Лечится  заменой на умножение:

<< 6
    => 
 *(1<<6) 
.  Или оставить как есть, положившись на компилятор.  При тестировании оба варианта работают одинаково:

4shift | Дизассемблер IDA Pro 7.5 для восстановления исходного кода игры (C/C++)

5) Выход за пределы объявленной переменной.  Декомпилятор любит такое: то, что массив на самом деле - объявляет как переменную, а потом адресует всё и вся относительно адреса этой переменной.  Лечится правкой дефайна - внешняя переменная превращается в массив с неизвестного размера:  было

extern "C" int data;
, стало:
extern "C" int data[];
  После этого санитайзер не ругается.

6space | Дизассемблер IDA Pro 7.5 для восстановления исходного кода игры (C/C++)

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

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

5audio_crash | Дизассемблер IDA Pro 7.5 для восстановления исходного кода игры (C/C++)


Следующий шаг - порт под WEB с помощью emscripten!

#102
7:09, 8 авг. 2021

Gradius, офигеть ты занялся... Не проще заново написать программу? )))

#103
8:27, 8 авг. 2021

Gradius

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

#104
(Правка: 8:40) 8:29, 8 авг. 2021

Mirrel
> Gradius, офигеть ты занялся... Не проще заново написать программу? )))

Тоже думал об этом до того, как решил реверсить )))  Пришёл к выводу, что писать аналогичную игру буду дольше. И не факт, что новая игра будет 1-в-1 как исходная.

Ну и у меня это первый опыт в реверсе, до этого занимался сборкой и портированием готовых исходников.  Пока не познакомился с проектом "Raptor", который является рекомпиляцией оригинального DOS'овского "раптора". До определённого момента времени я и не знал, что можно получить Си-подобный код из дизасма.  Который, чтобы довести до уровня рабочего, нужно:

1) обработать скриптами
2) дополнить утраченной информацией из дизасма
3) отладить путём инжектирования в код игры
4) перенести и отладить на целевой платформе независимо

Все 4 этапа пройдены. Сейчас стоит цель сделать Web-порт игры.

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

В данном случае это не требуется.  SDL_AudioCallback и основная программа.  По завершению работы программы, перед освобождением указателя, мы ждём пока флаг не будет равен единице (его выставит коллбэк).  После как флаг готов, запрещаем будущие  вызовы коллбэка  и освобождаем-деинитим всё.

Работает всегда, ошибка по закрытию ушла.  Частота вызова аудио-коллбэка - 120 Гц  (синхронно с 120 bps MIDI).

Страницы: 16 7 8 913 Следующая »
ФлеймФорумПрограммирование