Существует стереотип, что reverse engineering — это занятие для злых хакеров в темных очках и блестящих кожаных пальто. Под покровом ночи, в перерывах между беготней по стенам и рукопашными схватками с толпами спецназовцев, эти компьютерные нелюди творят страшные взломы программ, пентагонов и прочих баз данных. Сами взломы как правило не требуют никакой предварительной подготовки и занимают считанные секунды. Ну и конечно в процессе практически любого взлома по чОрным экранам адских хакерских ноутбуков с непонятной ОС ползут зелёные кракозяблы и/или крутится какая-то 3D-фиговина…
Сегодня я хочу отойти от затасканных голливудских штампов про злых компьютерных взломщиков и поведать вам, дорогие читатели, о том как мирный reverse engineering помог чуть-чуть улучшить приложение Яндекс.Деньги. Надеюсь эта история пошатнет устойчивый стереотип, что reverse engineering — это обязательно плохо и нужно только нехорошим людям.
Чуть меньше месяца назад я немножко реверсил Яндекс.Деньги версии 1.71 для Android (последняя версия на тот момент). Кроме всего прочего интересного я нашел там некий загадочный метод ru.yandex.core.CrashHandler.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:
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. Вроде бы ничего страшного, многие программы так делают. Однако код самого метода вызывает вопросы:
- Кому принадлежит домен
http://dmitriyap.dyndns.org
? Это явно не корпоративный домен Яндекса. - Что за информация передается методу
sendBug(String paramString)
в аргументеparamString
и потому отсылается наhttp://dmitriyap.dyndns.org
? - При каких условиях программа Яндекс.Деньги вызывает метод
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
:
.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:
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
. Что в этой строке? А в ней кроме всего прочего куча персональны данных пользователя — история платежей, какие-то приватные куки и т.п. Вот небольшой кусочек для примера (данные тут мои и они замазаны конечно):
Откуда же в обычном logcat-логе столько персональных данных? Все дело в том что код приложения Яндекс.Деньги просто утыкан вызовами android.util.Log.d(...)
. По ходу работы приложения в лог пишется просто куча всякой информации — включая персональную информацию пользователя. Зачем? Не знаю, не знаю…
Однако вернемся ко второму вопросу. Куда же эта строка с кучей персональных данных из поля log
девается после вызова doInBackground(...)
? Вы не поверите, но она как раз и передается методу sendBug(String paramString)
в аргументе paramString
и затем отсылается на http://dmitriyap.dyndns.org
. В незашифрованном виде. Что бы в этом убедится достаточно посмотреть на код метода onPostExecute(...)
того же анонимного внутреннего класса CrashHandler$1
:
.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:
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