R0 CREW

Реверс-инжиниринг баз данных. Часть 2. Основные подходы

Введение

Реверс-инжиниринг баз данных. Часть 1. Введение
Реверс-инжиниринг баз данных. Часть 2. Основные подходы
Реверс-инжиниринг баз данных. Часть 3. Переиспользование кода. Заключение

В первой части было дано краткое введение в область исследования проприетарных БД. С настоящей статьи начинается переход к практической стороне вопроса. Самое главное всегда даётся в начале, и то, о чём пойдёт речь ниже, является основной информацией, которой я хотел поделиться, начиная данный цикл статей. В статье изложено моё видение процесса обратной разработки БД, и когда я говорю «следует», «нужно» и «обязательно», это означает не призыв к чему-либо, а скорее обращение к самому себе в прошлом.

Постановка задачи

Мы будем исследовать базу данных, входящую в состав программы Microcat Ford USA. Эта программа - одна из тех, которые разрабатывают производители автомобилей для использования в сервисных центрах. В задачи такого софта входит отображение комплектующих, времени выполнения ремонтных работ, поиск информации о конкретном автомобиле по его VIN-номеру и т.д. Для реверс-инженера эти программы предоставляют большой простор для работы, ибо написаны они под разные архитектуры, на разных языках программирования, в разное время, программистами самого различного уровня и, что главное, являются актуальными и активно используемыми.

Одна из задач обратной разработки такого класса ПО - получение доступа к информации, которую программа предоставляет пользователю. Иначе говоря, необходимо научиться работать с данными напрямую, в обход программы-интерфейса. В принципе, здесь бы и не требовался реверс-инжиниринг, если бы разработчики использовали общепринятые СУБД на основе SQL, но, по неизвестным мне причинам, большинство из них предпочли создать свои собственные БД. Такие базы не стандартизированы среди разработчиков автомобильного ПО, полностью отличаются друг от друга и имеют сложную внутреннюю структуру.

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

Первый подход к обратной разработке БД

Исходя из того, что каждая программа, работающая с БД, содержит компонент СУБД, либо по своей сути является СУБД, справедливым будет точка зрения на исследуемую программу как на СУБД. Это полезнейшее допущение является первым основным подходом.

[2.1] Рассматривайте программу, работающую с БД, как СУБД.

Суть в том, что если имеется база и программа, взаимодействующая с этой базой, то для понимания устройства БД достаточно повторить те действия, которые выполняет СУБД. Нет необходимости реверсить базу с нуля, ведь уже есть программа, в которой структура этой базы описана. Польза этих утверждений будет обоснована на практике позже.

Первичный анализ

После установки программы обнаруживаем на диске две папки: MCFNA и FNA_Data.

C:\MCFNA
│   ANIBUTON.VBX
│   BTDESIGN.DLL
│   CMDIALOG.VBX
...
│   MCFNA.EXE
│   MCI.VBX
│   MCLANG02.DLL
...
│   XCDUNZIP.DLL
│   XCDZIP.DLL
│   XCEEDZIP.VBX
│   
├───DMS
	...
├───Lex
	...
├───Locator
	...
├───mcindex
	...
└───Print32
	...

Листинг 1. Директория MCFNA

C:\FNA_DATA
│   AAEXT.DAT
│   ASI.idx
│   avsmodel.dat
│   AVSXRef.DAT
│   Calib.dat
│   DSET.DAT
│   EngDate.dat
│   EngDateB.dat
│   EngXRef.dat
│   FNAc.DAT
│   grp.dat
│   grp.idx
│   grpavs.idx
│   grpbasic.idx
│   grpdesc.idx
│   grpinc.dat
│   grppub.dat
│   grpxref.idx
│   LM.txt
│   MCData.DAT
│   MCData.IDX
│   MCHELP32.EXE
│   MCImage.DAT
│   MCImage.IDX
│   MCImage2.DAT
│   MCOSI.DAT
│   MCPart.IDX
│   MCPartLx.IDX
│   MotorCS.IDX
│   MS.dat
│   MY.IDX
│   partid.DAT
│   PB.idx
│   PL.idx
│   RECALL.Dat
│   SecID.DAT
│   XGROUPS.IDX
│   
├───Help
	...
├───Install
	...
├───Lex
│       FEULex.DAT
│       LexInd.IDX
│       
├───MFNotes
	...
├───Preface
	...
├───Pricing
	...
├───Res
	...
└───VIN
    │   BrVIN.DAT
    │   Last8.idx
    │   MCMFC.DAT
    │   MCRego.DAT
    │   MCVIN.DAT
    │   
    ├───Decode
    │       10.dat
    │       11.dat
    │       123.dat
    │       80_C_1.dat
    │       80_C_2.dat
    │       80_C_34.dat
    │       80_C_5.dat
    │       9.dat
    │       BR_10.dat
    │       BR_11.dat
    │       BR_123.dat
    │       BR_4.dat
    │       BR_567.dat
    │       BR_8.dat
    │       C_4.dat
    │       C_5.dat
    │       C_67.dat
    │       C_8.dat
    │       E_8.dat
    │       LTT_4.dat
    │       LTT_567.dat
    │       LTT_8.dat
    │       MED_8.dat
    │       Prod20-FNARelease-Vin-Decode
    │       
    ├───TSL
    │       FNITVIN.dat
    │       TSL.dat
    │       TSLCatalogue.dat
    │       TSLHi.dat
    │       TSLPT.dat
    │       TSLRef.dat
    │       TSLSection.dat
    │       TSLVIN.IDX
    │       TSLVINR.IDX
    │       VBOM.dat
    │       
    └───VCIS
            1F01.xcd
            1F0B.xcd
            1F14.xcd
            ...
            4M2P.xcd
            4M2R.xcd
            4M2S.xcd
            ...
            WF05.xcd
            Z6FD.xcd
            Z6FE.xcd 

Листинг 2. Директория FNA_Data

По расширениям файлов понятно, что в MCFNA находится код, а в FNA_Data данные. Сразу же примечаем, что у данных всего три расширения: dat, idx и xcd. Не обманитесь, часто расширения не значат ничего.

Первичный анализ состоит из четырёх шагов:

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

Обзор графического интерфейса программы

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

Рис. 1. Выбор автомобиля

Рис. 2. Свойства автомобиля

Рис. 3. Схема деталей

Оценка файлов данных

Бегло просмотрим каждый файл в хексовом редакторе, чтобы оценить их организацию (текстовые/бинарные, стандартные/проприетарные и т.д.). Если вы используете Hiew, начинайте с текстового режима и при необходимости переключайтесь на хекс. Ниже приведены скриншоты нескольких файлов.

Рис. 4. Файл avsmodel.dat (текст)

Рис. 5. Файл FNAc.dat (текст)

Рис. 6. Файл MCData.dat (хекс)

Рис. 7. Файл MCImage.dat (хекс)

Как видите, несмотря на одинаковое расширение, файлы устроены совершенно по-разному. То же самое касается и расширения idx. Единственным стандартизированным расширением оказывается xcd – это архивы zip, но их мы изучать не будем.

Оценка файлов кода

Тулзой DiE, а можно тем же Hiew, обнаруживаем, что исполняемые файлы формата NE – New Executable, т. е. 16-битные виндовые программы. Многие из них с расширением VBX и, судя по внутренностям, это Visual Basic. Большинство DLL так же написаны на этом языке.

Рис. 8. Файл CMDIALOG.VBX

Помимо этого, некоторые DLL и VBX оказались упакованными пакером, который ни одной тулзой не определился.

Рис. 9. Файл FILE16.DLL

Гугление показало, что это Shrinker – древний упаковщик NE-файлов, созданных на Visual Basic 3.0. Забегая вперёд скажу, что найти распаковщик (NED 2.30) среди мёртвых ссылок было большой удачей, т. к. совершенно не хотелось тратить время на возню по распаковке в статике или дебажить NTVDM (что всё равно пришлось делать, но по другой причине).

Мы оценили, с какими данными и кодом имеем дело — это полный legacy. Резюмируем выполненные действия в виде основного подхода.

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

Поиск возможностей переиспользования кода

В программировании хорошим тоном считается переиспользование кода и, на мой взгляд, это так же справедливо для обратной разработки. Различие в том, что у программистов интерфейсы модулей обычно документированы, нам же необходимо сначала найти модули и функции, а затем разреверсить их интерфейс, но это намного проще, чем писать код с нуля. В одном из моих проектов была модифицированная open source SQL-база, реверс которой обещал быть долгим. Зато в программе оказалась библиотека с экспортами в духе хранимых процедур, так что мне достаточно было реконструировать несколько классов, передавать объекты функциям библиотеки и получать данные из БД в ответ.

Применим данную методику к нашей задаче и просмотрим экспорты во всех библиотеках. Почти в каждой находим много интересного и, кажется, полезного, хотя в названиях функций присутствует терминология БД, нам неизвестная (CODESTR, DMC, NISSANSECNUM, TMCSECNUM и т. д.).

Рис. 10. Экспорты библиотеки FNAUTIL2.DLL

[2.3] Оцените модули программы на потенциальную пригодность к переиспользованию. Применяйте этот подход периодически по мере прогресса в реверсе базы, поскольку сразу обычно неясно, что пригодно, а что — нет.

Это очень мощное средство, которое может сэкономить вам недели и даже месяцы, но у него есть недостаток: при увеличении количества чёрных ящиков и упрощении действий, ими выполняемых, возрастает сложность переиспользования. На скрине выше видно функции, что-то вычисляющие и проверяющие. Возможно, они должны вызываться в определённом порядке. Возможно, они зависят от глобального состояния программы. Сколько ещё таких «возможностей»? Было бы гораздо проще, будь там одна функция GetAllDatabaseRecords, не правда ли?

Я периодически сталкиваюсь с программами, написанными для хорошо декомпилируемых платформ (Java, .NET, etc). Лишь в одном случае имелся удобный интерфейс для доступа к БД, во всех остальных мне пришлось практически полностью игнорировать исходные (декомпилированные) коды, слишком большие и сложные для понимания за разумное время. Это те случаи, когда реверсить проще, чем читать исходники.

[2.4] Сложность переиспользования кода прямо пропорциональна количеству чёрных ящиков и обратно пропорциональна объёму действий, ими выполняемых.

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

На этом первичный анализ окончен. Он может включать и другие этапы, такие как чтение документации, сравнение исследуемой БД / программы с предыдущими проектами и так далее, однако три описанных выше этапа я выполняю всегда.

Выбор точки входа

Следующим шагом к пониманию БД является выбор точки входа — такого набора данных, структура которого, во-первых, не зависит от других структур, и, во-вторых, позволит изучать другие структуры. Образно это выглядит так: на оси размещаются точки, соответствующие данным, требуемым в задаче, причём данные справа концептуально зависимы от данных слева, после чего выбирается левая точка.

Рис. 11. Ось зависимостей

В нашей задаче первым набором, подлежащим обработке, является информация об автомобилях. Более того, нам известно, что программа при запуске отображает меню выбора машины, из чего можно сделать вывод, что автомобили — это оригинальная точка входа, своего рода OEP в БД. Из этого делаем предположение, что структура «автомобили» не зависит от других и является правильно выбранной EP.

Рис. 12. Соответствие предполагаемого и реального деревьев зависимостей

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

  1. Начать с требуемой точки в надежде, что знание структуры предыдущих точек не потребуется. Есть вероятность, что в дальнейшем это не позволит понять сложные структуры. Идите этим путём только тогда, когда хорошо знакомы с предметной областью и / или имеете опыт в реверсе БД.
  2. Изучить дерево от корня до требуемой точки. Это стабильный, но более затратный по времени вариант. Я всегда выбираю этот путь.

[2.5] Выберите точку входа в БД. Чем ближе она к оригинальной точке входа, тем меньше структур вам придётся изучать предварительно. Идеально, если они совпадают.

Как убедиться, что EP выбрана правильно? Мне неизвестны такие методы, кроме интуитивно-опытного. Продолжайте реверсить, и если окажетесь в тупике, то отступите и попробуйте найти другие варианты. В крайнем случае беритесь за дизассемблер.

Зондирование точки входа

Определившись с EP необходимо найти файл или файлы, содержащие нужные данные, в нашем случае информацию о машинах. Я делаю это отбором нескольких ключевых слов и поиском их по всем файлам. Ключевыми словами являются строки, точно характеризующие искомые данные. Например, известно, что есть автомобиль с названием «Bob Cat ML 1980», в выпадающем меню Engine есть строка «2.3 L OHC» и т. д.

Рис. 13. Выявляем ключевые слова

При поиске строк следует помнить несколько вещей.

  1. Они могут быть в разных кодировках, обычно ASCII или UTF-16. Эта проблема легко нивелируется Total Commander’ом, в котором можно искать сразу в нескольких кодировках (возможно, Far тоже это умеет).
  2. Они могут храниться в нестандартном виде, быть сжаты или находиться в сжатых файлах.

Поиск обнаружил строку «Bob Cat ML 1980» только в одном файле — FNAc.dat.

Рис. 14

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

[2.6] Определите, какой файл (файлы) отвечает за точку входа в БД.

Реверс-инжиниринг точки входа

В первой статье было дано определение БД, и самое время его вспомнить. База данных - набор бинарных файлов, содержащих упорядоченные данные и имеющих перекрёстные ссылки друг на друга. Сейчас нас интересует первая часть определения, а именно утверждение, что данные в файлах упорядочены. Свойство упорядоченности означает, что файл состоит из записей (record), имеющих один и тот же формат. Например, в каталоге одна запись будет описывать один автомобиль. Следовательно, определив формат одной записи, мы автоматически определяем формат всех записей. Иначе говоря, выяснив смысл байт, относящихся к Bob Cat ML 1980 со скрина выше, мы сразу же понимаем смысл байт для всех остальных авто. В действительности же файлы редко состоят из одного типа записей; как правило, файлы включают в себя несколько таблиц (table), которые содержат записи разных форматов.

Как разреверсить формат записи и файла в целом? Здесь применимы все техники из области исследования форматов файлов (если помните иерархию из первой статьи, реверс БД я расположил на один уровень выше). Они давно известны (см. ссылки в конце статьи), я буду применять их ниже, но выделять в отдельные подходы не стану.

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

Рис. 15

Рис. 16

Цветом выделены 4 записи, которые мы отнесём к таблице №1, за которой следует сплошной текст, который пусть принадлежит таблице №2. Каждая запись в таблице №1 имеет размер 0x3A. Размер записей в таблице №2 неизвестен, да и не факт, что весь текст это не одна большая запись. В итоге можно утверждать, что в файле grpinc.dat две таблицы, т. е. два разных формата записей. Это простой пример. Встречаются высокоэнтропийные бинарники, в которых отличить переход от одной таблице к другой можно лишь по исчезанию / появлению некоторого паттерна в байтах. Тем не менее, для намётанного глаза и они перестают быть проблемой.

Вернёмся к каталогу. Просмотрев его целиком, не находим каких-то контрастирующих данных: как в начале, так и в конце файла наличествуют названия автомобилей.

Рис. 17. Начало файла FNAc.dat

Рис. 18. Конец файла FNAc.dat

На следующем шаге необходимо выяснить размер одной записи. Для этого необходимо найти паттерн и вычислить разницу между двумя соседними паттернами. Что это значит, лучше сразу показать на примере. Переместимся в начало каталога и подумаем, какая информация повторяется раз за разом? Я не имею в виду буквальное повторение байт в байт, нет, речь о смысле информации. Повторяются названия автомобилей, строки, начинающиеся с «!CAT» и строки, начинающиеся с «VL». Это три разных паттерна. Возьмём один из них, название авто, и вычислим, каково расстояние в файле между первым названием машины и следующим.

Рис. 19

Рис. 20

Расстояние равно 0x800 – 0x0 = 0x800. Таким образом, размер записи в каталоге равен 0x800 байт. Это важная техника в реверсе файлов, ей нужно овладеть (как и остальными, впрочем). Ещё добавлю, что нет необходимости при вычислении расстояния между паттернами брать именно две первые или две последние записи. Когда вы собираетесь определить размер записей, которые располагаются в середине файла, вы не знаете наверняка, с какого байта они начинаются, но всё равно без проблем вычисляете дельту.

Пришло время документировать полученные результаты. Для описание форматов файлов будем использовать Kaitai Struct, упоминавшийся в прошлой статье. Если в будущем вам необходимо будет работать с файлом программно, вы скомпилируете описание в код на вашем языке, если тот поддерживается. На данный момент всё, что нам известно о файле, это размер записей, поэтому их содержимое пока будем рассматривать как просто байты.

meta:
  id: catalogue
  file-extension: dat
  endian: le
  encoding: ascii

seq:
  - id: vehicle
    type: vehicle
    repeat: eos

types:
  vehicle:
    seq:
      - id: unk_1
        size: 0x800

Листинг 3

В визуализаторе Kaitai Struct ksv это выглядит так.

Рис. 21

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

Рис. 22

Разберём каждое поле по отдельности. Первые 0x64 байта от начала записи до последнего пробела названы vehicle_name_1, поскольку это название машины. Следующие два байта (0x0026) скорее всего являются отдельным полем, но его смысл пока неясен, поэтому оно называется unk_1. Как понять, отдельное это поле или эти два байта принадлежат предыдущей строке, а может и вообще следующей? Однозначного ответа нет, нужно делать предположения и смотреть по аналогии: если все строки завершаются символом пробела (0x20) и без нулевого байта, то вряд ли первая строка является исключением. Также эти два байта могут быть полем длины для второй строки, но, во-первых, её длина равна 0x64 (от «Aerostar» до «VL38!»), и, во-вторых, это снова похоже на исключение, что сомнительно. Проверяем предположение на других записях (напомню, сейчас мы работаем с самой первой) и убеждаемся, что оно справедливо и для них.

После unk_1 идёт поле vehicle_name_2, т. к. в нём содержится тот же текст, что и в vehicle_name_1. Затем идут строки vl_code_1, cat_code и vl_code_2, названные так по первым буквам их значений. Первые два поля имеют размер 0x16 байт. Поле vl_code_2 до следующей строки «MX» может быть длиной 0x1A2, но не будем спешить и установим его размер равным 0x16.

Рис. 23

Дело в том, что поле vl_code_2 во многих записях имеет то же значение, что и vl_code_1, и справедливым было бы предположение, что длина их одинакова. С другой стороны, байты по смещению от vl_code_2 + 0x16 до строки «MX» во всех записях равны 0x20 (пробелы), поэтому их можно было бы включить в поле vl_code_2, но мы выделим их в отдельное поле empty_1 для ясности.

Следующее предполагаемое поле, от строки «MX» до строки «A», имеет длину 0xD2, но и здесь не будем спешить и посмотрим, чему оно равно в других записях.

Рис. 24

Оказывается, это не одна строка, а несколько размером 0xA байт. Если разделить 0xD2 на 0xA, то получим 0x15 – предполагаемый размер массива этих строк. Их смысл пока непонятен, хотя некоторые из них напоминают обозначения стран (US, CA) или штатов (CA, MX), поэтому называем массив как unk_2.

Рис. 25

Следом идёт строка «A», размер которой по аналогии определяется до последнего байта 0x20. Снова выполним действия, как и для предыдущего поля, и обнаружим, что это тоже массив из 0x9 строк длиной 0xC, назовём его unk_3.

Рис. 26

Смысл оставшихся байтов уже менее понятен, это не строки.

Рис. 27

Первое, что бросается в глаза, это два ворда (или дворда) в разных местах, один со значением 0x7C2, другой 0x7CB. Со временем вы начнёте примечать подобные числа, потому что при переводе их в десятичную систему они выглядят как года: 0x7C2 = 1986, 0x7CB = 1995. Вспоминаем, что vehicle_name_1 = «Aerostar A 1986-1995» и убеждаемся, что это годы начала и окончания производства, и обозначим соответствующие поля как year_from и year_to.

Байты между year_from и year_to во всех записях равны нулю, поэтому выделим их в отдельное поле длиной 0x28 и назовём zero_1. Столько же нулевых байт и в поле после year_to, которое так же назовём zero_2. Оставшиеся байты на данный момент не говорят ни о чём, поэтому называем их как unk_4.

Рис. 28

И последнее. Если посмотреть несколько записей и приглядеться к полям unk_1, vl_code_1, vl_code_2 и cat_code, можно заметить, что число из unk_1 присутствует в остальных перечисленных полях, только в символьном виде. Допустимо предположить, что это идентификатор автомобиля, поэтому переименуем unk_1 в vehicle_id. В итоге описание формата теперь выглядит так.

meta:
  id: catalogue
  file-extension: dat
  endian: le
  encoding: ascii

seq:
  - id: vehicle
    type: vehicle
    size: 0x800
    repeat: eos

types:
  vehicle:
    seq:
      - id: vehicle_name_1
        type: str
        size: 0x64
      - id: vehicle_id
        type: u2le
      - id: vehicle_name_2
        type: str
        size: 0x64
      - id: vl_code_1
        type: str
        size: 0x16
      - id: cat_code
        type: str
        size: 0x16
      - id: vl_code_2
        type: str
        size: 0x16
      - id: empty_1
        type: str
        size: 0x18C
      - id: unk_2
        type: str
        size: 0xA
        repeat: expr
        repeat-expr: 0x15
      - id: unk_3
        type: str
        size: 0xC
        repeat: expr
        repeat-expr: 9
      - id: year_from
        type: u2le
      - id: zero_1
        size: 0x28
      - id: year_to
        type: u2le
      - id: zero_2
        size: 0x28
      - id: unk_4
        size-eos: true

Листинг 4

Это был очень простой пример, потому что всё исследование свелось к последовательному выявлению назначения каждого из полей. В сколько-нибудь более сложных файлах изучение перестаёт быть линейным, приходится пробовать разные типы полей, их длину, искать связи между полями разных таблиц и т. д. Поскольку данная статья посвящена в первую очередь реверс-инжинирингу БД, а не форматов файлов, в дальнейшем подробного разбора отдельных файлов не будет, ибо нас интересует более высокий уровень — взаимосвязь между этими файлами.

[2.7] Изучите и опишите формат файла, отвечающего за точку входа в БД.

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

Поиск связей

Продолжать обратную разработку можно несколькими путями:

  1. по известной информации найти недостающую;
  2. найти недостающую информацию и понять её связь с известной.

Первый путь заключается в том, что мы берём значения таких полей каталога, как vehicle_id, vl_code_1, vl_code_2 и cat_code, и ищем их в других файлах. Если что-то найдётся, это будет сигнализировать о возможной связи между двумя файлами. Смысл второго пути таков: по ключевым словам мы ищем файл, содержащий недостающую информацию, реверсим его и находим связь между этим файлом и каталогом. Можно рассматривать эти пути как направленные друг на друга векторы. Это два способа достижения одной и той же цели.

Рис. 29. Два пути поиска связей

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

Рис. 30. Первый уровень дерева деталей

Рис. 31. Второй уровень дерева деталей

Рис. 32. Схема деталей с отмеченными на ней деталями

После выбора автомобиля появляется список разделов («10 WHEELS & RELATED PARTS», «20 BRAKE SYSTEM», etc). При выборе одного из них появляется второй список разделов («10 WHEELS AND WHEEL COVERS WHEELS 1980 – 1980 [P10853]», etc). После выбора из второго списка появляется схема деталей, на схеме отмечены гиперссылки в виде номеров деталей, при нажатии на которые появляется нижнее меню, на котором представлены все варианты выбранной детали. Наша цель в данный момент - «добраться» до списков деталей.

Как видно, автомобили и детали связаны друг с другом посредством двух списков разделов, которые далее мы будем называть деревом деталей. У дерева два уровня, на скринах выше они называются «Alpha Index – Major» и «Alpha Index – Minor», которые для простоты будем называть первым и вторым уровнями. Исходя из опыта изучения других БД этой же тематики, я делаю предположение, что данные о дереве деталей и о самих деталях располагаются либо в разных структурах данных, либо в разных файлах. Из этого следует, что необходимо скорректировать цель: от автомобилей мы идём к первому уровню дерева, от него ко второму, и от второго уже к деталям.

Рис. 33. Шкала прогресса

Как достичь поставленной цели? Если сделать поиск в файлах по названиям первого уровня дерева («10 WHEELS & RELATED PARTS», etc), то в результате получим лишь одно вхождение в файле FEULex.dat, в котором хранятся все строки программы на разных языках. Можно было бы плясать от него, взяв точный оффсет до строки и снова выполнив поиск по файлам, но уже не строки, а четырёх байт оффсета в little endian – это эффективная техника, но она не работает в данном случае, т. к. к файлу FEULex.dat выполняются обращения по оффсету не от начала файла.

Самое время применить другой подход, настолько мощный, насколько простой. Он основывается на подходе [2.1], который гласит, что есть смысл рассматривать подопытную программу как СУБД. Если это СУБД, то она обращается к БД, т. е. выполняет чтения из файлов. Поскольку СУБД должна быть эффективна, она не читает все файлы в память при старте программы. Значит, обращение к файлам происходит тогда, когда пользователь делает соответствующие запросы в программе, т. е. кликает на определённые меню, кнопки и так далее. Следовательно, отследив, к каким файлам обращается программа в момент действия, из множества всех файлов мы выделим подмножество тех, в которых находятся искомые данные.

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

Рис. 34. Оригинальный ProcMon

Рис. 35. Пропатченный ProcMon

Итак, мы собираемся узнать, в каком файле находится первый уровень дерева деталей. Запустим ProcMon, сделаем два фильтра: «Process Name is ntvdm.exe» и «Operation is ReadFile», остановим мониторинг. Загрузим программу, выберем автомобиль, включим мониторинг в ProcMon и нажмём на кнопку загрузки данных об автомобиле.

Рис. 36

Рис. 37

Бинго! Всего к нескольким файлам обратилась программа при чтении дерева деталей первого уровня: MCData.idx, MCData.dat, XGROUPS.idx, FEULex.dat, LexInd.idx. Два последних относятся к строкам, о чём уже говорилось. XGROUPS.idx выглядит неинтересно и не содержит каких-либо намёков на деревья. Первые два — это то, что мы ищем.

[2.8] Мониторьте обращения программы к файлам в момент, когда программа должна загрузить интересующие вас данные.

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

Название: MCData.idx
Размер: 5,5 МБ
Формат: бинарный
Количество таблиц: 4 (2 хидера + 2 таблицы данных)
Назначение: связывает автомобиль (vehicle_id) с первым уровнем дерева (ai_major_id), 
первый уровень со вторым (ai_minor_id), второй со списком деталей (part_list_offset) 
в файле MCData.dat и схемами деталей (image_offset) в файлах MCImage.dat, MCImage2.dat

Название: MCData.dat
размер: 1,5 ГБ
Формат: текстовый зашифрованный
Назначение: содержит список деталей по смещению part_list_offset для каждого дерева

Листинг 5

Обязательно нужно упомянуть про второй производный от [2.1] подход. Мониторинг в ProcMon показывает ещё две вещи, дающие буст к пониманию устройства файлов:

  1. смещение и длину блока, читаемого программой за раз;
  2. порядок чтения блоков.

Программы читают данные блоками, причём один блок обычно соответствует одной записи. Знание смещения и длины записи позволяет определить оффсет до первой записи в таблице, размер записи в таблице, количество записей в таблице, количество таблиц (разные размеры записи означают чтение из разных таблиц). Знание порядка чтения блоков позволяет спланировать этапы реверса файла: если программа сначала читает таблицу №1, а затем таблицу №2, стоит начать с изучение с первой таблицы — возможно, её понимание необходимо для понимания второй таблицы. К тому же, знание порядка чтения блоков иногда позволяет понять, по какому алгоритму работает программа. Посмотрите на скриншот ниже.

Рис. 38

Здесь происходит много «хаотичных» обращений к LexInd.idx, а затем одно точечное к FEULex.dat. Если выяснить, по какой причине выполняются эти обращения (действие пользователя), а затем сравнить последнее чтение LexInd.idx с остальными, то станет ясно, что это бинарный поиск. В данном случае ищется идентификатор строки в файле LexInd.idx, при нахождении которого берётся оффсет самой строки, а затем выполняется её чтение из FEULex.dat. Понимание алгоритма работы с этими двумя файлами позволило мне быстрее разобраться в их устройстве.

[2.9] Используйте знание смещений, размеров и порядка чтения записей, полученное в процессе мониторинга, для исследования файлов.

Результаты

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

Рис. 39. Шкала прогресса

Рис. 40. Архитектура БД

Но это не главное. Моей основной задачей было показать методы исследования баз данных, выработанные в течение нескольких лет. Я постарался вести изложение на двух уровнях, на одном из которых говорил о принципах, независимых от каких-либо деталей устройства баз данных, а на другом применял их на реальной БД для подтверждения жизнеспособности и практичности оных.

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

В следующей части мы закончим исследовать базу с помощью переиспользования кода для доступа к схемам деталей. Да, до сих пор дизассемблер и отладчик не использовались вообще.

Ссылки

Реверс-инжиниринг форматов файлов

Общее

Список основных подходов

Первый подход к обратной разработке БД

  • [2.1] Рассматривайте программу, работающую с БД, как СУБД.

Первичный анализ

  • [2.2] Выполните первичный анализ того, с какими данными и кодом работаете. Найдите максимум информации в сети по ключевым словам, встретившимся в процессе оценки.
  • [2.3] Оцените модули программы на потенциальную пригодность к переиспользованию. Применяйте этот подход периодически по мере прогресса в реверсе базы, поскольку сразу обычно неясно, что пригодно, а что — нет.
  • [2.4] Сложность переиспользования кода прямо пропорциональна количеству чёрных ящиков и обратно пропорциональна объёму действий, ими выполняемых.

Исследование точки входа

  • [2.5] Выберите точку входа в БД. Чем ближе она к оригинальной точке входа, тем меньше структур вам придётся изучать предварительно. Идеально, если они совпадают.
  • [2.6] Определите, какой файл (файлы) отвечает за точку входа в БД.
  • [2.7] Изучите и опишите формат файла, отвечающего за точку входа в БД.

Поиск связей

  • [2.8] Мониторьте обращения программы к файлам в момент, когда программа должна загрузить интересующие вас данные.
  • [2.9] Используйте знание смещений, размеров и порядка чтения записей, полученное в процессе мониторинга, для исследования файлов.

Отказ от ответственности

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

ned230.zip (8.0 KB)