Недавно я столкнулся с не очень распространенной проблемой работы с полностью недокументированным интерпретатором. Благодаря этой возможности я начал исследовать и разрабатывать инструменты для этого с нуля, и вместе с этим возникли некоторые интересные проблемы. Первоначально я начал с ассемблера и дизассемблера, но убедить людей, которые едва знакомы с кодом (если вообще знают), реконструировать и отредактировать тысячи немаркированных файлов, написанных на языке ассемблера, созданном реверс-инженером-любителем, — это… непростая задача. мягко говоря. Таким образом, следующим логическим шагом является его абстрагирование, лучшим выбором для которого является декомпиляция в существующий язык. Использование существующего языка упрощает обучение, поскольку те, кто знаком с ним, уже понимают его синтаксис, а те, кто не знаком с ним, имеют ресурсы для обучения.
Основы декомпиляции
Декомпиляция — это искусство абстракции, позволяющее брать простые части и превращать их в более крупную идею. Думайте об этом как о предложении по сравнению с его значением; предложение — это просто набор слов, каждое слово имеет свое простое значение, которое можно объединить, чтобы точно описать, что происходит.
Если мы посмотрим на эту аналогию, то увидим, откуда берутся некоторые трудности декомпиляции, поскольку порядок влияет на значение, слова могут быть соединены вместе в разных комбинациях, чтобы иметь совершенно разные значения, и что для понимания всей идеи мы должны понять, как части взаимодействуют друг с другом. Если идея смотреть на слова, чтобы понять смысл предложения, звучит немного пугающе, не волнуйтесь, поскольку аналогия с обработкой естественного языка немного сложнее, чем сама декомпиляция.
От понимания к реализации
Для начала вам, по крайней мере, нужно базовое понимание того, как различные структуры на выбранном вами языке могут быть воспроизведены в любой архитектуре, с которой вы работаете. Задавать такие вопросы, как «как будет выглядеть оператор if», «из чего состоит цикл в [архитектуре]» и «как переменные обрабатываются в архитектуре» — это те вещи, на которые вам нужно ответить еще до того, как вы начнете разрабатывать свой декомпилятор. Давайте рассмотрим простой пример с оператором C if и тем, как он будет скомпилирован в воображаемую архитектуру.
// C if(var0 < 0){ var0 = 0; } --------------------------- ; imaginaryAsm compareLessThan var0, 0 jumpIfNotTrue EndLabel set var0, 0 EndLabel:
Важные выводы:
- Любой оператор может быть использован для условия
- Внутри оператора if может быть любое количество операторов.
Это поднимает проблему: если любая комбинация операторов может использоваться в операторах if, нам нужно как-то обобщить нашу логику, чтобы не имело значения, что такое условие, и не имело значения, какие операторы находятся внутри операторов if. По сути, нам нужно относиться к нему почти как к шаблону.
[condition] jumpIfNotTrue EndLabel [statements] EndLabel: ----------------------------- if([condition]){ [statements] }
Таким образом, мы можем рекурсивно декомпилировать «большие» структуры и вместо того, чтобы обрабатывать все возможные типы операторов if, мы просто декомпилируем условие и внутренние операторы по отдельности (а также рекурсивно) и вставляем их в соответствующие позиции в операторе if.
Знание своего врага
Чтобы понять декомпиляцию, вам сначала нужно понять компиляцию, поскольку для получения каких-либо значимых результатов декомпиляции на многих архитектурах вам необходимо понять компилятор, который использовался для целевого файла. Хотя и GCC, и Clang являются компиляторами C, которые реализуют одни и те же стандарты, результирующие исполняемые файлы не будут идентичны из-за того, что существует более одного способа реализации переменных/потока управления, оптимизации выходных данных, или они могут даже использовать разные реализации C стандартная библиотека. Из-за этого вам нужно понимать нюансы компилятора, который использовался для файлов, на которые вы ориентируетесь, чтобы понять, что вы ищете.
Существует множество способов выяснить, как работает каждый компилятор, даже если у вас нет самого компилятора. Если вы можете отследить компилятор (или, что еще лучше, его источник), вы можете использовать его, чтобы узнать, как он работает. Однако, если вы, как и я, не являетесь большим поклонником попыток ознакомиться с внутренней работой компилятора, вы можете либо создать, либо найти примеры вывода компилятора, чтобы вручную декомпилировать код. Ручная декомпиляция — довольно простой процесс, как только вы освоите основы языка ассемблера, с которым работаете, просто:
- Прочитайте код, который вы хотите декомпилировать
- Поймите, как это работает и что это значит
- Переосуществите это на C
Хотя это слишком трудоемко и требует некоторой практики, это должно прийти к любому, у кого есть приличный опыт реверс-инжиниринга. Если у вас есть проблемы с пониманием того, как именно работает часть кода, полезно придумать несколько сценариев и выяснить, как именно этот код будет обрабатывать его. Если вы видите, что переменная X сравнивается с 5, подумайте, что будет делать код, если X меньше, равно или больше 5 соответственно. Хотя реальной формулы для обратного проектирования того, как работает код, не существует, практика очень поможет, поскольку вы начнете распознавать шаблоны и находить более быстрые способы понимания различных форм потока управления.
Знакомство с особенностями
Хотя я старался, чтобы большинство моих советов и мыслей были достаточно общими, чтобы их можно было применить к любому проекту декомпиляции, большая часть декомпиляции специфична для архитектуры. Хотя такие архитектуры, как ARM и x86, имеют много общего и из-за этого могут использовать очень похожие методы декомпиляции, эти методы могут быть неприменимы к другим архитектурам, таким как PowerPC или чему-то еще более проприетарному. В моем случае я написал декомпилятор для чего-то под названием MotionSCript или для краткости MSC, который представляет собой проприетарную архитектуру, разработанную и используемую в видеоигре Super Smash Brothers для Wii U за логику управления персонажами. В отличие от любой другой реальной архитектуры, с которой я когда-либо работал, это архитектура на основе стека, которая передает значения другим командам, помещая их в стек для извлечения. Преимущества архитектуры MSC заключаются в том, что довольно просто определить, как и где используются значения, поскольку они могут использоваться только следующей командой, которая извлекается из стека. Недостатком этого является то, что нестандартная архитектура не может быть основана на стандартных методах. Если вы пишете декомпилятор, ваша стратегия атаки будет зависеть от уникального дизайна архитектуры, с которой вы работаете, серебряной пули не существует.
Вывод
Написание декомпилятора, безусловно, является одним из самых забавных опытов реверс-инжиниринга. Это отличный опыт обучения, который помогает улучшить ваше понимание абстракций языков программирования и дает довольно удовлетворительные результаты, когда вы начинаете добиваться успеха. Если вам интересно узнать больше о понимании абстракций компьютеров, я настоятельно рекомендую слайды Cody Brocious Стать реверс-инженером полного стека.
Есть вопросы? Интересуют другие мои проекты? DM и/или подписывайтесь на меня в твиттере.