Отладка приложений для Android без исходных кодов: native методы

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

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

В двух предыдущих статьях я рассказывал как отлаживать приложения для Android без исходного кода на Java и о некоторых особенностях установки breakpoints. Если уважаемый читатель ещё не ознакомился с этими статьями — я настоятельно рекомендую начать с них, а уже потом читать эту статью.

Так уж вышло что до сих пор я рассказывал исключительно об отладке байткода Dalvik и ни словом не обмолвился об отладке native методов. А ведь именно в native методах часто скрывается самое вкусное — хитрые защиты, интересные malware фичи, уязвимости нулевого дня. Поэтому сегодня я сжато, без «воды», расскажу как отлаживать native методы без исходного кода на C/C++ (ну или на чем, уважаемый читатель, они у вас там написаны).

Что бы извлечь пользу из моего рассказа нужно быть уже немного «в теме». В частности желательно что бы читатель

  • понимал синтаксис Smali, мог вписывать в .smali файлы свой код и мог отличить декларации и вызовы native методов от обычных методов, умел пересобирать .apk файлы используя Apktool;
  • представлял себе что такое Java Native Interface (JNI) и как это работает;
  • знал для чего используются методы System.load(...) и System.loadLibrary(...), как они работают в Android, и по аргументам этим методов в Smali коде мог самостоятельно определить в каких .so библиотеках находятся JNI функции соответствующие тем или иным native методам;
  • умел найти эти JNI функций в .so библиотеках;
  • хотя бы на начальном уровне знал ассемблер ARM (в статье предполагается что отладка будет выполняться на устройстве с архитектурой ARM либо на эмуляторе который эту архитектуру эмулирует);
  • имел какой-то опыт работы с gdb и gdbserver;

Вот пожалуй и все знания и навыки которые будут нужны читателю. Перейдём к инструментам.

Инструменты

Сегодня нам понадобится:

  • gbd и gdbserver для ARM из последней версии Android NDK. Установка описана тут.
  • Утилита adb из последней версии Android SDK. Установка описана здесь.
  • Если отладка идёт на реальном Android-устройстве — на нём нужны права root.

Перейдём к подготовке.

Подготовка

Предполагается что читатель достаточно опытен что бы самостоятельно выполнить следующие подготовительные действия:

  • Дизассемблировать .apk файл приложения с помощью Apktools, исследовать файлы в поддиректории папка/куда/дизассемблировали/приложение/smali и выяснить:
    • какие native методы вызываются из байткода Dalvik;
    • в какой .so библиотеке находятся соответствующие этим методам JNI функции;

  • Вытащить эту .so библиотеку из поддиректории папка/куда/дизассемблировали/приложение/lib либо с устройства на котором будет производиться отладка, исследовать её и узнать как называются JNI функции которые соответствуют тем или иным native методам вызываемым из байткода Dalvik.
  • Пересобрать .apk приложение с помощью Apktools с оцпией -d и загнать по отладку в NetBeans как написало в этой моей статье.
  • Сделать так, что бы отладка в NetBeans остановилась после вызовов System.loadLibrary(...) или System.load(...), которые загружают .so с интересующими нас JNI функциями, но до первого вызова какого-либо из native методов. Можно использовать трюк о котором я писал тут.

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

Отладка

Идея в том, что бы загнать наше приложение сразу под два отладчика: под отладчик встроенный в NetBeans — что бы отлаживать байткод Dalvik, и под gdb — исключительно что бы отлаживать вызовы native методов. Звучит слегка странно, но на практике вполне себе работает. Хотя и не всегда — см. следующий раздел «Подводные камни».

Итак, если читатель выполнил все подготовительные действия из предыдущего раздела «Подготовка», то сейчас у него на устройстве или эмуляторе наверняка запущено пересобранное приложение, на компьютере открыт NetBeans и отладка стоит где-нибудь после вызова System.load(...) или System.loadLibrary(...), но ещё до первого вызова native метода. Причем читатель уже в курсе в какой библиотеке какие JNI функции каким native методам соответствуют. С этого мы и начнём.

Дальше идёт пошаговая инструкция. Она писалась для Windows, но думаю будет работать и для Linux и MacOS. Пожалуйста, следуйте инструкции в точности:

  1. Командной abd shell откройте ADB консоль вашего устройства или эмулятора. Найдите PID процесса вашего приложения воспользовавшись командой ps в ADB консоли. В этой же консоли выполните команду:
    gdbserver :5039 --attach %PID%

    где %PID% и есть PID процесса вашего приложения. В ответ gdbserver должен вывести что-то вроде:

    Attached; pid = %PID%
    Listening on port 5039

    Начиная с этого момента отладка в NetBeans «замёрзнет». Т.е. вы конечно сможете там кликать на кнопки, но это бесполезно т.к. приложение которое вы пытаетесь отлаживать в NetBeans в данный момент остановлено под отладчиком GDB. Не паникуйте, всё так и должно быть!

  2. Откройте новую консоль на вашем компьютере, выполните команду
    adb forward tcp:5039 tcp:5039

    на вашем компьютере.

  3. В этой же консоли запустите gdb из Android NDK:
    gdb libMyNativeLibrary.so

    где gdb libMyNativeLibrary.so — та самая .so библиотека в которой находятся интересующие нас JNI функции. В результате откроется консоль gdb.

  4. В консоли gdb наберите следующие команды:
    (gdb) target remote :5039

    После этих манипуляций консоли gdb должно появится сообщение наподобии

    Remote debugging using :5039
    0x4009d58c in ?? ()

    а в консоли ADB (она у нас ещё открыта, помните?) что-то типа

    Remote debugging from host 127.0.0.1
  5. В консоли GDB выполните
    (gdb) info functions

    что бы увидеть список функций. В списке среди прочих функций должны быть и интересующие вас JNI функции, что-то типа:

    0x5b5f7bac  Java_my_app_for_debug_MainActivity_coolNativeMethod
    0x5b5f7c0c  Java_my_app_for_debug_MainActivity_anotherCoolNativeMethod
    0x5b5f7c1c  Java_my_app_for_debug_MainActivity_theCoolestNativeMethodEver
  6. В консоли GDB поставте breakpoints на адреса интересующих вас JNI функций, в нашем случае это
    (gdb) break *0x5b5f7bac
    (gdb) break *0x5b5f7c0c
    (gdb) break *0x5b5f7c1c
  7. Возобновите выполнение вашего приложения с помощью команды c в GDB консоли. После выполнения этой команды, отладка в NetBeans «размёрзнется» и вы снова сможете отлаживать байткод Dalvik.

Теперь, каждый раз когда в байткоде Dalvik будет встречаться вызов native метода вроде

const/high16 v8, 0x4100
invoke-static {v8}, Lmy/app/for/debug/MainActivity;->theCoolestNativeMethodEver(F)V

отладка в NetBeans будет замирать, зато gdb будет вас радовать сообщениями вроде

Breakpoint 1, 0x5b5f7c1c in Java_my_app_for_debug_MainActivity_theCoolestNativeMethodEver () from libMyNativeLibrary.so

Вот это собственно оно и есть. Дальше x/i $pc, stepi — в общем вперёд отлаживать одну ARM-инструкцию за другой (помните я говорил в начале что ARM ассемблер будет нужен? — ну вот...)

Подводные камни

О, их тут много. Целый сад подводных камней. Вот наиболее запоминающиеся глюки, которые попались мне при использовании GNU gdb (GDB) 7.4.1 в связке с GNU gdbserver (GDB) 7.4.1 на Android 4.0.3 в устройстве Ainol Aurora (той, старой ещё):

  1. Если у вас после некоторого времени gdb регулярно отваливается от gdbserver по watchdog timeout, выполните в консоли gdb set watchdog 18000 — это должно помочь.
  2. Иногда в результате info functions в списке функций отображаются не адреса функций в памяти, а смещения в .so файле, например:
    0x000c0bac  Java_my_app_for_debug_MainActivity_coolNativeMethod
    0x000c0c0c  Java_my_app_for_debug_MainActivity_anotherCoolNativeMethod
    0x000c0c1c  Java_my_app_for_debug_MainActivity_theCoolestNativeMethodEver

    В этом случае положите libMyNativeLibrary.so в каталог, который для gdb является текущим при старте, и перезапустите gdb снова то й же самой командной gdb libMyNativeLibrary.so.

  3. Иногда в результате info functions в списке функций отображаются и адреса функций в памяти и смещения в .so файле, например:
    0x5b5f7bac  Java_my_app_for_debug_MainActivity_coolNativeMethod
    0x5b5f7c0c  Java_my_app_for_debug_MainActivity_anotherCoolNativeMethod
    0x5b5f7c1c  Java_my_app_for_debug_MainActivity_theCoolestNativeMethodEver
    0x000c0bac  Java_my_app_for_debug_MainActivity_coolNativeMethod
    0x000c0c0c  Java_my_app_for_debug_MainActivity_anotherCoolNativeMethod
    0x000c0c1c  Java_my_app_for_debug_MainActivity_theCoolestNativeMethodEver

    Игнорируйте смещения в .so файле, ставьте breakpoints на адреса функций в памяти.

  4. Если команда break на имена функций работает нормально — вы счастливчик, если нет… ну собственно у меня она и не работает, поэтому в данной статье я ставлю breakpoints на адреса функций.
  5. Может не работать set stop-on-solib-events. У меня не работает.
  6. Время от времени вы будете видеть надпись Cannot access memory at address 0x1. Игнорируйте.

Я уверен что это далеко не полный список глюков, которые таит в себе отладка native методов без исходных кодов, и что другим исследователям попадутся совершенно другие, уникальные глюки которые не попались мне. Если кто что ещё найдёт — прошу делится в комментариях. Также в комментария прошу задавать вопросы и/или вносить технические поправки к тексту. Постараюсь ответить как можно быстрее, но если буду тупить — наберитесь пожалуйста терпения. Постараюсь ответить всем.

Happy debugging!

P.S. Просьба к минусующим статью отписываться в комментариях что именно не понравилось. Постараюсь исправить, если это возможно.

Автор: dimakovalenko

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


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