LIMB
Вход Регистрация

тема

Китикэт, ЮФС, Данон, а может логика молока?..- Kitkat/Octagon APK RAT.

bezmyatezhnost · · 0 ответов · 545 просмотров

Не удалось прогрузить медиа
​

Всех приветствую. Сегодня разбираем Android-семпл, который на первый взгляд выглядит как очередной APK, завёрнутый в тот самый Golden Crypt: битый манифест, странные ZIP-заголовки, asset с нечитаемым путём, поверх всего немного библиотечного шума. Да-да, опять он. В прошлых Android-разборах уже всплывал этот сраный Golden Crypt, который щёлкается как орешки, но стандартным тулзам сначала делает больно. Но если не залипать в оболочку и не делать культ из его RC4-подобного слоя, внутри оказывается вполне взрослый Android RAT/банк-бот под названием OctagonPanel. Сам APK сразу показывает нам свои зубы, но это всё промыслы вышеописанного дерьма, все тулзы ломаются.

Не удалось прогрузить медиа

​
То есть классическая история: стандартные Android-тулзы ломаются не потому что там какая-то космическая защита, а потому что контейнер подпорчен ровно настолько, чтобы нормальные парсеры отказались работать. 7z всё равно вытаскивает файлы, а DEX можно читать напрямую через androguard. Манифест после извлечения - 0 байт, поэтому компоненты пришлось восстанавливать по коду, ресурсам и классам. Про оболочку отдельно расписывать смысла нет, это лодырь, который ломает стандартные инструменты, вытаскивает из asset'ов зашифрованный 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 overlay
  • GuardianSetNotice - title/text/colors/visible
  • GuardianSetDim - включить/выключить затемнение
  • GuardianShowPush - фейковый push с title/text/icon/actionPackage
  • PushAlertActivity - 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

Гостевой просмотр. Чтобы отвечать и ставить реакции, войдите или зарегистрируйтесь.