[0001] AmberAmethystDaisy -> QuartzBegonia -> LummaStealer
Disclaimer: I have personally noticed a significant difficulty in finding names for many loaders, even if they have been reported on due to the overwhelming focus on the final payload within infection chains. With this in mind, I utilize a custom loader taxonomy system, with the name of the loader in open-source reporting as a secondary identifier. More information on this taxonomy system can be found here. If you happen to know the name of a loader that I report on, please let me know!
Recently, I stumbled across a video on YouTube from "The PC Security Channel", which noted that there was malware being distributed through fake cracked software on GitHub. Unfortunately, the extent of the analysis performed within the video was to check VirusTotal in order to see if the file is malicious or not.
Video: How not to Pirate: Malware in cracks on Github (youtube.com)
Although this might be good enough for most, my disappointment is immeasurable, and my day is nearly ruined. However, we can do the digging ourselves and get to the bottom of this!
Although the original GitHub repo that was shown within the video is now taken down, the actual download URL for the first stage seems to be hosted on another repo, as seen in the hyperlink within the video:
The URL seen in the hyperlink leads us to https[:]//github[.]com/ravindrauppalapati/RoleManager/releases/tag/Client
, which is still up and available for download!
Stage 1 - QuartzDahlia
Also known as: Launch4j
TL;DR:
- Initial sample can be executed as a normal executable as well as a JAR
SHA-256 | Filename |
---|---|
8ed6a84101dfcafeac6ddbf5020312b0094576fd3f9106f7df460e1b8a7bd5e1 |
Win.Installer.x64.zip |
94edf5396599aaa9fca9c1a6ca5d706c130ff1105f7bd1acff83aff8ad513164 |
Win Installer x64.exe |
Unpacking the ZIP archive, we can observe the following file structure:
│ Win Installer x64.exe
│
└───v2024
├───bin
│ │ awt.dll
│ │ glass.dll
│ │ java.dll
│ │ javafx_font.dll
│ │ javafx_iio.dll
│ │ javaw.exe
│ │ msvcp120.dll
│ │ msvcr100.dll
│ │ msvcr120.dll
│ │ net.dll
│ │ nio.dll
│ │ prism_d3d.dll
│ │ sunec.dll
│ │ sunmscapi.dll
│ │ verify.dll
│ │ zip.dll
│ │
│ └───client
│ jvm.dll
│
└───lib
│ jce.jar
│ jfr.jar
│ jsse.jar
│ resources.jar
│ rt.jar
│
└───ext
jfxrt.jar
sunec.jar
sunjce_provider.jar
sunmscapi.jar
Taking a look at the executable, it's unclear at first as to where the malicious code lies. With this in mind, I decided to load it up in x64dbg to do some quick preliminary dynamic analysis.
Stepping through a few functions, I was able to see that the malware attempts to calls its own binary with the -jar
flag using its bundled Java runtime. It turns out that this actually a tool named Launch4j which allows for Java applications to be wrapped in an executable.
Since JAR files are able to be unzipped, we can go ahead and extract the contents of this executable with 7-Zip.
- Note: Detect-It-Easy also notifies us that this executable contains a ZIP archive, and we could have gone about it that way as well!
Stage 2 - AmberAmethystDaisy
Also known as: D3F@ck Loader, NestoLoader
SHA-256 | Filename |
---|---|
515d025ba2aa1096f65c13569de283b83d86824d08ca48c1fc3bc407d4cf3266 |
MainForm.phb |
TL;DR:
- Extracted contents of the JAR contains files with the
.phb
extension, indicative of JPHP - The entry point for JPHP-based applications can be found within
.system/application.conf
- In this case, the entry point resides in
app/forms/MainForm.phb
- In this case, the entry point resides in
- Utilizing Binary Refinery and jadx, the next stage payload URL is retrieved.
A few of the extracted files have the .phb
extension, which is indicative of JPHP, an implementation of PHP on the Java VM. For more information on triaging JPHP malware, this same malware family was recently showcased on a MalwareAnalysisForHedgehogs video.
The entry point for JPHP-based applications can be found within .system/application.conf
. The content of this file is as follows:
# MAIN CONFIGURATION
app.name = DarkLauncher
app.uuid = 6ccf8f8e-fb00-441b-a0f5-f3bc2fa6619b
app.version = 1
# APP
app.namespace = app
app.mainForm = MainForm
app.showMainForm = 1
app.fx.splash.autoHide = 0
We now know that the entry point that we are interested in would be located within the app
folder and should be called MainForm
. Let's go and take a look! Sure enough, a file titled MainForm.phb
exists in the forms
folder located within app
.
Viewing this file with a hex editor, we can very quickly see what looks to be parts of an embedded configuration. Now we can be fairly sure that this is the file we want to be looking further into.
Although we see a C2 IP address of 194.147.35[.]251
here, this is seemingly not where the next payload is hosted. Let's dig deeper to figure out where the next payload is actually hosted.
Dealing with PHB files
PHB files contain Java class files within them, which are denoted with a magic of CAFEBABE
. We can utilize these magic bytes as a marker in order to extract the embedded .class
files.
I set up the following Binary Refinery pipeline to extract the 2 class files from app/forms/MainForm.phb
:
ef MainForm.phb | resplit h:CAFEBABE [ \
| pop \
| ccp h:CAFEBABE \
| dump extracted_class_{index}.class \
]
Unit | Name | Definition |
---|---|---|
ef |
Emit File | Places a file into the pipeline |
resplit |
Regular Expression Split | Splits the data in the pipeline by the supplied regular expression |
pop |
Pop | Removes a chunk from the frame (and stores it in a meta variable) - Used here to remove the first chunk in the pipeline, which contains data before the first CAFEBABE header |
ccp |
ConCat Prepend | Concatenates a value to the beginning of each chunk |
dump |
Dump | Dumps the data stored in each chunk to disk |
Using jadx, we can decompile the recovered Java class files in order to get a better idea as to what the malicious code does.
Looking through the code, we come across 2 base64 encoded strings which decode to URLs. We can set up the following Binary Refinery pipeline to extract, defang, and print these indicators:
ef MainForm.phb | carve b64 [ \
| b64 \
| xtp url \
| defang \
| cfmt "{}\n" \
]
https[:]//pastebin[.]com/raw/md5jVrEB
https[:]//t[.]me/+JBdY0q1mUogwZWMy
Unit | Name | Definition |
---|---|---|
ef |
Emit File | Places a file into the pipeline |
carve |
Carve | Extracts pieces of the pipeline that matches a given format - in this case, base64 |
b64 |
Base64 | Base64 decodes each chunk in the pipeline |
xtp |
eXtracT Pattern | Extracts indicators from the data within the pipeline by a given pattern |
defang |
Defang | Defangs indicators within the pipeline |
cfmt |
Convert to ForMaT | Transforms each chunk in the pipeline by applying a string format operation |
The Pastebin URL holds a paste that contains the IP address 78.47.105[.]28
, which is where the next payload is hosted. We can now reconstruct the true URL of the next-stage payload:http[:]//78.47.105[.]28/auto/b0573cef5fbfef5a15e8a6527080ad25/93.exe
Stage 3 - QuartzBegonia
Also known as: N/A
SHA-256 | Filename |
---|---|
5b751d8100bbc6e4c106b4ef38f664fb031c86f919c3e2db59a36c70c57f54e0 |
93.exe |
The third-stage payload in this infection chain is a loader written in C++. Loading the sample in Binary Ninja quickly reveals a large amount of non-code data, which is very likely the encrypted payload.
Within the main function, we can see that a thread would be created, which would execute a function which I've named thread_start_addr
(0x401750
) with an argument - a pointer to a function I've named mal::thread_main
(0x41d7b0
).
When called, the function thread_start_addr
executes the function at the address that was passed-in as an argument:
Diving into the mal::thread_main
function, we come across an encrypted buffer and its corresponding decryption loop:
Re-implementing this decryption loop in Python, we can recover the content of the encrypted buffer:
dec_buf = bytearray()
for b in enc_buf:
first_dec = (b ^ 0x73) - 0x15
second_dec = ((((((((first_dec - 0x57) ^ 0x74) + 0x4e) ^ 0x70) - 0x65) ^ 0x22) - 0x73) ^ 0x2a) % 256
dec_buf.append(second_dec)
>> dec_buf
bytearray(b'U\x05\x00\x007\x13\x00\x00\x00\x00\x00\x00user32.dll\x00CreateProcessA\x00VirtualAlloc\x00GetThreadContext\x00ReadProcessMemory\x00VirtualAllocEx\x00WriteProcessMemory\x00SetThreadContext\x00ResumeThread\x009\x05\x00\x00\xbc\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00C:\\Windows\\Microsoft.NET\\Framework\\v4.0.30319\\RegAsm.exe\x007\x13\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00ww\x00\x0033\x00\x00UU\x00\x00001010101101110101010010110101010\x00TerminateProcess\x00Sleep\x00\xe8\x00\x00\x00\x00X-\xef\x00\x00\x00-\x9e\x00\x00\x00U\x89\xc5U1\xdbd\x8b{0\x8b\x7f\x0c\x8bw\x0c\x8b\x06\x8b\x00\x8b\x00\x8b@\x18\x89E\x08\x89\xc7\x03x<\x8bWx\x01\xc2\x8bz \x01\xc7\x89\xdd\x8b4\xaf\x01\xc6E\x81>Loadu\xf2\x81~\x08aryAu\xe9\x8bz$\x01\xc7f\x8b,o\x8bz\x1c\x01\xc7\x8b|\xaf\xfc\x01\xc7]\x89}\x00U\x8bE\x08\x89\xc7\x03x<\x8bWx\x01\xc2\x8bz \x01\xc7\x89\xdd\x8b4\xaf\x01\xc6E\x81>GetPu\xf2\x81~\nressu\xe9\x8bz$\x01\xc7f\x8b,o\x8bz\x1c\x01\xc7\x8b|\xaf\xfc\x01\xc7]\x89}\x04\x8bu\x08\x8bE\x04\x8d}\x17WV\xff\xd0\x89\x85P\x01\x00\x00\x8bu\x08\x8bE\x04\x8d}&WV\xff\xd0\x89\x85T\x01\x00\x00\x8bu\x08\x8bE\x04\x8d}3WV\xff\xd0\x89\x85X\x01\x00\x00\x8bu\x08\x8bE\x04\x8d}DWV\xff\xd0\x89\x85\\\x01\x00\x00\x8bu\x08\x8bE\x04\x8d}VWV\xff\xd0\x89\x85`\x01\x00\x00\x8bu\x08\x8bE\x04\x8d}eWV\xff\xd0\x89\x85d\x01\x00\x00\x8bu\x08\x8bE\x04\x8d}xWV\xff\xd0\x89\x85h\x01\x00\x00\x8bu\x08\x8bE\x04\x8d\xbd\x89\x00\x00\x00WV\xff\xd0\x89\x85l\x01\x00\x00\x8b\x85P\x01\x00\x00\x8d\x95\xef\x00\x00\x00R\x8d\xb5\xff\x00\x00\x00Vj\x00j\x00j\x04j\x00j\x00j\x00j\x00\x8d\xbd\xb6\x00\x00\x00W\xff\xd0\x8b\x85T\x01\x00\x00j\x04h\x00\x10\x00\x00j\x04j\x00\xff\xd0\x89\xc6\x89\xb5K\x01\x00\x00\xc7\x06\x07\x00\x01\x00\x8b\x85X\x01\x00\x00V\x8b\x95\xf3\x00\x00\x00R\xff\xd0\x8b\x85\\\x01\x00\x00j\x00j\x04\x8d\xbdC\x01\x00\x00W\x8b\x96\xa4\x00\x00\x00\x83\xc2\x08R\x8b\x95\xef\x00\x00\x00R\xff\xd0\x8b\x85`\x01\x00\x00j@h\x000\x00\x00\x89\xe2\x8bR\x10\x8bZ<\x01\xda\x83\xc2\x04\x8bJLQ\x8bJ0Q\x8b\x8d\xef\x00\x00\x00Q\xff\xd0\x85\xc0u \x8bE\x04\x8d\x95q\x01\x00\x00R\x8bU\x08R\xff\xd0j\x00\x8b\x95\xef\x00\x00\x00R\xff\xd0\xe91\xff\xff\xff\x89\xc1\x89\x8dG\x01\x00\x00\x8b\x85d\x01\x00\x00\x89\xe2\x8bR\x08\x8bZ<\x01\xdaRj\x00\x83\xc2\x04\x89\xd7)\xda\x83\xea\x04\x8b_PSRQ\x8b\x9d\xef\x00\x00\x00S\xff\xd0Z1\xff1\xc0f\x8bz\x06\x81\xc2\xf8\x00\x00\x001\xdbf9\xfbt=f\xb8(\x00Rf\xf7\xe3Z\x01\xc2PRj\x00\x8bB\x10P\x89\xe0\x8b@\x18\x03B\x14P\x8bB\x0c\x03\x85G\x01\x00\x00P\x8b\x85\xef\x00\x00\x00P\x8b\x85d\x01\x00\x00\xff\xd0ZX)\xc2fC\xeb\xbej\x00j\x04\x89\xe0\x8b@\x10\x8bX<\x01\xd8\x83\xc0\x18\x8dX\x1cS\x8b\x9dK\x01\x00\x00\x81\xc3\xa4\x00\x00\x00\x8b\x1b\x83\xc3\x08S\x8b\x9d\xef\x00\x00\x00S\x8b\x85d\x01\x00\x00\xff\xd0\x8b\x9dK\x01\x00\x00\x81\xc3\xb0\x00\x00\x00\x89\xe7\x8b\x7f\x08\x8bG<\x01\xc7\x83\xc7\x18\x83\xc7\x10\x8b?\x03\xbdG\x01\x00\x00\x89;\x8b\x85h\x01\x00\x00\x8b\x9dK\x01\x00\x00S\x8b\x9d\xf3\x00\x00\x00S\xff\xd0\x8b\x9d\xf3\x00\x00\x00S\x8b\x85l\x01\x00\x00\xff\xd0]\xc3')
However, this is very ugly, so I created a colorful and pretty template for the decrypted data within 010Editor in order to make better sense of it visually. Now we can see that the data is mostly a few function names and a shellcode buffer used in order to inject the final payload into RegAsm.exe
.
One thing that I tend to do when triaging loaders is to find the beginning of what is likely the encrypted content of the payload in order to find functions that cross-reference these buffers. I was able to locate a very large buffer (0x46600
bytes long) at 0x428038
, as well as a smaller buffer (0x31
bytes long) at 0x428000
.
A function located at 0x41d4d0
references both of these buffers and taking a look at the function—my suspicions of these buffers being the next-stage payload and its corresponding decryption key were confirmed.
Taking a look at the function located at 0x41d4d0
, we can see telltale signs of the RC4 encryption algorithm:
With this information, I set up a Binary Refinery pipeline to decrypt the final payload:
ef 93.exe | \
vsnip 0x428038:0x46600 | \
rc4 h:22a43b87df1edee294decd10f5e85c468fccf9fdda2e48841717965abedcd61ce4dbe9f3e0c9ca66fcea73762a5b0e5c53 | \
dump stage4.bin
Unit | Name | Definition |
---|---|---|
ef |
Emit File | Places a file into the pipeline |
vsnip |
Virtual Snip | Snips (extracts) data from PE/ELF/MACHO files based on virtual offset |
rc4 |
RC4 | RC4 decrypts the data in the pipeline, given a key |
dump |
Dump | Dumps the data stored in each chunk to disk |
Stage 4 - LummaStealer
Also known as: LummaC2 Stealer
SHA-256 | Filename |
---|---|
0cf55c7e1a19a0631b0248fb0e699bbec1d321240208f2862e37f6c9e75894e7 |
N/A |
Loading the LummaStealer sample in Binary Ninja, we see the following function:
Taking a look at the function sub_432130
, we immediately come across a problem:
Opaque Predicates
Here, we have an example of an obfuscation technique called Opaque Predicates. The jumps to the next section of code are obfuscated by making their destination the result of a mathematical operation. Typically, we would deal with these via patching, which is possible (this is not at the same place in code, but is an example of this technique):
However, I was recently informed by Xusheng from the Binary Ninja / Vector35 team (huge shoutouts to the team!) of a better way to tackle this:
By default, Binary Ninja believes that the value defined at data_440fe8
and data_440fec
can be modified by the program. Although this may be true, we know that this is likely not the case. With this in mind, if we convert the types—which are by default void*
—to const int32_t
, Binary Ninja can do its magic (dataflow analysis) in order to solve the opaque predicate for us!
Just like that, we can save our precious reverse-engineering time (and sanity...)! I originally was manually patching a whole bunch of these, and let me tell you—it was miserable.
However, going through the code a little more, we hit yet another roadblock:
In this case, the value data_440ffc
holds the address of 2 possible values used in order to calculate the destination. If we take a look at data_440ffc
, right now, it is only showing up as a void*
:
Let's go ahead and change this to a const int32_t[2]
in order to correctly reflect its type.
Now, if we change the type of data_441004
to const int32_t
, we can now see that the variable named data_440ffc
has automatically been changed to jump_table_440ffc
:
Going back to our function, we now see that the dataflow analysis has taken care of the opaque predicate! (and left two more of them in its wake...)
We'll have to go and do this a whole bunch of times, but it is still much better than calculating the location of the jump and patching it all manually (by a long shot).
After patching up the functions called by the main method, we have a much cleaner look at the binary. Let's move our focus over to the function located at 0x409f50
.
API Hash Resolution
Here, we come across a case of API Hash Resolution. The function sub_434a60
is used to take a module (data_4431bc
, which is a pointer to the base address of WinHttp.dll
) and a corresponding hash in order to resolve a function for further use.
I won't showcase sub_434a60
here, as it goes out of scope for this post—but this function essentially goes through the exports of WinHttp.dll
, hashes all the function names, and returns a pointer to the function matching the provided hash.
I was able to deduce that this copy of LummaStealer is utilizing a hashing algorithm, namely FNV-1a
with a modified offset. I went ahead and added this modified hashing algorithm to the hashdb project.
Now that the modified hashing algorithm has been deployed within hashdb, we can go ahead and simply utilize the hashdb plugin within Binary Ninja to find the names of the APIs used:
Decrypting C2 Addresses
Now that we have both the opaque predicates and API hash resolution out of the way, let's try to find the C2 addresses for LummaStealer.
Within the function that resolves the WinHttp
functions, we see a variable being assigned to a list of pointers. If we investigate this further, we see that the list of pointers contains what looks to be base64 encoded strings. However, if we try to base64 decode the strings, we do not end up with readable text. Let's dig deeper to see how these strings are decrypted!
In this case, it seems that each string is passed in as the first argument to a function at 0x00409cb0
.
Let's take a further look at that function:
At the beginning, we see that the length of the current encrypted C2 address is being calculated, alongside a call to a function at 0x00409e10
which calculates the length of the blob, if you were to base64 decode it. This is followed by a function that actually base64 decodes the data.
Continuing through the function, we see the following code:
This code takes the first 32 (0x20)
bytes of the decoded blob as a key and XORs the rest of the data with it. The resulting output is a C2 address for LummaStealer!
With this in mind, I set up the following Binary Refinery pipeline in order to decrypt the LummaStealer C2 addresses:
ef stage4.bin \
| vsnip 0x438df8:0x451 \
| carve b64 -n 5 [ \
| b64 \
| push [ \
| snip :32 \
| pop key \
] \
| snip 32: \
| xor var:key \
| defang \
| cfmt "{}\n" \
]
associationokeo[.]shop
turkeyunlikelyofw[.]shop
pooreveningfuseor[.]pw
edurestunningcrackyow[.]fun
detectordiscusser[.]shop
relevantvoicelesskw[.]shop
colorfulequalugliess[.]shop
wisemassiveharmonious[.]shop
sailsystemeyeusjw[.]shop
Unit | Name | Definition |
---|---|---|
ef |
Emit File | Places a file into the pipeline |
vsnip |
Virtual Snip | Snips (extracts) data from PE/ELF/MACHO files based on virtual offset |
carve |
Carve | Extracts pieces of the pipeline that matches a given format—in this case, base64 with a minimum length of 5 characters |
b64 |
Base64 | Base64 decodes each chunk in the pipeline |
push |
Push | Temporarily sets aside the current chunk of data and replaces it with a new chunk. This is useful if you want to perform operations on a piece of data while keeping the original data intact for later use. Think of this as a way to create a copy of the data in order to do some work on the data, before restoring the original data. |
snip |
Snip | On the copy of the data, retrieves (snips) the first 32 bytes, which is the XOR key |
pop |
Pop | Places the modified copy of the data into a meta-variable. Meta-variables can be later utilized with the var keyword |
snip |
Snip | On the original data, retrieves (snips) everything after the first 32 bytes, which is the encrypted C2 address |
xor |
XOR | Performs an exclusive-or operation on the data within the chunk with the popped key |
defang |
Defang | Defangs indicators within the pipeline |
cfmt |
Convert to ForMaT | Transforms each chunk in the pipeline by applying a string format operation |
And now, we can happily say that we actually know what this infection chain is, how it works, and we've successfully retrieved the final payload and its C2 addresses. Thanks for reading! 💖
Indicators of Compromise:
IoC | Description |
---|---|
https[:]//github[.]com/ravindrauppalapati/RoleManager/releases/tag/Client |
Sample Download URL |
8ed6a84101dfcafeac6ddbf5020312b0094576fd3f9106f7df460e1b8a7bd5e1 |
Sample ZIP |
94edf5396599aaa9fca9c1a6ca5d706c130ff1105f7bd1acff83aff8ad513164 |
QuartzDahlia EXE |
515d025ba2aa1096f65c13569de283b83d86824d08ca48c1fc3bc407d4cf3266 |
AmberAmethystDaisy PHB |
194.147.35[.]251 |
AmberAmethystDaisy Event Server |
https[:]//pastebin[.]com/raw/md5jVrEB |
AmberAmethystDaisy Dead-Drop |
https[:]//t[.]me/+JBdY0q1mUogwZWMy |
AmberAmethystDaisy Telegram |
http[:]//78.47.105[.]28/auto/b0573cef5fbfef5a15e8a6527080ad25/93.exe |
QuartzBegonia Download URL |
5b751d8100bbc6e4c106b4ef38f664fb031c86f919c3e2db59a36c70c57f54e0 |
QuartzBegonia EXE |
0cf55c7e1a19a0631b0248fb0e699bbec1d321240208f2862e37f6c9e75894e7 |
DiamondDaffodil Shellcode |
d6a40534d8a76509605e67ead55ef3506050c7df86701db13443d091c7a4bce2 |
LummaStealer EXE |
associationokeo[.]shop |
LummaStealer C2 |
turkeyunlikelyofw[.]shop |
LummaStealer C2 |
pooreveningfuseor[.]pw |
LummaStealer C2 |
edurestunningcrackyow[.]fun |
LummaStealer C2 |
detectordiscusser[.]shop |
LummaStealer C2 |
relevantvoicelesskw[.]shop |
LummaStealer C2 |
colorfulequalugliess[.]shop |
LummaStealer C2 |
wisemassiveharmonious[.]shop |
LummaStealer C2 |
sailsystemeyeusjw[.]shop |
LummaStealer C2 |
P.S - Huge thanks to my friend donaldduck8 for proofreading this post, be sure to check out his blog at https://sinkhole.dev