Целевая аудитория этой статьи — люди, которые решили заняться исследованием .apk файлов, имеющие опыт разработки под Android и знакомые с основами синтаксиса Smali. Эта статья является оригинальной и ранее нигде не публиковалась. При копировании, прошу указывать ссылку на этот источник.

Поскольку устройства, использующие операционную систему Android сегодня устойчиво сохраняют лидерство на рынке, программы для них не пишет разве что самый ленивый мобильный разработчик. Иногда программы ведут себя так, как мы этого ожидаем, а иногда (все чаще) не совсем так. И тогда, у пользователя программного продукта, особенно если он сам занимается разработкой, появляется ожидаемый интерес, и ему хочется понять почему программа делает то или это. Тогда пользователь, с чисто научно-позновательной целью, решает произвести вскрытие субъекта. Тут то на помощь ему и приходит множество инструментов для изучения файлов .apk.

Мой стандартный хирургический набор, на сегодняшний день, состоит из таких инструментов:
apktool - для декомпиляции и сборки apk
jadx-gui - когда хочется получить код приложения (или, что чаще — часть кода) в виде Java.
Bytecode Viewer — когда хочется получить код в виде Java, используя разные декомпиляторы, что дает иногда весьма интересные результаты.
zipalign — утилита из состава Android Studio, предназначенная для выравнивания содержимого файлов, упакованных в .apk.
apksigner - утилита из состава Android Studio, предназначенная для подписи исследуемого файла, и успешного его запуска на устройстве.

средства автоматизации собственной разработки — накапливаются с опытом. Иногда возникает столько рутинной работы, типа заменить A на B во всех файлах, что руки опускаются. Но, мы не из тех, кто сдается. И поэтому, исключительно из-за своей лени пишем утилиты, которые, собственно и позволяют нам лениться ). Однако, для меня, основными инструментами все же являются: файловый менеджер с хорошо организованным внутрифайловым поиском ( я использую Krusader, потому что Ubuntu), и редактор с какой-никакой подсветкой синтаксиса (я использую Kate, потому что Ubuntu).

На основании своего опыта исследования классических .apk (когда программа написана на Java или Kotlin c UI на xml шаблонах), ответственно заявляю, что получить из оригинального .apk рабочий проект Android Studio МОЖНО! И я говорю далеко не про Hello World. Но и тут есть свои особенности. Все зависит от того, насколько автор программы позаботился о защите ее кода. По моим наблюдениям до 80% программ уровня ширпотреба не имеют никакой защиты кроме примитивной обфускации.

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

Но, в большинстве случаев, задача получения рабочего проекта для Android Studio не стоит, а нужно просто исследовать некоторые участки кода, отвечающие за неадекватное поведение программы. В этом случае все немного проще. Утилита apktool прекрасно декомпилирует исполняемые файлы в более-менее читаемый формат — Smali. Этот формат, конечно менее удобен, чем Java, но с опытом его чтение не вызывает трудностей. Как сказал один умный тренер «Результат нужно настреливать». Чем больше разных файлов вы изучили, тем понятнее становится Smali.

На этом позвольте закончить лирическое вступление, и перейти к сути этой статьи. А суть в том, что во время исследования программ однознчно потребуется делать свои вставки в исходный код для вывода в Log ( Logcat или другой) содержимого тех или иных переменных, результатов работы методов или содержимого Error Stack Trace.

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

1. Собственно методы вывода в Log с заданным параметром severity level (debug, info, error..и т.д)

Метод 1, когда параметр TAG фиксирован и всегда один. В метод передается один параметр — строка, которую нужно вывести в log:

  .method public static logMsg(Ljava/lang/String;)V
    .locals 1
    .prologue    
     const-string v0, "TAG"      
     invoke-static {v0, p0}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I    
     return-void
  .end method

Этот метод статический, поэтому удобно его поместить в конец синглетона Application, который присутствует в любом приложении Android. Вот здесь:

Landroid/util/Log;->d

определяется severity level. В данном случае он — d (debug). Соответственно для info = i, error = e.

Пример использования:

const-string v0, "Login Start"
invoke-static {v0}, Lcom/example/MyApplication;->logMsg(Ljava/lang/String;)V  

Выводит в log сообщение "Login Start" с тэгом «TAG».

Метод 2, когда параметр TAG не фиксирован и может меняться. В метод передается два параметра — строка TAG и строка, которую нужно вывести в log:

.method public static logMsgWithTag(Ljava/lang/String;Ljava/lang/String;)V
   .locals 0
   .prologue
    invoke-static {p0, p1}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I    
    return-void
.end method

Пример использования:

const-string v0, "LOGIN TAG"
const-string v1, "Login Start"
invoke-static {v0, v1}, Lcom/example/MyApplication;->logMsgWithTag(Ljava/lang/String;Ljava/lang/String;)V 

Выводит в log сообщение "Login Start" с тэгом «LOGIN TAG».

Почему бы не использовать всегда Метод 2 ? Ну во-первых — если tag фиксирован, то выносим его «за скобки», и просто лень помнить о нем каждый раз. Во-вторых, особенность Smali — экономия переменных. Чем их меньше — тем лучше.

2. Конкатенация двух строк через StringBuilder.
Результат возвращается в виде Key=Value.

.method public static concatKeyValue(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
    .locals 3
    .prologue
    const-string v0, " = "
    new-instance v1, Ljava/lang/StringBuilder;
    invoke-direct {v1}, Ljava/lang/StringBuilder;-><init>()V
    invoke-virtual {v1, p0}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
    invoke-virtual {v1, v0}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
    invoke-virtual {v1, p1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
    invoke-virtual {v1}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
    move-result-object v2
    return-object v2
.end method

Пример использования:

const-string v0, "Login name"
const-string v1, "Misha"
invoke-static {v0, v1}, Lcom/example/MyApplication;->concatKeyValue(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
move-result-object v2 

Возвращает и сохраняет в V2 строку "Login name=Misha".

На основе этого примера каждый волен создавать свои, какие его душе угодно, методы конкатенации.

3. Используем реализацию метода toString
В некоторых классах может быть переопределен метод toString. Пример — класс okhttp3/Request. Согласитесь, что это очень удобно, и мы конечно хотим видеть как выглядит Request целиком. Тогда делаем так:

invoke-virtual {p0}, Lokhttp3/Request;->toString()Ljava/lang/String;
move-result-object v1
const-string v2, "REQUEST:"
invoke-static {v2}, Lcom/example/MyApplication;->logMsg(Ljava/lang/String;)V  
invoke-static {v1}, Lcom/example/MyApplication;->logMsg(Ljava/lang/String;)V  

В Log попадает две строки: первая — "REQUEST:", а под ней — сам запрос.

4. Выводим Error Stack Trace
Иногда, в программах разработчики применяют собственные алгоритмы обработки ошибок. Например StackTrace может отправляться на сервер в зашифрованном виде. А мы вот хотим посмотреть его до шифрования. Это ведь может быть полезно:

Здесь переменная p1 содержит объект типа Throwable. На нумерацию переменных внимания не обращаем — для каждого метода она, скорее всего, будет своя.

invoke-virtual {p1}, Ljava/lang/Throwable;->getStackTrace()[Ljava/lang/StackTraceElement;
move-result-object v4
array-length v0, v4
if-lez v0, :cond_0 	# проверяем количество элементов  StackTraceElement
array-length v1, v4
const/4 v0, 0x0
:goto_0
if-ge v0, v1, :cond_0
aget-object v2, v4, v0
invoke-virtual {v2}, Ljava/lang/StackTraceElement;->toString()Ljava/lang/String;
move-result-object v2     
invoke-static {v2}, Lcom/example/MyApplication;->logMsg(Ljava/lang/String;)V
add-int/lit8 v0, v0, 0x1
goto :goto_0
:cond_0

В Log последовательно попадают все элементы сообщения из Throwable.

Собственно этот алгоритм, с изменениями, может быть применен и для итерации по элементам Collections типа String.

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

P.S.
Я за чистоту Русского Языка, и терпеть не могу, когда иностранные слова используют в кириллице, типа «кейс» или «сниппет» и вставляют их в устную речь, даже когда общаются с широкой и разнородной аудиторией, искажая Великий и Могучий. Считаю это признаком недалекости ума (Личное мнение автора статьи).

Комментарии (0)