
Я из тех людей, кто с наступлением осени старается проводить на улице меньше времени. В Москве это не сложно: ограничиваешься маршрутом от дома до офиса и обратно. Однако промозглая погода может причинить дискомфорт и в помещении, особенно если ваше рабочее место, как и моё, находится у окна, а каждый второй коллега, жалуясь на духоту, просит проветрить кабинет. Чтобы не впасть в хандру, этой осенью я обновил гардероб.
Размышляя о судьбе ненужных вещей, я прикидывал, что с ними делать: выкинуть, порезать на тряпки, отдать донашивать младшему брату? Но для одной вещи ни один из этих способов не годился: то были кожаные сапоги 44 размера приличного вида, но порядком мне надоевшие. Их я решил продать на Avito. Загрузил фотографии, указал ненастоящее имя (информационная безопасность же), выставил сапоги, пару других вещей и пошёл спать. Откуда мне было знать, что это обернётся длительным анализом приложения на предмет скрытых угроз?

Приятная неожиданность
На следующий день после пары сомнительных звонков мне пришло интересное смс-сообщение со следующим содержимым:

Спустя пару дней я получил ещё одно похожее сообщение:

Удивившись, что кто-то каким-то образом смог перевести мне деньги по интернету (видимо, один я старый — пользуюсь до сих пор бумажными сберегательными книжками), я перешёл по ссылке в СМС.
После этого мне предложили скачать приложение для Android (apk-файл). Радостно скачав файл, я увидел следующее:

Вызывает доверие! Я с нетерпением захотел побыстрее всё установить и покончить с этим.
Но здесь, как обычно бывает, надоедливая операционная система Android почему-то не позволила мне запустить файл. «Да отдайте уже деньги!» — негодовал я. Пришлось перейти в настройки и включить какую-то опцию «Неизвестные источники», неужели телефон настолько глуп в 2018? Кстати, мой телефон — Xiaomi Remdi c Andoid 6.0.1 (примечание для технарей).

Далее последовала цепочка странных событий. Телефон продолжал сообщать о недостоверных источниках. Но ведь Avito — это достоверный источник! Пришлось погуглить, разобраться, как это обойти, и после отключить в настройках. Вскоре появился и некий антивирус, который я не устанавливал.

Наконец-то я увидел заветное стандартное окно установки — смотреть на разрешения в наше время нет смысла, сейчас даже блокнот не запустится, пока не дашь ему полный доступ к телефону. Радостный миг окончания установки приложения и ЗАПУСК! Я с нетерпением ждал обещанных денег. Дополнительно приложение запросило администраторские привилегии, с чем я радостно согласился. К сожалению, приложение странно себя вело и не хотело производить оплату, а вскоре и вовсе пропало из списка приложений на общем экране.


Cпойлер
Позже я проверил на другом телефоне — Lenovo с Android 4.4.2 на борту. Список разрешений при установке оказался гораздо больше. И никакие Play Защита и Антивирус не мешают, необходимо только разрешить установку из недостоверных источников.




Альтруизм
Итак, к чему мы пришли:
- На установку потрачено 20 минут времени.
- Деньги я не получил.
Подумал, что проблема в ошибке кода, как это часто бывает у программистов. Решил выявить, какие ошибки возникают при работе приложения на моём телефоне, и отправить отчёт об этом разработчику.
Cпойлер
Сейчас так мало осталось бескорыстных людей, один из них — я.
Понятно, что приложение такого типа должно работать при подключенном интернете, поэтому для начала я попробовал проанализировать трафик между телефоном и сервером приложения.
Техническая инструкция по настройке прокси
Можно прослушать интернет-трафик в любой точке, проходящей по пути от телефона до сервера, будь то домашний роутер, местный интернет-провайдер, магистральный интернет-провайдер или сервер приложения.
Проблемы:
Я задумал пройти более классическим путём и настроить прокси на телефоне с прокси сервером на собственном ноутбуке. Чтобы нивелировать проблему шифрования, решил импортировать свой сертификат, а для отделения трафика приложения не стал запускать другие приложения.
В качестве программы прокси-сервера выбрал burp suite. Настроил прокси-сервер и экспортировал сертификат на телефон.


Проблемы:
- Необходим доступ к данному оборудованию.
- Требуется отделить трафик нужного приложения от остального.
- В случае шифрования (а в 2018 шифруется уже всё) — необходимо знание ключа.
Я задумал пройти более классическим путём и настроить прокси на телефоне с прокси сервером на собственном ноутбуке. Чтобы нивелировать проблему шифрования, решил импортировать свой сертификат, а для отделения трафика приложения не стал запускать другие приложения.
В качестве программы прокси-сервера выбрал burp suite. Настроил прокси-сервер и экспортировал сертификат на телефон.


После настройки специализированного программного обеспечения мне удалось увидеть, какие запросы приложение отправляет на сервер:

Как видно, ��анные не отправляются (в столбце IP указано значение «unknown host»), более того, их нельзя проанализировать из-за наличия дополнительного шифрования на уровне самого приложения. Судя по ошибке, телефон не мог определить IP-адрес сервера и его поддоменов https://*.sky-sync.pw по его доменному имени.
Это могло означать только следующие варианты:
- Доменное имя перестало существовать
- Его заблокировал сам владелец.
- Его заблокировал регистратор по жалобе.
- Проблема с DNS-сервером
- DNS-сервер не знает адреса, так как разработчик в production выкатил локальный DNS-адрес.
- DNS-сервер специально блокировал данный запрос, что не удивительно в эпоху интернет-цензуры.
Чтобы проверить предположение о проблеме с DNS-сервером, я попробовал сделать запросы с разных крупных DNS-серверов: Google, Yandex, OpenDNS (обычно цензуре подвергаются местные DNS):

Здесь видно, что ни один из них ничего не знает о таком имени. Дальше я посмотрел whois-сведения о регистрации домена:

Любопытно: домен зарегистрирован, то есть, скорее всего, он не локальный, но поскольку домен не резолвится, возможно, он был заблокирован регистратором по жалобе (abuse). Но за что? Что плохого он сделал?
Чтобы выяснить, что же это всё-таки за приложение и как мне забрать свои деньги, я решил воспользоваться
Погружение в пучину
Если вы гуманитарий и дочитали до этого места, то это уже хорошо — за освоение последующего вы достойны награды
Инструментарий
Для выяснения, что же находится «под капотом» у приложения, нам понадобится скачать специализированные инструменты. Можно загрузить их по отдельности:
- Распаковщик apk-контейнера
- Классика — ApkTool.
- Можно распаковать обычным архиватором, но тогда все бинарые ресуры, включая приложения и файл Manifest, будут нечитабельны.
- Декомпилятор smali-кода
- Стандартом является Dex2Jar, но учите, что данная программа часто криво отрабатывает.
- Нужно подойти к делу очень внимательно, потому что декомпилятор декомпилятору рознь, рассмотрим это позднее.
- Программа для просмотра декомпилированного кода, я бы рекомендовал jd-gui
Либо можно воспользоваться продуктом, обычно платным, где есть всё сразу. Я предпочитаю JebDecompiler: ему можно просто подать на вход apk-приложение, и он сам аккуратно разложит всё по вкладочкам, плюс в нём удобно переключаться между smali и декомпилированным Java-кодом.
Отдельно хочется отметить:
- При наличии нативных либ (папка /libs в структуре приложения) понадобился бы дизассемблер.
- Может понадобиться набор утилит для работ со smali-кодом (dex-to-smali, smali-to-dex).
- Подсветка синтаксиса в Notepad++, чтобы не ломать глаза.
В бой идут одни старики
Обзор
При открытии декомпилированного кода сразу становится понятно, что он обфусцирован.

Как я это понял?
- Человеко-нечитаемые названия классов
isqpwcmx.isfdztgb.adscjobz.nxscomkr.jypbdxnt.utagwpym.wprtdznb.swldgrhm.yrbjpktq.wukovicq; - Недостижимый код
if(0 != 0) {</li> String v1 = "flnwznvh";</li> if(v1.length() != 661 && v1.charAt(0) == 104) {</li> v1.length();</li> } - Шифрование строк
vcgrnfjx.execSQL(nvhdzjfo.xipswfqb(new String[]{"f741f04a4991fc2f0a0029f610bbd1c250dfe115fb7770b892f75d8718b822d273251013991b4407e224fa3f9d4e92f6","378f40211b6e32a5406cd97e85bcf9ad","6378a459b1c20edf", "gexnfwok", "meazfhdp", "bsmotaxn"})
Вряд ли программист настолько рехнулся, чтобы изначально так разрабатывать код. Скорее всего, он использовал один из общедоступных обфускаторов. Этот шаг вполне обычный для усложнения анализа кода с целью защиты интеллектуальной собственности, например, но какой же это «вырвиглаз» для исследователя.
Обратим внимание на основную функцию шифрования:

Сама функция шифрования принимает на вход 3 строки (если больше, то остальные не имеют смысла):
- шифротекст
- ключ
- вектор инициализации для CBC — AES
На эту функцию ссылаются в программе минимум 213 раз:

Отмечу, что она является важным ключом для нормального анализа кода. Дальше
- Восстановить логику работы функции, собрать все вызовы в статическом анализе, дешифровать строки. Может быть, сложно и долго, зато даст 100%-ый результат.
- Внести изменения в smali-код приложения, скомпилировать заново, запустить приложение и ловить в логах дешифрованные строки. Делается легко, но как поведёт себя приложение в конкретном запуске — неизвестно, и можно не увидеть всей картины в целом (не получить вызовы всех функций). К тому же, возможны проблемы с самопроверкой приложением сертификата и (или) целостности.
- Если восстановить логику работы функции сложно, то можно собрать все вызовы функций и дергать самим эти функции с нужными параметрами прямо в динамике (с помощью, например, ПО Frida.
Мы выберем способ №1 как самый
Деобфускация
Сразу оговорюсь, деобфускация — это часто долгий и нудный процесс, поэтому нужно правильно оценить свои временные рамки. Для нашего анализа достаточно расшифровать все строки хоть каким-нибудь способом и сделать это за минимальное время, пусть даже костыльным способом, чем месяц возиться ради смутного идеального варианта.
Качественная деобфускация имеет смысл в случае полной обратной разработки, например, это приходится делать ворам интеллектуальной собственности, когда они пытаются скопировать решение конкурента, либо, если вам часто приходится анализировать программы, обработанные одним обфускатором, но это не наш случай.
Исходный код после декомпилятора JEB Decompiler v.1.4
Cпойлер
public static String podxiwkt(String[] args) { int v6; int v4; byte[] v2; Cipher v1; String v10 = args[0]; String v7 = args[1]; String v0 = args[2]; if(v10 == null) { goto label_9; } if(v10.length() != 0) { goto label_11; } goto label_9; label_11: IvParameterSpec v5 = new IvParameterSpec(v0.getBytes()); try { v1 = Cipher.getInstance("AES/CBC/NoPadding"); goto label_15; } catch(NoSuchPaddingException v3) { } catch(NoSuchAlgorithmException v3_1) { } String v11 = ""; goto label_10; label_15: SecretKeySpec v9 = new SecretKeySpec(v7.getBytes(), "AES"); int v11_1 = 2; try { v1.init(v11_1, ((Key)v9), ((AlgorithmParameterSpec)v5)); v2 = Base64.decode(v1.doFinal(bwdoclkr.xkvasepi(v10)), 0); if(v2.length <= 0) { goto label_48; } v4 = 0; v6 = v2.length - 1; label_29: if(v6 < 0) { goto label_38; } if(v2[v6] != 0) { goto label_33; } } catch(Exception v3_2) { goto label_51; } ++v4; label_33: --v6; goto label_29; label_38: if(v4 <= 0) { goto label_48; } try { byte[] v8 = new byte[v2.length - v4]; System.arraycopy(v2, 0, v8, 0, v2.length - v4); v2 = v8; } catch(Exception v3_2) { label_51: v11 = ""; goto label_10; } label_48: v11 = new String(v2); goto label_10; label_9: v11 = ""; label_10: return v11; } }
Примечание о декомпиляторах
Кстати, dex2jar часто даёт сбои. Так, на рисунке ниже видно, что dex2jar версии 2.0 не смог справиться и просто выдал smali-код.

Его свежая версия, собранная из исходников, выдала декомпилированный код данной функции, но не смогла декомпилировать многие другие (вот так фокусы).


Итог: внимательно отнеситесь к выбору декомпилятора — это сэкономит вам кучу времени и будет проще, чем анализ smali-кода.

Его свежая версия, собранная из исходников, выдала декомпилированный код данной функции, но не смогла декомпилировать многие другие (вот так фокусы).


Итог: внимательно отнеситесь к выбору декомпилятора — это сэкономит вам кучу времени и будет проще, чем анализ smali-кода.
Итак, если мы сейчас просто вставим данный код в IDE, то он не отработает из-за ошибок.
Важно запомнить: Декомпилятор не обязан выдавать валидный код, написанный разработчиком. Он всего лишь палочка-выручалочка при анализе и делает предположения, как код мог быть написан. В большинстве случаев после оптимизации компилятором задача восстановления оригинального кода и вовсе перестает быть тривиальной.
Пример плохой декомпиляции:
if(v10 == null) { goto label_9; } if(v10.length() != 0) { goto label_11; } goto label_9; … label_9: v11 = ""; return v11;
Видим, что получилось плохо и неработоспособно. Перепишем:
if ((v10 == null) || (v10.length() == 0)) { return ""; }
Теперь стало намного понятнее, здесь обычная проверка входных данных. В данном случае нам нужно:
- Заменить все «goto» другими конструкциями языка, т.к. «goto» уже давно невалидный оператор.
- Заменить вызовы библиотек Android на вызовы библиотек Java (если мы пытаемся выполнить код в Java IDE).
- Вставить зависимые классы, на которые ссылается наш код.
- Подумать самим, что не так.
В итоге получаем:
package com.company; //package isqpwcmx.isfdztgb.adscjobz.nxscomkr.jypbdxnt.utagwpym.wprtdznb.swldgrhm.yrbjpktq; import java.util.Base64; //import android.util.Base64; //import bnxvhlyg.nkhoirul.zfxogwqi.mdpqejcw.srnepbly.pcbvwxrs.vixdqclm.wnuqvrhp.bnvceayd.bwdoclkr; //вставим свой класс import java.security.Key; import java.security.NoSuchAlgorithmException; import java.security.spec.AlgorithmParameterSpec; import javax.crypto.Cipher; import javax.crypto.NoSuchPaddingException; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; public abstract class Main { public Main() { super(); } //hex to ascii public static byte[] xkvasepi(String str) { byte[] v0 = null; if(str != null && str.length() >= 2) { int v2 = str.length() / 2; v0 = new byte[v2]; int v1; for(v1 = 0; v1 < v2; ++v1) { v0[v1] = ((byte)Integer.parseInt(str.substring(v1 * 2, v1 * 2 + 2), 16)); } } return v0; } public static String podxiwkt(String[] args) { int v6; int v4; byte[] v2; Cipher v1; String v10 = args[0]; //text String v7 = args[1]; //key String v0 = args[2]; //IV //check if ((v10 == null) || (v10.length() == 0)) { return ""; } IvParameterSpec v5 = new IvParameterSpec(v0.getBytes()); try { v1 = Cipher.getInstance("AES/CBC/NoPadding"); } catch(NoSuchPaddingException v3) { return ""; } catch(NoSuchAlgorithmException v3_1) { return ""; } SecretKeySpec v9 = new SecretKeySpec(v7.getBytes(), "AES"); int v11_1 = 2; try { v1.init(v11_1, ((Key)v9), ((AlgorithmParameterSpec)v5)); //v2 = Base64.decode(v1.doFinal(bwdoclkr.xkvasepi(v10)), 0); v2=v1.doFinal(xkvasepi(v10)); //check if(v2.length <= 0) { return new String(v2); } } catch(Exception v3_2) { return ""; } v4=0; for (v6=v2.length-1;v6>=0;v6--){ if (v2[v6]==0) ++v4; } if(v4 > 0) { try { byte[] v8 = new byte[v2.length - v4]; System.arraycopy(v2, 0, v8, 0, v2.length - v4); v2 = v8; } catch (Exception v3_2) { return ""; } } v2 = Base64.getDecoder().decode(v2); return new String(v2); } public static void main(String[] args) { // write your code here System.out.println(podxiwkt(new String[] { "b1acd584a6eae4ca6321b1f7cdf9ba9617112b4fb39e76c8def876346e3032fbd32b2d188a09715f27124c1bf9facfdc", "637904cd08aeb2d3f6a21b5c7e84f519", "8f4c796d5a3120eb", "zcmwgvdn", "mkngbsyr", "rwcdaieu" })); } }
Данный код успешно отрабатывает. После того, как становится понятна его работа, его можно упрощать и упрощать, приводя к предполагаемому изначально лаконичному виду, написанному программистом (если это, конечно, у него руки не были кривыми изначально).
Примечание
Кстати, в данном случае дешифрование строк можно продемонстрировать с помощью связки онлайн-ресурсов. Пример вызова шифрованной строки внутри программы:

Здесь вектор инициализации нужно сначала перевести в Hex-формат:

Подставляем все значения:

И в конце декодируем из base64:

В результате получаем обычную строку, и вызов приобретает осмысленный вид.

Здесь вектор инициализации нужно сначала перевести в Hex-формат:

Подставляем все значения:

И в конце декодируем из base64:

В результате получаем обычную строку, и вызов приобретает осмысленный вид.
Таким образом, нужно пройтись по всему коду и собрать все шифрованные строки, теперь мы можем самостоятельно их дешифровать. Важный момент здесь в том, что на этапе «модификации и комментирования кода» мы можем работать как на уровне smali, так и на уровне Java (декомпилированный smali).
| Плюсы модификации | Минусы модификации | |
| Smali | Можно внести изменения, заново собрать в dex и декомпилировать с новыми строками | Не всегда просто работать по smali-коду. При неправильном изменении приложение не соберется |
| Java | Чаще гораздо проще извлекать данные из высокоуровневых операций | Редактировать в большинстве Java-просмотрщиках кода нельзя. |
Ещё один пример строки
vcgrnfjx.execSQL(nvhdzjfo.xipswfqb(new String[]{"f741f04a4991fc2f0a0029f610bbd1c250dfe115fb7770b892f75d8718b822d273251013991b4407e224fa3f9d4e92f6","378f40211b6e32a5406cd97e85bcf9ad","6378a459b1c20edf", "gexnfwok", "meazfhdp", "bsmotaxn"})
Отсюда очень легко забрать параметры с помощью регулярного выражения, чем написать регулярку на следующий код:
Smali-код, пример 1
00000280 new-instance v13, Ljava/lang/StringBuilder; 00000284 invoke-direct {v13}, Ljava/lang/StringBuilder;-><init>()V 0000028A const/4 v14, 0x6 0000028C new-array v14, v14, [Ljava/lang/String; 00000290 const/4 v15, 0x0 00000292 const-string v16, "f741f04a4991fc2f0a0029f610bbd1c250dfe115fb7770b892f75d8718b822d273251013991b4407e224fa3f9d4e92f6" 00000296 aput-object v16, v14, v15 0000029A const/4 v15, 0x1 0000029C const-string v16, "378f40211b6e32a5406cd97e85bcf9ad" 000002A0 aput-object v16, v14, v15 000002A4 const/4 v15, 0x2 000002A6 const-string v16, "6378a459b1c20edf" 000002AA aput-object v16, v14, v15 000002AE const/4 v15, 0x3 000002B0 const-string v16, "gexnfwok" 000002B4 aput-object v16, v14, v15 000002B8 const/4 v15, 0x4 000002BA const-string v16, "meazfhdp" 000002BE aput-object v16, v14, v15 000002C2 const/4 v15, 0x5 000002C4 const-string v16, "bsmotaxn" 000002C8 aput-object v16, v14, v15
Smali-код, пример 2
0000008E new-array v0, v0, [Ljava/lang/String; 00000092 move-object/from16 v89, v0 00000096 const/16 v90, 0x0 0000009A const-string v91, "4500b5e2e2ad26b7545eb54d70ab360ae28c9d031e2afcc3f6a2b2ac488ea440" 0000009E aput-object v91, v89, v90 000000A2 const/16 v90, 0x1 000000A6 const-string v91, "da96f678922d4b07350b3a184ecc1f5e" 000000AA aput-object v91, v89, v90 000000AE const/16 v90, 0x2 000000B2 const-string v91, "0cf69e3d2745a1b8" 000000B6 aput-object v91, v89, v90 000000BA const/16 v90, 0x3 000000BE const-string v91, "jhiqsaoe" 000000C2 aput-object v91, v89, v90 000000C6 const/16 v90, 0x4 000000CA const-string v91, "khbqxurn" 000000CE aput-object v91, v89, v90
Smali-код, пример 3
00000D3E new-array v0, v0, [Ljava/lang/String; 00000D42 move-object/16 v298, v0 00000D48 const/4 v0, 0x0 00000D4A move/16 v299, v0 00000D50 const-string v0, "b286945744e085f4d5c19916fd261481" 00000D54 move-object/16 v300, v0 00000D5A move-object/from16 v0, v300 00000D5E move-object/from16 v1, v298 00000D62 move/from16 v2, v299 00000D66 aput-object v0, v1, v2 00000D6A const/4 v0, 0x1 00000D6C move/16 v299, v0 00000D72 const-string v0, "df6883742b2911ac5ac7b4dee065390f" 00000D76 move-object/16 v300, v0 00000D7C move-object/from16 v0, v300 00000D80 move-object/from16 v1, v298 00000D84 move/from16 v2, v299 00000D88 aput-object v0, v1, v2 00000D8C const/4 v0, 0x2 00000D8E move/16 v299, v0 00000D94 const-string v0, "90a463ce2df17b58" 00000D98 move-object/16 v300, v0 00000D9E move-object/from16 v0, v300 00000DA2 move-object/from16 v1, v298 00000DA6 move/from16 v2, v299 00000DAA aput-object v0, v1, v2 00000DAE const/4 v0, 0x3 00000DB0 move/16 v299, v0 00000DB6 const-string v0, "cupyzsgq" 00000DBA move-object/16 v300, v0 00000DC0 move-object/from16 v0, v300 00000DC4 move-object/from16 v1, v298 00000DC8 move/from16 v2, v299 00000DCC aput-object v0, v1, v2
Как мы видим, внутренние переменные меняются, последовательность команд различается, количество аргументов тоже разнится, вдобавок в программе функция дешифрования вызывается не напрямую, а через функции прослойки. Попробуйте написать правило самостоятельно для поиска данной конструкции, не допустить ошибок по захвату строк из других функций и всё это сделать быстро (
План-капкан:
- Извлечём все значения из декомпилированного кода.
- Дешифруем.
- Заменим шифротекст на открытый в smali-коде. Подставим, например, взамен первого оператора. (Профессиональнее будет вырезать весь вызов функции и оставить возвращаемую дешифрованную строку, но тогда опять же есть большой риск сломать программу).
- Соберём smali-код в dex-файл.
- Дальше будем удобно смотреть в анализаторе кода, с чего и начали.
Если собрать весь декомпилированный код в один файл, то получится около 20000 строк, что для ручного анализа требует много времени, которое стоит явно больше, чем выставленные мной на продажу сапоги. Для начала соберём регулярным выражением все строки.

Видим 593 совпадения, плюс с десяток, которые не попали под данное правило,

Сортируем, отсеиваем, итого 422 уникальные строки:

Пропускаем через функцию дешифрования, восстановленную нами ранее. Результат:

Заменим шифротекст на открытый в smali-коде при помощи Python:
import os words_replace=dict() words_replace["0018aacad3d146266317d8d8c51785fd"]="imei" words_replace["016d15e4d0a72667c61428e736a6f3b8"]="WakeLock" words_replace["032c534efb6c9990cd845a08c5a08b95"]="inbox" #… и т.д. #Открыть smali-файл #Найти все вхождения массива и заменить def change(path): print("file="+path) file_handle = open(path, 'r') context_full = file_handle.read() file_handle.close() for i in words_replace: context_full=context_full.replace(i, words_replace[i]) #print (i+""+words_replace[i]) file_handle = open(path, 'w') context_full = file_handle.write(context_full) file_handle.close() #Пройтись по всем подпапкам и открыть smali-файлы for top, dirs, files in os.walk('C:\\work\\test'): for nm in files: path=os.path.join(top, nm) print (path) change(path)
Cобираем smali-файлы в dex:

Теперь это можно хоть как-то анализировать (читая из всей конструкции первый аргумент):

Анализ
Итак, имеем 20 000 более-менее читабельных строк кода, задачи делать полный разбор перед нами не стоит. Необходимо понять функционал в целом. Здесь, по сути, требуется только навык чтения исходного кода на Java. Ходить по коду, смотреть на перекрёстные ссылки, переименовывать переменные и функции.
Как же лучше анализировать Android-приложение, особенно большое?
Вариант 1: Можно двигаться от файла Manifest
Например, последовательно от LAUNCHER попытаться раскрутить всю цепочку вызовов. Кстати, не забывайте, что ещё есть «Receiver» и «Service», которые могут изменить линейное выполнение программы.

Полный файл Manifest
<?xml version="1.0" encoding="utf-8" standalone="no"?><manifest xmlns:android="http://schemas.android.com/apk/res/android" android:installLocation="internalOnly" package="xfmpuwon.mtnbupnc.ihqdgjal.ndgmqawx.bjunzerq.cznfpnoq.fzevcuym.jmpdiqft"> <uses-permission android:name="xfmpuwon.mtnbupnc.ihqdgjal.ndgmqawx.bjunzerq.cznfpnoq.fzevcuym.jmpdiqft.permission.C2D_MESSAGE"/> <uses-permission android:name="com.google.android.c2dm.permission.RECEIVE"/> <uses-permission android:name="android.permission.SEND_SMS"/> <uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.READ_PHONE_STATE"/> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> <uses-permission android:name="android.permission.WAKE_LOCK"/> <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/> <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/> <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/> <uses-permission android:name="android.permission.RECEIVE_SMS"/> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/> <uses-permission android:name="android.permission.QUICKBOOT_POWERON"/> <uses-permission android:name="android.permission.READ_SMS"/> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/> <uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/> <permission android:name="xfmpuwon.mtnbupnc.ihqdgjal.ndgmqawx.bjunzerq.cznfpnoq.fzevcuym.jmpdiqft.permission.C2D_MESSAGE" android:protectionLevel="signature"/> <application android:allowBackup="true" android:icon="@drawable/icon" android:label="@string/tgiwmpqy" android:noHistory="true"> <activity android:configChanges="orientation" android:excludeFromRecents="true" android:label="@string/tgiwmpqy" android:launchMode="singleTop" android:name="zemquyog.csrtmnak.xrkfygen.wkahrnjd.acnfunjh.rgipxbuf.lruiwxeg.blqndche.dcjihbou" android:screenOrientation="portrait"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity> <activity android:configChanges="orientation" android:launchMode="singleTop" android:name="xbfrscou.hxrvwnoi.djvpcqri.enlnrfio.aoegxbiu.heywzmnb.znfnxcht.nazcxobq" android:screenOrientation="portrait"/> <activity android:configChanges="orientation" android:launchMode="singleTop" android:name="hcfkagds.timkagsd.oetvghzr.fcioynvl.psynofdj.slcghdjz.tapnwsdk.gzvwnban.htenafdb.qwebhzgy" android:noHistory="true" android:screenOrientation="portrait"/> <activity android:configChanges="orientation" android:excludeFromRecents="true" android:launchMode="singleTop" android:name="njfbwmre.voefarqx.ftuxvngl.wrmshxqj.zdenywgn.eiwyunlg.jysgkbam.yrijthab.vstqxpuo.iplamgxf" android:priority="2147483647" android:screenOrientation="portrait"/> <receiver android:name="gfbaznoc.asyoqtnm.kbetoqca.mqysobzu.gqwfibrv.dorxijuk.wgzkmiep.ywnnurzv.csfpqhrn" android:permission="android.permission.BIND_DEVICE_ADMIN"> <meta-data android:name="@string/pkzrlscm" android:resource="@xml/ynqukvnb"/> <intent-filter android:priority="2147483646"> <action android:name="android.app.action.DEVICE_ADMIN_ENABLED"/> </intent-filter> </receiver> <receiver android:name="ykwbodxc.gymjhibn.kgmdfqor.hbasvmfz.yegkmaif.ortzknvm.quplincn.cuxytvhs.fqonzuts.cyuoxgqi.znumwyct" android:permission="com.google.android.c2dm.permission.SEND"> <intent-filter> <action android:name="com.google.android.c2dm.intent.RECEIVE"/> <action android:name="com.google.android.c2dm.intent.REGISTRATION"/> <action android:name="com.google.android.c2dm.intent.UNREGISTRATION"/> <category android:name="xfmpuwon.mtnbupnc.ihqdgjal.ndgmqawx.bjunzerq.cznfpnoq.fzevcuym.jmpdiqft"/> </intent-filter> </receiver> <receiver android:enabled="true" android:exported="true" android:name="kqwihjot.nvkqjloc.grjnyknm.owydvckh.mugknwdx.enhcyvja.mhvbpcue.ztbwjhfo"> <intent-filter android:priority="2147483646"> <action android:name="android.intent.action.LOCKED_BOOT_COMPLETED"/> <action android:name="com.htc.intent.action.QUICKBOOT_POWERON"/> <action android:name="android.intent.action.QUICKBOOT_POWERON"/> <action android:name="android.intent.action.BOOT_COMPLETED"/> <action android:name="android.intent.action.USER_PRESENT"/> <action android:name="android.intent.action.BATTERY_OKAY"/> <action android:name="android.intent.action.BATTERY_LOW"/> <action android:name="android.intent.action.ACTION_POWER_CONNECTED"/> <action android:name="android.intent.action.ACTION_POWER_DISCONNECTED"/> <action android:name="android.intent.action.APP_ERROR"/> <action android:name="android.intent.action.HEADSET_PLUG"/> <action android:name="android.intent.action.PHONE_STATE"/> <action android:name="android.intent.action.NEW_OUTGOING_CALL"/> <action android:name="android.provider.Telephony.SMS_RECEIVED"/> <action android:name="android.intent.action.TIME_TICK"/> <action android:name="android.intent.action.SCREEN_ON"/> <action android:name="android.intent.action.SCREEN_OFF"/> <action android:name="android.net.conn.CONNECTIVITY_CHANGE"/> <action android:name="android.net.wifi.WIFI_STATE_CHANGED"/> <action android:name="android.intent.action.DREAMING_STOPPED"/> <category android:name="android.intent.category.HOME"/> </intent-filter> </receiver> <receiver android:name="btnsxnuz.wmjizbky.lynvjxqz.zinomjuv.yizlgcnf.qwoikgnc.wnrskjea.wfqgmeny.lcgvqrms.ocwkgblp"> <intent-filter android:priority="2147483646"> <action android:name="android.provider.Telephony.SMS_RECEIVED"/> </intent-filter> </receiver> <service android:name="ltvsrezg.ehxndrat.twnnyxrf.nqynefws.dhbalcnr.ynjkuxod.nhoxmsbq.nackoyhn.voycgfek.znhwkqba.taxvnfyn"/> <service android:name="rbnakfzo.qsreiubk.pwvlnngs.twoxnhfv.mftarcnd.pfioxcub.xjlaftqr.nxrqvlwh"/> <service android:enabled="true" android:name="xfmpuwon.mtnbupnc.ihqdgjal.ndgmqawx.bjunzerq.cznfpnoq.fzevcuym.jmpdiqft.ugshpjvo"/> </application> </manifest>
Вариант 2: Можно двигаться от интересных строк

Часть дешифрованных строк
system_update.apk (Общее) (Перехват) , error = , unregistered = , в .permission.C2D_MESSAGE //sky-sync.pw/ //sms/inbox /system_update.apk ALLCONTACTS ALLMSG AUTHENTICATION_FAILED Acquiring wakelock Application BLOCKER_BANKING_START BLOCKER_EXTORTIONIST_START BLOCKER_STOP BLOCKER_UPDATE_START Banking CHANGE_GCM_ID CONTACTS CONTACTS_PRO CREATE TABLE IF NOT EXISTS END Error|No process list|No access Extortionist Foreground GCM returned invalid number of GCMBaseIntentService GCMBroadcastReceiver GCMIntentService- GCMRegistrar GCM_LIB GET MESSAGE Mobile Network NEWMSG Not retrying failed operation ONLINE PAGE POST Process finished with exit code 0 RESTART Received deleted messages Registering receiver Releasing wakelock SERVICE_NOT_AVAILABLE SSL START STOP Saving regId on app version Scheduling registration retry, backoff = Setting registeredOnServer status as Stop System UNBLOCK UPDATE UPDATE_PATTERNS URL UTF-8 Update WakeLock Wakelock reference is null Wi-Fi WiMax _success add_msg_ok address android.intent.action.QUICKBOOT_POWERON answer_text answer_to api_url app appVersion application application/vnd.android.package-archive apps_list ask backoff_ms blocker blocker_banking blocker_banking_autolock blocker_banking_forced_access blocker_banking_success blocker_extortionist blocker_extortionist_autolock blocker_extortionist_forced_access blocker_extortionist_success blocker_update blocker_update_forced_access blocker_update_success body build callback cardSuccess check com.android.settings com.google.android.c2dm.intent.RECEIVE com.google.android.c2dm.intent.REGISTER com.google.android.c2dm.intent.REGISTRATION com.google.android.c2dm.intent.UNREGISTER com.google.android.gcm com.google.android.gcm.intent.RETRY com.google.android.gsf com.htc.intent.action.QUICKBOOT_POWERON command command_receive contactslist country data date delete deleted_messages device_block disableDataConnectivity enableDataConnectivity error failure file deleted. first_start force-locked gafzpjxb.cix gcm gcm_id gcm_register gcm_register_ok getITelephony get_message_list id integer primary key autoincrement, id=? imei immunity inbox init_bootable init_imei is_admin is_awake_display is_imunnity is_locked is_network_type is_top_activity job job_date job_id komgejif.hqr locked message message_delivered message_type method model msg msg_id msglist name not nypjtinq.nvp ok onServer onServerExpirationTime onServerLifeSpan operator org.android.sys.admin.disabled org.android.sys.admin.enabled org.android.sys.admin.request org.android.sys.command.receive org.android.sys.launch.first org.android.sys.sms.pro.sent org.android.sys.sms.push org.android.sys.sms.sent outbox page params pattern patterns personal phone phone_list privet process_list protocol qwertyuiopasdfghjklzxcvbnm receive regId regex register register_ok registrationId = registration_id repeat resetting backoff for ru save_contacts_list save_message_history sender sent sent_status sid ss status stop_blocker text text, text/html time token total_deleted type unknown unregistered until url useragent utf-8 value version xpls yes Идет инициализация приложения! Не удалось установить приложение Подождите... Приложение вылетело в Приложение лишилось прав администратора в Приложение получило права администратора в Приложение проверено и является полностью безопасным! Выполнить запуск? Приложение пытаются лишить прав администратора в Устройство перезапущено в отказ безопасности в перезапускаем его является системным!
Вариант 3: Можно двигаться от интересных ресурсов (assets, libs)
В данном случае 3 вариант оказался предпочтительным. В папке /assets (apk-контейнера) лежат три интересные html-файла. Вот их вид в браузере:


Выглядит сомнительно для официальной программы Avito по передаче платежей, вам так не кажется? Отследим, что происходит при нажатии клавиши отправки банковских данных на странице с логотипом Сбербанка. JavaScript’ом вызывается функция
sendCardData():
И дальше передается в Java-код через вызов
ok.performClick():
В Java-коде выполняется обработка:

Далее всё это шифруется в классе
mcrypt:
Внутри функции происходит шифрование данных аналогично рассмотренному ранее:

Но для всего остального ключи жёстко вшиты:

Пробуем расшифровать через online-ресурс:

И преобразовать из base64. Успех! Мы можем дешифровать все данные приложения: проверено на трафике, захваченном ранее.
Приложение сообщает на сервер обо всех событиях
{ "sid":15, "imei":"861117030537111", "phone":"System", "message":"Приложение получило права администратора в 22.10.2018 23:30:47", "time":"1540240247", "msg_id":1, "status":"unknown", "type":"inbox", "method":"message" }
А также периодически передает все запущенные приложения
{ "sid": 15, "imei": "861117030537111", "country": "ru", "operator": "MTS RUS", "phone": "", "model": "Xiaomi Redmi 3X", "version": "6.0.1", "application": "ПРК", "build": "30.0.2", "process_list": [ "Background|com.android.bluetooth|com.android.bluetooth.hid.HidService", "Background|com.android.settings:remote|com.android.settings.wifi.MiuiWifiService", "Background|com.android.phone|org.codeaurora.ims.ImsService", "Background|system|com.qualcomm.location.LocationService", ..., "Background|xfmpuwon.mtnbupnc.ihqdgjal.ndgmqawx.bjunzerq.cznfpnoq.fzevcuym.jmpdiqft|ltvsrezg.ehxndrat.twnnyxrf.nqynefws.dhbalcnr.ynjkuxod.nhoxmsbq.nackoyhn.voycgfek.znhwkqba.taxvnfyn" ], "apps_list": [ "com.introspy.config", "com.google.android.youtube", "com.google.android.googlequicksearchbox", "org.telegram.messenger", ..., "com.google.android.inputmethod.latin", "jakhar.aseem.diva" ], "method": "register" }
Если бы мне в динамике выпало окно ввода банковских данных, то и данные были бы в трафике. Таким образом, можно сделать вывод, что это «фишинговое» приложение.
Те, кто были внимательны, заметили, что в файле Manifest довольно много разрешений, и приложение обладает более богатым функционалом. Глубокий разбор функционала мы проведём в другой статье. А пока успехов!
Выводы
Я разочарован тем, что не продал сапоги. А выводы такие:
Не продавайте сапоги на Avito- Не переходите по непонятным ссылкам (даже если от друзей и даже если «нужно срочно занять 100 рублей — вопрос жизни и смерти»)
- Не скачивайте приложения, кроме как с Google Play или AppStore
- Отключите установку из «недостоверных источников», если реально не разбираетесь, что к чему.
- Не отключайте «Play Защиту».
- Помните, что и в Google Play может быть вредоносное ПО
- Установите антивирус на телефон (реально работает).
- Если вы разработчик, не обфусцируйте код, дайте людям убедиться в ваших добрых намерениях (шутка)
- Если вы исследователь, не работайте за еду анализируйте приложения в свободное время и публикуйте отчеты. Вместе мы сделаем мир лучше.
P.S. Я постарался написать статью слегка в юмористическом формате и подать максимально просто, потому что даже мне не захотелось бы, наверное, читать в пятницу серьезный лонгрид с названием «Реверс-инжиниринг обфусцированного вредоносного приложения на ОС Android».
