Отладка приложений для Android без исходного кода на Java: пару слов о breakpoints

в 19:36, , рубрики: android, apktool, debugging, reverse engineering, smali, информационная безопасность, отладка, метки: , , , ,

О чем эта статья

Это продолжения моей вчерашней статьи об отладке приложений для Adnroid без исходного кода на Java (если кто-то её не читал — я очень советую начать с неё). Вчера я давал пошаговую инструкцию как настроить и начать использовать связку Apk-tool плюс NetBeans. Два последних пункта там звучали примерно так:

13. Установите breakpoint на интересующую вас инструкцию… blah-blah-blah...

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

Дальше, в разделе «Подводные камни», я рассказывал почему мы не может начать отладку приложения с самого начала, например поставив breakpoint на какую-нибудь инструкцию метода onCreate(...) в activity, с которой начинает выполняться приложение.

В этой статье я расскажу как всё же можно начать отлаживать приложение без исходного кода на Java с самого начала. Эта статья опять-таки не для новичков. Нужно как минимум понимать синтаксис ассемблера Smali и уметь ручками патчить .smali файлы, грамотно вписывая туда свой код.

Инструменты

Нам снова нужны Apk-tool 1.4.1 и NetBeans 6.8 — причем именно эти устаревшие на сегодняшний день версий. С более новыми версиями заставить работать отладку у меня не получается. И судя по дискуссиям на тематических форумах — не только у меня.

Установку Apk-tool и NetBeans я уже описывал во вчерашней статье, но всё же повторюсь. NetBeans устанавливается по умолчанию, просто кликаем Next-Next-Next в мастере установки. Установка Apk-tools заключается в обычном извлечении файла apktool.jar из архива в любую папку.

Как поставить breakpoint в самом начале приложения

Идея в общем-то простая. Нужно найти activity, которая стартует в приложении первой, и вписать бесконечный цикл в начало метода onCreate(...) этой activity. Приложение стартует и сразу после вызова конструктора этой activity будет вызван метод onCreate(...). В результате управление попадёт в наш бесконечный цикл. Пока цикл будет там крутиться, мы неспеша присоединим отладчик к работающему приложению, поставим breakpoint сразу после нашего бесконечного цикла, а потом воспользуемся возможностями отладчика и сделаем так что бы управление из этого цикла вышло. И сразу же попало на наш breakpoint. Как видим, всё элементарно.

В этом разделе дана пошаговая инструкция. Инструкция написана для Windows, но скорее всего будет работать на Linux и Mac OS.

Пожалуйста в точности следуйте инструкции — это важно!

  1. Декодируйте ваш .apk файл в директорию temp с помощью Apk-tool. Не используйте опцию -d:
    java -jar apktool.jar d my.app.apk temp

    В результате в директории temp/smali у вас будет куча .smali файлов.

  2. В файле temp/AndroidManifest.xml найдите activity с фильтрами
    <intent -filter="-filter">
    		<action android:name="android.intent.action.MAIN">
    		<category android:name="android.intent.category.LAUNCHER">
    </intent>

    Это и есть activity которая стартует в приложении первой.

  3. Нашли activity которая стартует в приложении первой? Теперь найдите .smali файл, в котором реализован класс для этой activity (как правило это потомок класса android.app.Activity). Ищите в глубине директории temp/smali.
  4. Теперь найдите в этом классе метод onCreate(...) и сразу после вызова (обычно этот вызов идёт в самом начале)
    invoke-super {p0, p1}, Landroid/app/Activity;->onCreate(Landroid/os/Bundle;)V

    в onCreate(...) впишите следующий код:

    :debug
    sget v0, Lmy/activity/class/MyActivity;->debugFlag:I
    if-nez v0, :debug

    Внимательный читатель вероятно уже догадался, что этот код и есть тот бесконечный цикл о котором мы говорили раньше. Естественно, вместо v0 в этом коде можно использовать любой подходящий локальный регистр. Если подходящего регистра нет — добавьте его, отредактировав директивы .locals и/или .registers соответствующим образом.

  5. Также добавьте в класс поле
    .field static debugFlag:I = 0x01

    иначе код бесконечного цикла в предыдущем пункте не заработает.

  6. Пересоберите директорию temp обратно в ваш .apk файл, опять-таки без опции -d:
    java -jar apktool.jar b temp my.app.apk

    Конечно, оригинальный my.app.apk стоит где-нибудь перед этим сохранить.

Теперь у нас есть пропатченый my.app.apk. В начало метода onCreate(...) в классе той activity, с которой начинается выполнение приложения, мы вписали бесконечный цикл. Что ж, берите этот пропатченый my.app.apk и следуйте пошаговой инструкции из моей вчерашней статьи (см. раздел «Отладка»). Учтите, что на девятом шаге этой инструкции, после того как вы запустите приложение, вы увидите черный экран. Это нормально, так и должно быть! Это просто означает, что сразу после запуска приложения был вызван наш пропатченый метод onCreate(...) и управление попало в наш бесконечный цикл. Если через некоторое время Android предложит вам закрыть приложение потому что оно не отвечает — откажитесь и идите дальше строго по инструкции!

На двенадцатом шаге инструкции откройте в NetBeans тот .java файл, в котором находится пропатченный вами метод onCreate(...). Воспользуйтесь кнопкой «пауза» на панели отладки в NetBeans. Затем в этом открытом .java файле поставьте breakpoint на первую инструкцию после кода бесконечного цикла, который вы вписали в onCreate(...). Потом, пользуясь функцией просмотра и редактирования переменных в отладчике NetBeans, поменяйте значение поля debugFlag на 0 и кликните на кнопку «продолжить отладку» на панели отладки в NetBeans. Управление выйдет из бесконечного цикла и тут же попадёт на ваш breakpoint.

Всё, теперь можно спокойно отлаживать приложение фактически с самого начала самого первого onCreate(...)!

Пару слов про waitForDebugger()

Читатель, который немного в теме, наверное читал на тематических форумах про использование метода android.os.Debug.waitForDebugger() для тех же целей, для которых мы в этой статье используем бесконечный цикл. И этот самый читатель вероятно удивлён, что мы тут нагородили какой-то огород с циклом, хотя можно было бы просто вписать в начало нашего onCreate(...) вызов всего одного статического метода:

invoke-static {}, Landroid/os/Debug;->waitForDebugger()V

Заметьте, метод вызывается без параметров, а значит не нужно мучиться с добавлением локальных регистров если нету подходящего. Казалось бы — красота! Чего ещё надо?

В теории — ничего не надо, бери да пользуйся. Но на практике всё чуток сложнее. По факту фокус с android.os.Debug.waitForDebugger() работает далеко не всегда и не у всех. У многих (в том числе и у меня) сразу после вызова android.os.Debug.waitForDebugger() приложение действительно «замирает» и ждёт когда к нему присоединится отладчик. Это видно даже в DDMS — напротив приложения появляется маленький значок «красный жук». Но как только мы присоединяем отладчик к приложению, управление тут же переходит на следующую инструкцию после android.os.Debug.waitForDebugger() и приложение начинает выполняться дальше без остановки. Мы просто не успеваем поставить breakpoint после android.os.Debug.waitForDebugger(). Обсуждение по этому поводу см. например тут.

Почему android.os.Debug.waitForDebugger() у кого-то работает так, а у кого-то эдак — мне пока что неизвестно. Может в комментариях кто-то даст пояснения по этому поводу. Также в комментариях можно и нужно задавать вопросы по статье. Постараюсь ответить по-возможности оперативно, но если буду тупить — пожалуйста наберитесь терпения. Постараюсь ответить всем.

Автор: dimakovalenko

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js