[0001] AmberAmethystDaisy -> QuartzBegonia -> LummaStealer

[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
  • 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.

DiamondDaffodil shellcode seen in buffer decrypted within QuartzBegonia

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.

Key and encrypted content of the final payload, located within the .data segment

Taking a look at the function located at 0x41d4d0, we can see telltale signs of the RC4 encryption algorithm:

Tip: Seeing two loops and the number 256 (0x100) is often indicative 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!

Encrypted LummaStealer C2 addresses

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