How AMOS macOS stealer avoids detection
Check out the blog post on the orginal site: Kandji Blog (Credit to Christopher Lopez who wrote this article with me)
Atomic macOS Stealer (AMOS) was first spotted in early 2023. It’s a powerful piece of malware that targets Apple users and tricks them into installing the software on their computers. The malware is sold via Telegram; as of January 20, 2024, the price was $3,000 a month.
Once installed, Atomic Stealer can exfiltrate an extensive amount of data, including keychain passwords, user documents, system info, cookies, browser data, credit card information, cryptocurrency wallets, and more.
Adversaries leveraging AMOS direct victims to a website (the URL of which changes constantly) to download an unsigned disk image (DMG). If a user were to go that URL accidentally, they’d see only a benign welcome message.
On that website is a subdirectory where 12 DMG files are available for download: Six in the /name directory, another six in /name/files.
Each of these twelve DMG files gives you a different hash. This is a way to avoid hash-based detection. (We’ll explain how they do that in a bit.) Because of this, hash-based detection for this new version will not work, since every time someone downloads the sample, they’d need to look for a new hash. From our research, this is new: AMOS creators have not previously had a way to iterate small portions of code to change hashes.
If you did download one of these DMG files, a pop-up would appear that looks similar to any other application you download from outside the Apple store.
Inside that DMG is a Mach-O that, if the user right-clicks and opens it, the malware will execute.
XOR Encoding
Since it first appeared, AMOS has continued to evolve. A recent sample included XOR instructions to prevent detections of strings known to be associated with this stealer. The capabilities of the latest sample do not appear to have changed, but its obfuscation tactics differ from previously reported samples.
In the example below, some of the strings are visible even with the XOR encoding. The XOR key for these is 0x90, so we can see how these are separated by null bytes. It appears that, for many of these, only half of the strings were XOR encoded.
In a more recent sample, we can see that no strings are visible in the const section. Something appears to have changed with how the encoding is completed. This further highlights how quickly and often the AMOS malware changes.
Let’s dive into an example of how an XOR function is called, along with the arguments that are passed in the arm64 slice of the Universal binary.
1 10000c7cc 28108052 mov w8, #0x81
2 10000c7d0 e8430039 strb w8, [sp, #0x10 {xorKey}] {0x81}
3 10000c7d4 e8430091 add x8, sp, #0x10 {xorKey}
(...)
4 10000c84c 29008052 mov w9, #0x1
5 10000c850 e013813c stur q0, [sp, #0x11 {var_af}]
6 10000c854 3f0d02f1 cmp x9, #0x83
7 10000c858 80000054 b.eq 0x10000c868
8 10000c85c ea434039 ldrb w10, [sp, #0x10 {xorKey}]
9 10000c860 b20d0094 bl _OUTLINED_FUNCTION_1
10 10000c864 fcffff17 b 0x10000c854
11 10000c868 a0110094 bl _systemStarting at line 1, the value 0x81 is moved to register W8. This will be the XOR key used later to decode the string stored on the stack.
At line 2, the XOR key is stored on the stack at the address SP + 0x10.
Line 3 indicates that a pointer to this offset on the stack is moved to register X8.
Between line 3 and 4, hex values are moved into registers and stored on the stack. This will be the string to decode.
Line 4 shows the value 0x1 is moved into W9. This will serve as the index used to iterate through the string.
Line 5 indicates that the value stored at register Q0 is saved on the stack at SP + 0x11. This is one byte away from where the XOR key is stored.
Line 6 compares the value stored at X9 and 0x83, which would be the length of the string.
Line 7 shows a branch if equals instruction, which would branch to the address 0x10000c82c when X9 = 0x83. The system() function is at 0x10000c82c, which indicates that, once the decoding is complete for 0x82 characters, the output is passed as the argument to the system() function.
Line 8 is a LDRB instruction that loads the byte value stored at SP +0x10 (XOR Key) to W10.
Line 9 is the branch to the function called _OUTLINED_FUNCTION_1, where the XOR instructions occur.
Line 10 indicates that once the XOR function is completed, a branch back to the comparison of the value stored in W9 is completed to continue this loop.
Line 11 is the system() function call after the loop completes. This sets up the call to the system() function after decoding the string using an XOR function.
Let’s now look at the XOR function called _OUTLINED_FUNCTION_1, which we branched to above.
10000ff28 int64_t _OUTLINED_FUNCTION_1(char* arg1 @ x8, int64_t arg2 @ x9,
10000ff28 char arg3 @ x10)
10000ff28 0b696938 ldrb w11, [x8, x9]
10000ff2c 6a010a4a eor w10, w11, w10
10000ff30 0a692938 strb w10, [x8, x9]
10000ff34 29050091 add x9, x9, #0x1
10000ff38 c0035fd6 ret The function definition indicates that the values stored in X8, X9, and X10 are passed as arguments: X8 is pointing to the stack at SP +0x10; X9 is our index, which at the start of this loop was given the value 0x1; and X10 has the XOR key byte value 0x81.
Using the value in X9 as the index, the byte stored at the memory address X8 + X9 is loaded into W11. The encoded string starts at the address of SP +0x11, one byte away from what X8 is pointing to. This is why the loop starts with X9 having the value 0x1 as the index.
The EOR (XOR) instruction then uses the key (0x81) stored in W10 to calculate the hex value stored back into W10. The STRB instruction then stores this byte back to the location at [X8, X9] which is the string pointed to by X8 with the index in X9.
This example creates an osascript command that is common in AMOS samples and is passed to the system function to execute.
osascript -e 'display dialog "Some error occurred while running the application." buttons {"OK"} default button 1 with icon stop'
This example is just to demonstrate how one string is decoded in this manner using the 0x81 key. There are many other examples in the binary that leverage similar functionality to decode the strings. To highlight the use of the XOR function described above, we can see that _OUTLINED_FUNCTION_1 is called 120 times by other functions in the binary, further highlighting the use of this encoding.
AMOS is an active malware that is constantly changing and frequently being updated. As we post this, the source website is still live and is being actively updated; on March 1, the site was updated between 2:00 and 8:00 AM PST. Kandji is actively tracking the group that is distributing this malware and will continue to monitor and update our security processes as needed. We suggest that all security devs/teams should do the same.
Indicators Of Compromise
SHA256
- CrackSetup.dmg/dowload1.php:
c988fd14753a8ee73d5c2747e4deeda8ef798deeb747435a112744d243cfb7ba - CrackSetup-2.dmg/dowload2.php:
400303250be2414d340675226aea7f78757ea8d1413af8dcd8f2c7a8d3ff8e21 - CrackSetup-3.dmg/dowload3.php:
0e8aa909c1fefe12fd12f8f1e0073203ef4111f27fefb48568fd6ab02c13fb38 - CrackSetup-4.dmg/dowload4.php:
9e52d52e53393ac49447a8fa1d0e98b4f7835346b7f7b85df1b9c891b554924c - CrackSetup-5.dmg/dowload5.php:
785e96c34123d1b2e30e9cc43972ab165f584815cdb323bd892b78fe7c9980e7 - CrackSetup-6.dmg/dowload6.php:
64abdcd49e9f12f6f92457a006dc1876f04dc1ad9bcab5d1766f126a87a5c60a - CrackInstaller.dmg:
ed46d8865491e81d14197eafc71165eb0358086680a604e4bf6fe9c6372e741c - hendro.dmg:
469a0176993e13c28b5b8f7f85a382e576f5e93310c8f5dec53a860783b97f0f - riesling.dmg:
d7c7aad5899fa10e47cd1f7a224c62a44ea2858b1205762e183d69cdf356a6c2 - vera.dmg:
cb6ebdc900d730f844f07d9d66ee4008aeedff1eac0662553f1713fffb29f058 - whiskey.dmg:
596330886e915d9905d72284dcba6b663d8db95e84c00d25e1e19a3d79b01664 - zinfandel.dmg:
aebd77032f05029f0c6d614d40a5891926daec04fba4254a671b59709dfa625f