Pull to refresh

Модификация стоковых прошивок для Android. Часть 2

Reading time10 min
Views79K
Здравствуй Хабр!

После написания первой части, в течение всей недели я размышлял о том, стоит ли писать о том что обещал или же по просьбе некоторых читателей дать разъяснения и объяснения по поводу некоторых, на мой взгляд базовых, вещей.
Не хотелось бы, но придется некоторых из вас расстроить. Писать самому — займет слишком много времени и сил, «копипастить» я не люблю больше всего, а делать из статьи кучу ссылок на разные ресурсы — будет моветон.

Например, описывать как происходит загрузка Android в данной статье будет не уместно. Если вы знаете принцип инициализации аппаратного обеспечения вашего компьютера в BIOSе, а затем его загрузка через ядро системы, то Android в этом плане ничем не отличается. Разница лишь в процессорной архитектуре. Структура файловой системы? Ну господа, это же чистой воды UNIX система, и писать где, что и как хранится — абсурдно! Править build.prop — это тюнинг системы. Да, это модификация, но большую часть этих параметров можно сделать сторонними приложениями, причем удобными для пользования, например System Tuner.

Понять самому принципы системы Android заняло у меня пару месяцев, столько же займет времени и писать статьи, чтобы осветить все базовые вещи. Так что давайте лучше будем писать о конкретных примерах как разбирать Dalvik код и создавать на телефоне удобства «пользования».

И так, поехали! Сегодня я расскажу как я реализовал функционал автоматической записи телефонных разговоров родными средствами.

Преамбула


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

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

Поехали!


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

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


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

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

    — это API вашей версии Android. Для JB — это 16
    — папка, где находятся все фреймворки прошивки.

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

    Собираем наш файл обратно: java -Xmx512m -jar smali.jar -a 16 Phone -o classes.dex
    Заменяем полученный classes.dex в оригинальном файле любым архиватором
    Перезаписываем 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 - ужаснулся, закрыл страничку и не открывал ее в течение полу года. Когда прижало к стенке, в течение двух недель быстро разобрался что к чему и как это реализовать на практике. Что радует, за всю практику ни разу не получал кирпич.

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

    Русскоязычный и Англоязычный
    На обоих ресурсах я присутствую с тем же ником.

    А пока до следующей статьи, надеюсь через неделю.
Tags:
Hubs:
+62
Comments46

Articles