Не удалось прогрузить медиа
Всех приветствую. Сегодня разбираем Android-семпл, который на первый взгляд выглядит как очередной APK, завёрнутый в тот самый Golden Crypt: битый манифест, странные ZIP-заголовки, asset с нечитаемым путём, поверх всего немного библиотечного шума. Да-да, опять он. В прошлых Android-разборах уже всплывал этот сраный Golden Crypt, который щёлкается как орешки, но стандартным тулзам сначала делает больно. Но если не залипать в оболочку и не делать культ из его RC4-подобного слоя, внутри оказывается вполне взрослый Android RAT/банк-бот под названием OctagonPanel. Сам APK сразу показывает нам свои зубы, но это всё промыслы вышеописанного дерьма, все тулзы ломаются.
Не удалось прогрузить медиа
ZGdSEl.jar и подсовывает его в ClassLoader.Не удалось прогрузить медиа
Авторские классы не обфусцированы, и это сразу даёт нам минимальное понимание всей архитектуры:
Code:
MainActivity
OctagonConfig
accessibility/WardAccessibilityService
service/WardForegroundService
service/GuardService
receiver/SmsReceiver
receiver/WapPushReceiver
bg/BootReceiver
bg/ServiceWatchdog
bg/WatchdogWorker
capture/LockscreenPasswordCapture
capture/MediaProjectionScreenCapture
capture/MediaProjectionRequestActivity
overlay/GateOverlayActivity
overlay/PushAlertActivity
overlay/ScreenOverlayController
net/WardClient
protocol/FrameCodec
protocol/OctagonCipher
protocol/msg/*
sync/FakeAccountAuthenticator
sync/FakeContentProvider
sync/FakeSyncAdapter
util/PermissionChain
Названия настолько говорящие, что тут даже гадать особо не надо. Ward - это клиент на устройстве, Guardian - операторская сторона. Отсюда все сообщения вида
WardHello, GuardianAction, WardScreenshot, GuardianRequestSmsHistory.Не удалось прогрузить медиа
server_host и server_port - куда бот стучится как клиент, то есть C2 endpoint. А passphrase - общий сЕкРеТ для шифрования. Из него код делает SHA-256(passphrase), получает AES-ключ и дальше шифрует/расшифровывает все фреймы через AES/GCM/NoPadding. Серверная сторона должна знать ту же passphrase, иначе она просто не сможет прочитать WardHello от бота и не сможет отправить ему валидные GuardianAction/GuardianRequestSmsHistory/прочие команды. То есть практический смысл такой: IP:порт говорят боту "куда идти", passphrase говорит "как разговаривать так, чтобы тебя поняла именно эта панель".Не удалось прогрузить медиа
И вот тут первая смешная практическая деталь. В конфиге всего один С2, это почему-то главная боль большинства ботов на дроид. В коде есть поддержка fallback-адресов, но они не лежат в дефолтном
config.json. Их должен прислать уже живой сервер командой GUARDIAN_SET_FALLBACKS, после чего бот сохранит их локально в SQLite. А теперь вопрос: что будет, если этот C2 умер до первого успешного коннекта? А что если оператор не выслал и не контролировал пришла абуза на сервер или нет? Правильно, ничего хорошего. Бот будет стучаться в мёртвую дверь, делать reconnect, спать, снова стучаться.В DEX те же значения продублированы как fallback.
Code:
server_host: 209.99.184.50
server_port: 4444
passphrase: octagon-default-key-change-me
buildId: DevLRT
version: 0.2.0
Код
OctagonConfig.applyDefaultsIfFirstRun() на первом запуске читает R.raw.config и складывает значения в SharedPreferences с именем octagon. Если ресурса нет или он не читается, берутся DEX defaults. То есть конфиг не спрятан, не тянется с какого-нибудь хитрого CDN, не лежит в native blob.Не удалось прогрузить медиа
com.kisa.octagonpanel.net.WardClient. Никаких Retrofit/OkHttp для C2, никаких WebSocket, самый обычный java.net.Socket.- connect timeout - 5000 ms в коде клиента
setTcpNoDelay(true)setKeepAlive(true)setSoTimeout(12000)- отдельный writer thread
WardClient-Writer - heartbeat каждые 5 секунд
- если сервер молчит больше 15 секунд - сокет закрывается
- reconnect; ротация fallback-адресов появляется только если сервер успел их прислать
Причём эти цифры не берутся из
config.json. В WardClient они зашиты как константы: HEARTBEAT_MS = 5000, RECONNECT_DELAY_MS = 3000, SERVER_SILENCE_LIMIT_MS = 15000, connect timeout в connectAndServe() - 5000 ms. А в JSON при этом лежит connect_timeout_ms: 7000. Это не критично, но хороший маркер качества сборки, конфиг уже начал превращаться в декорацию.При подключении клиент отправляет
WardHello:
Code:
protocolVersion
wardId
displayName // Build.MODEL
clientVersion // 0.2.0
buildId // DevLRT
wardId генерируется как UUID и сохраняется в локальную SQLite-базу. То есть бот получает стабильный идентификатор на устройстве.Фрейминг простой, 4 байта длины big-endian, затем зашифрованный payload. Максимальный размер фрейма 16 MB. Внутри
FrameCodec сначала сериализует сообщение через BinaryWriter, первый байт, тип сообщения, потом тело.Не удалось прогрузить медиа
Code:
key = SHA-256(passphrase UTF-8)
algorithm = AES/GCM/NoPadding
nonce = 12 random bytes
GCM tag = 128 bit
wire = nonce || ciphertext || tag
Для мобильного бота это нормальное решение. Не TLS, зато трафик не читается без passphrase, а протокол не выглядит как HTTP. С другой стороны, дефолтная passphrase лежит в raw JSON, поэтому если сборка не переупаковывалась под конкретного оператора, секретность примерно уровня "ключ под ковриком". А с доступностью ещё хуже, один статический IP без первичных fallback'ов означает, что бан/abuse/nullroute этого адреса режет ботнет до состояния "ждём чуда". Fallback'и помогают только тем инсталлам, которые уже успели получить их от панели.
Не удалось прогрузить медиа
WardClient.dispatch(). На стороне сервера они называются Guardian:
Code:
GUARDIAN_HELLO
GUARDIAN_PULSE
GUARDIAN_ACTION
GUARDIAN_LAUNCH_APP
GUARDIAN_REQUEST_SCREENSHOT
GUARDIAN_REQUEST_INSTALLED_APPS
GUARDIAN_REQUEST_SMS_HISTORY
GUARDIAN_REQUEST_CONTACTS
GUARDIAN_SET_KNOWN_PACKAGES
GUARDIAN_SET_WATCHERS
GUARDIAN_SET_CLICK_TRIGGERS
GUARDIAN_SET_GATE_LIST
GUARDIAN_SET_GATE_TEMPLATE
GUARDIAN_SET_PACKAGE_TEMPLATES
GUARDIAN_CLEAR_PACKAGE_TEMPLATES
GUARDIAN_SET_NOTICE
GUARDIAN_SET_DIM
GUARDIAN_SHOW_PUSH
GUARDIAN_SET_FALLBACKS
Самая жирная часть -
GuardianAction. Поля там такие:
Code:
actionId
actionKind
targetNodeId
text
x1 y1 x2 y2
durationMs
globalAction
pathX[]
pathY[]
Типы действий:
- click по координатам или accessibility node
- long click
- scroll/swipe
- gesture path
- global action через AccessibilityService
- set text в поле
- clipboard paste fallback
Это удалённое управление UI через Accessibility. Оператор видит дерево экрана, отправляет клик/свайп/ввод текста, бот выполняет, возвращает
WardActionResult с success/message. Приятно знать, что сразу логируются и ошибки и отправляются оператору, ведь иначе ничё не было бы понятно, и сидели бы с мыслью: "а как свайпать бля?".
Code:
Жест по пути не отправлен
Слишком мало точек жеста
GLOBAL не выполнено
Узел не найден
SET_TEXT и clipboard fallback не сработали
Long-click не выполнен
Не удалось прогрузить медиа
Главный класс -
WardAccessibilityService. По ресурсам видно, что сервис запрашивает:
Code:
canRetrieveWindowContent
canPerformGestures
canTakeScreenshot
canRequestFilterKeyEvents
В рантайме он делает несколько вещей одновременно.
1. Отправляет события accessibility
onAccessibilityEvent() собирает:
Code:
eventType
packageName
className
text
contentDescription
timestampMs
И отправляет это как
WardNodeEvent.2. Снимает дерево UI
sendTreeSnapshot() берёт getRootInActiveWindow(), рекурсивно сериализует node tree и шлёт WardTreeSnapshot. В каждом node blob:
Code:
nodeId
className
packageName
text
contentDescription
viewIdResourceName
hintText
tooltipText
boundsLeft/boundsTop/boundsRight/boundsBottom
isClickable
isLongClickable
isCheckable
isChecked
isEditable
isPassword
isScrollable
isVisibleToUser
actionsMask
children[]
Это почти VNC по accessibility. Не пиксели, а структурированная модель экрана: где кнопки, где поля, что написано, какие bounds. Для банковских приложений это часто даже удобнее скриншота.
3. Отслеживает foreground package
При смене окна отправляется
WardForegroundPackage:
Code:
packageName
appLabel
timestampMs
Дальше на это завязаны overlays/gates. Если открыли интересный пакет, можно показать подложную форму или затемнить экран.
4. Снимает скриншоты
Есть два пути. Для Android R+ используется
AccessibilityService.takeScreenshot(). Отдельно есть MediaProjectionScreenCapture через MediaProjection, VirtualDisplay, ImageReader и JPEG quality. Сервер управляет периодом и качеством через GuardianRequestScreenshot:
Code:
enabled
periodMs
jpegQuality
maxLongSide
Ответ
WardScreenshot:
Code:
data
format
widthPx
heightPx
rotationDeg
capturedAtMs
То есть оператор может получать как дерево элементов, так и картинку экрана.
Не удалось прогрузить медиа
Класс
capture/LockscreenPasswordCapture отдельно занимается PIN/password/pattern на локскрине. Он слушает accessibility events, следит за lockscreen packages, собирает ввод, координаты нажатий и пытается восстановить тип:
Code:
password
pwdType
confidence
length
hasPlaceholders
keyTapsX[]
keyTapsY[]
timestampMs
Для pattern lock есть отдельное сообщение:
Code:
nodes[]
cellCoordsX[]
cellCoordsY[]
timestampMs
Локально это ещё и пишется в файлы:
Code:
captured_passwords.json
captured_patterns.json
А затем уходит на сервер как
WardPasswordCapture или WardPatternCapture.Не удалось прогрузить медиа
res/raw/gate_template.html лежит готовая HTML-форма на русском:
Code:
<title>OctagonPanel</title>
Пример
Пример пароль
placeholder="Напишите здесь…"
кнопки: продолжить / помогите / закрыть
JS bridge: OctagonBridge.proceed(), askHelp(), cancel()
GateOverlayActivity подставляет {{APP_NAME}}, показывает WebView-форму поверх нужного приложения и сохраняет ответ как WardGateAnswer:
Code:
answerId
packageName
outcome
comment
kind
askedAtMs
answeredAtMs
wasOffline
Если связи нет, ответы складываются в
pending_answers, а WardClient потом их флашит. Это хороший инженерный штрих: оператор не теряет введённые жертвой данные при временном отвале C2.Кроме gate-форм есть:
ScreenOverlayController- затемнение экрана и notice overlayGuardianSetNotice- title/text/colors/visibleGuardianSetDim- включить/выключить затемнениеGuardianShowPush- фейковый push с title/text/icon/actionPackagePushAlertActivity- activity для показа такого push alert
То есть оператор может не только читать экран, но и активно подсовывать пользователю UI.
Не удалось прогрузить медиа
Здесь OctagonPanel неплох. Есть сразу несколько слоёв.
Foreground service
WardForegroundService стартует с notification text Updating..., держит shared WardClient, вешает status listener, при наличии accessibility service ставит её listener'ом клиента.WakeLock и WifiLock
acquireLocks() берёт PowerManager.WakeLock и WifiManager.WifiLock. Для Huawei/Honor wake lock tag маскируется под:
Code:
LocationManagerService
Для остальных:
Code:
octagon:ward
octagon:wifi
Silent audio keepalive
В
res/raw/silence.mp3 лежит 88 KB audio resource. На самом деле file определяет его как WAV PCM, просто назвали .mp3. Service создаёт MediaPlayer, ставит цикл, volume 0/0 и запускает. Достаточно старый трюк, но до сих пор рабочий.Watchdog
Есть
ServiceWatchdog, WatchdogWorker, WatchdogAlarmReceiver, DozeHeartbeatReceiver, NetworkReviveReceiver, ConnectionReviveReceiver. Watchdog проверяет, жив ли service, и перезапускает через Alarm/WorkManager/foreground service.onTaskRemoved
Если задачу смахнули,
WardForegroundService.onTaskRemoved() снова стартует себя, если клиент был активен.Fake account/sync adapter
В ресурсах есть:
Code:
ward_authenticator.xml
ward_syncadapter.xml
Тип аккаунта:
Code:
com.kisa.octagonpanel.account
Provider:
Code:
com.kisa.octagonpanel.provider
Это ещё один способ вписаться в системные механизмы синхронизации и периодического пробуждения. В сумме это хорошая практичная живучесть: foreground, locks, watchdog, sync adapter, boot/network/doze receivers, battery optimization bypass.
Не удалось прогрузить медиа
Сама малварь уже заслуживает внимания. Он не идеален. Конфиг наружу, имена классов наружу, строки наружу, статический C2 наружу. Причём статический C2 тут особенно забавный: бот может быть технически жирным, но если
209.99.184.50:4444 лёг до выдачи fallback'ов, вся эта красота превращается в набор локальных collectors без оператора. Добавьте сюда protocol_version: 2 в JSON при реальном protocol 3, неиспользуемые таймауты из конфига, полусломанные флаги REQUEST_SMS_PERMISSION/REQUEST_CONTACTS- и становится понятно, что это не до конца коммерческий имплант, а рабочая, но довольно грязная сборка. Но по функционалу он всё равно очень-очень неприятный: если пользователь выдал Accessibility и SMS/default-role, оператор получает почти всё, что нужно для ручного fraud на устройстве. Поки, не болейте!
👍40
🔥11
🤔4
😢8
💯11
LIMB