Русский
Русский
English
Статистика
Реклама

Ida pro

Перевод Руткиты на основе BIOS. Часть 1

03.08.2020 16:04:44 | Автор: admin
Привет, Хабровчане!
В конце августа в OTUS запускается 2 мощных курса по обратной разработке кода (реверс-инжиниринг). В связи с этим приглашаем вас на День Открытых дверей, где Артур Пакулов (Ex-вирусный аналитик в Kaspersky Lab.) расскажет подробнее о программах, особенностях онлайн-формата, навыках, компетенциях и перспективах, которые ждут выпускников после обучения. А также приглашаем вас принять участие в бесплатных открытых уроках: Анализ буткита и Анализ банковского трояна.



Предпосылки


Все описанное здесь основано на проекте, который я завершил в начале 2011 года, спустя аж несколько лет после его начала. Принимая участие в CanSecWest в 2009 году, Анибал Сакко и Альфредо Ортега из Core Security провели презентацию Persistent BIOS Infection, где продемонстрировали, как можно пропатчить BIOS, чтобы совершить некоторые неприятные/удивительные вещи. Можете ознакомится с их докладом здесь. На то время это действительно впечатляло, но мне так и не выпал шанс попробовать это на практике. Год спустя мне нужно было подготовить групповой проект для учебы, поэтому я решил вернуться к взлому BIOS и самостоятельно реализовать что-нибудь из этого.


За последние несколько лет в мире BIOS для ПК многое изменилось, и теперь подписанный встроенный BIOS является стандартом, а архитектура UEFI внесла множество изменений по сравнению с традиционным дизайном BIOS. Менее чем через год после того, как я завершил этот проект, были обнаружены первые признаки того, что вредоносные программы действительно заражают BIOS в несколько похожей манере (ссылка). Простые меры, такие как подпись обновлений BIOS, легко предотвратили бы модификации BIOS такого типа. Также уже были проведены некоторые исследования для поиска путей решения этой проблемы (например, презентация Attacking Intel BIOS Рафаля Войчука и Александра Терешкина). Поэтому стоит отметить, что ничего из того, что описано здесь, не предназначено для представления миру какой-либо новой уязвимости, а скорее является демонстрацией концепции, которую можно легко проверить и изменить.


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


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


Подход


В настоящее время существует весьма ограниченное количество примеров кода, подходящих для создания BIOS-руткитов. В публичном доступе можно найти только один из них, представленный миру на первой демонстрации BIOS-руткитов в марте 2009 года (насколько я знаю). Моей изначальной целью было воспроизвести находки, сделанные Core Security в 2009 году, а затем выяснить, как я могу их модифицировать. Моей конечной целью было создание своего рода BIOS-руткита, который можно было бы легко развернуть.


В 2009 году было проведено исследование в смежной области безопасности, которая занимается руткитами загрузочного сектора. В отличие от BIOS-руткитов, разработки в этой области развивались очень быстрыми темпами, что привело к созданию и выпуску ряда различных MBR(master boot record)-руткитов. Этот тип руткитов был назван Bootkit, и, подобно BIOS-руткиту, он стремится загрузить себя до загрузки ОС. Это сходство побудило многих разработчиков буткитов отметить, что данная атака должна быть осуществимой непосредственно из BIOS, а не из MBR. Несмотря на комментарии и предложения о том, что код буткита можно было перенести для выполнения в BIOS, пока не было опубликовано ни одного примера такого кода.


Первым этапом реализации этого проекта было создание среды тестирования и разработки, в которой можно было бы вносить и отлаживать модификации BIOS. В своей статье об устойчивом заражении BIOS Сакко и Ортега подробно описывают, что они обнаружили, что VMware содержит BIOS ROM, а также GDB сервер, который можно использовать для отладки приложений, начиная с самого BIOS. После успешной работы в VMware была проделана работа по переносу модификаций BIOS VMware на другие подобные BIOS это будет описано во второй половине этой статьи.


Конфигурация VMware BIOS


Ладно, достаточно предыстории, займемся!


Первый необходимый шаг извлечь BIOS из самой VMware. В Windows это можно сделать, открыв исполняемый файл vmware-vmx.exe с помощью любого экстрактора ресурсов, например, Resource Hacker. В этом приложении объединено несколько различных двоичных ресурсов, и BIOS хранится в ресурсе с идентификатором 6006 (по крайней мере, в VMware 7). Это может разниться от версии к версии, но ключевым моментом является размер файла ресурса 512 КБ. На следующем изображении показано, как это выглядит в Resource Hacker:



Хотя этот BIOS-образ связан с приложением vmware-vmx.exe, его также можно использовать отдельно, без необходимости вносить правки в исполняемый файл VMware после каждого изменения. VMware позволяет указывать ряд скрытых параметров в файле настроек образа VMX. В какой-то момент я планирую документировать некоторые их них на вкладке Tools этого сайта, потому что они действительно очень полезны! Вот те, которые пригодятся для модификации и отладки BIOS:


bios440.filename = "BIOS.ROM"debugStub.listen.guest32 = "TRUE"debugStub.hideBreakpoint = "TRUE"monitor.debugOnStartGuest32 = "TRUE"

Первая настройка позволяет BIOS ROM загружаться не из приложения vmware-vmx, а из файла. Следующие две строки подключают встроенный GDB сервер. Этот сервер прослушивает соединения через порт 8832, когда образ работает. Последняя строка указывает VMware остановить выполнение кода в первой строке гостевого образа BIOS. Это очень полезно, так как позволяет определять точки останова и проверять память до того, как произойдет какое-либо выполнение BIOS. Тестирование было выполнено с использованием IDA Pro в качестве GDB клиента. Пример гостевого образа VMware, остановленного на первой инструкции BIOS, можно увидеть на скриншоте ниже:



При первом использовании этой тестовой среды у меня были значительные проблемы с подключением IDA к GDB серверу. После долгих проб, ошибок и тестирования с различными GDB клиентами было выявлено, что виновата моя версия VMware. Версии 6 и 6.5, похоже, не очень хорошо работают с IDA, поэтому для большинства испытаний мной использовалась VMware 7. BIOS состоит из 16-битного кода, а не 32-битного кода по умолчанию для IDA, поэтому было необходимо определить Manual Memory Regions в параметрах отладки IDA. Это позволило адресам памяти быть определенным как 16-битный код, чтобы они правильно декомпилировались.


Воссоздание готовых наработок модификация VMware BIOS


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


Работа Сакко и Ортега довольно подробно описывает их сетап и методы тестирования. Установка VMware была завершена, как описано выше, и следующим шагом было внедрение кода модификации BIOS, который они предоставили. Предоставленный код требовал извлечения BIOS ROM в отдельные модули. BIOS ROM, входящий в комплект VMware, является Phoenix BIOS. Исследование говорит, что существует два основных инструмента для работы с этим типом BIOS, инструмент с открытым исходным кодом phxdeco, и коммерческий Phoenix BIOS Editor, который предоставляется непосредственно Phoenix. В работе Сакко и Ортега рекомендовано использовать приложение Phoenix BIOS Editor они разрабатывали свой код для использования под ним. Пробная версия была загружена мной из интернета, и, похоже, она обладает всеми функциями, необходимыми для этого проекта. Разыскивая ссылку для скачивания снова, я не могу найти ничего, что кажется даже наполовину легитимным, но Google предлагает большой список вариантов. Я просто предположу, что найти какую-нибудь легитимную пробную версию не составит труда. После того, как инструменты установлены, следующим шагом будет создание пользовательского BIOS.


Сначала я убедился, что небольшая модификация образа BIOS вступит в силу в VMware, что и произошло (изменился цвет логотипа VMware). Затем я запустил скрипт сборки на Python, предоставленный Сакко и Ортега для модификации BIOS. Помимо одной опечатки в питоновском скрипте сборки BIOS все работало отлично, и новый BIOS был сохранен на диск. Однако загрузка этого BIOS в VMware не привела к тому же самому успеху: VMware выдает сообщение о том, что в виртуальной машине что-то пошло не так и что оно закрывается. Отладка этой проблемы совершалась в IDA и GDB, но проблему было трудно отследить (плюс в IDA были проблемы с версией). В целях ускорения работы была загружена другая версия VMware, чтобы тестовая среда соответствовала среде Сакко и Ортега. После некоторых поисков была найдена и установлена точная версия VMware, которую они использовали. Это, к сожалению, не решило проблему, VMware сообщила о той же ошибке. Хотя я видел, как эта модификация BIOS работала, когда демонстрировалась как часть их презентации, теперь было ясно, что их пример кода потребует дополнительной модификации, прежде чем он сможет работать на какой-либо тестовой системе.


В результате отладки кода Сакко и Ортега было изучено много разных моментов, и в итоге проблема была в инструкции на ассемблере, которая выполняла far вызов абсолютного адреса, который не был правильным для используемой BIOS. После ввода правильного адреса код BIOS был успешно выполнен, и руткит начал искать на жестком диске файлы для изменения. Этот код требовал очень много времени для сканирования на жестком диске (который был всего 15 ГБ), и он запускался несколько раз до запуска системы. Код подтверждения концепции включал в себя возможность исправления notepad.exe, чтобы он отображал сообщение при запуске, или изменения файла /etc/passwd в системе Unix, чтобы root пароль был установлен на фиксированное значение. Это показало, что руткиты могут функционировать как в Windows системах, так и в Linux, даже если они используются только в простых целях.


Тестирование буткита


Значительно позже, в процессе работы над проектом, мной была также протестирована функциональность различных буткитов, и результаты были пересмотрены, чтобы определить, какой из них будет работать лучше не только в качестве буткита, но и как BIOS-руткит. Были исследованы четыре разных буткита: Stoned, Whistler, Vbootkit и Vbootkit2. Буткиты Stoned и Whistler были разработаны для того, чтобы функционировать в большей степени как вредоносные программы, нежели руткиты, и не отличались простотой структуры исходного кода. Vbootkit2 сильно выделялся, так как он в меньшей степени предполагал из себя вредоносное ПО и имел (относительно) хорошо документированный исходный код. Этот буткит предназначался для запуска с CD, но был протестирован только с бета-версией Windows 7. При использовании с коммерческой копией Windows 7, буткит просто не загружался, так как Windows использовала другие сигнатуры файлов. Я потратил некоторое время на определение новых сигнатур файлов, чтобы можно было протестировать этот буткит, но он все равно не загружался. В целях тестирования, я нашел бета-версию Windows 7. Когда Vbootkit2 запускался на бете Windows 7, все работало, как и ожидалось. Vbootkit2 включал в себя возможность повысить процесс до привилегий уровня системы (выше уровня администратора), чтобы захватывать нажатия клавиш и сбрасывать пароли пользователей. Все эти элементы были бы полезны для включения в руткит, но для переноса этого приложения в стандартный Windows 7 требовалась значительная работа. Далее был рассмотрен Vbootkit; он был разработан для работы с Windows 2003, XP и 2000. Хотя он не был собран так, чтобы его можно было запускать с компакт-диска, для добавления этой функциональности потребовались лишь незначительные изменения. Это программное обеспечение включало только возможность повышения привилегий процесса, но даже само по себе это уже очень ценная функция. Так этот программный пакет был выбран для использования с BIOS-руткитом, который описан в следующем разделе. NVLabs являются авторами этого буткита, который по большому счету представляет основную функциональность этого проекта, поэтому большое спасибо им за то, что они опубликовали свой код! Похоже, что исходный код больше не доступен на их веб-сайте, но его все еще можно скачать с Archive.org здесь.


Внедрение кода в BIOS посмотрим в следующей части. Следите за новостями!



Узнать подробнее о курсах: Реверс-инжиниринг. Продвинутый, Реверс-инжиниринг



Подробнее..

Из песочницы Вскрытие покажет Решаем лёгкий crackme и пишем генератор ключа

02.11.2020 16:19:26 | Автор: admin
Доброго времени суток читающий. Мне хочется рассказать тебе про алгоритм решения одного лёгкого crackme и поделиться кодом генератора. Это был один из первых crackme, который я решил.

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

  • Немного языка Assembler
  • Логика вместе с отладчиком (IDA PRO)

Лекарство изготовим из яда австралийской змеи с помощью Python. Не будем терять времени.

image



Сей crackme не очень сложный. Рассмотрим алгоритм генерации ключа на правильном ключе 8365-5794-2566-0817. В IDA Pro я добавил комментарии по поводу кода.

Осмотр пациента


Поведение на первый взгляд обычное. Расширение .exe. Не упаковано. Запустим.





Что это? Требует ключ. Нужно лечить :)

Вскрытие больного


При ошибке была надпись Fail, Serial is invalid!. Надём место, где она используется в программе.



Видим 1 функцию key_check_func перед условным переходом. Осмотрим её.

Интересное древо получается.



Ставим точку останова и начинаем отладку.

Необходимо, чтобы ключ был длиной 19 символов.



Затем программа проверяет присутствие знака тире в ключе через каждые 5 символов, входя в цикл 3 раза.





После проверки на наличие тире, программ изучает состоит ли блок ключа (1/4 ключа) из цифр. Есть предположение, чтобы понять, какая цифра была передана компилятор исполняет команду add eax, 0FFFFFFD0h



Например, сложим 8 (38h) c данным числом. Получившиеся число чрезмерно большое ( 10000008h ) и на конце располагается 8, следовательно, обрезается. Остаётся 8. Это переданная нами цифра. Так происходит 4 раза в цикле.



Что теперь? Нынче начинается сложение кодов каждой цифры проверяемого блока друг с другом, однако последняя 4 цифра складывается 3 раза подряд. Получившуюся сумму опять складывают. Код последней цифры блока + получившиеся сумма 150h. Результат добавляется в r10d. Весь этот цикл повторяется 4 раза для каждого блока ключа.



В нашем случае рассмотрим на примере первого блока ключа 8365: 38h (8) + 33h (3) + 36h (6) + 35h (5) + 35h (5) + 35h (5) = 140h + 35h 150h = 25h. 25 прибавляется в r10d и записывается в память. Пометим сие место, как A. Сумма прочих блоков ключа также равна 25h. Значит, умножаем 25h * 4 = 94.

Далее происходит побитовый сдвиг в право на 2 байта. Это место для себя пометим, как B.



У нас есть значение обозначенное, как A (25h) и B (25h). Впоследствии произойдёт сравнение данных чисел. Они должны быть одинаковые. Эта операция происходит для каждого блока ключа.



Последняя, что осуществляет программа это проверяет схожи ли цифры в блоках. Поначалу сравниваются цифры 1 блока с цифрами 2 блока. Потом проверка 2 блока с 3 блоком. Заключительная проверка 3 блока с 4 блоком. Вся это проверка происходит не сразу, а постепенно в цикле.





Анализ закончен. Больной изучен.

Время для лекарства


Для генерации ключа используем нечто необычное. Python + Библиотеку random.
Сам код находится ниже. Комментарии в коде:

import randomdef gen_key_part():    # Генерация цифр    num1 = str(random.randint(0, 9))    num2 = str(random.randint(0, 9))    num3 = str(random.randint(0, 9))    num4 = str(random.randint(0, 9))    # Генерация части ключа (1 блок)    final = num1 + num2 + num3 + num4    return finaldef sum_ord(key_part):    # Раскладываем переданную часть ключа на цифры    num1 = key_part[0]    num2 = key_part[1]    num3 = key_part[2]    num4 = key_part[3]    # Алгорит сложения в crackme    sum = ord(num1) + ord(num2) + ord(num3) + ord(num4) + ord(num4) + ord(num4)    sum_final = ord(num4) + sum - 336    return sum_finaldef shr(key):    # Разбиваем ключ на блоки    a = key[0:4]    b = key[5:9]    c = key[10:14]    d = key[15:19]    # Алгоритм сдвига по битам в crackme    x = sum_ord(a) + sum_ord(b) + sum_ord(c) + sum_ord(d)    x = x >> 2    return xdef check_key(key):    i = 0 # Счётчик    while i != 4:        # Переменная i будет увеличиваться на 1 до 4. Это будет указатель на проверяемый символ        first = 0 + i        second = 5 + i        third = 10 + i        four = 15 + i        # Проверка ключа на соответсвие сдвига по битам и суммы чисел ( Обозначали, как A и B)        if sum_ord(key[0:4]) != shr(key) or sum_ord(key[5:9]) != shr(key) or sum_ord(key[10:14]) != shr(key) or sum_ord(key[15:19]) != shr(key):            return False        # Проверка ключа на совпадающие цифры        if int(key[first]) == int(key[second]):            return False        if int(key[second]) == int(key[third]):            return False        if int(key[third]) == int(key[four]):            return False        i += 1 # # Счётчик увеличиваемdef generate_key():    # Генерация ключа    key = gen_key_part() + '-' + gen_key_part() + '-' + gen_key_part() + '-' + gen_key_part()    # Проверяем ключ и печатаем true или false    while True: #        if check_key(key) == False:            # Генерация ключа если не верно            key = gen_key_part() + '-' + gen_key_part() + '-' + gen_key_part() + '-' + gen_key_part()            print('Checking this key -> ' + key)        else:            # Ключ правильный            print('This is the correct key -> ' + key)            break# Генерируем ключ, вызывая функциюif __name__ == "__main__":    generate_key()

Запускаем.



Вводим ключ и видим.



Пациент излечен.

Спасибо за внимание. Жду ваших комментариев и критики. Не болейте.
Подробнее..

IDA Pro работа с библиотечным кодом (не WinAPI)

01.07.2020 20:13:32 | Автор: admin

Всем привет,



При работе в IDA мне, да и, наверняка, вам тоже, часто приходится иметь дело с приложениями, которые имеют достаточно большой объём кода, не имеют символьной информации и, к тому же, содержат много библиотечного кода. Зачастую, такой код нужно уметь отличать от написанного пользователем. И, если на вход библиотечного кода подаются только int, void *, да const char *, можно отделаться одними лишь сигнатурами (созданные с помощью FLAIR-утилит sig-файлы). Но, если нужны структуры, аргументы, их количество, тут без дополнительной магии не обойдёшься В качестве примера я буду работать с игрой для Sony Playstation 1, написанной с использованием PSYQ v4.7.


Дополнительная магия


Представим ситуацию: вам попалась прошивка от какой-нибудь железки. Обычный Bare-metal ROM (можно даже с RTOS). Или же ROM игры. В подобных случаях, скорее всего, при компиляции использовался какой-то SDK/DDK, у которого имеется набор LIB/H/OBJ файлов, которые вклеиваются линкером в итоговый файл.


Наш план действий будет примерно таким:


  1. Взять все lib/obj-файлы, и создать из них сигнатуры (или набор сигнатур). Это поможет нам отделить статически влинкованный библиотечный код.
  2. Взять все h-файлы и создать из них библиотеку типов. Эта библиотека хранит не только типы данных, но и информацию об именах и типах аргументов функций, в которых объявленные типы используются.
  3. Применить сигнатуры, чтобы у нас определились библиотечные функции и их имена.
  4. Применить библиотеки типов, чтобы применить прототипы функций и используемые типы данных.

Создаём sig-файлы


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


  • pcf LIB/OBJ-parser, создаёт PAT-файл из COFF-объектных файлов
  • pelf LIB/OBJ-парсер, создаёт PAT-файл из ELF-файлов (Unix)
  • plb LIB/OBJ-parser, создаёт PAT-файл из OMF-объектных файлов
  • pmacho MACH-O-парсер, создаёт PAT-файл из MACH-O-файлов (MacOS)
  • ppsx OBJ-парсер, создаёт PAT-файл из библиотечных файлов PSYQ
  • ptmobj OBJ-парсер, создаёт PAT-файл из библиотечных файлов Trimedia
  • sigmake конвертирует ранее созданный PAT-файл в SIG-файл, перевариваемый IDA

В моём случае это ppsx. Собираю bat-файл, в котором перечисляю все lib- и obj-файлы, и добавляю к каждой строке вызов ppsx, чтобы получилось формирование итогового PAT-файла. Получилось следующее содержимое:


run_47.bat
@echo offppsx -a 2MBYTE.OBJ psyq47.patppsx -a 8MBYTE.OBJ psyq47.patppsx -a LIBAPI.LIB psyq47.patppsx -a LIBC.LIB psyq47.patppsx -a LIBC2.LIB psyq47.patppsx -a LIBCARD.LIB psyq47.patppsx -a LIBCD.LIB psyq47.patppsx -a LIBCOMB.LIB psyq47.patppsx -a LIBDS.LIB psyq47.patppsx -a LIBETC.LIB psyq47.patppsx -a LIBGPU.LIB psyq47.patppsx -a LIBGS.LIB psyq47.patppsx -a LIBGTE.LIB psyq47.patppsx -a LIBGUN.LIB psyq47.patppsx -a LIBHMD.LIB psyq47.patppsx -a LIBMATH.LIB psyq47.patppsx -a LIBMCRD.LIB psyq47.patppsx -a LIBMCX.LIB psyq47.patppsx -a LIBPAD.LIB psyq47.patppsx -a LIBPRESS.LIB psyq47.patppsx -a LIBSIO.LIB psyq47.patppsx -a ashldi3.obj psyq47.patppsx -a ashrdi3.obj psyq47.patppsx -a CACHE.OBJ psyq47.patppsx -a clear_cache.obj psyq47.patppsx -a CLOSE.OBJ psyq47.patppsx -a cmpdi2.obj psyq47.patppsx -a CREAT.OBJ psyq47.patppsx -a ctors.obj psyq47.patppsx -a divdi3.obj psyq47.patppsx -a dummy.obj psyq47.patppsx -a eh.obj psyq47.patppsx -a eh_compat.obj psyq47.patppsx -a exit.obj psyq47.patppsx -a ffsdi2.obj psyq47.patppsx -a fixdfdi.obj psyq47.patppsx -a fixsfdi.obj psyq47.patppsx -a fixtfdi.obj psyq47.patppsx -a fixunsdfdi.obj psyq47.patppsx -a fixunsdfsi.obj psyq47.patppsx -a fixunssfdi.obj psyq47.patppsx -a fixunssfsi.obj psyq47.patppsx -a fixunstfdi.obj psyq47.patppsx -a fixunsxfdi.obj psyq47.patppsx -a fixunsxfsi.obj psyq47.patppsx -a fixxfdi.obj psyq47.patppsx -a floatdidf.obj psyq47.patppsx -a floatdisf.obj psyq47.patppsx -a floatditf.obj psyq47.patppsx -a floatdixf.obj psyq47.patppsx -a FSINIT.OBJ psyq47.patppsx -a gcc_bcmp.obj psyq47.patppsx -a LSEEK.OBJ psyq47.patppsx -a lshrdi3.obj psyq47.patppsx -a moddi3.obj psyq47.patppsx -a muldi3.obj psyq47.patppsx -a negdi2.obj psyq47.patppsx -a new_handler.obj psyq47.patppsx -a op_delete.obj psyq47.patppsx -a op_new.obj psyq47.patppsx -a op_vdel.obj psyq47.patppsx -a op_vnew.obj psyq47.patppsx -a OPEN.OBJ psyq47.patppsx -a PROFILE.OBJ psyq47.patppsx -a pure.obj psyq47.patppsx -a read.obj psyq47.patppsx -a shtab.obj psyq47.patppsx -a snctors.obj psyq47.patppsx -a SNDEF.OBJ psyq47.patppsx -a SNMAIN.OBJ psyq47.patppsx -a SNREAD.OBJ psyq47.patppsx -a SNWRITE.OBJ psyq47.patppsx -a trampoline.obj psyq47.patppsx -a ucmpdi2.obj psyq47.patppsx -a udiv_w_sdiv.obj psyq47.patppsx -a udivdi3.obj psyq47.patppsx -a udivmoddi4.obj psyq47.patppsx -a umoddi3.obj psyq47.patppsx -a varargs.obj psyq47.patppsx -a write.obj psyq47.patppsx -a LIBSND.LIB psyq47.patppsx -a LIBSPU.LIB psyq47.patppsx -a LIBTAP.LIB psyq47.patppsx -a LOW.OBJ psyq47.patppsx -a MCGUI.OBJ psyq47.patppsx -a MCGUI_E.OBJ psyq47.patppsx -a NOHEAP.OBJ psyq47.patppsx -a NONE3.OBJ psyq47.patppsx -a NOPRINT.OBJ psyq47.patppsx -a POWERON.OBJ psyq47.pat

LIBSN.LIB файл имеет формат, отличный от остальных библиотек, поэтому пришлось разложить его на OBJ-файлы утилитой PSYLIB2.EXE, которая входит в комплект PSYQ. Запускаем run_47.bat. Получаем следующий выхлоп:


run_47.bat output
2MBYTE.OBJ: skipped 0, total 18MBYTE.OBJ: skipped 0, total 1LIBAPI.LIB: skipped 0, total 89LIBC.LIB: skipped 0, total 55LIBC2.LIB: skipped 0, total 50LIBCARD.LIB: skipped 0, total 18LIBCD.LIB: skipped 0, total 51LIBCOMB.LIB: skipped 0, total 3LIBDS.LIB: skipped 0, total 36LIBETC.LIB: skipped 0, total 8LIBGPU.LIB: skipped 0, total 60LIBGS.LIB: skipped 0, total 167LIBGTE.LIB: skipped 0, total 535LIBGUN.LIB: skipped 0, total 2LIBHMD.LIB: skipped 0, total 585LIBMATH.LIB: skipped 0, total 59LIBMCRD.LIB: skipped 0, total 7LIBMCX.LIB: skipped 0, total 31LIBPAD.LIB: skipped 0, total 21LIBPRESS.LIB: skipped 0, total 7LIBSIO.LIB: skipped 0, total 4ashldi3.obj: skipped 0, total 1ashrdi3.obj: skipped 0, total 1CACHE.OBJ: skipped 0, total 1clear_cache.obj: skipped 0, total 1CLOSE.OBJ: skipped 0, total 1cmpdi2.obj: skipped 0, total 1CREAT.OBJ: skipped 0, total 1ctors.obj: skipped 0, total 0divdi3.obj: skipped 0, total 1dummy.obj: skipped 0, total 1Fatal: Illegal relocation information at file pos 0000022Deh_compat.obj: skipped 0, total 1exit.obj: skipped 0, total 1ffsdi2.obj: skipped 0, total 1fixdfdi.obj: skipped 0, total 1fixsfdi.obj: skipped 0, total 1fixtfdi.obj: skipped 0, total 0fixunsdfdi.obj: skipped 0, total 1fixunsdfsi.obj: skipped 0, total 1fixunssfdi.obj: skipped 0, total 1fixunssfsi.obj: skipped 0, total 1fixunstfdi.obj: skipped 0, total 0fixunsxfdi.obj: skipped 0, total 0fixunsxfsi.obj: skipped 0, total 0fixxfdi.obj: skipped 0, total 0floatdidf.obj: skipped 0, total 1floatdisf.obj: skipped 0, total 1floatditf.obj: skipped 0, total 0floatdixf.obj: skipped 0, total 0FSINIT.OBJ: skipped 0, total 1gcc_bcmp.obj: skipped 0, total 1LSEEK.OBJ: skipped 0, total 1lshrdi3.obj: skipped 0, total 1moddi3.obj: skipped 0, total 1muldi3.obj: skipped 0, total 1negdi2.obj: skipped 0, total 1Fatal: Illegal relocation information at file pos 0000013Dop_delete.obj: skipped 0, total 1op_new.obj: skipped 0, total 1op_vdel.obj: skipped 0, total 1op_vnew.obj: skipped 0, total 1OPEN.OBJ: skipped 0, total 1PROFILE.OBJ: skipped 0, total 1pure.obj: skipped 0, total 1Fatal: Unknown record type 60 at 0000015Fshtab.obj: skipped 0, total 0Fatal: Unknown record type 60 at 000000EESNDEF.OBJ: skipped 0, total 0SNMAIN.OBJ: skipped 0, total 1SNREAD.OBJ: skipped 0, total 1SNWRITE.OBJ: skipped 0, total 1trampoline.obj: skipped 0, total 0ucmpdi2.obj: skipped 0, total 1udiv_w_sdiv.obj: skipped 0, total 1udivdi3.obj: skipped 0, total 1udivmoddi4.obj: skipped 0, total 1umoddi3.obj: skipped 0, total 1varargs.obj: skipped 0, total 1Fatal: Unknown record type 60 at 00000160LIBSND.LIB: skipped 0, total 223LIBSPU.LIB: skipped 0, total 126LIBTAP.LIB: skipped 0, total 1LOW.OBJ: skipped 0, total 1Fatal: can't find symbol F003MCGUI_E.OBJ: skipped 0, total 1NOHEAP.OBJ: skipped 0, total 1NONE3.OBJ: skipped 0, total 1NOPRINT.OBJ: skipped 0, total 1POWERON.OBJ: skipped 0, total 1

Видим некоторое количество ошибок парсинга, но, в тех файлах всего 1 сигнатура (total 1), поэтому, думаю, что это не критично. Далее преобразовываем PAT-файл в SIG-файл:


sigmake -n"PsyQ v4.7" psyq47.pat psyq47.sigpsyq47.sig: modules/leaves: 1345/2177, COLLISIONS: 21See the documentation to learn how to resolve collisions.

В итоге получаем следующий список файлов:


  • psyq47.err его не трогаем
  • psyq47.exc его нужно будет отредактировать
  • psyq47.pat его тоже не трогаем

Открываем на редактирование .exc-файл. Видим:


;--------- (delete these lines to allow sigmake to read this file); add '+' at the start of a line to select a module; add '-' if you are not sure about the selection; do nothing if you want to exclude all modules

Если удалить ---------, всё, что содержится в файле ниже, будет учитываться. Давайте взглянем на то, что там есть. Вот пример:


CdPosToInt                                          60 A21C 0000839001008690022903008010050021104500401002000F00633021104300DsPosToInt                                          60 A21C 0000839001008690022903008010050021104500401002000F00633021104300

Видим, что две функции имеют одну и ту же сигнатуру, и нам нужно выбрать, какую из них использовать. Для этого слева, рядом с именем нужной функции ставим +. Я выбираю первую. То же самое повторяем с остальными строками.


В итоге, если всё сделано правильно, получаем SIG-файл. Его нужно положить в соответствующую папку в каталоге установка IDA.


Создаём til-файлы


Эти файлы нужны для хранения информации о типах, об аргументах функций и т.п. По-умолчанию. Создаются они с помощью утилиты tilib, которую необходимо положить в каталог с Идой (ей, почему-то, нужен ida.hlp файл).


Данной утилите нужно скормить include-файлы вашего SDK/DDK. При том парсинг этой утилитой отличается от такового средством "Parse C header file..." в самой IDA. Вот описание из readme:


Its functionality overlaps with "Parse C header file..." from IDA Pro.
However, this utility is easier to use and provides more control
over the output. Also, it can handle the preprocessor symbols, while
the built-in command ignores them.

У этой утилиты есть один нюанс: она по умолчанию использует режим, когда символы заманглены, либо имеют стоящее в начале имени нижнее подчёркивание. В случае работы со статически влинкованным кодом этот режим нужно выключить флагом -Gn.


По-умолчанию, данная утилита принимает на вход только один include-файл. Если же файлов много, нужно соорудить include-файл следующего содержания:


#include "header1.h"#include "header2.h"#include "header3.h"// ...

Этот файл передаётся с помощью флага -hFileName.h. Далее, передаём путь поиска остальных header-файлов и получаем следующую командую строку:


tilib -c -Gn -I. -hpsyq47.h psyq47.til

На выходе получаем til-файл, пригодный для использования. Кладём его в соответствующий каталог IDA: sig\mips.


Проверяем результат


Закидываем ROM-файл в IDA, дожидаемся окончания анализа. Далее, необходимо указать компилятор. Для этого заходим в Options->Compiler:



Теперь просто меняем Unknown на GNU C++ (в случае PSX). Остальное оставляем как есть:



Теперь жмём Shift+F5 (либо меню View->Open subviews->Signatures), жмём Insert и выбираем нужный файл сигнатур:



Жмём OK и ждём, пока применяются сигнатуры (у меня получилось 482 распознанных функции).



Далее необходимо применить библиотеку типов (til-файл). Для этого жмём Shift+F11 (либо View->Open subviews->Type libraries) и понимаем, что IDA не может определить компилятор (не смотря на то, что мы его уже указали):



Но это нам всё равно не помешает выбрать til-файл (всё так же, через Insert):



Получаем то, что так хотели:



Теперь декомпилятор успешно подхватывает информацию о типах, и выхлоп становится куда лучше:



P.S.


Надеюсь, эта информация окажется для вас полезной. Удачного реверс-инжиниринга!

Подробнее..

IDA Pro каким не должен быть SDK

05.07.2020 22:23:42 | Автор: admin

Приветствую,



Эта статья будет о том, как не нужно делать, когда разрабатываешь SDK для своего продукта. А примером, можно даже сказать, самым ярким, будет IDA Pro. Те, кто хоть раз что-то разрабатывал под неё и старался поддерживать, при чтении этих строк, наверняка, сейчас вздрогнули и покрылись холодным потом. Здесь я собрал опыт сопровождения проектов, начиная с IDA v6.5, и заканчивая последней на момент написания статьи версии v7.5. В общем, погнали.


Краткое описание


SDK для IDA Pro позволяет вам разрабатывать следующие типы приложений:


  • Загрузчики различных форматов
  • Процессорные модули
  • Плагины, расширяющие функционал (процессорных модулей, интерфейса и т.п.)
  • IDC-скрипты (свой внутренний язык) и Python-скрипты (вторые использует стороннюю разработку IDAPython, которая стала неотъемлемой частью IDA)

По информации с сайта Hex-Rays, стоимость плана поддержки SDK 10000 USD. На практике же если у вас есть лицензия, вам даётся код доступа к Support-зоне, в которой вы его скачиваете и работаете с ним. Стоимость же указана на тот случай, если у вас будут появляться вопросы и вы захотите задать их разработчикам: без плана поддержки вам скажут, мол, напишите это сами; с поддержкой же, как я понимаю, отказать вам не могут. К сожалению, я не знаю ни одного человека (фирмы), который купил данный план.


Немного подробнее


С того момента, как у вас появляется желание написать что-то под IDA, и вы скачиваете SDK, вы ступаете на достаточно скользкую дорожку, на которой, к тому же, очень легко сойти с ума. И вот почему:


1) В современной разработке вы, скорее всего, уже привыкли к тому, что у любого более-менее серьёзного проекта есть либо doxygen-сгенерированная справка по API, либо вручную написанная хорошая справка, которая вам говорит что куда передаётся, что нужно заполнять, чтобы работало, и т.п., с кучей примеров.
В IDA практически в большинстве доступных пользователю API указаны лишь имена параметров и их типы, а что туда передавать можно отгадывать лишь по именам аргументов. В последнее время, конечно, со справкой у IDA стало получше, но не то что бы значительно.


2) Во многих заполняемых структурах требуется задавать callback-функции, при этом некоторые указаны как необязательные, мол, не укажешь (передашь NULL) и ладно. В действительности крэши приложения при попытке запуска вашего плагина. И, т.к. колбэков много (пример плагин-отладчик), ты начинаешь поочерёдно задавать все, которые "можно не задавать". В итоге это очень сильно утомляет, ты открываешь x64dbg/ollyDbg, в нём idaq.exe/ida.exe, грузишь плагин, ставишь точки остановки, и пытаешься словить момент, когда управление передаётся в 0x00000000.


Эх, помню те времена, когда так много папок с проектами были забиты 200MB dmp-файлами, которые создавались при крэше IDA Но мне они ничем не помогали.


3) Самая болезненная тема для IDA Pro обратная совместимость. В принципе, это достаточно тяжёлая для любого разработчика задача наперёд продумывать интерфейсы, структуры, модульность и т.п. Поэтому здесь и возникает два пути:


  • Хранить обратную совместимость со всеми старыми версиями
  • Не заниматься обратной совместимостью

В первом случае очень быстро накапливается legacy-код, от которого потом становится невозможно избавиться. Вычистить его редко кому удаётся. Потому как, фактически, нужно бросать все силы не на разработку нового функционала, а на выпиливание/перенос старого.


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


Что же в случае Hex-Rays? Вы удивитесь, но они пошли двумя путями одновременно! Известно, что проект развивается с очень и очень бородатых времён, когда основной целевой платформой был лишь MS-DOS (и, следовательно, реверс-инжиниринг написанных под него приложений). Нужно было поддерживать сегментные регистры, селекторы, параграфы и другую подобную атрибутику. Шло время, в IDA начали появляться другие платформы, процессорные модули и загрузчики, где модель памяти уже была плоской (flat), но пережиток в виде перечисленных мной MS-DOS "фич" сохраняется до сих пор! Весь интерфейс IDA пронизан этим. При разработке процессорных модулей, в который только flat, вам всё равно придётся указываться сегментные регистры (правда уже виртуальные).


А вот с SDK ни о какой обратной совместимости речи идти не может вовсе. В каждой новой версии (даже внутри минорных билдов 6.x и 7.x) что-то да ломается: у колбэков появляются новые аргументы, у старых структур переименовываются и теряются поля, функции API, которые раньше делали одну задачу, теперь делают другую. И таких примеров много.


И ладно бы это всё хоть как-то сопровождалось разработчиком, мол, в этой версии поменялось то и это, теперь нужно так и так. Так нет же! Гайд есть, да: IDA 7.0 SDK: Porting from IDA 4.9-6.x API to IDA 7.0 API, но это всё. Более того, по нему вам не удастся перевести свой проект на новую версию, т.к. он не включает очень многих, но мелких, изменений, о которых, конечно же, вам никто не сообщит. К тому же, это последний гайд для C/C++ разработчика, а с тех пор вышло ещё где-то 5-6 версий SDK.


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


Реальный пример


Когда-то я взял на себя смелость попытаться разработать свой первый плагин-отладчик Motorola 68000 под IDA. В поставляемом SDK был пример отладчика (который, фактически, используется в IDA Pro и сейчас в качестве локального и удалённого), но он был выполнен настолько плохо, что пытаться по нему сделать свой было невозможно. Тогда я полез в интернет и нашёл единственный плагин-отладчик для PS3, который, что забавно, был выполнен на базе того самого кода из SDK.


Чтобы вы понимали, почему код из SDK я так и не решился использовать, вот вам скриншот участка кода, на котором я уже тогда понял, что будет несладко:



Видите cpp-файлы, которые включены через #include? И так по всему исходнику. Тем не менее, тщательно изучив исходный код отладчика PS3, мне удалось вычленить из него что-то рабочее и сделать свой для Sega Mega Drive.


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



Для этого пришлось снова отлаживать IDA, процессорный модуль m68k, и делать исправления для последнего (об этом я писал в "Модернизация IDA Pro. Исправляем косяки процессорных модулей").


Несмотря на все трудности, мне удалось написать хороший отладчик! К сожалению, вышла новая версия SDK В ней изменилась структура debugger_t, отвечающая за отладчик и его колбэки, и всё, что я пытался сделать, приводило к крэшам самой IDA. Спустя время я и с этим справился.


Но вышла новая версия SDK x64, без совместимости с x86! А эмулятор Gens, на базе которого я делал отладчик, не умел в x64, и проект заглох на много лет. Когда же я нашёл эмулятор, который был способен работать в x64, вышло так много версий SDK, что снова пытаться понять, почему мой плагин не работает, я не решился.


Выводы


Проблемы SDK для IDA Pro в отсутствии нормальной документации об изменениях в каждой из версий; в том, что изменения выходят скопом их много, они кардинальные, и за ними очень тяжело угнаться.


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


Я видел на Github большое количество полезных проектов, которые так и остались непортированными на IDA v7.x. Можно подумать, что их функционал стал ненужным в новых версиях? Может и так, но, как по мне, это усталость и нежелание бороться с постоянно меняющимся API в совокупности с хоббийностью проекта.


IDA Pro Book


Ещё хотелось бы вспомнить об одной бесценной книге, которая мне когда-то очень помогла, но которая сейчас абсолютно бесполезна для разработчика плагинов к IDA IDA Pro Book от Chris Eagle. Всё описанное в ней относится к версии 6.x (ориентировочно v6.5-v6.8). С тех пор изменилось практически всё.


Спасибо.

Подробнее..

Получение исходного кода PowerPacker Cruncher от AmigaOS

10.08.2020 22:11:31 | Автор: admin


Всем привет,


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


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


Немного о PowerPacker


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


Сначала PowerPacker распространялся в виде самостоятельных исполняемых файлов: упаковщика и распаковщика. Затем, похоже, спрос на данный алгоритм сжатия вырос, и автор (Nico Franois) решил сделать своё творение платным, при этом перейдя на распространение уже в виде библиотеки powerpacker.library.


Получение исходников


Для получения исходников, как и в случае с RNC ProPack, пришлось написать множество вспомогательного инструментария:


  1. Плагин-отладчик для IDA Pro (не работает, забросил)
  2. Загрузчик Amiga Hunk для Ghidra (помог)
  3. Загрузчик для library-файлов для Ghidra (очень помог)
  4. gdb-сервер для AmigaOS, работающий на ней же (не работал на моих файлах)

Отдельным пунктом идёт покупка kickstart rom (это что-то типа биоса, нутрянки AmigaOS, без него работать ничего не будет).


Потом у IDA появилась возможность отлаживать через GDB в том числе и для m68k. Правда серверной части, которая могла бы при этом эмулировать и мои файлы, и AmigaOS, у меня не было. WinUAE не умеет в gdb до сих пор.


Затем, спустя несколько лет, появилось расширение для Visual Code: https://github.com/BartmanAbyss/vscode-amiga-debug, которое позволяет отлаживать исходные файлы на C, при помощи модифицированного WinUAE с добавленным в него gdb-сервером. Вот здесь я и осознал шанс на декомпиляцию есть.


Декомпиляция


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


  • циклы (бесконечные goto)
  • использование одного и того же регистра как для хранения 16-битных значений, так и для хранения 32-битных. А ещё они в какой-то момент становятся знаковыми, хотя до этого использовались как беззнаковые.

Отладочный стенд


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


C:\Users\<USER>\.vscode\extensions\bartmanabyss.amiga-debug-1.0.0

Создаём и компилируем тестовый пример (да, у расширения он имеется). В подпапке .\bin имеется следующий список файлов:


  • dh0\
  • dh0\runme.exe
  • dh0\s\
  • dh0\s\startup-sequence
  • opt\
  • default.uae
  • elf2hunk.c
  • elf2hunk.exe
  • gnumake.exe
  • winuae.ini
  • winuae-gdb.exe

Подкаталог .\dh0\s содержит файл startup-sequence, в котором указываются команды, запускаемые при старте операционной системы. У меня он выглядит вот так:


:runme.exe

Здесь можно добавить нужные аргументы или команды. Для моих целей необходимо заменить файл runme.exe на исполняемый файл от PowerPacker-а, который затем будет загружать ту самую powerpacker.library. А вот куда класть эту библиотеку я понял не сразу. Оказывается, нужно было создать в каталоге .\dh0\ подкаталог Libs (я подсмотрел эту структуру в уже запущенной AmigaOS) и положить туда. Запускаю.



После выполнения данной команды произойдёт запуск winuae-gdb.exe, открытие порта 2345 для работы с gdb, и остановка на точке входа запускаемой программы. Остаётся только подключиться с помощью IDA и её Remote GDB debugger к сессии WinUAE.



Меняем порт на 2345, жмём Debugger->Attach to process..., затем выбираем процесс с id = 0.



После этого у нас появляется окно отладки:



Как видим, адрес на котором мы стоим, отличается от адреса, на котором создавалась idb 0x10000, поэтому останавливаем отладку и делаем Rebase на 0x27D30. Это поможет в дальнейшей отладке не терять изменений, сделанных в базе.


С этого момента можно спокойно заниматься пошаговой отладкой до тех пор, пока вы не превысите количество брейкпоинтов равное 20. Сначала я не догадывался, в чём причина, но мои брейкопоинты вдруг становились неактивными, невалидными. Лишь посмотрев в исходник WinUAE (который, к тому же, собрать совершенно не просто), я нашёл ограничение в 20 брейкопоинтов. Собрав новую сборку с количеством, равным 999, мне удалось наконец-то безболезненно заниматься самим процессом отладки.


Библиотека powerpacker.library


Тут пришлось изощряться, попутно найдя изящное решение, которое может помочь и вам при отладке загружаемых библиотек. Дело в том, что загруженные в память библиотеки (как и другие появляющиеся только во время отладки регионы памяти), можно сохранять прямо в idb, и работать с ними, при желании, в статике. При этом, при перезапуске процесса отладки, вы не потеряете свои наработки по переименованию переменных, меток, и т.п. Для проворачивания этой хитрости необходимо на необходимом сегменте, после загрузки нужной библиотеки, зайти в его свойства (выбрав Edit segment...):




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


Теперь можно входить в вызовы экспортируемых функций, и, при одном и том же адресе загрузки библиотеки, вы будете попадать в свой, уже проанализированный, код. Удобно.


В случае AmigaOS вызовы экспортируемых функций выглядят как вызовы по отрицательным смещениям относительно базы, по которой загружена библиотека:



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


Результаты работы


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


Протестировав всё на 210 файлах, найдя и исправив другие вылезающие баги (такие как выход за границы массива в оригинальном алгоритме), я готов опубликовать результаты своей работы:



Ссылки


Исходники: https://github.com/lab313ru/powerpacker_src
Релизы: https://github.com/lab313ru/powerpacker_src/releases

Подробнее..

Пишем плагин отладки для SNES игр в IDA v7

16.04.2021 04:04:08 | Автор: admin


Приветствую,


Моя очень старая мечта сбылась я написал модуль-отладчик, с помощью которого можно отлаживать SNES (Super Nintendo) игры прямо в IDA! Если интересно узнать, как я это сделал, "прошу под кат" (как тут принято говорить).


Введение


Я давно увлекаюсь реверс-инжинирингом. Сначала это было просто хобби, затем стало работой (и при этом хобби никуда не делось). Только на работе "всё серьёзно", а дома это баловство в виде обратной разработки игр под ретро-приставки: Sega Mega Drive / Genesis, PS1, AmigaOS. Задача обычно стоит следующая: понять как работает игра, если есть сжатие победить его, понять как строится уровень, как размещаются враги на уровне и т.д.


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


Мне удалось разреверсить один очень крутой shoot'em-up: Thunder Force 3 (а именно благодаря этой игре я и познакомился с Идой). Я написал редактор уровней, разреверсил игру до исходников на ассемблере, и всё это попутно создавая и улучшая инструмент, который в последствии и облегчал данную работу плагин-отладчик сеговских ромов для IDA, который я назвал просто Gensida (т.к. в основе лежал один очень популярный эмулятор этой платформы GENS, а точнее его модификация).



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

Со временем я узнал, что у Thunder Force 3 есть и версия для SNES Thunder Spirits, которая имеет несколько новых уровней и некоторые изменения в интерфейсе. Так вот, мне захотелось портировать всё это на Сегу, дополнив игру. Но, знаний как о самой Super Nintendo, так и о том, как её реверсить, у меня не было. Я пошёл гуглить и понял, что как-то всё плохо с отладкой у "сеги подороже". На данный момент существует всего ДВА (!) эмулятора SNES с отладкой, и у одного нет исходников, а второй второй имеет настолько убогий исходный код, что я боялся даже с ним работать.


Тем не менее, овладев некоторыми знаниями и умениями, и переборов желание не ввязываться в такой ужасный код (эмулятора), я смог написать и Snesida отладчик SNES ромов для под IDA. И, я считаю, что теперь то уж настал тот момент, когда я готов рассказать о том, как создать более-менее полноценный отладчик для этого ревёрсерского инструмента.


Что нам потребуется


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


  1. IDA v7.x
  2. IDA SDK
  3. Эмулятор-отладчик (можно и без отладки, главное с исходниками, которые захочется допилить)
  4. Thrift (да, я выбрал его за сериализацию и RPC прямо "из коробки")
  5. Умение писать на C++

Думаю, список достаточно простой и понятный. Если чего-то из этого у вас нет, то плагин не получится, увы.


А теперь пишем код


Прежде чем начать, советую ознакомиться со статьёй "Модернизация IDA Pro. Отладчик для Sega Mega Drive (часть 2)", т.к. многие моменты здесь будут повторяться, но будут и некоторые новые (т.к. SDK Иды обновляется, и то, что работало раньше, теперь не применимо).


Собственно, написание любого плагина для IDA всегда начинается с создания кода-шаблона. Я использую для этого Visual Studio (на данный момент самой свежей является версия 2019).


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


  • d:\idasdk76\lib\x64_win_vc_32\ это для плагина, который будет работать с 32-битными приложениями (открываться в ida.exe)
  • d:\idasdk76\lib\x64_win_vc_64\ это для плагина, который будет работать с 64-битными приложениями (открываться в ida64.exe)
  • Если у вас не Windows и компилятор не Visual Studio, посмотрите другие имеющиеся папки в d:\idasdk76\lib\

В линкуемые библиотеки добавляем ida.lib. Теперь создаём пустой cpp-файл, чтобы VS показала свойства C/C++ компилятора и указываем:


  • d:\idasdk76\include\ в спискок путей к инклудам
  • Меняем /MDd и /MD на /MTd и /MT соответственно в свойствах Code Generation просто, чтобы не зависеть от лишних библиотек, которые не всегда установлены
  • __NT__;__IDP__;__X64__; в Preprocessor Definitions компилятора
  • __EA64__; дополнительно к предыдущим флагам, если плагин будет работать с 64-битными приложениями
  • Убираем SDL Checks с ним будет сложнее писать код

С подготовкой вроде бы всё. Теперь начнём писать код.


Плагин


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


ida_plugin.cpp
#include <ida.hpp>#include <idp.hpp>#include <dbg.hpp>#include <loader.hpp>#include "ida_plugin.h"extern debugger_t debugger;static bool plugin_inited;static bool init_plugin(void) {    return (ph.id == PLFM_65C816);}static void print_version(){    static const char format[] = NAME " debugger plugin v%s;\nAuthor: DrMefistO [Lab 313] <newinferno@gmail.com>.";    info(format, VERSION);    msg(format, VERSION);}static plugmod_t* idaapi init(void) {    if (init_plugin()) {        dbg = &debugger;        plugin_inited = true;        print_version();        return PLUGIN_KEEP;    }    return PLUGIN_SKIP;}static void idaapi term(void) {    if (plugin_inited) {        plugin_inited = false;    }}static bool idaapi run(size_t arg) {    return false;}char comment[] = NAME " debugger plugin by DrMefistO.";char help[] =    NAME " debugger plugin by DrMefistO.\n"    "\n"    "This module lets you debug SNES roms in IDA.\n";plugin_t PLUGIN = {    IDP_INTERFACE_VERSION,    PLUGIN_PROC | PLUGIN_DBG,    init,    term,    run,    comment,    help,    NAME " debugger plugin",    ""};

Здесь мы описываем наш плагин, инициализируем структуру dbg, т.к. мы отладчик, и указываем, что работаем мы только с платформой PLFM_65C816 (в моём случае). Более подробно в статье про отладчик для Сеги.


Следом идёт ida_plugin.h. Тут всё просто константы для cpp-файла плагина:


#pragma once#define NAME "snesida"#define VERSION "1.0"

Код самого отладчика


Собственно, пока у нас в голове только идея отладчика, и мы ей горим, всё что мы можем пока написать, это базовый код, который будем постепенно дополнять. Начиная с этой части, если сравнивать с предыдущей статьёй, появились значительные изменения в коде и концепции в написании отладчика, поэтому читаем внимательно:


ida_debug.cpp
#include <ida.hpp>#include <dbg.hpp>#include <auto.hpp>#include <deque>#include <mutex>#include "ida_plugin.h"#include "ida_debmod.h"#include "ida_registers.h"static ::std::mutex list_mutex;static eventlist_t events;static const char* const p_reg[] ={    "CF",    "ZF",    "IF",    "DF",    "XF",    "MF",    "VF",    "NF",};static register_info_t registers[] = {    {"A", 0, RC_CPU, dt_word, NULL, 0},    {"X", 0, RC_CPU, dt_word, NULL, 0},    {"Y", 0, RC_CPU, dt_word, NULL, 0},    {"D", 0, RC_CPU, dt_word, NULL, 0},    {"DB", 0, RC_CPU, dt_byte, NULL, 0},    {"PC", REGISTER_IP | REGISTER_ADDRESS, RC_CPU, dt_dword, NULL, 0},  {"S", REGISTER_SP | REGISTER_ADDRESS, RC_CPU, dt_word, NULL, 0},    {"P", REGISTER_READONLY, RC_CPU, dt_byte, p_reg, 0xFF},  {"m", REGISTER_READONLY, RC_CPU, dt_byte, NULL, 0},  {"x", REGISTER_READONLY, RC_CPU, dt_byte, NULL, 0},    {"e", REGISTER_READONLY, RC_CPU, dt_byte, NULL, 0},};static const char* register_classes[] = {    "General Registers",    NULL};static drc_t idaapi init_debugger(const char* hostname, int portnum, const char* password, qstring* errbuf){  return DRC_OK;}static drc_t idaapi term_debugger(void){  return DRC_OK;}static drc_t s_get_processes(procinfo_vec_t* procs, qstring* errbuf) {  process_info_t info;  info.name.sprnt("bsnes");  info.pid = 1;  procs->add(info);  return DRC_OK;}static drc_t idaapi s_start_process(const char* path,  const char* args,  const char* startdir,  uint32 dbg_proc_flags,  const char* input_path,  uint32 input_file_crc32,  qstring* errbuf = NULL){  ::std::lock_guard<::std::mutex> lock(list_mutex);  events.clear();  return DRC_OK;}static drc_t idaapi prepare_to_pause_process(qstring* errbuf){  return DRC_OK;}static drc_t idaapi emul_exit_process(qstring* errbuf){  return DRC_OK;}static gdecode_t idaapi get_debug_event(debug_event_t* event, int timeout_ms){  while (true)  {    ::std::lock_guard<::std::mutex> lock(list_mutex);    // are there any pending events?    if (events.retrieve(event))    {      return events.empty() ? GDE_ONE_EVENT : GDE_MANY_EVENTS;    }    if (events.empty())      break;  }  return GDE_NO_EVENT;}static drc_t idaapi continue_after_event(const debug_event_t* event){  dbg_notification_t req = get_running_notification();  switch (event->eid())  {  case PROCESS_SUSPENDED:    break;  case PROCESS_EXITED:    break;  }  return DRC_OK;}static drc_t idaapi s_set_resume_mode(thid_t tid, resume_mode_t resmod) // Run one instruction in the thread{  switch (resmod)  {  case RESMOD_INTO:    ///< step into call (the most typical single stepping)    break;  case RESMOD_OVER:    ///< step over call    break;  }  return DRC_OK;}static drc_t idaapi read_registers(thid_t tid, int clsmask, regval_t* values, qstring* errbuf){  if (clsmask & RC_CPU)  {    }    return DRC_OK;}static drc_t idaapi write_register(thid_t tid, int regidx, const regval_t* value, qstring* errbuf){  if (regidx >= static_cast<int>(SNES_REGS::SR_PC) && regidx <= static_cast<int>(SNES_REGS::SR_EFLAG)) {    }    return DRC_OK;}static drc_t idaapi get_memory_info(meminfo_vec_t& areas, qstring* errbuf){  memory_info_t info;  info.start_ea = 0x0000;  info.end_ea = 0x01FFF;  info.sclass = "STACK";  info.bitness = 0;  info.perm = SEGPERM_READ | SEGPERM_WRITE;  areas.push_back(info);  // Don't remove this loop  for (int i = 0; i < get_segm_qty(); ++i)  {    segment_t* segm = getnseg(i);    info.start_ea = segm->start_ea;    info.end_ea = segm->end_ea;    qstring buf;    get_segm_name(&buf, segm);    info.name = buf;    get_segm_class(&buf, segm);    info.sclass = buf;    info.sbase = get_segm_base(segm);    info.perm = segm->perm;    info.bitness = segm->bitness;    areas.push_back(info);  }  // Don't remove this loop    return DRC_OK;}static ssize_t idaapi read_memory(ea_t ea, void* buffer, size_t size, qstring* errbuf){  return size;}static ssize_t idaapi write_memory(ea_t ea, const void* buffer, size_t size, qstring* errbuf){  return size;}static int idaapi is_ok_bpt(bpttype_t type, ea_t ea, int len){  switch (type)  {  case BPT_EXEC:  case BPT_READ:  case BPT_WRITE:  case BPT_RDWR:    return BPT_OK;  }  return BPT_BAD_TYPE;}static drc_t idaapi update_bpts(int* nbpts, update_bpt_info_t* bpts, int nadd, int ndel, qstring* errbuf){  for (int i = 0; i < nadd; ++i)  {    ea_t start = bpts[i].ea;    ea_t end = bpts[i].ea + bpts[i].size - 1;    bpts[i].code = BPT_OK;  }  for (int i = 0; i < ndel; ++i)  {    ea_t start = bpts[nadd + i].ea;    ea_t end = bpts[nadd + i].ea + bpts[nadd + i].size - 1;    bpts[nadd + i].code = BPT_OK;  }  *nbpts = (ndel + nadd);  return DRC_OK;}static ssize_t idaapi idd_notify(void*, int msgid, va_list va) {  drc_t retcode = DRC_NONE;  qstring* errbuf;  switch (msgid)  {  case debugger_t::ev_init_debugger:  {    const char* hostname = va_arg(va, const char*);    int portnum = va_arg(va, int);    const char* password = va_arg(va, const char*);    errbuf = va_arg(va, qstring*);    QASSERT(1522, errbuf != NULL);    retcode = init_debugger(hostname, portnum, password, errbuf);  }  break;  case debugger_t::ev_term_debugger:    retcode = term_debugger();    break;  case debugger_t::ev_get_processes:  {    procinfo_vec_t* procs = va_arg(va, procinfo_vec_t*);    errbuf = va_arg(va, qstring*);    retcode = s_get_processes(procs, errbuf);  }  break;  case debugger_t::ev_start_process:  {    const char* path = va_arg(va, const char*);    const char* args = va_arg(va, const char*);    const char* startdir = va_arg(va, const char*);    uint32 dbg_proc_flags = va_arg(va, uint32);    const char* input_path = va_arg(va, const char*);    uint32 input_file_crc32 = va_arg(va, uint32);    errbuf = va_arg(va, qstring*);    retcode = s_start_process(path,      args,      startdir,      dbg_proc_flags,      input_path,      input_file_crc32,      errbuf);  }  break;  case debugger_t::ev_get_debapp_attrs:  {    debapp_attrs_t* out_pattrs = va_arg(va, debapp_attrs_t*);    out_pattrs->addrsize = 3;    out_pattrs->is_be = false;    out_pattrs->platform = "bsnes";    out_pattrs->cbsize = sizeof(debapp_attrs_t);    retcode = DRC_OK;  }  break;  case debugger_t::ev_rebase_if_required_to:  {    ea_t new_base = va_arg(va, ea_t);    retcode = DRC_OK;  }  break;  case debugger_t::ev_request_pause:    errbuf = va_arg(va, qstring*);    retcode = prepare_to_pause_process(errbuf);    break;  case debugger_t::ev_exit_process:    errbuf = va_arg(va, qstring*);    retcode = emul_exit_process(errbuf);    break;  case debugger_t::ev_get_debug_event:  {    gdecode_t* code = va_arg(va, gdecode_t*);    debug_event_t* event = va_arg(va, debug_event_t*);    int timeout_ms = va_arg(va, int);    *code = get_debug_event(event, timeout_ms);    retcode = DRC_OK;  }  break;  case debugger_t::ev_resume:  {    debug_event_t* event = va_arg(va, debug_event_t*);    retcode = continue_after_event(event);  }  break;  case debugger_t::ev_thread_suspend:  {    thid_t tid = va_argi(va, thid_t);    retcode = DRC_OK;  }  break;  case debugger_t::ev_thread_continue:  {    thid_t tid = va_argi(va, thid_t);    retcode = DRC_OK;  }  break;  case debugger_t::ev_set_resume_mode:  {    thid_t tid = va_argi(va, thid_t);    resume_mode_t resmod = va_argi(va, resume_mode_t);    retcode = s_set_resume_mode(tid, resmod);  }  break;  case debugger_t::ev_read_registers:  {    thid_t tid = va_argi(va, thid_t);    int clsmask = va_arg(va, int);    regval_t* values = va_arg(va, regval_t*);    errbuf = va_arg(va, qstring*);    retcode = read_registers(tid, clsmask, values, errbuf);  }  break;  case debugger_t::ev_write_register:  {    thid_t tid = va_argi(va, thid_t);    int regidx = va_arg(va, int);    const regval_t* value = va_arg(va, const regval_t*);    errbuf = va_arg(va, qstring*);    retcode = write_register(tid, regidx, value, errbuf);  }  break;  case debugger_t::ev_get_memory_info:  {    meminfo_vec_t* ranges = va_arg(va, meminfo_vec_t*);    errbuf = va_arg(va, qstring*);    retcode = get_memory_info(*ranges, errbuf);  }  break;  case debugger_t::ev_read_memory:  {    size_t* nbytes = va_arg(va, size_t*);    ea_t ea = va_arg(va, ea_t);    void* buffer = va_arg(va, void*);    size_t size = va_arg(va, size_t);    errbuf = va_arg(va, qstring*);    ssize_t code = read_memory(ea, buffer, size, errbuf);    *nbytes = code >= 0 ? code : 0;    retcode = code >= 0 ? DRC_OK : DRC_NOPROC;  }  break;  case debugger_t::ev_write_memory:  {    size_t* nbytes = va_arg(va, size_t*);    ea_t ea = va_arg(va, ea_t);    const void* buffer = va_arg(va, void*);    size_t size = va_arg(va, size_t);    errbuf = va_arg(va, qstring*);    ssize_t code = write_memory(ea, buffer, size, errbuf);    *nbytes = code >= 0 ? code : 0;    retcode = code >= 0 ? DRC_OK : DRC_NOPROC;  }  break;  case debugger_t::ev_check_bpt:  {    int* bptvc = va_arg(va, int*);    bpttype_t type = va_argi(va, bpttype_t);    ea_t ea = va_arg(va, ea_t);    int len = va_arg(va, int);    *bptvc = is_ok_bpt(type, ea, len);    retcode = DRC_OK;  }  break;  case debugger_t::ev_update_bpts:  {    int* nbpts = va_arg(va, int*);    update_bpt_info_t* bpts = va_arg(va, update_bpt_info_t*);    int nadd = va_arg(va, int);    int ndel = va_arg(va, int);    errbuf = va_arg(va, qstring*);    retcode = update_bpts(nbpts, bpts, nadd, ndel, errbuf);  }  break;  default:    retcode = DRC_NONE;  }  return retcode;}debugger_t debugger{    IDD_INTERFACE_VERSION,    NAME,    0x8000 + 6581, // (6)    "65816",    DBG_FLAG_NOHOST | DBG_FLAG_CAN_CONT_BPT | DBG_FLAG_SAFE | DBG_FLAG_FAKE_ATTACH | DBG_FLAG_NOPASSWORD |    DBG_FLAG_NOSTARTDIR | DBG_FLAG_NOPARAMETERS | DBG_FLAG_ANYSIZE_HWBPT | DBG_FLAG_DEBTHREAD | DBG_FLAG_PREFER_SWBPTS,    DBG_HAS_GET_PROCESSES | DBG_HAS_REQUEST_PAUSE | DBG_HAS_SET_RESUME_MODE | DBG_HAS_THREAD_SUSPEND | DBG_HAS_THREAD_CONTINUE | DBG_HAS_CHECK_BPT,    register_classes,    RC_CPU,    registers,    qnumber(registers),    0x1000,    NULL,    0,    0,    DBG_RESMOD_STEP_INTO | DBG_RESMOD_STEP_OVER,    NULL,    idd_notify};

Основное изменение, коснувшееся кода плагинов отладчиков по сравнению с тем, что мы писали в статье про отладчик для Сеги, это то, что колбэк теперь всего один idd_notify, но он один теперь обрабатывает все те сообщения, которые раньше приходилось обрабатывать по отдельности. Так что, если захотите просто портировать свой старый код плагина-отладчика, возьмите шаблон колбэка из данной статьи, и адаптируйте его под имеющийся код.


Вторым важным изменением стало введением "стандартизированных" кодов возврата у функций отладчика drc_t. Тут всё просто: если функция отработала без ошибок, возвращаем DRC_OK, иначе DRC_FAILED.


Остальные инклуды:


ida_registers.h
#pragma once#define RC_CPU (1 << 0)#define RC_PPU (1 << 1)enum class SNES_REGS : uint8_t{    SR_A,    SR_X,    SR_Y,    SR_D,    SR_DB,    SR_PC,    SR_S,    SR_P,    SR_MFLAG,    SR_XFLAG,    SR_EFLAG,};

ida_debmod.h
#pragma once#include <deque>#include <ida.hpp>#include <idd.hpp>//--------------------------------------------------------------------------// Very simple class to store pending eventsenum queue_pos_t{    IN_FRONT,    IN_BACK};struct eventlist_t : public std::deque<debug_event_t>{private:    bool synced;public:    // save a pending event    void enqueue(const debug_event_t &ev, queue_pos_t pos)    {        if (pos != IN_BACK)            push_front(ev);        else            push_back(ev);    }    // retrieve a pending event    bool retrieve(debug_event_t *event)    {        if (empty())            return false;        // get the first event and return it        *event = front();        pop_front();        return true;    }};

В ida_registers.h мы просто перечисляем список регистров для удоства обращений к ним в коде, а в ida_debmod.h описан формат eventlist_t, который мы будем использовать для хранения событий, с которыми будет работать IDA.


Подготовка завершена


Теперь, когда код шаблона у нас имеется, стоит понять, что мы будем делать дальше. А дальше нам нужно соорудить модель, по которой между IDA и эмулятором будет происходить общение. Для этого нужно держать в голове следующее:


  1. Эмулятор с функцией отладки должен уметь реагировать на запросы Иды "добавить/убрать брейкпоинт", "прочитать/записать память", "получить/изменить регистры"
  2. Эмулятор также должен: уведомлять IDA о том, что: "брейкпоинт сработал", "шаг при пошаговой отладке выполнен", или "процесс отладки начат или завершён"
  3. Ида должна уметь сообщать эмулятору о том, что есть необходимость: "добавить/убрать брейкпоинт", "прочитать/записать память", "получить/изменить регистры"
  4. Ида должна реагировать на сообщения от эмулятора о том, что: "брейкпоинт сработал", "шаг при пошаговой отладке выполнен", или "процесс отладки начат или завершён"

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


  1. IDA => эмулятор
  2. Эмулятор => IDA

Учитывая это, можно, опять же, пойти по стопам предыдущей статьи про сеговский отладчик, а можно захотеть использовать "модные и современные" технологии для реализации RPC и сериализации любых данных. Мой выбор пал в сторону Thrift, т.к. с ним работать гораздо удобнее, и он практически не требует дополнительной подготовки (как, например, доклеивание RPC в protobuf, но тут, скорее, на любителя). Единственная сложность, это компиляция сего зверя, но, я оставлю это за рамками данной статьи.


Thrift пишем прототип RPC


Давайте ещё раз посмотрим на те 4 пункта, которые я описал выше, и которые мы всё ещё держим в голове, откроем блокнот, и напишем что-то вроде этого:


service IdaClient {  oneway void start_event(),  oneway void add_visited(1:set<i32> visited, 2:bool is_step),  oneway void pause_event(1:i32 address),  oneway void stop_event(),}

Как видим, в Thrift нету ничего сложного. Здесь мы описали сервис IdaClient, которым будет пользоваться эмулятор, и обработчик которого будет располагаться в IDA. Все эти методы помечены ключевым словом oneway, т.к., по сути, нам не нужно дожидаться их выполнения, и в принципе ожидать, что их обработают.


start_event() будет сообщать Иде о том, что ром выбрал и его эмуляция началась.


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


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


stop_event() думаю, тут всё понятно. Эмуляция завершилась, например, по причине завершения процесса эмуляции.


С этим разобрались, теперь часть посложнее отладочный RPC:


service BsnesDebugger {  i32 get_cpu_reg(1:BsnesRegister reg),  BsnesRegisters get_cpu_regs(),  void set_cpu_reg(1:BsnesRegister reg, 2:i32 value),  binary read_memory(1:DbgMemorySource src, 2:i32 address, 3:i32 size),  void write_memory(1:DbgMemorySource src, 2:i32 address, 3:binary data),  void add_breakpoint(1:DbgBreakpoint bpt),  void del_breakpoint(1:DbgBreakpoint bpt),  void pause(),  void resume(),  void start_emulation(),  void exit_emulation(),  void step_into(),  void step_over(),}

Здесь у нас описана серверная часть, которая будет крутиться в эмуляторе, и к которой Ида время от времени будет приставать. Давайте разберём её более детально:


  i32 get_cpu_reg(1:BsnesRegister reg),  void set_cpu_reg(1:BsnesRegister reg, 2:i32 value),

Эти методы мы будем использовать тогда, когда нам потребуется прочитать или записать один регистр. Использованный enum BsnesRegister выглядит так:


enum BsnesRegister {  pc,  a,  x,  y,  s,  d,  db,  p,  mflag,  xflag,  eflag,}

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


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


struct BsnesRegisters {  1:i32 pc,  2:i32 a,  3:i32 x,  4:i32 y,  5:i32 s,  6:i32 d,  7:i16 db,  8:i16 p,  9:i8 mflag,  10:i8 xflag,  11:i8 eflag,}service BsnesDebugger {  ...  BsnesRegisters get_cpu_regs(),  ...}

Здесь я завёл одну общую структуру под регистры, указав их размеры и указал её в качестве возвращаемого значения для метода get_cpu_regs().


Теперь работа с памятью:


enum DbgMemorySource {  CPUBus,  APUBus,  APURAM,  DSP,  VRAM,  OAM,  CGRAM,  CartROM,  CartRAM,  SA1Bus,  SFXBus,  SGBBus,  SGBROM,  SGBRAM,}service BsnesDebugger {  ...  binary read_memory(1:DbgMemorySource src, 2:i32 address, 3:i32 size),  void write_memory(1:DbgMemorySource src, 2:i32 address, 3:binary data),  ...}

Здесь мы использовали встроенный в Thrift тип данных binary, и указали различные области памяти, которые могут быть прочитаны (взято из эмулятора).


Теперь пришла очередь брейкпоинтов:


enum BpType {  BP_PC = 1,  BP_READ = 2,  BP_WRITE = 4,}enum DbgBptSource {  CPUBus,  APURAM,  DSP,  VRAM,  OAM,  CGRAM,  SA1Bus,  SFXBus,  SGBBus,}struct DbgBreakpoint {  1:BpType type,  2:i32 bstart,  3:i32 bend,  4:bool enabled,  5:DbgBptSource src,}service BsnesDebugger {  ...  void add_breakpoint(1:DbgBreakpoint bpt),  void del_breakpoint(1:DbgBreakpoint bpt),  ...}

Т.к. список областей памяти, которые можно читать, и на которые можно ставить брейкпоинты отличаются, заводим отдельный список DbgBptSource. Также указываем тип брейкпоинта BpType и адрес его начала/конца bstart/bend. Ещё нам может понадобиться включать брейкпоинт не сразу enabled.


С основными сложными частями протокола закончили, теперь можно описать более простые:


service BsnesDebugger {  ...  void pause(),  void resume(),  void start_emulation(),  void exit_emulation(),  void step_into(),  void step_over(),  ...}

Метод pause() будет приостанавливать процесс отладки по запросу от IDA, resume() продолжать.


start_emulation() нужен для того, чтобы IDA могла сообщить эмулятору, что она начала процесс отладки, и ожидает от него какие-либо события. Фактически, используется в качестве синхронизации начала эмуляции между плагином-отладчиком и собственно эмулятором.


exit_emulation() на случай, если мы захотим остановить отладку из IDA, а не из эмулятора.


step_into() и step_over() пошаговая отладка.


Итоговый debug_proto.thrift
enum BsnesRegister {  pc,  a,  x,  y,  s,  d,  db,  p,  mflag,  xflag,  eflag,}struct BsnesRegisters {  1:i32 pc,  2:i32 a,  3:i32 x,  4:i32 y,  5:i32 s,  6:i32 d,  7:i16 db,  8:i16 p,  9:i8 mflag,  10:i8 xflag,  11:i8 eflag,}enum BpType {  BP_PC = 1,  BP_READ = 2,  BP_WRITE = 4,}enum DbgMemorySource {  CPUBus,  APUBus,  APURAM,  DSP,  VRAM,  OAM,  CGRAM,  CartROM,  CartRAM,  SA1Bus,  SFXBus,  SGBBus,  SGBROM,  SGBRAM,}enum DbgBptSource {  CPUBus,  APURAM,  DSP,  VRAM,  OAM,  CGRAM,  SA1Bus,  SFXBus,  SGBBus,}struct DbgBreakpoint {  1:BpType type,  2:i32 bstart,  3:i32 bend,  4:bool enabled,  5:DbgBptSource src,}service BsnesDebugger {  i32 get_cpu_reg(1:BsnesRegister reg),  BsnesRegisters get_cpu_regs(),  void set_cpu_reg(1:BsnesRegister reg, 2:i32 value),  binary read_memory(1:DbgMemorySource src, 2:i32 address, 3:i32 size),  void write_memory(1:DbgMemorySource src, 2:i32 address, 3:binary data),  void add_breakpoint(1:DbgBreakpoint bpt),  void del_breakpoint(1:DbgBreakpoint bpt),  void pause(),  void resume(),  void start_emulation(),  void exit_emulation(),  void step_into(),  void step_over(),}service IdaClient {  oneway void start_event(),  oneway void add_visited(1:set<i32> changed, 2:bool is_step),  oneway void pause_event(1:i32 address),  oneway void stop_event(),}

От RPC-прототипа к реализации


На этом процесс написания RPC-прототипа завершён. Чтобы сгенерировать из него код для языка C++, качаем Thrift-компилятор, выполняем из командной строки следующее:


thrift --gen cpp debug_proto.thrift

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



Добавляем сгенерированные файлы в студийный проект (кроме файлов *_server.skeleton.cpp). Также необходимо слинковать наш проект плагина (и эмулятора) со скомпилированными статичными библиотеками thrift-а и libevent-а (мы будем использовать "nonblocking" вариант Thrift). У этих библиотек имеется CMake вариант сборки, который значительно упрощает процесс.


Код IdaClient хэндлера


Теперь давайте напишем шаблон кода, реализующий IdaClient-сервис:


Необходимые инклуды и адресные пространства
#include "gen-cpp/IdaClient.h"#include "gen-cpp/BsnesDebugger.h"#include <thrift/protocol/TBinaryProtocol.h>#include <thrift/transport/TSocket.h>#include <thrift/transport/TBufferTransports.h>#include <thrift/server/TNonblockingServer.h>#include <thrift/transport/TNonblockingServerSocket.h>#include <thrift/concurrency/ThreadFactory.h>using namespace ::apache::thrift;using namespace ::apache::thrift::protocol;using namespace ::apache::thrift::transport;using namespace ::apache::thrift::server;using namespace ::apache::thrift::concurrency;::std::shared_ptr<BsnesDebuggerClient> client;::std::shared_ptr<TNonblockingServer> srv;::std::shared_ptr<TTransport> cli_transport;

Реализация серверной части IdaClient
static void pause_execution(){  try {    if (client) {      client->pause();    }  }  catch (...) {  }}static void continue_execution(){  try {    if (client) {      client->resume();    }  }  catch (...) {  }}static void stop_server() {  try {    srv->stop();  }  catch (...) {  }}static void finish_execution(){  try {    if (client) {      client->exit_emulation();    }  }  catch (...) {  }  stop_server();}class IdaClientHandler : virtual public IdaClientIf {public:    void pause_event(const int32_t address) override {    ::std::lock_guard<::std::mutex> lock(list_mutex);    debug_event_t ev;    ev.pid = 1;    ev.tid = 1;    ev.ea = address | 0x800000;    ev.handled = true;    ev.set_eid(PROCESS_SUSPENDED);    events.enqueue(ev, IN_BACK);    }    void start_event() override {    ::std::lock_guard<::std::mutex> lock(list_mutex);    debug_event_t ev;    ev.pid = 1;    ev.tid = 1;    ev.ea = BADADDR;    ev.handled = true;    ev.set_modinfo(PROCESS_STARTED).name.sprnt("BSNES");    ev.set_modinfo(PROCESS_STARTED).base = 0;    ev.set_modinfo(PROCESS_STARTED).size = 0;    ev.set_modinfo(PROCESS_STARTED).rebase_to = BADADDR;    events.enqueue(ev, IN_BACK);    }    void stop_event() override {    ::std::lock_guard<::std::mutex> lock(list_mutex);    debug_event_t ev;    ev.pid = 1;    ev.handled = true;    ev.set_exit_code(PROCESS_EXITED, 0);    events.enqueue(ev, IN_BACK);    }  void add_visited(const std::set<int32_t>& changed, bool is_step) override {  }};

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


Теперь напишем код, который будет отвечать за поднятие сервиса на стороне Иды (будем использовать порт 9091), и ожидание подключения к эмулятору:


init_ida_server и init_emu_client
static void init_ida_server() {    try {    ::std::shared_ptr<IdaClientHandler> handler(new IdaClientHandler());    ::std::shared_ptr<TProcessor> processor(new IdaClientProcessor(handler));    ::std::shared_ptr<TNonblockingServerTransport> serverTransport(new TNonblockingServerSocket(9091));    ::std::shared_ptr<TFramedTransportFactory> transportFactory(new TFramedTransportFactory());    ::std::shared_ptr<TProtocolFactory> protocolFactory(new TBinaryProtocolFactory());    srv = ::std::shared_ptr<TNonblockingServer>(new TNonblockingServer(processor, protocolFactory, serverTransport));    ::std::shared_ptr<ThreadFactory> tf(new ThreadFactory());    ::std::shared_ptr<Thread> thread = tf->newThread(srv);    thread->start();    } catch (...) {    }}static void init_emu_client() {  ::std::shared_ptr<TTransport> socket(new TSocket("127.0.0.1", 9090));  cli_transport = ::std::shared_ptr<TTransport>(new TFramedTransport(socket));  ::std::shared_ptr<TBinaryProtocol> protocol(new TBinaryProtocol(cli_transport));  client = ::std::shared_ptr<BsnesDebuggerClient>(new BsnesDebuggerClient(protocol));  show_wait_box("Waiting for BSNES-PLUS emulation...");  while (true) {    if (user_cancelled()) {      break;    }    try {      cli_transport->open();      break;    }    catch (...) {    }  }  hide_wait_box();}

Осталось дополнить имеющийся шаблон ida_debug.cpp кодом для работы со Thrift. Вот что получилось:


Полный код ida_debug.cpp
#include "gen-cpp/IdaClient.h"#include "gen-cpp/BsnesDebugger.h"#include <thrift/protocol/TBinaryProtocol.h>#include <thrift/transport/TSocket.h>#include <thrift/transport/TBufferTransports.h>#include <thrift/server/TNonblockingServer.h>#include <thrift/transport/TNonblockingServerSocket.h>#include <thrift/concurrency/ThreadFactory.h>using namespace ::apache::thrift;using namespace ::apache::thrift::protocol;using namespace ::apache::thrift::transport;using namespace ::apache::thrift::server;using namespace ::apache::thrift::concurrency;#include <ida.hpp>#include <dbg.hpp>#include <auto.hpp>#include <deque>#include <mutex>#include "ida_plugin.h"#include "ida_debmod.h"#include "ida_registers.h"::std::shared_ptr<BsnesDebuggerClient> client;::std::shared_ptr<TNonblockingServer> srv;::std::shared_ptr<TTransport> cli_transport;static ::std::mutex list_mutex;static eventlist_t events;static const char* const p_reg[] ={    "CF",    "ZF",    "IF",    "DF",    "XF",    "MF",    "VF",    "NF",};static register_info_t registers[] = {    {"A", 0, RC_CPU, dt_word, NULL, 0},    {"X", 0, RC_CPU, dt_word, NULL, 0},    {"Y", 0, RC_CPU, dt_word, NULL, 0},    {"D", 0, RC_CPU, dt_word, NULL, 0},    {"DB", 0, RC_CPU, dt_byte, NULL, 0},    {"PC", REGISTER_IP | REGISTER_ADDRESS, RC_CPU, dt_dword, NULL, 0},  {"S", REGISTER_SP | REGISTER_ADDRESS, RC_CPU, dt_word, NULL, 0},    {"P", REGISTER_READONLY, RC_CPU, dt_byte, p_reg, 0xFF},  {"m", REGISTER_READONLY, RC_CPU, dt_byte, NULL, 0},  {"x", REGISTER_READONLY, RC_CPU, dt_byte, NULL, 0},    {"e", REGISTER_READONLY, RC_CPU, dt_byte, NULL, 0},};static const char* register_classes[] = {    "General Registers",    NULL};static struct apply_codemap_req : public exec_request_t {private:  const std::set<int32_t>& _changed;  const bool _is_step;public:  apply_codemap_req(const std::set<int32_t>& changed, bool is_step) : _changed(changed), _is_step(is_step) {};  int idaapi execute(void) override {    auto m = _changed.size();    if (!_is_step) {      show_wait_box("Applying codemap: %d/%d...", 1, m);    }    auto x = 0;    for (auto i = _changed.cbegin(); i != _changed.cend(); ++i) {      if (!_is_step && user_cancelled()) {        break;      }      if (!_is_step) {        replace_wait_box("Applying codemap: %d/%d...", x, m);      }      ea_t addr = (ea_t)(*i | 0x800000);      auto_make_code(addr);      plan_ea(addr);      show_addr(addr);      x++;    }    if (!_is_step) {      hide_wait_box();    }    return 0;  }};static void apply_codemap(const std::set<int32_t>& changed, bool is_step){  if (changed.empty()) return;  apply_codemap_req req(changed, is_step);  execute_sync(req, MFF_FAST);}static void pause_execution(){  try {    if (client) {      client->pause();    }  }  catch (...) {  }}static void continue_execution(){  try {    if (client) {      client->resume();    }  }  catch (...) {  }}static void stop_server() {  try {    srv->stop();  }  catch (...) {  }}static void finish_execution(){  try {    if (client) {      client->exit_emulation();    }  }  catch (...) {  }  stop_server();}class IdaClientHandler : virtual public IdaClientIf {public:    void pause_event(const int32_t address) override {    ::std::lock_guard<::std::mutex> lock(list_mutex);    debug_event_t ev;    ev.pid = 1;    ev.tid = 1;    ev.ea = address | 0x800000;    ev.handled = true;    ev.set_eid(PROCESS_SUSPENDED);    events.enqueue(ev, IN_BACK);    }    void start_event() override {    ::std::lock_guard<::std::mutex> lock(list_mutex);    debug_event_t ev;    ev.pid = 1;    ev.tid = 1;    ev.ea = BADADDR;    ev.handled = true;    ev.set_modinfo(PROCESS_STARTED).name.sprnt("BSNES");    ev.set_modinfo(PROCESS_STARTED).base = 0;    ev.set_modinfo(PROCESS_STARTED).size = 0;    ev.set_modinfo(PROCESS_STARTED).rebase_to = BADADDR;    events.enqueue(ev, IN_BACK);    }    void stop_event() override {    ::std::lock_guard<::std::mutex> lock(list_mutex);    debug_event_t ev;    ev.pid = 1;    ev.handled = true;    ev.set_exit_code(PROCESS_EXITED, 0);    events.enqueue(ev, IN_BACK);    }  void add_visited(const std::set<int32_t>& changed, bool is_step) override {    apply_codemap(changed, is_step);  }};static void init_ida_server() {    try {    ::std::shared_ptr<IdaClientHandler> handler(new IdaClientHandler());    ::std::shared_ptr<TProcessor> processor(new IdaClientProcessor(handler));    ::std::shared_ptr<TNonblockingServerTransport> serverTransport(new TNonblockingServerSocket(9091));    ::std::shared_ptr<TFramedTransportFactory> transportFactory(new TFramedTransportFactory());    ::std::shared_ptr<TProtocolFactory> protocolFactory(new TBinaryProtocolFactory());    srv = ::std::shared_ptr<TNonblockingServer>(new TNonblockingServer(processor, protocolFactory, serverTransport));    ::std::shared_ptr<ThreadFactory> tf(new ThreadFactory());    ::std::shared_ptr<Thread> thread = tf->newThread(srv);    thread->start();    } catch (...) {    }}static void init_emu_client() {  ::std::shared_ptr<TTransport> socket(new TSocket("127.0.0.1", 9090));  cli_transport = ::std::shared_ptr<TTransport>(new TFramedTransport(socket));  ::std::shared_ptr<TBinaryProtocol> protocol(new TBinaryProtocol(cli_transport));  client = ::std::shared_ptr<BsnesDebuggerClient>(new BsnesDebuggerClient(protocol));  show_wait_box("Waiting for BSNES-PLUS emulation...");  while (true) {    if (user_cancelled()) {      break;    }    try {      cli_transport->open();      break;    }    catch (...) {    }  }  hide_wait_box();}static drc_t idaapi init_debugger(const char* hostname, int portnum, const char* password, qstring* errbuf){  return DRC_OK;}static drc_t idaapi term_debugger(void){  finish_execution();  return DRC_OK;}static drc_t s_get_processes(procinfo_vec_t* procs, qstring* errbuf) {  process_info_t info;  info.name.sprnt("bsnes");  info.pid = 1;  procs->add(info);  return DRC_OK;}static drc_t idaapi s_start_process(const char* path,  const char* args,  const char* startdir,  uint32 dbg_proc_flags,  const char* input_path,  uint32 input_file_crc32,  qstring* errbuf = NULL){  ::std::lock_guard<::std::mutex> lock(list_mutex);  events.clear();  init_ida_server();  init_emu_client();  try {    if (client) {      client->start_emulation();    }  }  catch (...) {    return DRC_FAILED;  }  return DRC_OK;}static drc_t idaapi prepare_to_pause_process(qstring* errbuf){  pause_execution();  return DRC_OK;}static drc_t idaapi emul_exit_process(qstring* errbuf){  finish_execution();  return DRC_OK;}static gdecode_t idaapi get_debug_event(debug_event_t* event, int timeout_ms){  while (true)  {    ::std::lock_guard<::std::mutex> lock(list_mutex);    // are there any pending events?    if (events.retrieve(event))    {      return events.empty() ? GDE_ONE_EVENT : GDE_MANY_EVENTS;    }    if (events.empty())      break;  }  return GDE_NO_EVENT;}static drc_t idaapi continue_after_event(const debug_event_t* event){  dbg_notification_t req = get_running_notification();  switch (event->eid())  {  case STEP:  case PROCESS_SUSPENDED:    if (req == dbg_null || req == dbg_run_to) {      continue_execution();    }    break;  case PROCESS_EXITED:    stop_server();    break;  }  return DRC_OK;}static drc_t idaapi s_set_resume_mode(thid_t tid, resume_mode_t resmod) // Run one instruction in the thread{  switch (resmod)  {  case RESMOD_INTO:    ///< step into call (the most typical single stepping)    try {      if (client) {        client->step_into();      }    }    catch (...) {      return DRC_FAILED;    }    break;  case RESMOD_OVER:    ///< step over call    try {      if (client) {        client->step_over();      }    }    catch (...) {      return DRC_FAILED;    }    break;  }  return DRC_OK;}static drc_t idaapi read_registers(thid_t tid, int clsmask, regval_t* values, qstring* errbuf){  if (clsmask & RC_CPU)  {        BsnesRegisters regs;    try {      if (client) {        client->get_cpu_regs(regs);                values[static_cast<int>(SNES_REGS::SR_PC)].ival = regs.pc | 0x800000;                values[static_cast<int>(SNES_REGS::SR_A)].ival = regs.a;                values[static_cast<int>(SNES_REGS::SR_X)].ival = regs.x;                values[static_cast<int>(SNES_REGS::SR_Y)].ival = regs.y;                values[static_cast<int>(SNES_REGS::SR_S)].ival = regs.s;                values[static_cast<int>(SNES_REGS::SR_D)].ival = regs.d;                values[static_cast<int>(SNES_REGS::SR_DB)].ival = regs.db;                values[static_cast<int>(SNES_REGS::SR_P)].ival = regs.p;        values[static_cast<int>(SNES_REGS::SR_MFLAG)].ival = regs.mflag;        values[static_cast<int>(SNES_REGS::SR_XFLAG)].ival = regs.xflag;                values[static_cast<int>(SNES_REGS::SR_EFLAG)].ival = regs.eflag;      }    }    catch (...) {      return DRC_FAILED;    }    }    return DRC_OK;}static drc_t idaapi write_register(thid_t tid, int regidx, const regval_t* value, qstring* errbuf){  if (regidx >= static_cast<int>(SNES_REGS::SR_PC) && regidx <= static_cast<int>(SNES_REGS::SR_EFLAG)) {    try {      if (client) {        client->set_cpu_reg(static_cast<BsnesRegister::type>(regidx), value->ival & 0xFFFFFFFF);      }    }    catch (...) {      return DRC_FAILED;    }    }    return DRC_OK;}static drc_t idaapi get_memory_info(meminfo_vec_t& areas, qstring* errbuf){  memory_info_t info;  info.start_ea = 0x0000;  info.end_ea = 0x01FFF;  info.sclass = "STACK";  info.bitness = 0;  info.perm = SEGPERM_READ | SEGPERM_WRITE;  areas.push_back(info);  // Don't remove this loop  for (int i = 0; i < get_segm_qty(); ++i)  {    segment_t* segm = getnseg(i);    info.start_ea = segm->start_ea;    info.end_ea = segm->end_ea;    qstring buf;    get_segm_name(&buf, segm);    info.name = buf;    get_segm_class(&buf, segm);    info.sclass = buf;    info.sbase = get_segm_base(segm);    info.perm = segm->perm;    info.bitness = segm->bitness;    areas.push_back(info);  }  // Don't remove this loop    return DRC_OK;}static ssize_t idaapi read_memory(ea_t ea, void* buffer, size_t size, qstring* errbuf){  std::string mem;  try {    if (client) {      client->read_memory(mem, DbgMemorySource::CPUBus, (int32_t)ea, (int32_t)size);      memcpy(&((unsigned char*)buffer)[0], mem.c_str(), size);    }  }  catch (...) {    return DRC_FAILED;  }  return size;}static ssize_t idaapi write_memory(ea_t ea, const void* buffer, size_t size, qstring* errbuf){  std::string mem((const char*)buffer);  try {    if (client) {      client->write_memory(DbgMemorySource::CPUBus, (int32_t)ea, mem);    }  }  catch (...) {    return 0;  }  return size;}static int idaapi is_ok_bpt(bpttype_t type, ea_t ea, int len){  DbgMemorySource::type btype = DbgMemorySource::CPUBus;  switch (btype) {  case DbgMemorySource::CPUBus:  case DbgMemorySource::APURAM:  case DbgMemorySource::DSP:  case DbgMemorySource::VRAM:  case DbgMemorySource::OAM:  case DbgMemorySource::CGRAM:  case DbgMemorySource::SA1Bus:  case DbgMemorySource::SFXBus:    break;  default:    return BPT_BAD_TYPE;  }  switch (type)  {  case BPT_EXEC:  case BPT_READ:  case BPT_WRITE:  case BPT_RDWR:    return BPT_OK;  }  return BPT_BAD_TYPE;}static drc_t idaapi update_bpts(int* nbpts, update_bpt_info_t* bpts, int nadd, int ndel, qstring* errbuf){  for (int i = 0; i < nadd; ++i)  {    ea_t start = bpts[i].ea;    ea_t end = bpts[i].ea + bpts[i].size - 1;    DbgBreakpoint bp;    bp.bstart = start;    bp.bend = end;    bp.enabled = true;    switch (bpts[i].type)    {    case BPT_EXEC:      bp.type = BpType::BP_PC;      break;    case BPT_READ:      bp.type = BpType::BP_READ;      break;    case BPT_WRITE:      bp.type = BpType::BP_WRITE;      break;    case BPT_RDWR:      bp.type = BpType::BP_READ;      break;    }    DbgMemorySource::type type = DbgMemorySource::CPUBus;    switch (type) {    case DbgMemorySource::CPUBus:      bp.src = DbgBptSource::CPUBus;      break;    case DbgMemorySource::APURAM:      bp.src = DbgBptSource::APURAM;      break;    case DbgMemorySource::DSP:      bp.src = DbgBptSource::DSP;      break;    case DbgMemorySource::VRAM:      bp.src = DbgBptSource::VRAM;      break;    case DbgMemorySource::OAM:      bp.src = DbgBptSource::OAM;      break;    case DbgMemorySource::CGRAM:      bp.src = DbgBptSource::CGRAM;      break;    case DbgMemorySource::SA1Bus:      bp.src = DbgBptSource::SA1Bus;      break;    case DbgMemorySource::SFXBus:      bp.src = DbgBptSource::SFXBus;      break;    default:      continue;    }    try {      if (client) {        client->add_breakpoint(bp);      }    }    catch (...) {      return DRC_FAILED;    }    bpts[i].code = BPT_OK;  }  for (int i = 0; i < ndel; ++i)  {    ea_t start = bpts[nadd + i].ea;    ea_t end = bpts[nadd + i].ea + bpts[nadd + i].size - 1;    DbgBreakpoint bp;    bp.bstart = start;    bp.bend = end;    bp.enabled = true;    switch (bpts[i].type)    {    case BPT_EXEC:      bp.type = BpType::BP_PC;      break;    case BPT_READ:      bp.type = BpType::BP_READ;      break;    case BPT_WRITE:      bp.type = BpType::BP_WRITE;      break;    case BPT_RDWR:      bp.type = BpType::BP_READ;      break;    }    DbgMemorySource::type type = DbgMemorySource::CPUBus;    switch (type) {    case DbgMemorySource::CPUBus:      bp.src = DbgBptSource::CPUBus;      break;    case DbgMemorySource::APURAM:      bp.src = DbgBptSource::APURAM;      break;    case DbgMemorySource::DSP:      bp.src = DbgBptSource::DSP;      break;    case DbgMemorySource::VRAM:      bp.src = DbgBptSource::VRAM;      break;    case DbgMemorySource::OAM:      bp.src = DbgBptSource::OAM;      break;    case DbgMemorySource::CGRAM:      bp.src = DbgBptSource::CGRAM;      break;    case DbgMemorySource::SA1Bus:      bp.src = DbgBptSource::SA1Bus;      break;    case DbgMemorySource::SFXBus:      bp.src = DbgBptSource::SFXBus;      break;    default:      continue;    }    try {      if (client) {        client->del_breakpoint(bp);      }    }    catch (...) {      return DRC_FAILED;    }    bpts[nadd + i].code = BPT_OK;  }  *nbpts = (ndel + nadd);  return DRC_OK;}static ssize_t idaapi idd_notify(void*, int msgid, va_list va) {  drc_t retcode = DRC_NONE;  qstring* errbuf;  switch (msgid)  {  case debugger_t::ev_init_debugger:  {    const char* hostname = va_arg(va, const char*);    int portnum = va_arg(va, int);    const char* password = va_arg(va, const char*);    errbuf = va_arg(va, qstring*);    QASSERT(1522, errbuf != NULL);    retcode = init_debugger(hostname, portnum, password, errbuf);  }  break;  case debugger_t::ev_term_debugger:    retcode = term_debugger();    break;  case debugger_t::ev_get_processes:  {    procinfo_vec_t* procs = va_arg(va, procinfo_vec_t*);    errbuf = va_arg(va, qstring*);    retcode = s_get_processes(procs, errbuf);  }  break;  case debugger_t::ev_start_process:  {    const char* path = va_arg(va, const char*);    const char* args = va_arg(va, const char*);    const char* startdir = va_arg(va, const char*);    uint32 dbg_proc_flags = va_arg(va, uint32);    const char* input_path = va_arg(va, const char*);    uint32 input_file_crc32 = va_arg(va, uint32);    errbuf = va_arg(va, qstring*);    retcode = s_start_process(path,      args,      startdir,      dbg_proc_flags,      input_path,      input_file_crc32,      errbuf);  }  break;  case debugger_t::ev_get_debapp_attrs:  {    debapp_attrs_t* out_pattrs = va_arg(va, debapp_attrs_t*);    out_pattrs->addrsize = 3;    out_pattrs->is_be = false;    out_pattrs->platform = "snes";    out_pattrs->cbsize = sizeof(debapp_attrs_t);    retcode = DRC_OK;  }  break;  case debugger_t::ev_rebase_if_required_to:  {    ea_t new_base = va_arg(va, ea_t);    retcode = DRC_OK;  }  break;  case debugger_t::ev_request_pause:    errbuf = va_arg(va, qstring*);    retcode = prepare_to_pause_process(errbuf);    break;  case debugger_t::ev_exit_process:    errbuf = va_arg(va, qstring*);    retcode = emul_exit_process(errbuf);    break;  case debugger_t::ev_get_debug_event:  {    gdecode_t* code = va_arg(va, gdecode_t*);    debug_event_t* event = va_arg(va, debug_event_t*);    int timeout_ms = va_arg(va, int);    *code = get_debug_event(event, timeout_ms);    retcode = DRC_OK;  }  break;  case debugger_t::ev_resume:  {    debug_event_t* event = va_arg(va, debug_event_t*);    retcode = continue_after_event(event);  }  break;  case debugger_t::ev_thread_suspend:  {    thid_t tid = va_argi(va, thid_t);    pause_execution();    retcode = DRC_OK;  }  break;  case debugger_t::ev_thread_continue:  {    thid_t tid = va_argi(va, thid_t);    continue_execution();    retcode = DRC_OK;  }  break;  case debugger_t::ev_set_resume_mode:  {    thid_t tid = va_argi(va, thid_t);    resume_mode_t resmod = va_argi(va, resume_mode_t);    retcode = s_set_resume_mode(tid, resmod);  }  break;  case debugger_t::ev_read_registers:  {    thid_t tid = va_argi(va, thid_t);    int clsmask = va_arg(va, int);    regval_t* values = va_arg(va, regval_t*);    errbuf = va_arg(va, qstring*);    retcode = read_registers(tid, clsmask, values, errbuf);  }  break;  case debugger_t::ev_write_register:  {    thid_t tid = va_argi(va, thid_t);    int regidx = va_arg(va, int);    const regval_t* value = va_arg(va, const regval_t*);    errbuf = va_arg(va, qstring*);    retcode = write_register(tid, regidx, value, errbuf);  }  break;  case debugger_t::ev_get_memory_info:  {    meminfo_vec_t* ranges = va_arg(va, meminfo_vec_t*);    errbuf = va_arg(va, qstring*);    retcode = get_memory_info(*ranges, errbuf);  }  break;  case debugger_t::ev_read_memory:  {    size_t* nbytes = va_arg(va, size_t*);    ea_t ea = va_arg(va, ea_t);    void* buffer = va_arg(va, void*);    size_t size = va_arg(va, size_t);    errbuf = va_arg(va, qstring*);    ssize_t code = read_memory(ea, buffer, size, errbuf);    *nbytes = code >= 0 ? code : 0;    retcode = code >= 0 ? DRC_OK : DRC_NOPROC;  }  break;  case debugger_t::ev_write_memory:  {    size_t* nbytes = va_arg(va, size_t*);    ea_t ea = va_arg(va, ea_t);    const void* buffer = va_arg(va, void*);    size_t size = va_arg(va, size_t);    errbuf = va_arg(va, qstring*);    ssize_t code = write_memory(ea, buffer, size, errbuf);    *nbytes = code >= 0 ? code : 0;    retcode = code >= 0 ? DRC_OK : DRC_NOPROC;  }  break;  case debugger_t::ev_check_bpt:  {    int* bptvc = va_arg(va, int*);    bpttype_t type = va_argi(va, bpttype_t);    ea_t ea = va_arg(va, ea_t);    int len = va_arg(va, int);    *bptvc = is_ok_bpt(type, ea, len);    retcode = DRC_OK;  }  break;  case debugger_t::ev_update_bpts:  {    int* nbpts = va_arg(va, int*);    update_bpt_info_t* bpts = va_arg(va, update_bpt_info_t*);    int nadd = va_arg(va, int);    int ndel = va_arg(va, int);    errbuf = va_arg(va, qstring*);    retcode = update_bpts(nbpts, bpts, nadd, ndel, errbuf);  }  break;  default:    retcode = DRC_NONE;  }  return retcode;}debugger_t debugger{    IDD_INTERFACE_VERSION,    NAME,    0x8000 + 6581, // (6)    "65816",    DBG_FLAG_NOHOST | DBG_FLAG_CAN_CONT_BPT | DBG_FLAG_SAFE | DBG_FLAG_FAKE_ATTACH | DBG_FLAG_NOPASSWORD |    DBG_FLAG_NOSTARTDIR | DBG_FLAG_NOPARAMETERS | DBG_FLAG_ANYSIZE_HWBPT | DBG_FLAG_DEBTHREAD | DBG_FLAG_PREFER_SWBPTS,    DBG_HAS_GET_PROCESSES | DBG_HAS_REQUEST_PAUSE | DBG_HAS_SET_RESUME_MODE | DBG_HAS_THREAD_SUSPEND | DBG_HAS_THREAD_CONTINUE | DBG_HAS_CHECK_BPT,    register_classes,    RC_CPU,    registers,    qnumber(registers),    0x1000,    NULL,    0,    0,    DBG_RESMOD_STEP_INTO | DBG_RESMOD_STEP_OVER,    NULL,    idd_notify};

Дабы не описывать весь этот код, здесь я опишу лишь типичный код для работы со Thrift со стороны IDA:


    try {      if (client) {        client->step_over();      }    }    catch (...) {      return DRC_FAILED;    }    return DRC_OK;

Т.е. мы просто оборачиваем код для работы с клиентом BsnesDebugger (серверную часть которого сейчас также напишем) в обработчик исключения и возвращаем либо ошибку, либо ОК.


Код BsnesDebugger хэндлера


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


remote_debugger.cpp
#include "gen-cpp/IdaClient.h"#include "gen-cpp/BsnesDebugger.h"#include <thrift/protocol/TBinaryProtocol.h>#include <thrift/transport/TSocket.h>#include <thrift/transport/TBufferTransports.h>#include <thrift/server/TNonblockingServer.h>#include <thrift/transport/TNonblockingServerSocket.h>#include <thrift/concurrency/ThreadFactory.h>using namespace ::apache::thrift;using namespace ::apache::thrift::protocol;using namespace ::apache::thrift::transport;using namespace ::apache::thrift::server;using namespace ::apache::thrift::concurrency;#include "../ui-base.hpp"static ::std::shared_ptr<IdaClientClient> client;static ::std::shared_ptr<TNonblockingServer> srv;static ::std::shared_ptr<TTransport> cli_transport;static ::std::mutex list_mutex;::std::set<int32_t> visited;static void send_visited(bool is_step) {  const auto part = visited.size();  ::std::lock_guard<::std::mutex> lock(list_mutex);  try {    if (client) {      client->add_visited(visited, is_step);    }  }  catch (...) {  }  visited.clear();}static void stop_client() {  try {    if (client) {      send_visited(false);      client->stop_event();    }    cli_transport->close();  }  catch (...) {  }}static void init_ida_client() {  ::std::shared_ptr<TTransport> socket(new TSocket("127.0.0.1", 9091));  cli_transport = ::std::shared_ptr<TTransport>(new TFramedTransport(socket));  ::std::shared_ptr<TBinaryProtocol> protocol(new TBinaryProtocol(cli_transport));  client = ::std::shared_ptr<IdaClientClient>(new IdaClientClient(protocol));  while (true) {    try {      cli_transport->open();      break;    }    catch (...) {      Sleep(10);    }  }  atexit(stop_client);}static void toggle_pause(bool enable) {  application.debug = enable;  application.debugrun = enable;  if (enable) {    audio.clear();  }}class BsnesDebuggerHandler : virtual public BsnesDebuggerIf {public:  int32_t get_cpu_reg(const BsnesRegister::type reg) override {    switch (reg) {    case BsnesRegister::pc:    case BsnesRegister::a:    case BsnesRegister::x:    case BsnesRegister::y:    case BsnesRegister::s:    case BsnesRegister::d:    case BsnesRegister::db:    case BsnesRegister::p:      return SNES::cpu.getRegister((SNES::CPUDebugger::Register)reg);    case BsnesRegister::mflag:      return (SNES::cpu.usage[SNES::cpu.regs.pc] & SNES::CPUDebugger::UsageFlagM) ? 1 : 0;    case BsnesRegister::xflag:      return (SNES::cpu.usage[SNES::cpu.regs.pc] & SNES::CPUDebugger::UsageFlagX) ? 1 : 0;    case BsnesRegister::eflag:      return (SNES::cpu.usage[SNES::cpu.regs.pc] & SNES::CPUDebugger::UsageFlagE) ? 1 : 0;    }  }  void get_cpu_regs(BsnesRegisters& _return) override {    _return.pc = SNES::cpu.getRegister(SNES::CPUDebugger::Register::RegisterPC);    _return.a = SNES::cpu.getRegister(SNES::CPUDebugger::Register::RegisterA);    _return.x = SNES::cpu.getRegister(SNES::CPUDebugger::Register::RegisterX);    _return.y = SNES::cpu.getRegister(SNES::CPUDebugger::Register::RegisterY);    _return.s = SNES::cpu.getRegister(SNES::CPUDebugger::Register::RegisterS);    _return.d = SNES::cpu.getRegister(SNES::CPUDebugger::Register::RegisterD);    _return.db = SNES::cpu.getRegister(SNES::CPUDebugger::Register::RegisterDB);    _return.p = SNES::cpu.getRegister(SNES::CPUDebugger::Register::RegisterP);    _return.mflag = (SNES::cpu.usage[SNES::cpu.regs.pc] & SNES::CPUDebugger::UsageFlagM) ? 1 : 0;    _return.xflag = (SNES::cpu.usage[SNES::cpu.regs.pc] & SNES::CPUDebugger::UsageFlagX) ? 1 : 0;    _return.eflag = (SNES::cpu.usage[SNES::cpu.regs.pc] & SNES::CPUDebugger::UsageFlagE) ? 1 : 0;  }  void set_cpu_reg(const BsnesRegister::type reg, const int32_t value) override {    switch (reg) {    case BsnesRegister::pc:    case BsnesRegister::a:    case BsnesRegister::x:    case BsnesRegister::y:    case BsnesRegister::s:    case BsnesRegister::d:    case BsnesRegister::db:    case BsnesRegister::p:      SNES::cpu.setRegister((SNES::CPUDebugger::Register)reg, value);    }  }  void add_breakpoint(const DbgBreakpoint& bpt) override {    SNES::Debugger::Breakpoint add;    add.addr = bpt.bstart;    add.addr_end = bpt.bend;    add.mode = bpt.type;    add.source = (SNES::Debugger::Breakpoint::Source)bpt.src;    SNES::debugger.breakpoint.append(add);  }  void del_breakpoint(const DbgBreakpoint& bpt) override {    for (auto i = 0; i < SNES::debugger.breakpoint.size(); ++i) {      auto b = SNES::debugger.breakpoint[i];      if (b.source == (SNES::Debugger::Breakpoint::Source)bpt.src && b.addr == bpt.bstart && b.addr_end == bpt.bend && b.mode == bpt.type) {        SNES::debugger.breakpoint.remove(i);        break;      }    }  }  void read_memory(std::string& _return, const DbgMemorySource::type src, const int32_t address, const int32_t size) override {    _return.clear();    SNES::debugger.bus_access = true;    for (auto i = 0; i < size; ++i) {      _return += SNES::debugger.read((SNES::Debugger::MemorySource)src, address + i);    }    SNES::debugger.bus_access = false;  }  void write_memory(const DbgMemorySource::type src, const int32_t address, const std::string& data) override {    SNES::debugger.bus_access = true;    for (auto i = 0; i < data.size(); ++i) {      SNES::debugger.write((SNES::Debugger::MemorySource)src, address, data[i]);    }    SNES::debugger.bus_access = false;  }  void exit_emulation() override {    try {      if (client) {        send_visited(false);        client->stop_event();      }    }    catch (...) {    }    application.app->exit();  }  void pause() override {    step_into();  }  void resume() override {    toggle_pause(false);  }  void start_emulation() override {    init_ida_client();    try {      if (client) {        client->start_event();        visited.clear();        client->pause_event(SNES::cpu.getRegister(SNES::CPUDebugger::RegisterPC));      }    }    catch (...) {    }  }  void step_into() override {    SNES::debugger.step_type = SNES::Debugger::StepType::StepInto;    application.debugrun = true;    SNES::debugger.step_cpu = true;  }  void step_over() override {    SNES::debugger.step_type = SNES::Debugger::StepType::StepOver;    SNES::debugger.step_over_new = true;    SNES::debugger.call_count = 0;    application.debugrun = true;    SNES::debugger.step_cpu = true;  }};static void stop_server() {  srv->stop();}void init_dbg_server() {  ::std::shared_ptr<BsnesDebuggerHandler> handler(new BsnesDebuggerHandler());  ::std::shared_ptr<TProcessor> processor(new BsnesDebuggerProcessor(handler));  ::std::shared_ptr<TNonblockingServerTransport> serverTransport(new TNonblockingServerSocket(9090));  ::std::shared_ptr<TFramedTransportFactory> transportFactory(new TFramedTransportFactory());  ::std::shared_ptr<TProtocolFactory> protocolFactory(new TBinaryProtocolFactory());  srv = ::std::shared_ptr<TNonblockingServer>(new TNonblockingServer(processor, protocolFactory, serverTransport));  ::std::shared_ptr<ThreadFactory> tf(new ThreadFactory());  ::std::shared_ptr<Thread> thread = tf->newThread(srv);  thread->start();  atexit(stop_server);  SNES::debugger.breakpoint.reset();  SNES::debugger.step_type = SNES::Debugger::StepType::StepInto;  application.debugrun = true;  SNES::debugger.step_cpu = true;}void send_pause_event(bool is_step) {  try {    if (client) {      client->pause_event(SNES::cpu.getRegister(SNES::CPUDebugger::RegisterPC));      send_visited(is_step);    }  }  catch (...) {  }}

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


Часть объектов и методов я не делал статичными, т.к. к ним нам нужно будет обращаться из других участков кода эмулятора. Эти методы и объекты представлены в следующем списке:


  • ::std::set<int32_t> visited; сюда мы будем добавлять код, который выполнялся во время эмуляции, и который мы будем отправлять в Иду
  • void init_dbg_server() будем запускать RPC-сервер не при запуске эмулятора, а при запуске эмуляции выбранного рома
  • void send_pause_event(bool is_step) данный метод я использую не только для уведомления Иды о том, что эмуляция приостановлена, но и для отправки перед этим карты кода (codemap). Подробнее про параметр bool is_step и codemap я расскажу чуть позже

Теперь остаётся найти, где же эмулятору стоит сообщать о паузе, где начинается эмуляция, и где заполняется карта кода. Вот эти места:


Выполнение одной инструкции:


alwaysinline uint8_t CPUDebugger::op_readpc() {  extern std::set<int32_t> visited; // я решил не использовать отдельный header  visited.insert(regs.pc); // вставляем в карту кода текущее значение регистра PC  usage[regs.pc] |= UsageExec;  int offset = cartridge.rom_offset(regs.pc);  if (offset >= 0) cart_usage[offset] |= UsageExec;  // execute code without setting read flag  return CPU::op_read((regs.pc.b << 16) + regs.pc.w++);}

Открытие SNES рома:



Пошаговое исполнение:



Реакция на срабатывание брейкпоинта:



Хитрости применения codemap в Иде


Осталось рассказать о хитростях работы с функциями анализатора в IDA, и затем со спокойной (но переживающей "сомпилируется ли") душой нажать на Build Solution.


Оказалось, что просто так взять и в цикле выполнять функции, которые меняют IDB (файлы проектов в IDA) во время отладки нельзя будет вылетать через раз, и доводить своим непостоянством до сумасшествия. Нужно делать по-умному, например, вот так:


Как правильно менять IDB во время отладки
static struct apply_codemap_req : public exec_request_t {private:  const std::set<int32_t>& _changed;  const bool _is_step;public:  apply_codemap_req(const std::set<int32_t>& changed, bool is_step) : _changed(changed), _is_step(is_step) {};  int idaapi execute(void) override {    auto m = _changed.size();    if (!_is_step) {      show_wait_box("Applying codemap: %d/%d...", 1, m);    }    auto x = 0;    for (auto i = _changed.cbegin(); i != _changed.cend(); ++i) {      if (!_is_step && user_cancelled()) {        break;      }      if (!_is_step) {        replace_wait_box("Applying codemap: %d/%d...", x, m);      }      ea_t addr = (ea_t)(*i | 0x800000);      auto_make_code(addr);      plan_ea(addr);      show_addr(addr);      x++;    }    if (!_is_step) {      hide_wait_box();    }    return 0;  }};static void apply_codemap(const std::set<int32_t>& changed, bool is_step){  if (changed.empty()) return;  apply_codemap_req req(changed, is_step);  execute_sync(req, MFF_FAST);}

Если вкратце, то суть в использовании метода execute_sync() и реализации своего варианта структуры exec_request_t и её колбэка int idaapi execute(void). Это рекомендованный разработчиками способ.


Выводы и компиляция


Фактически, мы закончили писать свой собственный плагин-отладчик для IDA. Мне показалось, что как раз для реализации общения между Идой и эмулятором и создания отладчика Thrift подошёл как нельзя кстати. С минимальными усилиями мне удалось написать и серверную и клиентскую часть для обеих сущностей, не городя велосипеды в виде открытия сокетов по разному для разных платформ, и изобретения RPC реализации с нуля.


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


Всем спасибо!




Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru