ARM-декомпилятор — различия между версиями
м (переименовал «Блог:Виталий Филиппов/2011-03-25 ARM-декомпилятор» в «ARM-декомпилятор») |
м |
||
Строка 1: | Строка 1: | ||
− | Задумавшись о | + | Задумавшись о «простеньком плагинчике» для исключения тупой ручной работы в IDA — для «трассировки констант», пришёл к выводу, что я изобрёл <s>велосипед</s>декомпилятор, и что не такая уж это и сложная вещь — декомпилятор. |
− | + | Потом понял, что парсить вывод IDA — занятие тяжкое и приносящее малый профит, и в случае ARM точно лучше использовать objdump, хотя он и не является рекурсивным дизассемблером. Вообще, в случае ARM всё ну очень просто, так как любая инструкция занимает 4 байта и всегда выровнена на границу 4 байт. Поэтому натравленный на файл objdump всегда дизассемблирует его корректно, только некоторые «инструкции», которые на самом деле не инструкции, нужно будет заменить на данные. | |
− | + | Поэтому сначала делаем начальный анализ: | |
− | * | + | * Дизассемблировать код objdump’ом и разобрать вывод на адреса, инструкции, их аргументы и комментарии — как уже сказано, это гораздо легче, чем парсить бешеные листинги из IDA. |
− | * | + | * Представить адреса, которые очевидно читаются/пишутся, данными: |
− | * | + | ** Взятые относительно PC и числа — сразу. Которые только читаются, можно сразу принять за константы и подставить в код (аналог <tt>LDR xx, =const</tt> в IDA). |
+ | ** Потом, уже в процессе декомпиляции, могут выявиться новые адреса с данными — нужно уметь их «сделать» данными в будущем. ''Чувствуете? Уже появляется «интерактивщина», аналогичная букве I в названии IDA.'' | ||
+ | * Выделяем очевидные функции: | ||
+ | ** Все константные адреса, на которые делается BL или BLX — это «ближние» (short) вызовы функций. | ||
+ | ** В будущем, при трассировке выражений, могут появиться ещё функции — нужно уметь их тоже «сделать» функциями. Ещё чуть-чуть интерактивщины. | ||
+ | * Разобрать код на непрерывные блоки, забыть про условные суффиксы. | ||
+ | * Построить граф ветвлений — литературное название также «Control Flow Graph», или «граф управляющей логики». | ||
+ | ** Граф тоже может меняться в процессе анализа. | ||
− | + | Далее начинается более сложный анализ: | |
− | * | + | |
− | * цикл: | + | * Пройтись по всем возможным ветвям выполнения, на каждом шаге сохраняя выражения для значений регистров и флагов сравнений, транслировать инструкции в выражения, а условия EQ/NE/… — в выражения сравнений. Наибольшая проблема здесь — '''циклы'''. |
− | * | + | * Отобразить ветвления либо на if’ы, либо на циклы, либо, на крайний случай, на goto («кривые» конструкции). |
+ | * Можно выделять ещё не выделенные функции — ими можно сделать всё, на что (ещё?) не найдены BL-переходы, но что оканчивается записью LR в PC (BX LR / MOV PC, LR / LDM …), а начинается с инструкции, к которой нет перехода с предыдущей. То есть когда предыдущая — либо данные (не инструкция), либо когда она содержит безусловный переход, обходящий следующую инструкцию. NOP’ы между предыдущей и началом функции не учитывать. | ||
+ | * Можно выделять дальние вызовы (long call) функции — то есть, вызовы, в которых адрес функции сначала грузится в регистр, а потом вызывается из регистра; это несложно, потому что сложных выражений отслеживать не надо, в качестве адреса используется константа. | ||
+ | * Можно осилить даже часть непрямых вызовов функций (по хз откуда взятому адресу). | ||
+ | * Найти все ASCII-строки в бинарнике, и подставить в обращения к ним. | ||
+ | |||
+ | Далее нужно перейти к выделению блоков — то есть, циклов, условных операторов: | ||
+ | |||
+ | * Каждый цикл: | ||
+ | ** Начинается с какого-либо узла, являющегося к тому же единственной точкой входа в этот цикл. | ||
+ | ** Должен включать в себя все пути, ведущие в любой внешний узел, и возвращающиеся в любой внутренний узел (''обобщённое требование цикла''). | ||
+ | ** Если добавили какой-то узел, и получили две точки входа, то этот узел не принадлежит циклу. | ||
+ | ** Если добавили какой-то узел и единственная точка входа изменилась, идём нафиг — цикл из изначального узла не растёт. | ||
+ | *: Когда залезаем в цикл, переходы на начало и выход из цикла заменяем на continue и break для удобочитаемости. | ||
+ | * Условный оператор: | ||
+ | *: Если что-то откуда-то ветвится и это не цикл, это условный оператор :) | ||
+ | *: Транслировать его следует так: | ||
+ | ** Ищем точку сбора всех веток, выходящих из начального узла (узел, в котором все они объединяются). | ||
+ | ** Залезаем в каждую ветку, всё обрабатываем точно так же, как до этого, и точно так же (в обработке линейных участков и циклов тоже помечаем). | ||
+ | ** Помечаем пройденные узлы как обработанные. | ||
+ | ** Когда где-то видим уже обработанный узел, заменяем переход к нему на goto. То есть, if «без дублирования» кусков веток будет «нормальным», без goto. | ||
+ | *: Пример «плохого» if’а: 0->1->3->4, 0->2->3, 0->4, 0->5->4. | ||
+ | |||
+ | Всё это соответствует стандартной теории декомпиляции, см. например http://www.backerstreet.com/decompiler/creating_statements.php |
Версия 12:44, 29 марта 2011
Задумавшись о «простеньком плагинчике» для исключения тупой ручной работы в IDA — для «трассировки констант», пришёл к выводу, что я изобрёл велосипеддекомпилятор, и что не такая уж это и сложная вещь — декомпилятор.
Потом понял, что парсить вывод IDA — занятие тяжкое и приносящее малый профит, и в случае ARM точно лучше использовать objdump, хотя он и не является рекурсивным дизассемблером. Вообще, в случае ARM всё ну очень просто, так как любая инструкция занимает 4 байта и всегда выровнена на границу 4 байт. Поэтому натравленный на файл objdump всегда дизассемблирует его корректно, только некоторые «инструкции», которые на самом деле не инструкции, нужно будет заменить на данные.
Поэтому сначала делаем начальный анализ:
- Дизассемблировать код objdump’ом и разобрать вывод на адреса, инструкции, их аргументы и комментарии — как уже сказано, это гораздо легче, чем парсить бешеные листинги из IDA.
- Представить адреса, которые очевидно читаются/пишутся, данными:
- Взятые относительно PC и числа — сразу. Которые только читаются, можно сразу принять за константы и подставить в код (аналог LDR xx, =const в IDA).
- Потом, уже в процессе декомпиляции, могут выявиться новые адреса с данными — нужно уметь их «сделать» данными в будущем. Чувствуете? Уже появляется «интерактивщина», аналогичная букве I в названии IDA.
- Выделяем очевидные функции:
- Все константные адреса, на которые делается BL или BLX — это «ближние» (short) вызовы функций.
- В будущем, при трассировке выражений, могут появиться ещё функции — нужно уметь их тоже «сделать» функциями. Ещё чуть-чуть интерактивщины.
- Разобрать код на непрерывные блоки, забыть про условные суффиксы.
- Построить граф ветвлений — литературное название также «Control Flow Graph», или «граф управляющей логики».
- Граф тоже может меняться в процессе анализа.
Далее начинается более сложный анализ:
- Пройтись по всем возможным ветвям выполнения, на каждом шаге сохраняя выражения для значений регистров и флагов сравнений, транслировать инструкции в выражения, а условия EQ/NE/… — в выражения сравнений. Наибольшая проблема здесь — циклы.
- Отобразить ветвления либо на if’ы, либо на циклы, либо, на крайний случай, на goto («кривые» конструкции).
- Можно выделять ещё не выделенные функции — ими можно сделать всё, на что (ещё?) не найдены BL-переходы, но что оканчивается записью LR в PC (BX LR / MOV PC, LR / LDM …), а начинается с инструкции, к которой нет перехода с предыдущей. То есть когда предыдущая — либо данные (не инструкция), либо когда она содержит безусловный переход, обходящий следующую инструкцию. NOP’ы между предыдущей и началом функции не учитывать.
- Можно выделять дальние вызовы (long call) функции — то есть, вызовы, в которых адрес функции сначала грузится в регистр, а потом вызывается из регистра; это несложно, потому что сложных выражений отслеживать не надо, в качестве адреса используется константа.
- Можно осилить даже часть непрямых вызовов функций (по хз откуда взятому адресу).
- Найти все ASCII-строки в бинарнике, и подставить в обращения к ним.
Далее нужно перейти к выделению блоков — то есть, циклов, условных операторов:
- Каждый цикл:
- Начинается с какого-либо узла, являющегося к тому же единственной точкой входа в этот цикл.
- Должен включать в себя все пути, ведущие в любой внешний узел, и возвращающиеся в любой внутренний узел (обобщённое требование цикла).
- Если добавили какой-то узел, и получили две точки входа, то этот узел не принадлежит циклу.
- Если добавили какой-то узел и единственная точка входа изменилась, идём нафиг — цикл из изначального узла не растёт.
- Когда залезаем в цикл, переходы на начало и выход из цикла заменяем на continue и break для удобочитаемости.
- Условный оператор:
- Если что-то откуда-то ветвится и это не цикл, это условный оператор :)
- Транслировать его следует так:
- Ищем точку сбора всех веток, выходящих из начального узла (узел, в котором все они объединяются).
- Залезаем в каждую ветку, всё обрабатываем точно так же, как до этого, и точно так же (в обработке линейных участков и циклов тоже помечаем).
- Помечаем пройденные узлы как обработанные.
- Когда где-то видим уже обработанный узел, заменяем переход к нему на goto. То есть, if «без дублирования» кусков веток будет «нормальным», без goto.
- Пример «плохого» if’а: 0->1->3->4, 0->2->3, 0->4, 0->5->4.
Всё это соответствует стандартной теории декомпиляции, см. например http://www.backerstreet.com/decompiler/creating_statements.php