Логотип ШифрыШифры
ГлавнаяЖурналЛотыУзел ИИВойти
ГлавнаяЖурналЛотыУзел ИИ
Войти
Логотип ШифрыШифры2К26Версияъ1
EmailMAX
Назад к логу

PamStealer крадёт пароль и фотографию лица через поддельный криптокошелёк Wavel

7 октября 2026 г.•Репа
#атаки#тулзы

Жёлтая папка с одним красным файлом внутри – так выглядит иконка DMG с multichain-кошельком Wavel. Внутри лежит документ без имени, с расширением .scpt, и Finder по умолчанию показывает его серой иконкой generic-документа. Пользователь macOS кликает дважды, Script Editor открывает содержимое – и дальше начинается третий известный вариант стилера семейства PamStealer. Ход у этого варианта такой, какого на macOS-стилерах давно не встречалось: его второй stage не расшифровывается без ответа сервера. Эфемерный ключ выписывается на запуск, X25519-пара закрывается серверным приватным ключом, который на хосте не лежит в принципе – статический анализ бинаря даёт обёртку, не содержимое. Четыре слоя закрепления, SIGSTOP с SIGKILL по трём системным процессам, PAM-валидация пароля на жертве, 17 браузеров через тринадцать подставных помощников, фотография лица из Open Directory – всё остальное в том же пакете.

Фальшивый криптокошелёк и .scpt-файл без имени

Приманка здесь ожидаемая для macOS-стилеров последних месяцев: поддельный сайт hxxps://wavel[.]app, аккуратно сверстанный под multichain-кошелёк, кнопка «Download for macOS» и DMG, который прилетает с чужого хоста – hxxps://y32me8[.]com/Wavel.dmg. То, что интересно внутри DMG, – не само скрытое приложение, а решение про имя файла.

Файл внутри один, расширение .scpt, имени у файла нет. Finder по умолчанию прячет расширения, и для пользователя этот файл – серая иконка generic-документа. Он даже не похож на исполняемый, просто какой-то «документ установщика». Двойной клик по .scpt – и macOS открывает штатный Script Editor, потому что он default handler для этого расширения. Script Editor исполняет JXA-источник, встроенный в бинарник, и дальше вся карусель уже на машине жертвы.

JXA свёрнут в курьера на 24 868 символов base64

Снаружи это тот же самый compiled JXA-формат, что был у Maccy, Scoppr, Nancy и у первой версии PamStealer: магические байты JsOsaDAS1.001.00, следом binary property list с UTF-16BE-кодированным JXA-источником. Семейный почерк. А вот внутри – поворот.

В ранних вариантах JXA-слой был рабочим: он сам делал RC4-дешифровку встроенного пэйлоада, шёл через JXA-мост в Foundation и NSData, собирал staging руками. Здесь JXA – чистый курьер. У него одна переменная _b длиной 24 868 символов base64 и одна команда: декодировать и подать в /bin/zsh -s на стандартный вход. Script Editor исполняет это и тут же выходит. JXA-процесс мёртв до того, как кто-нибудь успел подумать про process ancestry, а функция в декодированном zsh в этот момент уже отвалилась в фон:

daemon_function & ; exit 0

Функция запускается в фон, скрипт тут же выходит, и когда Script Editor закрывается, бэкграунд-процесс остаётся без родителя – подхватывает его launchd, а видимая цепочка выполнения обрывается. Для процессов-наблюдателей, строящих дерево от родителя к ребёнку, следа от Script Editor → sh → zsh больше нет; остаётся висящий zsh без очевидной истории.

daemon_function несёт всю работу дроппера: скачать расшифровщик, скачать зашифрованный пэйлоад, обменяться ключами с C2, расшифровать и разложить bundle, подавить нотификацию macOS о новом login-item, установить четыре слоя закрепления, дождаться архива со stage 2 и залить его обратно на C2.

pkgunpack: своя крипто-утилита и ключ, которого на хосте нет

Главная новая деталь дроппера – отдельный маленький Mach-O по имени pkgunpack, FAT (arm64 + x86_64), который dropper качает с того же C2 отдельным запросом. У pkgunpack одна задача – криптография. X25519 он реализует через статически слинкованный curve25519_donna, AES-256-GCM и SHA-256 берёт из системного CommonCrypto. Экспортированных subcommand два: genkey – сгенерировать свежую эфемерную пару и записать её на диск, и decrypt – провести полную ECIES-последовательность расшифровки.

Прежде чем запустить, dropper сам чистит атрибут quarantine и подписывает бинарь ad-hoc подписью – именно dropper, не пользователь и не Gatekeeper:

xattr -cr /tmp/.pkgunpack-$$
codesign -fs - --deep /tmp/.pkgunpack-$$

Ad-hoc подпись здесь нужна ровно для того, чтобы Gatekeeper пропустил неподписанный скачанный бинарь без диалога авторизации. Два этих шага – тоже работа dropper'а, не самого pkgunpack и не пользователя.

Дальше dropper вызывает pkgunpack genkey – получается эфемерная X25519-пара на этот конкретный запуск, пара пишется в /tmp/.eph-$$.key и /tmp/.eph-$$.pub, после чего публичная часть и nonce вида <ts>-<rand>-<PID> уходят POST-ом на /v1/loader/dek под обязательный заголовок X-Upload-Token. В ответ приходит короткий (65 байт) блоб с магиком SNWK1, за ним AES-GCM IV, зашифрованный DEK и аутентификационный тег. pkgunpack decrypt на хосте делает тот же X25519 между своим приватным ключом и server_pub, захардкоженным в бинаре (I6VuXPzLJfPEXgVRO5ycNXdMWHWvMAkrLMV6OpEuwDw=), прогоняет shared secret через SHA-256 с доменным сепаратором "sn-dek-wrap-v1", получает KEK, под ним снимает AES-GCM-обёртку с DEK и этим DEK расшифровывает уже сам пэйлоад с магиком SNP1, вторым AES-GCM. Внутри SNP1-оболочки – tar.gz, развёрнутый в /tmp/.upd-$$/.

Схема ECIES-обмена: dropper скачивает pkgunpack и зашифрованный пэйлоад, pkgunpack генерирует эфемерную X25519-пару, dropper шлёт её на /v1/loader/dek, сервер возвращает SNWK1-wrap, pkgunpack делает ECIES и расшифровывает SNP1-пэйлоад в tar.gz
Восемь шагов обмена: без ответа сервера на четвёртом шаге цепочка стоит, статический анализ бинаря даёт обёртку, не содержимое

И вот почему без сервера расшифровать нельзя, даже если выдернуть бинарь из песочницы и дать ему любую нагрузку из C2. Приватную часть, закрывающую X25519-пару, держит сервер; без неё shared secret со стороны аналитика не выводится, DEK не снимается, пэйлоад остаётся непрозрачным куском зашифрованных байтов. Эфемерная пара на каждый запуск убивает replay – даже перехваченный dek-wrap нельзя переиграть в другом окружении. Для статического анализа без живого C2-сеанса пэйлоад – чёрный ящик.

Это и есть первое инженерное решение, на котором стоит остановиться. В первой версии PamStealer ключ лежал прямо в JXA-источнике: RC4, маленький, встроенный, работал без сети. Простота плюс off-grid, но первый же дамп JXA отдавал plain-ключ любому, кто туда заглянул. Здесь оператор заплатил другой монетой: live-ECIES с server-held private ключом даёт стойкость к static analysis, но делает всю цепочку заложницей доступности C2. Песочница с интернет-карантином видит ровно обёртку, разбор второго stage становится платным – нужен работающий сервер и живой адрес. У соседнего варианта этой же семьи оператор решал ровно эту задачу иначе – самодельным ECDH на Curve25519 с тремя разными AES-ключами за три стадии; у PamStealer v3 ход экономнее – один обмен, одна эфемерная пара, одна зависимость от сервера. На выходе – одинаковая цена для аналитика и разная цена для оператора: три ключа в C2 за три стадии против одного /v1/loader/dek на запуск.

Параметр X-Upload-Token, кстати, в этом обмене стабилен – он един для всех C2-запросов конкретного build'а и, значит, остаётся IOC сам по себе. Шанс спалить build здесь выше, чем хотелось бы оператору; это тоже цена за единый токен вместо, скажем, подписанного ответа или rotation'а.

Три системных процесса под SIGSTOP, потом SIGKILL

Разложив stage 2 в ~/Library/.../Finder.app (bundle, кстати, переименован из Wavel.app при копировании, а внутренний бинарь остался r8afup9un0 – маскарад под системный компонент на уровне пути), dropper сталкивается с небольшой, но дорогой проблемой: при регистрации нового LaunchAgent macOS штатно показывает пользователю нотификацию о новом background login item. Это тот самый всплывающий баннер «"Finder" added as a login item» – дорого для скрытности, легко замечается.

Решений, которых не было в предыдущих версиях PamStealer, два, и они идут в связке. Dropper шлёт SIGSTOP трём системным процессам, которые эту нотификацию поднимают: BackgroundTaskManagementAgent, BTMNotificationAgent и NotificationCenter. Пока те заморожены, выполняет launchctl bootstrap gui/<UID> с plist'ом com.apple.finder.agent. И потом в петле SIGKILL-ит каждый из трёх замороженных процессов, пока не убедится, что все три отвалились. На этом интервале регистрация агента произошла, а код, который должен был показать пользователю нотификацию, прибит до того, как успел что-либо сказать.

Это второе решение, где цена заметна с обеих сторон. У оператора выбор между двумя плохими вариантами: смириться с нотификацией BTM и видимой тревогой у пользователя – или прибивать системные процессы Apple и получить в trace убийство NotificationCenter, которое для любого внятного EDR само по себе аномалия. В v1 и v2 авторы жили с первым; в v3, судя по тому, что этот блок появился, они столкнулись с юзерами, замечавшими баннер. Выбрали второй вариант: убить NotificationCenter, принять шум в trace, выиграть тишину у пользователя. Инвариант «тише локально ≠ тише в журналах» срабатывает здесь буквально – тишина перенесена из UI в proc-уровень.

Четыре слоя закрепления с меткой # sysnotif-repair

Регистрация com.apple.finder.agent – это только первый якорь из четырёх. У каждого слоя закрепления в его теле есть опознавательный комментарий # sysnotif-repair; по этому маркеру dropper проверяет, не поставил ли он уже этот слой на предыдущем запуске, и не перезатирает чужое. Это сторона рутины; интересно другое – сам набор якорей.

Первый – классический: LaunchAgent com.apple.finder.agent, маскирующийся под системный компонент, запускается at-login и далее каждые 15 секунд, рестартует при падении. Это основной путь выживания импланта. Второй – standalone zsh-скрипт ~/Library/Application Support/System/.repair-run, который проверяет, лежит ли bundle в install-пути и зарегистрирован ли LaunchAgent в launchd. Если чего-то недостаёт, восстанавливает оба из локального бэкапа .sysnotif-backup.tgz, который лежит рядом с установленным bundle'ом. Главное в этом слое – он работает полностью без сети: удалить агент и bundle мало, вернёт.

Третий слой – блок, аккуратно дописанный в ~/.zshrc, который на каждой интерактивной сессии zsh в фоне запускает .repair-run. Пользователь открывает новую вкладку в терминале – имплант восстановлен. Четвёртый – самый вкусный: global git-хуки. Dropper копирует тот же .repair-run в ~/Library/Application Support/System/.githooks/post-checkout и pre-commit, после чего переключает глобальный hooksPath в git:

git config --global core.hooksPath ~/Library/Application\ Support/System/.githooks

С этого момента любой git checkout или git commit в любом репозитории на хосте тихо дёргает .repair-run. Разработчик, у которого «mac начал тормозить и странно моргает Finder», – самый плотный источник триггеров для этого якоря.

Это третье место для инженерной оценки. Выбор оператора – четыре независимых слоя с опорой на пользовательские триггеры. Цена ясна: ~/.zshrc и core.hooksPath – громкие артефакты для любого адекватного macOS-EDR, видны простым diff'ом конфига, грубо падают под сигнатурные правила по маркеру # sysnotif-repair. Четыре слоя – четыре цели для детекта. Альтернатива – один тщательно замаскированный LaunchAgent с хорошим именем и KeepAlive, периодически реинсталлируемый. Это тише, но хрупче: один launchctl bootout плюс rm -rf bundle – и импланта нет. В v3 оператор закрыл хрупкость, заплатив шумом; у хрупкости и шумности, похоже, устойчивая цена на этом рынке.

Rust уступил место Swift в MacClient

Stage 2 – тот самый r8afup9un0 внутри поддельного Finder.app – это FAT Mach-O (arm64 + x86_64), и он теперь Swift, а был Rust. По кодовой базе этого хватило бы, чтобы говорить про другую семью, но collection-objectives и семейный почерк пересекаются с v1 насквозь, поэтому классификация честно идёт как смена toolchain в той же линии. Внутреннее имя проекта вытащено из strings: MacClient, entry-point _MacClient_main, класс MacClient.AuthPromptWindow, строка "MacClient archive: " в assembly. Экспортированных по имени функций четыре: _MacClient_main, _pam_verify_login, _kc_grab_storage_item, _sqlite_backup_database. Связанные фреймворки – AppKit, Security, libpam.2.dylib, libsqlite3.dylib. Весь набор – чистый macOS-native-стек.

Переход с Rust на Swift при сохранении целей – редкий ход; обычно идут обратно, из macOS-экосистемы в кросс-платформенный Rust, ради единого бинаря под Linux/Windows и компактности. Здесь оператор заплатил кросс-платформенностью и перенёс кодовую базу: вся stage 2 теперь получает на macOS прямой доступ к AppKit, нативные Objective-C-мосты к Security.framework и к libpam без FFI, и Cocoa-паттерны вроде SONOMA_DISPLAY_NAME – переменной окружения, которая, судя по строкам, управляет именем приложения в системном prompt (точное её применение на момент анализа не подтверждено). Цена альтернативы, оставить Rust, была бы в том, что бóльшую часть Security.framework-вызовов и всю PAM-работу пришлось бы делать через objc crate или ручной FFI, по дорогим контрактам: тестирование, падения, сигнатуры символов. Swift снял этот слой целиком – ценой того, что теперь этот stage запустится только на macOS.

PAM-валидация пароля на самой жертве

Та часть, из-за которой вся семья и названа PamStealer, никуда не делась. В MacClient.AuthPromptWindow – полноценный AppKit-декор на двух окнах. Первое – классический macOS-prompt «Wavel wants to make changes», с иконкой замочка в шапке, двумя полями «admin» и «Password» и кнопкой OK. Второе – имитация системного баннера «"Wavel" is damaged and can't be opened» с иконкой корзины и кнопкой «Move to Trash». Это две разные приманки: первая вытягивает пароль, вторая объясняет пользователю, почему приложение теперь «не запускается», и уводит внимание в сторону от того, что в фоне уже закрепилось.

Поддельный системный диалог macOS: шапка с иконкой замочка, строка «Wavel wants to make changes», два поля «admin» и «Password», кнопки Cancel и OK
Приманка на пароль из AuthPromptWindow – вводимое значение уходит в _pam_verify_login и валидируется через PAM-API сервиса «login» на самой жертве

Введённое в поле «Password» уходит в экспортированную функцию _pam_verify_login, которая валидирует его через Pluggable Authentication Modules – нативное PAM API macOS, с указанием сервиса "login" (то же, что в v1). Валидация идёт на хосте, не на сервере. Это и есть четвёртое место для оценки. Выбор оператора: получить от PAM однозначный ответ «пароль валидный» прямо на жертве. Цена: лишний системный вызов, который и EDR, и опытному пользователю виден отдельным trace-слоем. Что это даёт: C2 не грузится мусорными строками с опечатками и фейками. Альтернатива – слать всё введённое на C2 без локальной валидации – тише локально, звено _pam_verify_login снимается, но на сервере копится шум и оператор тратит время на фильтрацию. Выбор здесь принимается в пользу качества улова: PamStealer хочет только валидные пароли и готов заплатить за это звено детекта на хосте.

Login keychain с двумя шагами на разблокировку

Пароль, полученный от PAM, идёт прямиком в кейчейн. Экспортированная _kc_grab_storage_item перечисляет и забирает элементы через _SecItemCopyMatching из Security.framework, но перед этим бинарь дважды дёргает shell-команды:

/usr/bin/security unlock-keychain -p "$pw" "login.keychain-db"
/usr/bin/security set-generic-password-partition-list -S apple-tool:,apple: -k "$pw" -s "$svc" "login.keychain-db"

Первый вызов разблокирует login keychain – стандартный путь, с одним отличием: пароль уже известен. Второй правит access control list и расширяет partition-list на запись, снимая ограничения на доступ к generic-password entries. Переменная окружения KC_PERSIST_PROMPT контролирует, появится ли повторный keychain-prompt при следующем обращении в той же сессии; разумно предположить, что оператор его выключает – чтобы не привлекать внимания к факту, что что-то лезет в keychain из-под чужого имени.

На этом тонкость заканчивается и начинается грубая работа: вся база keychain'а копируется напрямую через %s/Library/Keychains/login.keychain-db в staging. Эта копия – offline-резерв для post-exploitation; её можно открыть на любом другом хосте, подложив тот же пароль.

Семнадцать браузеров через тринадцать курьеров

Для браузерных кредов есть отдельная функция _sqlite_backup_database, которая делает ту самую штуку, за которую в стилер-мире принято снимать шляпу: открывает исходный SQLite в read-only через _sqlite3_open_v2, инициализирует копию через _sqlite3_backup_init, передвигает страницы через _sqlite3_backup_step и закрывает _sqlite3_backup_finish. Это атомарная копия с валидным снимком страниц, не cp под работающим браузером, который легко повреждает базу.

Проблема в том, что браузер держит file lock на свой профиль, и даже backup API на него может напороться. Поэтому непосредственно перед сбором stage 2 сносит все helper-процессы разом:

pkill -9 -fi 'Google Chrome Helper|Brave Browser Helper|Microsoft Edge Helper|Chromium Helper|Vivaldi Helper|Opera Helper|Firefox Helper'

После этого замка на профилях не остаётся, и бинарь спокойно обходит целевой список. Bundle id'шники из strings подтверждают семнадцать браузеров, и это самое интересное в этой части, потому что список явно шире, чем у любого commodity-стилера для macOS. Четыре канала Chrome – stable, Beta, Dev, Canary – плюс Brave, Edge, Vivaldi, Opera, Opera GX, Chromium и Firefox составляют привычный baseline. А дальше идут те, за кем commodity-стилеры обычно даже не ходят: Arc, Zen, Waterfox, LibreWolf, Yandex Browser и CocCoc. Arc и Zen – редкие modern-браузеры с собственным хранилищем профилей; Waterfox и LibreWolf – privacy-fork'и Firefox'а; Yandex и CocCoc – региональные под русскоязычный и вьетнамский рынок соответственно. Это не ковровое перечисление, а целевой выбор, который кто-то сел и прописал вручную.

Отдельная тонкость – keychain-ACL. На macOS keychain-записи, созданные браузером, прописаны с точным bundle id того браузера, и стандартный процесс стилера, лезущий в них от своего имени, схватит cross-app authorization prompt – именно тот, который видно пользователю и который убивает всю скрытность. PamStealer решает это через тринадцать подставных помощников, лежащих прямо внутри своего собственного bundle: Contents/Resources/Helpers/Helper_<Browser>Storage.app, у каждого внутри Contents/MacOS/KcHelper. В runtime stage 2 резолвит путь через NSBundle.mainBundle.bundleURL с добавлением Contents/Resources/Helpers и дёргает нужный KcHelper, передавая ему окружением всё, что нужно: KC_PASS (пароль юзера), KC_STAGING (куда класть выборку), KC_SERVICE и KC_ACCOUNT (что именно забрать). Для macOS этот KcHelper – нормальное приложение с подписью и bundle id'шником соответствующего браузера; keychain-ACL сматчивает его как «тот самый браузер» и отдаёт запись без cross-app prompt'а. Пользователю ничего не всплывает, потому что для системы это тот же процесс, что час назад открывал ту же базу легально.

Фотография лица из Open Directory

Параллельно с кредами идёт сбор системного fingerprint'а – 21 атрибут из последовательных вызовов system_profiler: hardware UUID, serial number, provisioning UDID, модели железа, чип, firmware, system version, kernel version, boot volume, username, computer name, статус SIP, secure virtual memory, количество ядер GPU, поддержка Metal, разрешение экрана. Uptime добавляется отдельно – sysctl kern.boottime. Этого достаточно, чтобы оператор на другом конце цепочки сразу знал, какая именно машина, какой владелец и в каком состоянии находится.

А следом идёт сущность, которой в commodity-стилерах macOS нет вообще. Stage 2 читает JPEG-данные пользовательского аватара – ту самую картинку, которая показывается на экране логина и в System Settings, – но не из файловой системы, а из Open Directory, локального каталога macOS:

dscl . -read /Users/<user> JPEGPhoto

Атрибут JPEGPhoto хранит JPEG напрямую в каталоге, и dscl его отдаёт без всяких прав, кроме читательских. Параллельно собираются .zsh_history, .zshrc, .bash_history и .gitconfig – тоже целевые файлы, явные строки в бинаре; в sandboxed-прогоне подтверждён только .zsh_history, остальное есть в string table как цели, но в тестовой среде отсутствовало.

Это пятое место для оценки, и оно самое неожиданное. Красть аватар – расход опсек-актива в чистом виде: dscl . -read ... JPEGPhoto – нетипичный вызов и в легитимном использовании, и в стилерах, и оседает в proc-trace явной записью. Альтернатива – просто не брать: кредов и системного fingerprint'а достаточно для 95% экономики стилера. Что оператор получает за эту цену: реальное лицо жертвы вместе с контекстом аккаунта – это KYC-обход на бирже, где обычно нужно селфи с паспортом; это SIM-swap с видео-подтверждением, которое закрывается украденной фотографией; это целевой фишинг с реальным лицом в lures. Один атрибут из dscl даёт класс операций, который без фото просто недоступен. Цена записана в trace'е, но за эту цену покупается другой уровень post-exploitation.

Файловые метаданные дособирают контекст: lsappinfo list 2>/dev/null | head -200 – какие приложения установлены и запущены, ps -ax -o pid,comm 2>/dev/null | head -400 – текущие процессы с короткими именами. Дальше всё это уйдёт в один архив.

Архив ditto, lock-dir и три стабильных read'а

Сборка финального архива – отдельный аккуратный блок. Stage 2 кладёт собранное в staging с довольно экзотической структурой: /tmp/.65486b/2966269/ c подкаталогами Password/, Username/, Mode/ (всё атомарно, через sandbox-mediated rename), Keychains/login.keychain-db, Browsers/, Extensions/, Wallets/, UserFiles/ и u2whj/ – отдельная папка под то, что собрано помощниками. Для сборки архива вызывается ditto, а не tar:

ditto -c -k --sequesterRsrc /tmp/.65486b/2966269 /tmp/.5ffa071d.zip.part

-c -k – создать zip-архив, --sequesterRsrc – сохранить resource forks и extended attributes в стандартном для macOS виде __MACOSX/... файлов. На macOS это важно: часть браузерных файлов и расширений несут метаданные в resource fork, которые tar потерял бы, а ditto сохраняет. Параллельно существует lock-dir /tmp/.5ffa071d.zip.lockdir, чтобы повторный запуск stage 2 не начал собирать поверх уже собираемого; атомарный rename .part → .zip гарантирует, что dropper, который по-прежнему висит из первой секции в фоне, не прочитает полуготовый архив.

Dropper как раз в этот момент занят polling-петлёй над .zip-файлом – ждёт три подряд двухсекундных стабильных read'а, в которых размер файла не меняется, после чего прогоняет zip -T на валидность и льёт PUT'ом на wavel.apple03cloudstore[.]com/v1/asset/1789753519-10913-1182 с тем же стабильным X-Upload-Token в заголовке. Три стабильных read'а – простейший семафор, не требующий координации между stage 1 и stage 2 через IPC или lock-файл; выбран сознательно, пэйлоад из разных секций приходит с разными темпами, и жёсткая синхронизация стоила бы дороже, чем пара лишних секунд опроса.

Калибр третьего PamStealer

Если собрать всё в один взгляд, этот вариант не commodity, и каждая его деталь это подтверждает. Фронтир macOS-стилеров сейчас – embedded RC4 с ключом прямо в JXA, один LaunchAgent, пара жадных curl'ов и десяток браузеров под сбор. PamStealer v3 стоит ощутимо дальше по шкале операционной зрелости: live-ECIES на X25519 с server-held private key оттаскивает статический анализ от пэйлоада; четыре слоя закрепления с глобальными git-хуками закрывают хрупкость обычной одно-агентной схемы ценой заметности; SIGSTOP с петлёй SIGKILL по BackgroundTaskManagementAgent, BTMNotificationAgent и NotificationCenter снимает user-visible-тревогу о новом login-item ценой proc-level аномалии; переход stage 2 с Rust на Swift отдаёт cross-platform ради нативной работы с AppKit, Security.framework и libpam; _pam_verify_login платит отдельным системным вызовом за фильтрацию мусора в C2; тринадцать подставных KcHelper-ов обходят браузерный keychain-ACL без cross-app prompt'а; семнадцать таргетов с Arc, Zen, Waterfox, LibreWolf, Yandex и CocCoc расширяют улов до аудитории, которую commodity-стилер не достаёт; dscl JPEGPhoto переводит post-exploitation за грань тривиального KYC-обхода и целевого фишинга.

На каждом из этих решений оператор отдал что-то и получил что-то. Единого «гениального хода» в этой кампании нет; есть дисциплинированная инвестиция в delivery infrastructure, в host-residency и в то, что происходит с жертвой уже после первого клика. У commodity-линий этого жанра обычно отстаёт либо ядро, либо обвязка; оба слоя сразу – редко. У PamStealer v3 оба слоя подтянуты на один и тот же уровень, и именно это сочетание выбивает его из массы «fake crypto-wallet macOS stealer» и делает материалом, который стоит разобрать до конца.

Разбор и индикаторы компрометации опубликованы Jamf Threat Labs.

Автор: hacker@shifry.local