Существует стереотип, что 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-приложения Яндекс.Деньги. Давно пора было.
