Как мирный reverse engineering помог чуть-чуть улучшить приложение Яндекс.Деньги

в 21:50, , рубрики: android, reverse engineering, информационная безопасность, отладка, яндекс, яндекс.деньги, метки: , , , ,

Существует стереотип, что reverse engineering — это занятие для злых хакеров в темных очках и блестящих кожаных пальто. Под покровом ночи, в перерывах между беготней по стенам и рукопашными схватками с толпами спецназовцев, эти компьютерные нелюди творят страшные взломы программ, пентагонов и прочих баз данных. Сами взломы как правило не требуют никакой предварительной подготовки и занимают считанные секунды. Ну и конечно в процессе практически любого взлома по чОрным экранам адских хакерских ноутбуков с непонятной ОС ползут зелёные кракозяблы и/или крутится какая-то 3D-фиговина…

Как мирный reverse engineering помог чуть чуть улучшить приложение Яндекс.Деньги

Сегодня я хочу отойти от затасканных голливудских штампов про злых компьютерных взломщиков и поведать вам, дорогие читатели, о том как мирный reverse engineering помог чуть-чуть улучшить приложение Яндекс.Деньги. Надеюсь эта история пошатнет устойчивый стереотип, что reverse engineering — это обязательно плохо и нужно только нехорошим людям.

Чуть меньше месяца назад я немножко реверсил Яндекс.Деньги версии 1.71 для Android (последняя версия на тот момент). Кроме всего прочего интересного я нашел там некий загадочный метод ru.yandex.core.CrashHandler.sendBug(String paramString):

Smali код метода sendBug(String paramString) (довольно объемный, надо сказать)

.class public abstract Lru/yandex/core/CrashHandler;
.super Landroid/app/Activity;
.source "CrashHandler.java"

# ...
# неинтересный кода -  пропущено
# ...

.method sendBug(Ljava/lang/String;)V
    .locals 5
    .parameter "p1"

    .prologue
    .line 76
    new-instance v0, Lorg/json/JSONObject;

    .line 79
    .local v0, v0:Ljava/lang/Object;
    invoke-direct {v0}, Lorg/json/JSONObject;-><init>()V

    .line 84
    .local v0, v0:Ljava/lang/Object;
    :try_start_5
    const-string v1, "model"

    .line 87
    .local v1, v1:Ljava/lang/Object;
    sget-object v2, Landroid/os/Build;->MODEL:Ljava/lang/String;

    .line 90
    .local v2, v2:Ljava/lang/Object;
    invoke-virtual {v0, v1, v2}, Lorg/json/JSONObject;->
        put(Ljava/lang/String;Ljava/lang/Object;)Lorg/json/JSONObject;

    .line 93
    const-string v1, "systemVersion"

    .line 95
    sget-object v2, Landroid/os/Build$VERSION;->RELEASE:Ljava/lang/String;

    .line 97
    invoke-virtual {v0, v1, v2}, Lorg/json/JSONObject;->
        put(Ljava/lang/String;Ljava/lang/Object;)Lorg/json/JSONObject;

    .line 100
    const-string v1, "component"

    .line 102
    const-string v2, "Android"

    .line 104
    invoke-virtual {v0, v1, v2}, Lorg/json/JSONObject;->
        put(Ljava/lang/String;Ljava/lang/Object;)Lorg/json/JSONObject;

    .line 107
    const-string v1, "appVersion"

    .line 109
    invoke-static {}, Lru/yandex/core/CoreApplication;->getAppBuildIdFromNative()Ljava/lang/String;

    .line 111
    move-result-object v2

    .line 113
    invoke-virtual {v0, v1, v2}, Lorg/json/JSONObject;->
        put(Ljava/lang/String;Ljava/lang/Object;)Lorg/json/JSONObject;

    .line 116
    const-string v1, "appName"

    .line 118
    invoke-static {}, Lru/yandex/core/CoreApplication;->getAppNameFromNative()Ljava/lang/String;

    .line 120
    move-result-object v2

    .line 122
    invoke-virtual {v0, v1, v2}, Lorg/json/JSONObject;->
        put(Ljava/lang/String;Ljava/lang/Object;)Lorg/json/JSONObject;

    .line 125
    const-string v1, "summary"

    .line 127
    const-string v2, "Android Native Crash"

    .line 129
    invoke-virtual {v0, v1, v2}, Lorg/json/JSONObject;->
        put(Ljava/lang/String;Ljava/lang/Object;)Lorg/json/JSONObject;
    :try_end_33
    .catch Lorg/json/JSONException; {:try_start_5 .. :try_end_33} :catch_80

    .line 137
    .end local v2           #v2:Ljava/lang/Object;
    :goto_33
    :try_start_33
    new-instance v1, Lru/yandex/core/ClientHttpRequest;

    .line 140
    .local v1, v1:Ljava/lang/Object;
    new-instance v2, Ljava/net/URL;

    .line 143
    .local v2, v2:Ljava/lang/Object;
    new-instance v3, Ljava/lang/StringBuilder;

    .line 146
    .local v3, v3:Ljava/lang/Object;
    const-string v4, "http://dmitriyap.dyndns.org:9091/rest/jconnect/latest/issue/create?project="

    .line 149
    .local v4, v4:Ljava/lang/Object;
    invoke-direct {v3, v4}, Ljava/lang/StringBuilder;-><init>(Ljava/lang/String;)V

    .line 152
    .local v3, v3:Ljava/lang/Object;
    invoke-virtual {p0}, Lru/yandex/core/CrashHandler;->getJiraProjectName()Ljava/lang/String;

    .line 154
    move-result-object v4

    .line 156
    invoke-virtual {v3, v4}, Ljava/lang/StringBuilder;->
        append(Ljava/lang/String;)Ljava/lang/StringBuilder;

    .line 158
    move-result-object v3

    .line 160
    invoke-virtual {v3}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;

    .line 162
    move-result-object v3

    .line 164
    invoke-direct {v2, v3}, Ljava/net/URL;-><init>(Ljava/lang/String;)V

    .line 167
    .local v2, v2:Ljava/lang/Object;
    invoke-direct {v1, v2}, Lru/yandex/core/ClientHttpRequest;-><init>(Ljava/net/URL;)V

    .line 171
    .local v1, v1:Ljava/lang/Object;
    const-string v2, "issue"

    .line 173
    const-string v3, "issue.json"

    .line 175
    new-instance v4, Ljava/io/ByteArrayInputStream;

    .line 178
    .local v4, v4:Ljava/lang/Object;
    invoke-virtual {v0}, Lorg/json/JSONObject;->toString()Ljava/lang/String;

    .line 180
    move-result-object v0

    .line 182
    invoke-virtual {v0}, Ljava/lang/String;->getBytes()[B

    .line 184
    move-result-object v0

    .line 186
    invoke-direct {v4, v0}, Ljava/io/ByteArrayInputStream;-><init>([B)V

    .line 189
    .local v4, v4:Ljava/lang/Object;
    const-string v0, "application/json"

    .line 191
    invoke-virtual {v1, v2, v3, v4, v0}, Lru/yandex/core/ClientHttpRequest;->
             setParameter(Ljava/lang/String;Ljava/lang/String;Ljava/io/InputStream;Ljava/lang/String;)V

    .line 194
    const-string v0, "crash"

    .line 196
    const-string v2, "log.txt"

    .line 198
    new-instance v3, Ljava/io/ByteArrayInputStream;

    .line 201
    .local v3, v3:Ljava/lang/Object;
    invoke-virtual {p1}, Ljava/lang/String;->toString()Ljava/lang/String;

    .line 203
    move-result-object v4

    .line 205
    invoke-virtual {v4}, Ljava/lang/String;->getBytes()[B

    .line 207
    move-result-object v4

    .line 209
    invoke-direct {v3, v4}, Ljava/io/ByteArrayInputStream;-><init>([B)V

    .line 212
    .local v3, v3:Ljava/lang/Object;
    invoke-virtual {v1, v0, v2, v3}, Lru/yandex/core/ClientHttpRequest;->
        setParameter(Ljava/lang/String;Ljava/lang/String;Ljava/io/InputStream;)V

    .line 215
    invoke-virtual {v1}, Lru/yandex/core/ClientHttpRequest;->post()Ljava/io/InputStream;
    :try_end_7d
    .catch Ljava/io/IOException; {:try_start_33 .. :try_end_7d} :catch_7e

    .line 222
    .end local v1           #v1:Ljava/lang/Object;
    .end local v2           #v2:Ljava/lang/Object;
    .end local v3           #v3:Ljava/lang/Object;
    .end local v4           #v4:Ljava/lang/Object;
    :goto_7d
    return-void

    .line 226
    :catch_7e
    move-exception v0

    .line 228
    goto :goto_7d

    .line 232
    :catch_80
    move-exception v1

    .line 235
    .local v1, v1:Ljava/lang/Object;
    goto :goto_33
.end method

Вот тот же метод sendBug(String paramString) в Java-подобном псевдокоде, который после определённых манипуляций с dex файлом получается с помощью Java Decompiller:

Тот же метод в Java-подобном псевдокоде

package ru.yandex.core;

# ...
# импорт - не важно, пропущено
# ...

public abstract class CrashHandler extends Activity {

# ...
#  неинтересный код -  пропущено
# ...

  void sendBug(String paramString) {
    JSONObject localJSONObject = new JSONObject();
    try {
      localJSONObject.put("model", Build.MODEL);
      localJSONObject.put("systemVersion", Build.VERSION.RELEASE);
      localJSONObject.put("component", "Android");
      localJSONObject.put("appVersion", CoreApplication.getAppBuildIdFromNative());
      localJSONObject.put("appName", CoreApplication.getAppNameFromNative());
      localJSONObject.put("summary", "Android Native Crash");
      try {
         ClientHttpRequest localClientHttpRequest =
            new ClientHttpRequest(
                new URL("http://dmitriyap.dyndns.org:9091/rest/jconnect/latest/issue/create?project=" +
                getJiraProjectName()));
        localClientHttpRequest.setParameter("issue", "issue.json", 
                new ByteArrayInputStream(localJSONObject.toString().getBytes()), "application/json");
        localClientHttpRequest.setParameter("crash", "log.txt", 
                new ByteArrayInputStream(paramString.toString().getBytes()));
        localClientHttpRequest.post();
        return;
      }
      catch (IOException localIOException) {
        // Тут Java Decompiller сгенерировал бред - пропущено
        // ...
      }
    }
    catch (JSONException localJSONException) {
      // Тут тоже... вообще с исключениями Java Decompiller не дружит, увы
      // ...
    }
  }
}

Этот псевдокод конечно не совсем валиден с точки зрения синтаксиса языка Java, но зато он наглядно демонстрирует логику работы метода sendBug(String paramString). При вызове этого метода, на некий адрес http://dmitriyap.dyndns.org:9091 с помощью ClientHttpRequest.post() без какого-либо шифрования отсылается куча различной информации. В частности, в параметре crash отсылается переданный методу аргумент paramString. Судя по строке запроса и названиями переменных, «на той стороне» поднята Atlassian Jira, в которой метод sendBug(String paramString) создает issue сразу внося в него всю отсылаемую информацию. Т.е. по сути метод sendBug(String paramString) делает ровно то что следует из его названия — отсылает bug report'ы разработчикам в bugtracker. Вроде бы ничего страшного, многие программы так делают. Однако код самого метода вызывает вопросы:

  1. Кому принадлежит домен http://dmitriyap.dyndns.org? Это явно не корпоративный домен Яндекса.
  2. Что за информация передается методу sendBug(String paramString) в аргументе paramString и потому отсылается на http://dmitriyap.dyndns.org?
  3. При каких условиях программа Яндекс.Деньги вызывает метод sendBug(String paramString)?

Ответ на первый вопрос находится достаточно быстро. Небольшой поиск в Google дает что dmitriyap — это интернет-ник главы Mobile Services Development Department в Яндексе. Вероятно, домен http://dmitriyap.dyndns.org зарегистрировал именно он. Тот факт что данные никак не шифруются и отсылаются на поддомен dyndns.org, а не на какой-нибудь домен Яндекса, наводит на мысль, что вся эта система bug report'инга была сделана разработчиками Android-приложения Яндекс.Деньги наспех, «на коленке». Вероятно она использовалась в процессе разработки и не должна было попасть в релиз. Но, наверное по недосмотру, попала.

Что же с первым вопросом более менее ясно. Перейдем ко второму вопросу: что за информация передается методу sendBug(String paramString) в аргументе paramString и затем отсылается на http://dmitriyap.dyndns.org? Для этого мы сначала посмотрим на код метода doInBackground(...) анонимного внутреннего класса CrashHandler$1:

Smali код метода doInBackground(...)

.field log:Ljava/lang/String;

.method protected varargs doInBackground([Ljava/lang/Void;)Ljava/lang/Void;
    .locals 5
    .parameter "p1"

    .prologue
    .line 59
    const/4 v4, 0x1

    .line 64
    .local v4, v4:I
    :try_start_1
    invoke-static {}, Ljava/lang/Runtime;->getRuntime()Ljava/lang/Runtime;

    .line 66
    move-result-object v0

    .line 69
    .local v0, v0:Ljava/lang/Object;
    const/4 v1, 0x4

    .line 72
    .local v1, v1:B
    new-array v1, v1, [Ljava/lang/String;

    .line 75
    .local v1, v1:Ljava/lang/Object;
    const/4 v2, 0x0

    .line 78
    .local v2, v2:Ljava/lang/Object;
    const-string v3, "logcat"

    .line 81
    .local v3, v3:Ljava/lang/Object;
    aput-object v3, v1, v2

    .line 83
    const/4 v2, 0x1

    .line 86
    .local v2, v2:I
    const-string v3, "-d"

    .line 88
    aput-object v3, v1, v2

    .line 90
    const/4 v2, 0x2

    .line 93
    .local v2, v2:B
    const-string v3, "-v"

    .line 95
    aput-object v3, v1, v2

    .line 97
    const/4 v2, 0x3

    .line 99
    const-string v3, "threadtime"

    .line 101
    aput-object v3, v1, v2

    .line 103
    invoke-virtual {v0, v1}, Ljava/lang/Runtime;->exec([Ljava/lang/String;)Ljava/lang/Process;

    .line 105
    move-result-object v0

    .line 107
    iput-object v0, p0, Lru/yandex/core/CrashHandler$1;->process:Ljava/lang/Process;

    .line 110
    iget-object v0, p0, Lru/yandex/core/CrashHandler$1;->process:Ljava/lang/Process;

    .line 112
    invoke-virtual {v0}, Ljava/lang/Process;->getInputStream()Ljava/io/InputStream;

    .line 114
    move-result-object v0

    .line 116
    invoke-virtual {p0, v0}, Lru/yandex/core/CrashHandler$1;->
        readAllOf(Ljava/io/InputStream;)Ljava/lang/String;

    .line 118
    move-result-object v0

    .line 120
    iput-object v0, p0, Lru/yandex/core/CrashHandler$1;->log:Ljava/lang/String;
    :try_end_2e
    .catch Ljava/io/IOException; {:try_start_1 .. :try_end_2e} :catch_30

    .line 127
    .end local v2           #v2:B
    .end local v3           #v3:Ljava/lang/Object;
    :goto_2e
    const/4 v0, 0x0

    .line 130
    .local v0, v0:Ljava/lang/Object;
    return-object v0

    .line 135
    .end local v0           #v0:Ljava/lang/Object;
    .end local v1           #v1:Ljava/lang/Object;
    :catch_30
    move-exception v0

    .line 139
    .local v0, v0:Ljava/lang/Object;
    iget-object v1, p0, Lru/yandex/core/CrashHandler$1;->
        this$0:Lru/yandex/core/CrashHandler;

    .line 142
    .local v1, v1:Ljava/lang/Object;
    invoke-virtual {v0}, Ljava/io/IOException;->toString()Ljava/lang/String;

    .line 144
    move-result-object v0

    .line 146
    invoke-static {v1, v0, v4}, Landroid/widget/Toast;->
       makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;

    .line 148
    move-result-object v0

    .line 150
    invoke-virtual {v0}, Landroid/widget/Toast;->show()V

    .line 152
    goto :goto_2e
.end method

Соответствующий Java-подобный псевдокод полученный с помощью Java Decompiller:

Тот же метод в Java-подобном псевдокоде

String log;

protected Void doInBackground(Void[] paramArrayOfVoid) {
    try {
      Runtime localRuntime = Runtime.getRuntime();
      String[] arrayOfString = new String[4];
      arrayOfString[0] = "logcat";
      arrayOfString[1] = "-d";
      arrayOfString[2] = "-v";
      arrayOfString[3] = "threadtime";
      this.process = localRuntime.exec(arrayOfString);
      this.log = readAllOf(this.process.getInputStream());
      return null;
    }
    catch (IOException localIOException) {
     // Тут Java Decompiller выдал совсем полный бред, я эту пургу исключил что бы не смущать читателей
     // ...
    }
  }

Этот псевдокод опять-таки не совсем валиден с точки зрения синтаксиса языка Java, но зато из него понятно что делает doInBackground(...). Он запускает на Adnroid-устройстве командную строку

logcat -d -v threadtime

потом с помощью метода readAllOf(...) (определён в том же классе) захватывает вывод и в виде строки помещает его в поле log типа String. Что в этой строке? А в ней кроме всего прочего куча персональны данных пользователя — история платежей, какие-то приватные куки и т.п. Вот небольшой кусочек для примера (данные тут мои и они замазаны конечно):

Как мирный reverse engineering помог чуть чуть улучшить приложение Яндекс.Деньги

Откуда же в обычном logcat-логе столько персональных данных? Все дело в том что код приложения Яндекс.Деньги просто утыкан вызовами android.util.Log.d(...). По ходу работы приложения в лог пишется просто куча всякой информации — включая персональную информацию пользователя. Зачем? Не знаю, не знаю…

Однако вернемся ко второму вопросу. Куда же эта строка с кучей персональных данных из поля log девается после вызова doInBackground(...)? Вы не поверите, но она как раз и передается методу sendBug(String paramString) в аргументе paramString и затем отсылается на http://dmitriyap.dyndns.org. В незашифрованном виде. Что бы в этом убедится достаточно посмотреть на код метода onPostExecute(...) того же анонимного внутреннего класса CrashHandler$1:

Smali код метода onPostExecute(...)

.method protected onPostExecute(Ljava/lang/Void;)V
    .locals 2
    .parameter "p1"

    .prologue
    .line 188
    iget-object v0, p0, Lru/yandex/core/CrashHandler$1;->this$0:Lru/yandex/core/CrashHandler;

    .line 191
    .local v0, v0:Ljava/lang/Object;
    iget-object v1, p0, Lru/yandex/core/CrashHandler$1;->log:Ljava/lang/String;

    .line 194
    .local v1, v1:Ljava/lang/Object;
    invoke-virtual {v0, v1}, Lru/yandex/core/CrashHandler;->sendBug(Ljava/lang/String;)V

    .line 197
    iget-object v0, p0, Lru/yandex/core/CrashHandler$1;->val$progress:Landroid/app/ProgressDialog;

    .line 199
    invoke-virtual {v0}, Landroid/app/ProgressDialog;->dismiss()V

    .line 202
    iget-object v0, p0, Lru/yandex/core/CrashHandler$1;->this$0:Lru/yandex/core/CrashHandler;

    .line 204
    invoke-virtual {v0}, Lru/yandex/core/CrashHandler;->finish()V

    .line 207
    const/4 v0, 0x0

    .line 210
    .local v0, v0:Ljava/lang/Object;
    invoke-static {v0}, Ljava/lang/System;->exit(I)V

    .line 213
    return-void
.end method

Соответствующий Java-подобный псевдокод полученный с помощью Java Decompiller:

Тот же метод в Java-подобном псевдокоде

  protected void onPostExecute(Void paramVoid) {
    this.this$0.sendBug(this.log);
    this.val$progress.dismiss();
    this.this$0.finish();
    System.exit(0);
  }

Вот мы и ответили на второй вопрос. Интересно получается, да? В Яндекс.Деньги есть некий метод sendBug(String paramString), который отсылает logcat-лог с кучей персональных данных пользователя на какой-то http://dmitriyap.dyndns.org в незашифрованном виде.

В такой ситуации третий вопрос — при каких же условиях программа Яндекс.Деньги вызывает этот страшный метод sendBug(String paramString)? — становится особенно интересным. Правильный ответ получается после тщательного исследования кода приложения:

Метод sendBug(String paramString) не вызывается ни при каких условиях! Никогда!

Да-да, этот метод не вызывается никогда. Это мертвый код. Исследования кода приложения Яндекс.Денег заставляют думать что метод sendBug(String paramString) раньше вызывался при краше native компонента libcache_local.so (компонент отвечает за взаимодействие с Яндекс.Картами). Но потом вызов убрали, хотя сам метод убрать забыли. Поэтому приложение Яндекс.Деньги никуда не отсылает никаких персональных данных. И пользователи Яндекс в безопасности.

Наверное те самые злые компьютерные взломщики в темных очках и блестящих плащах, о которых я упоминал в самом начале, сейчас разочарованы. Они вероятно ожидали что я расскажу как нашел в Яндекс.Деньгах бэкдор, а может даже дам им ключ от этого бэкдора. Но нет, ребята! Нету никакого бэкдора (по крайней мере тут). Есть просто стремный, но мертвый код, и наш мирный reverse engineering его выявил.

Все вышесказанное я изложил в репорте Яндексу (Ticket#12092801010226151). Я написал что несмотря на то что метод sendBug(String paramString) безопасен для пользователей, само наличие этого метода в Яндекс.Деньгах — форменное безобразие. Мы мило пообщались по почте с security team Яндекса — ребята оказались очень адекватные. И уже в следующем релизе Яндекс.Денег версии 1.80, который кстати вышел очень скоро, все вышеупомянутые недочеты были исправлены: приложение больше не пишет личных данных пользователей в logcat-лог и стремный метод sendBug(String paramString) убрали. Так наш мирный reverse engineering помог сделать приложение Яндекс.Деньги немного лучше.

Я надеюсь что моя история про мирный reverse engineering вас развлекла, хотя она получилась немного длинной и путанной. Извините если вдруг кому показалось что я слил концовку.

Happy debugging!

Автор: dimakovalenko

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


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