Модификация Android. Часть 2.

17 ноября 2013 г.

Преамбула

Законодательство некоторых стран запрещает производить запись телефонных разговоров техническим средствами. Например в США запрещается записывать личные телефонные разговоры без предварительного согласия сторон. А на нашем пост-советском пространстве разрешено записывать беседу, в которой вы являетесь одной из сторон, без предупреждения других участников беседы. В том же самом Китае запись просто напросто приветствуется и «стучать» на соседа — идеологический принцип. И это касается не только записей разговоров. Пользование интернетом, отправка СМС сообщений, пользование соц сетями, алгоритмы шифрования, телефоны с двумя SIM картами и многое другое также индивидуально регламентируются законодательством разных стран. А теперь представьте, что вы владелец бизнеса и экспортируете свою IT продукцию по всему миру. Разумеется, свое программное обеспечение вы будете «писать» одной веткой, а не несколькими, но вот конфигурации будут отличаться от региона к региону. Для стран СНГ одно, для Европы — другое. Также и поступают производители телефонов. Операционная система Android дорабатывается только одной группой программистов и в основную ветку, а для каждого региона при компиляции финальных релизов используются специфичные конфигурационные файлы. Говорю это с уверенностью, так как работал в свое время у одного из вендоров.

Модификация Android. Часть 1.

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

Поехали!

Модификация Android
Читая дизассемблированый JAVA код Phone.apk я случайно наткнулся на занимательный флаг. Интересного, но скрытого от нас функционала на самом деле очень много. Буквально сегодня обнаружил, что на моем телефоне есть параметры для японского оператора KDDI. Специально сформированное СМС сообщение от провайдера может заставить мой телефон истошно издавать звуки и вибрировать несколько минут в случае землетрясения или цунами. Но вернемся к нашему флагу.

public static final boolean IS_INCALL_RECORDING_ENABLE = false;

Занимательно, подумал я. Если есть такой флаг, значит он где-то используется. Но вот где — не понятно. Тем не менее, я предположил, что кнопка записи звонка должна появляться во время звонка. Дело за малым! Я изменил FALSE на TRUE, перезаписал патченный Phone.apk, позвонил на домашний телефон, поднял трубку и увидел кнопку «Начать запись». А ведь раньше ее не было!!!

android

Еще раз расскажу как делать подобные вещи для усвоения материала:

  1. Создаем отельную папку и кладем туда Phone.apk файл и к нему smali и backsmali
  2. Потрошим файлик, чтобы разобрать до Dalvik кода командой java -Xmx512m -jar baksmali.jar -a -d -o Phone -x Phone.apk — это API вашей версии Android. Для JB — это 16

    — папка, где находятся все фреймворки прошивки.

  3. Открываем в текстовом редакторе файл, в котором нашли флаг Phone\com\android\phone\util\VoiceRecorderHelper.smali
  4. Заменяем
    .field public static final IS_INCALL_RECORDING_ENABLE:Z = false
    

    на

    .field public static final IS_INCALL_RECORDING_ENABLE:Z = true
    
  5. Собираем наш файл обратно: java -Xmx512m -jar smali.jar -a 16 Phone -o classes.dex
  6. Заменяем полученный classes.dex в оригинальном файле любым архиватором
  7. Перезаписываем Phone.apk в телефоне

Протестировав запись нескольких звонков, я обнаружил, что телефон записывает разговоры стандартным встроенным диктофоном и при чем в очень и очень хорошем качестве. Название файлов формируется автоматически и содержит номер или имя собеседника, а также дату и длительность звонка. Формат сохранения файлов также можно настроить в родном приложении. А зачем вы спросите мне вообще нужны эти записи звонков?

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

Во-вторых, по телефону мне приходится разговаривать с клиентами и заказчиками и порой нужная и важная информация может ускользнуть или быть не расслышана. Или, допустим, состоялся важный разговор, который надо прослушать и проанализировать и принять своевременные решения иди действия вместе с партнером по бизнесу или кем-то еще.

В-третьих, очень удобная доказательная база в работе и быту.

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

  • Я не доверяю сторонним и не проверенным приложениям. Зачастую, из-за них садится батарейка, так как каждое подобное приложение висит постоянно в памяти телефона и отжирает процессорное время.
  • Качество записи не всегда соответствует обещанному.
  • Я привередлив к интерфейсу. Приложением может быть богато функционалом, но если мне не удобен GUI, я его не буду использовать. Этим хромают многие отечественные разработки, к сожалению.

Как это вообще работает?

Всем разработчикам под Android известно, что в системе полно различных стандартных широковещательных сообщений. Что бы ни произошло в системе, любое приложение его может получить, если реализовать «своего» получателя такого сообщения. Можно сделать свои широковещательные сообщения, только получатель этого сообщения будет само приложение или все приложении, которые сделали вы сами и они это сообщение как-то обрабатывают. Я такое в практике видел в GoDialer и GoSMSPro.

Таким же образом работают и сторонние приложения записи звонков. Как только прошло сообщение, что был установлен звонок, включается запись. Как только звонок прекратился, запись останавливается и буфер записывается в файл.

Моя задача состояла в том, чтобы найти то место, где формируется это самое сообщение или обрабатывается и принудительно начинать запись звонка без лишних телодвижений. Ведь часто бывает, что или забываешь включать или просто-напросто не успеваешь. Как вообще искать нужное место в тонне кода прошивки — тема следующей статьи, а пока перейдем сразу к «нашему месту».

Обработчик, а точнее два, оказались в файле \com\android\phone\CallNotifier.java

Декомпилированный код (здесь показана только часть кода) из Dalvik в Java оказался следующим:

  private void onCallConnected(AsyncResult paramAsyncResult)
  {
    Connection localConnection = (Connection)paramAsyncResult.result;
    String str = ((IfConnection)localConnection).getDialString();
    VLog.d("onCallConnected() dialed number:" + str);
    removeMessages(120000);
    removeMessages(120001);
    this.mIsEccNeedRetry = false;
    this.mEccIsSwitchingForRetrying = false;
    // много много кода
  }

и

  private void onDisconnect(AsyncResult paramAsyncResult)
  {
    Phone.State localState = this.mCM.getState();
    if (CallNotifier.VDBG)
      super.log("onDisconnect()...  CallManager state: " + this.mCM.getState());
    VLog.d(this, "onDisconnect()");
    removeMessages(120000);
    removeMessages(120001);
    // много много кода
  }

Задача усложняется, по сравнению с прежней статьей. Если в прошлый раз нам надо было исправить простую функцию, то здесь мы ее исправить не сможем, так как у нас нет исходного кода, чтобы его переписать. Здесь нам нужно вживить свой код.

Что такое Dalvik?

Об этом кратко написано здесь и здесь. Если говорить на еще более простым языком — байт код виртуальной машины основан на регистрах (выделенная область памяти для оперирования переменными) и множества инструкций и операторов. Смысл и принцип работы довольно прост: вы записываете в регистр какое-то значение и затем совершаете над ним операции, результат операции возвращаете туда, от куда за действиями обращались. Более подробно обо всех операторах и инструкциях можно подсмотреть здесь: Bytecode for the Dalvik VM.

Вживляем наш код

Для того чтобы что-то вписать, нам надо знать что туда вписывать. Слизать код можно с обработчика кнопки «Начать запись».

Найти где хранится обработчик даже начинающим программистам для Android это не составит труда. Мы потом еще вернемся к тому что и как искать в будущих статьях. Суть первых статей объяснить принципы.

При нажатии кнопки, срабатывает следующий код:

     VoiceRecorderHelper localVoiceRecorderHelper = VoiceRecorderHelper.getInstance();
      if (!localVoiceRecorderHelper.isRecording())
      {
        localVoiceRecorderHelper.start();
      }

То есть чтобы автоматизировать запись всех звонков, надо этот код добавить в обработчик onCallConnected.

Dalvik код этой записи выглядит как

    invoke-static {}, Lcom/android/phone/util/VoiceRecorderHelper;->getInstance()Lcom/android/phone/util/VoiceRecorderHelper;

    move-result-object v1

    invoke-virtual/range {v1 .. v1}, Lcom/android/phone/util/VoiceRecorderHelper;->isRecording()Z

    move-result v2

    const/4 v3, 0x0

    if-ne v3, v2, :cond_a9

    invoke-virtual/range {v1 .. v1}, Lcom/android/phone/util/VoiceRecorderHelper;->start()Z

    :cond_a9

Разберем код построчно:

  1. invoke-static вызывает экземпляр класса VoiceRecorderHelper
  2. сохраняем экземпляр в регистр v1
  3. вызываем метод этого класса под названием isRecording, который возвращает true или false
  4. Результат записываем в регистр v2
  5. Записываем в регистр v3 значение 0
  6. Делаем сравнение между двумя регистрами v2 и v3. Логика: v2 != v3. Если isRecording вернет TRUE, значит v2 будет иметь значение 1 и если FALSE то наоборот. Если условие НЕ срабатывает, то прыгаем на маркер cond_a9. Если нет, то
  7. Вызывается метод start экземпляра класса, который хранится в регистре v1
  8. Наш разговор начал записываться.

Возвращаемся к нашему onCallConnected. Его dalvik код выглядит следующим образом:

.method private onCallConnected(Landroid/os/AsyncResult;)V
    .registers 8
    .parameter "r"

    .prologue

    .line 2302
    iget-object v0, p1, Landroid/os/AsyncResult;->result:Ljava/lang/Object;

    check-cast v0, Lcom/android/internal/telephony/Connection;

    .local v0, c:Lcom/android/internal/telephony/Connection;

    move-object v2, v0

Давайте и этот код разберем, чтобы было понятно что к чему относится

  • .registers 8 — количество регистров памяти, необходимое и используемое для данной функции
  • parameter "r" — название параметра, которое было использовано в исходном коде. Нас оно редко интересует.
  • prologue — начало алгоритма функции
  • .line 2302 — номер строки в исходном коде. Это только для отладки.
  • iget-object v0, p1, Landroid/os/AsyncResult;->result:Ljava/lang/Object; следующая строка соответствует (Connection)paramAsyncResult.result;
  • check-cast v0, Lcom/android/internal/telephony/Connection; соответствует Connection
  • .local v0, c:Lcom/android/internal/telephony/Connection; соответствует localConnection
  • move-object v2, v0 — клонирование локальной переменной v0 в регистр v2
  • и т.д.

Разбирать код не так уж и сложно, если обращаться к описанию и сравнивать с Java кодом.

Казалось бы, нам нужно всего лишь скопировать код из обработчика нажатия кнопки и вставить в начало нашего обработчика звонка и готово. Не тут то было. Иногда это работает, но большей частью нет. Дело в том, что регистры, куда мы записываем данные, могут использоваться в дальнейшем коде программы и если в начале мы возьмем неправильный регистр и запишем в него что-то, то по ходу исполнения программы могут вылезть ошибки и поломаться весь алгоритм. Наш случай прост в том, что мы вписываем в начало функции и можем использовать любые регистры, которые не инициализированы в начале, потому как они будут перезаписаны в дальнейшем. Но часто код приходится вживлять где-то в середине программы и с регистрами нужно быть осторожным. Об этом тоже в будущих статьях.

Модификация регистров

Наши первые две строки вживляемого кода имеют следующее:

    invoke-static {}, Lcom/android/phone/util/VoiceRecorderHelper;->getInstance()Lcom/android/phone/util/VoiceRecorderHelper;

    move-result-object v1

Самый простой способ искать подходящие номера регистра — это поискать в самом методе какие виды данных записываются в необходимые номера регистров. Если поискать в коде, то обнаружим, что move-result-object у нас записывается и в v2 и v 3.

Соответственно все наши v1 во вживляемом кода заменим на v2 или v3

Проделав все операции по замене номеров регистров во вживляемом кода получаем следующую картину:

    invoke-static {}, Lcom/android/phone/util/VoiceRecorderHelper;->getInstance()Lcom/android/phone/util/VoiceRecorderHelper;

    move-result-object v3

    invoke-virtual/range {v3 .. v3}, Lcom/android/phone/util/VoiceRecorderHelper;->isRecording()Z

    move-result v4

    const/4 v5, 0x0

    if-ne v5, v4, :cond_27

    invoke-virtual/range {v3 .. v3}, Lcom/android/phone/util/VoiceRecorderHelper;->start()Z

    :cond_27

Стоит отметить, что маркер cond_a9 мы изменили на cond_27. Дело в том, что маркер cond_a9 уже имелся в том файле, куда мы вживляли код и второй раз такой маркер не может быть использован. Номер маркера — шестнадцатиричный код и может быть любым, главное уникальным.

Теперь в исходном файле заменяем строку .line 2302 на наш вживляемый код и получаем

.method private onCallConnected(Landroid/os/AsyncResult;)V
    .registers 8
    .parameter "r"

    .prologue

    invoke-static {}, Lcom/android/phone/util/VoiceRecorderHelper;->getInstance()Lcom/android/phone/util/VoiceRecorderHelper;

    move-result-object v3

    invoke-virtual/range {v3 .. v3}, Lcom/android/phone/util/VoiceRecorderHelper;->isRecording()Z

    move-result v4

    const/4 v5, 0x0

    if-ne v5, v4, :cond_27

    invoke-virtual/range {v3 .. v3}, Lcom/android/phone/util/VoiceRecorderHelper;->start()Z

    :cond_27
    .line 2302

    iget-object v0, p1, Landroid/os/AsyncResult;->result:Ljava/lang/Object;

    check-cast v0, Lcom/android/internal/telephony/Connection;

    .local v0, c:Lcom/android/internal/telephony/Connection;

    move-object v2, v0

Осталось теперь собрать наш код с помощью команды java -Xmx512m -jar smali.jar -a 16 Phone -o classes.dex, заменить в Phone.apk и протестировать.

В Java варианте наша работа стала выглядеть следующим образом:

  private void onCallConnected(AsyncResult paramAsyncResult)
  {
     VoiceRecorderHelper localVoiceRecorderHelper = VoiceRecorderHelper.getInstance();
     if (!localVoiceRecorderHelper.isRecording())
     {
       localVoiceRecorderHelper.start();
     }
    Connection localConnection = (Connection)paramAsyncResult.result;
    String str = ((IfConnection)localConnection).getDialString();
    VLog.d("onCallConnected() dialed number:" + str);
    removeMessages(120000);
    removeMessages(120001);

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

Для этого нам надо прописать подобный код и в начале функции (метода) onDisconnect, только с обратной логикой.

     VoiceRecorderHelper localVoiceRecorderHelper = VoiceRecorderHelper.getInstance();
     if (localVoiceRecorderHelper.isRecording())
     {
       localVoiceRecorderHelper.stop();
     }

По аналогии с началом записи, мы пишем небольшую процедуру, заменяя номера регистров

.method private onDisconnect(Landroid/os/AsyncResult;)V
    .registers 41
    .parameter "r"

    .prologue

    invoke-static {}, Lcom/android/phone/util/VoiceRecorderHelper;->getInstance()Lcom/android/phone/util/VoiceRecorderHelper;

    move-result-object v34

    invoke-virtual/range {v34 .. v34}, Lcom/android/phone/util/VoiceRecorderHelper;->isRecording()Z

    move-result v4

    if-eqz v4, :cond_33

    invoke-virtual/range {v34 .. v34}, Lcom/android/phone/util/VoiceRecorderHelper;->stop()Z

    .line 2487
    :cond_33

собираем наши изменения, заменяем в телефоне и вуаля — все работает так как должно.

Эпилог

Уверен, данный материал по сравнению с предыдущей статьей оказался в несколько раз сложнее и запутанней. Какие-то регистры, операторы, модификаторы… Похоже на бред. Я сам в первый раз когда увидел Dalvik — ужаснулся, закрыл страничку и не открывал ее в течение полу года. Когда прижало к стенке, в течение двух недель быстро разобрался что к чему и как это реализовать на практике. Что радует, за всю практику ни разу не получал кирпич.

Не для рекламы ради, хочу посоветовать два ресурса, на которых можно почерпнуть много информации:

Русскоязычный и Англоязычный

Написал Нурлан Муханов aka Falseclock.

Теги:
рубрика Android
  • Похожие статьи
  • Предыдущие из рубрики