Существует стереотип, что reverse engineering — это занятие для злых хакеров в темных очках и блестящих кожаных пальто. Под покровом ночи, в перерывах между беготней по стенам и рукопашными схватками с толпами спецназовцев, эти компьютерные нелюди творят страшные взломы программ, пентагонов и прочих баз данных. Сами взломы как правило не требуют никакой предварительной подготовки и занимают считанные секунды. Ну и конечно в процессе практически любого взлома по чОрным экранам адских хакерских ноутбуков с непонятной ОС ползут зелёные кракозяблы и/или крутится какая-то 3D-фиговина…
Сегодня я хочу отойти от затасканных голливудских штампов про злых компьютерных взломщиков и поведать вам, дорогие читатели, о том как мирный reverse engineering помог чуть-чуть улучшить приложение Яндекс.Деньги. Надеюсь эта история пошатнет устойчивый стереотип, что reverse engineering — это обязательно плохо и нужно только нехорошим людям.
Чуть меньше месяца назад я немножко реверсил Яндекс.Деньги версии 1.71 для Android (последняя версия на тот момент). Кроме всего прочего интересного я нашел там некий загадочный метод
Вот тот же метод
Этот псевдокод конечно не совсем валиден с точки зрения синтаксиса языка Java, но зато он наглядно демонстрирует логику работы метода
Ответ на первый вопрос находится достаточно быстро. Небольшой поиск в Google дает что dmitriyap — это интернет-ник главы Mobile Services Development Department в Яндексе. Вероятно, домен
Что же с первым вопросом более менее ясно. Перейдем ко второму вопросу: что за информация передается методу
Соответствующий Java-подобный псевдокод полученный с помощью Java Decompiller:
Этот псевдокод опять-таки не совсем валиден с точки зрения синтаксиса языка Java, но зато из него понятно что делает
Откуда же в обычном logcat-логе столько персональных данных? Все дело в том что код приложения Яндекс.Деньги просто утыкан вызовами
Однако вернемся ко второму вопросу. Куда же эта строка с кучей персональных данных из поля
Соответствующий Java-подобный псевдокод полученный с помощью Java Decompiller:
Вот мы и ответили на второй вопрос. Интересно получается, да? В Яндекс.Деньги есть некий метод
В такой ситуации третий вопрос — при каких же условиях программа Яндекс.Деньги вызывает этот страшный метод
Метод sendBug(String paramString) не вызывается ни при каких условиях! Никогда!
Да-да, этот метод не вызывается никогда. Это мертвый код. Внимательное исследование (которое я здесь опускаю, ибо оно долгое и нудное) кода приложения Яндекс.Денег заставляют думать что метод
Наверное те самые злые компьютерные взломщики в темных очках и блестящих кожаных пальто, о которых я упоминал в самом начале, сейчас разочарованы. Они вероятно ожидали что я расскажу как нашел в Яндекс.Деньгах бэкдор, а может даже дам им ключ от этого бэкдора. Но нет, ребята! Нету никакого бэкдора (по крайней мере тут). Есть просто стремный, но мертвый код, и наш мирный reverse engineering его выявил.
Все вышесказанное я изложил в репорте Яндексу (Ticket#12092801010226151). Я написал что несмотря на то что метод
Я надеюсь что моя история про мирный reverse engineering вас развлекла, хотя она получилась немного длинной и путанной. Извините если вдруг кому показалось что я слил концовку.
Happy debugging!
P.S. И — да, в версии 1.80 Яндекс наконец-то обсфуцировал Java-код Android-приложения Яндекс.Деньги. Давно пора было.
Сегодня я хочу отойти от затасканных голливудских штампов про злых компьютерных взломщиков и поведать вам, дорогие читатели, о том как мирный 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)
. При вызове этого метода, на некий адрес dmitriyap.dyndns.org:9091
с помощью ClientHttpRequest.post()
без какого-либо шифрования отсылается куча различной информации. В частности, в параметре crash
отсылается переданный методу аргумент paramString
. Судя по строке запроса и названиями переменных, «на той стороне» поднята Atlassian Jira, в которой метод sendBug(String paramString)
создает issue сразу внося в него всю отсылаемую информацию. Т.е. по сути метод sendBug(String paramString)
делает ровно то что следует из его названия — отсылает bug report'ы разработчикам в bugtracker. Вроде бы ничего страшного, многие программы так делают. Однако код самого метода вызывает вопросы:- Кому принадлежит домен
dmitriyap.dyndns.org
? Это явно не корпоративный домен Яндекса. - Что за информация передается методу
sendBug(String paramString)
в аргументеparamString
и потому отсылается наdmitriyap.dyndns.org
? - При каких условиях программа Яндекс.Деньги вызывает метод
sendBug(String paramString)
?
Ответ на первый вопрос находится достаточно быстро. Небольшой поиск в Google дает что dmitriyap — это интернет-ник главы Mobile Services Development Department в Яндексе. Вероятно, домен
dmitriyap.dyndns.org
зарегистрировал именно он. Тот факт что данные никак не шифруются и отсылаются на поддомен dyndns.org, а не на какой-нибудь домен Яндекса, наводит на мысль, что вся эта система bug report'инга была сделана разработчиками Android-приложения Яндекс.Деньги наспех, «на коленке». Вероятно она использовалась в процессе разработки и не должна было попасть в релиз. Но, наверное по недосмотру, попала.Что же с первым вопросом более менее ясно. Перейдем ко второму вопросу: что за информация передается методу
sendBug(String paramString)
в аргументе paramString
и затем отсылается на 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
. Что в этой строке? А в ней кроме всего прочего куча персональных данных пользователя — история платежей, какие-то приватные куки и т.п. Вот небольшой кусочек для примера (данные тут мои и они замазаны конечно):Откуда же в обычном logcat-логе столько персональных данных? Все дело в том что код приложения Яндекс.Деньги просто утыкан вызовами
android.util.Log.d(...)
. По ходу работы приложения в лог пишется просто куча всякой информации — включая персональную информацию пользователя. Зачем? Не знаю, не знаю… Наверное это использовалось для отладки приложения в процессе разработки, а потом эти вызовы просто забыли убрать из релиза.Однако вернемся ко второму вопросу. Куда же эта строка с кучей персональных данных из поля
log
девается после вызова doInBackground(...)
? Вы не поверите, но она как раз и передается методу sendBug(String paramString)
в аргументе paramString
и затем отсылается на 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-лог с кучей персональных данных пользователя на какой-то dmitriyap.dyndns.org
в незашифрованном виде.В такой ситуации третий вопрос — при каких же условиях программа Яндекс.Деньги вызывает этот страшный метод
sendBug(String paramString)
? — становится особенно интересным. Правильный ответ получается после тщательного исследования кода приложения:Метод sendBug(String paramString) не вызывается ни при каких условиях! Никогда!
Да-да, этот метод не вызывается никогда. Это мертвый код. Внимательное исследование (которое я здесь опускаю, ибо оно долгое и нудное) кода приложения Яндекс.Денег заставляют думать что метод
sendBug(String paramString)
раньше вызывался при краше native компонента libcache_local.so
(компонент отвечает за взаимодействие с Яндекс.Картами). Но потом вызов убрали, хотя сам метод убрать забыли. Поэтому приложение Яндекс.Деньги никуда не отсылает никаких персональных данных. И пользователи Яндекс в безопасности.Наверное те самые злые компьютерные взломщики в темных очках и блестящих кожаных пальто, о которых я упоминал в самом начале, сейчас разочарованы. Они вероятно ожидали что я расскажу как нашел в Яндекс.Деньгах бэкдор, а может даже дам им ключ от этого бэкдора. Но нет, ребята! Нету никакого бэкдора (по крайней мере тут). Есть просто стремный, но мертвый код, и наш мирный reverse engineering его выявил.
Все вышесказанное я изложил в репорте Яндексу (Ticket#12092801010226151). Я написал что несмотря на то что метод
sendBug(String paramString)
безопасен для пользователей, само наличие этого метода в Яндекс.Деньгах — форменное безобразие. К тому же приложение пишет кучу персональных данных пользователя в logcat лог. В результате мы мило пообщались по почте с security team Яндекса — ребята оказались очень адекватные. И уже в следующем релизе Яндекс.Денег версии 1.80, который кстати вышел очень скоро, все вышеупомянутые недочеты были исправлены: приложение больше не пишет личных данных пользователей в logcat-лог и стремный метод sendBug(String paramString)
убрали. Так наш мирный reverse engineering помог сделать приложение Яндекс.Деньги немного лучше.Я надеюсь что моя история про мирный reverse engineering вас развлекла, хотя она получилась немного длинной и путанной. Извините если вдруг кому показалось что я слил концовку.
Happy debugging!
P.S. И — да, в версии 1.80 Яндекс наконец-то обсфуцировал Java-код Android-приложения Яндекс.Деньги. Давно пора было.