Дилетант и back-инжиниринг. Часть 2: Каркас
В прошлый раз я описал начало моих отношений с обратной разработкой. Прошло еще немного времени и вот, в некоторой степени, результат моих исследований.
Я пытаюсь восстановить исходники по .dll-библиотеке и .pdb-базе. Использование IDA конечно принесло кое-какие результаты, но не удовлетворительные. Возможно я просто недостаточно усидчив. Поэтому я начал с другой стороны — с восстановления каркаса проекта библиотеки. Так как у меня есть .pdb-база я вполне могу это сделать. Теоретически. Теоретически, потому что в базу записывается информация с препроцессированых файлов, а не с исходников. А значит нужно работать дальше.
НаполнениеНачну рассказ с теории. Структурно .pdb-база представляет собой набор символов (любая переменная, структура, функция, перечисление, тип, все это — символы) связанных между собой. Символы делятся по типам, и в зависимости от типа могу иметь разные свойства. Считывая свойства можно получить описание структур, функций, переопределений, перечислений, констант, включая связи между всем этим, имена файлов и .obj-модулей в которых находятся функции, и много чего еще. Для доступа к символам существует DIA SDK (Debug Interface Access), она хорошо документирована и разобраться с ней не очень сложно. Единственная «проблема» — DIA из коробки доступен только для C/С++, и если хочется работать на .Net нужно будет поработать перегоняя интерфейс в .Net .dll, но это другая история. Можно просто найти готовый модуль. Лично я выбрал второй вариант найдя Dia2Lib.dll, но в ней некоторые функции переведены неверно (вместо массива в параметре некоторых функций всунули простую переменную).
Возможно есть какое-то готовое решение для генерации кода по .pdb-базе, но я его не нашел. И теперь пишу свое. Пишу я на C#, меньше мороки с памятью, хотя и ценой удобства работы с файлами. Для начала нужны были классы для описания символов. Стандартные (те что из Dia2Lib) немного неудобны. Точнее если хочется вертеть данными в трех степенях свободы они просто этого не выдержат.
Конечно все можно достаточно легко вернуть к первоначальному виду. Но ситуаций где нужна ручная обработка может быть достаточно много. Например для той библиотеки, которую я хочу использовать, в базе записано 2673 структур. Из них только около 250 реально нужных, остальное — развертки шаблонов std и прочие «стандартные» вещи. Остаётся только надеяться что все пройдет без проблем. Ладно, предположим, что заготовки для структур есть. Дальше нужно позаписывать их по файлам.
ГенерацияДля начала нужны сами файлы для записи. Немного теории. При компиляции каждый исходник с кодом после препроцессора переводится, с помощью компилятора, в машинные коды. С каждого исходника с кодом получается .obj-файл или .o-файл в зависимости от компилятора. При помощи DIA SDK можно получить список всех файлов с каждого .obj-модуля (короче весь список того, что подключаются в #include). О том, как получить список файлов, было рассказано в прошлой статье (ну как рассказано… в общем, там есть код). Если говорить языком дилетанта — из каждого .obj-модуля можно получить имя исходника, которым был модуль когда-то (у них будут имена одинаковые), и вель список подключаемых библиотек (сюда входят все файлы кроме .cpp, хотя бывают и исключения). После создания общей структуры, и связывания частей между собой, можно приступать к записи структур.
Напрямую получить имя файла, в котором была структура при ее существовании в виде исходника, насколько я знаю, нельзя. Но можно узнать по каким файлам была раскидана реальзация методов структуры. Поэтому я предлагаю просто собирать все файлы, в которые входят функции-методы, выбрать из них тот что будет хедером, записать туда описание, и связать остальные файлы с хедером. Но при получении имени исходника, в которой находится метод может быть неприятный или баг, или проявление ошибки файла. Для получения имени сначала нужно найти по RVA (relative virtual address) список линий исходников, и потом по этому списку линий найти файл в котором есть эти линии. Но иногда количество линий отвечающих методу равно нулю, но имя файла всё равно находится. Причем обычно неправильное имя. Обычно это проявляется при анализе конструктора. Может конструктор просто не рассматривать…
Обычно, что и не удивительно, структуры находятся в двух файлах, хедер.h и код.cpp, но есть и другие варианты. Например структура имеет только хедер, или файл с кодом представлен с расширением .inl, или структура вообще нигде, по мнению .pdb-базы, не записана. Я использовал следующий алгоритм. Если в списке файлов, в который входит структура, есть хедер — пишем структуру в хедер, и подключаем его к файлу с кодом, если он есть. Проходим по структуре, составляя список всех типов, что используются. Если тип — структура, и для нее есть список файлов — подключаем хедер этой структуры, иначе записываем эту структуру в начало файла. Есть еще один неприятный момент: структуры очень любят дублироваться. Не имею ни малейшего понятия, почему многие их них встречаются по несколько раз, причем одна за другой (на самом деле не одна за другой, между ними есть много стандартных шаблонов, но если включить фильтр, то одна за другой). При этом свойства\методы у таких структур совпадают, а отличаются они только порядковым номером. Лично я просто отсортировал массив с структурами за именами структур, и при переборе всех элементов сравнивал имя текущего с именем предыдущего. И все заработало.
РезультатОсновные ошибки: нету namespace-ов, нету фильтра для стандартных шаблонов и замены их на подключения библиотек, нету внутренней структуры файла, автор дно и пусть не генерит говнокод и многое другое. Короче работы тут еще завалом. На этом пока все, код генератора я прилагаю, может кому-то он пригодится. Дилетантский кодогенератор на гитхабе