zloader: Simpler XLM and hidden encoded strings

It has been awhile since an interesting document came across my desk. The XLM and sandbox evasion checks in today’s document were different enough where I wasn’t sure if it was going to be zloader or not. But grabbing the executable and tossing it into any.run (or tria.ge, if you’re one of the cool kids) confirmed it was zloader.

Document: https://app.any.run/tasks/476caae4-d8c4-4b62-a33b-f9ce3258cd1e
Executable (any.run): https://app.any.run/tasks/7d3acbcc-d47c-408e-a42f-a8865d758f21
Executable (tria.ge): https://tria.ge/201202-6crv4myx46

Decoding Macro

As per usual, the main XLM macro is in one single column. However, it wasn’t the usual massive block of XLM code. It is conceptually the same in that variables will be set up to control the decoding loops. But rather than that information being in the XLM code itself, the XLM will grab numbers and encoded strings a different sheet. In this case, the decoding macro is in column 1 of sheet JattwdYRiEl and the encoded strings and loop control variables are in Sheet1. Here is a truncated view of the macro to make it easier to understand its flow. Auto_Open sets up a location that points to the beginning of the macro to start decoding strings. vGdNGoO jumps execution to the top of the macro.

SHEET1: Populating Variables

Sheet1 contains what looks very much like some sort of encoded string. Some of the numbers and values in Sheet1 are used to populate the counting variables in the decoding loop. Here are a few examples.

A new development is the location of the encoded strings. They are now defined names within the excel document itself. These names can be found in the Name Manager in Excel. If you look carefully, you can see that a semicolon is used to separate the encoded strings in DLZETTDk.

While the decoding mechanism itself is interesting from an academic perspective, I won’t be diving deeply into it. Suffice it to say, these encoded string gets decoded character by character and tossed into variable ovTMv. Once completed, the XLM code will write the decoded string to R114C1 and overwrite =HALT(). The next string will get decoded and written to R115C1 and so on.

Decoded Strings 1

Once the first round of strings are decoded, RETURN() will jump execution to R114C1. What follows are some more sandbox checks and variable setup. It looks like a file might get written to C:\Users\Public\Documents\. R125C1:R127C1 sets up new variables for the second round of decoding. R131C1 points execution back to the top of the decoding loop which starts the second round of decoding. Those strings will get written starting on R132C1.

Decoded Strings 2

There is a lot going on in the next block of XLM code. The .exe is downloaded to C:\Users\Public\Documents\dfdMmb2W.txt as a text file. Rundll32.exe is used to execute it. Notice the GET.WORKSPACE(1) check in R134C1. If it fails, execution jumps down to R143C1. This sets up a .vbs file, populates it with commands, and then executes it. It will ultimately do the same thing by downloading the executable and executing it with rundll32.exe.

So that’s pretty much it. The main decoding XLM code is cleaner, but the main difference is that the control variables are in another sheet and the encoded strings are defined names.

Thanks for reading!

zLoader XLM Update: Macro code and behavior change

We’ve got ourselves a change to the zloader XLM code and also some document behavior. Here’s today’s sample:



Central Loop Mechanism

The decoding part of the central loop mechanism still exists as it did before. It grabs hex characters from elsewhere in the document, decodes them, and writes those strings to new cells. However in this case, the document only runs through two rounds of this decoding.

Round 1

The first round behaves pretty much the same as it did before. It checks to see if it’s in a sandbox, checks the registry, and if VBAWarnings is turned on, the code will go back to the loop and start round 2.

Round 2

This is where the main difference lies. A series of lines get written to a file called QP0L3.vbs and then executed.


The code in the .vbs file is nothing that special. It’s just an array of URLs going through a For Each loop. The file gets downloaded and then saved as an .html to the Temp folder.

Back to Round 2

At this point, the .html file is executed with what looks to be rundll32.exe.

And that’s pretty much it! Again, not a major change, but I thought it was a noteworthy one.

Thanks for reading!

2020-08-05: Update on zloader XLM code

On August 5, 2020, @abuse_ch warned about more ZLoader activity:

It had been awhile since an XLM document had crossed my desk and I was wondering if anything had changed since the last one I did in mid-July? It hadn’t. I’ll be honest, these ones are a real pain to get through. But if you know what to look for, they’re not that difficult to unravel. I’ve even got some tips on how to become more efficient at investigating this particular document. Let’s get to it.

XLM Code Location

This version of the zloader document is interesting because the main activity takes place all in one column rather than being scattered all over the place. This column contains all of the loops and functions that will create, write, and execute the commands needed to reach out to a URL and begin downloading. The XLM commands begin in R337C185.

While the XLM code starts there, that is not where the entry point is located. It took some time to figure it out, but execution actually starts in R443C185.

This XLM code contains three basic steps.

  1. Set up variables and locations from which to grab hex characters and where to write new strings.
  2. Grab hex characters, convert them to ASCII, and write them to a new location.
  3. Jump to new location and execute XLM commands.

Step 1: Set up variables

For space-saving purposes, I copied the XLM commands to Notepad++ and removed the spaces between lines. Starting at the entry point (R443C185) and continuing down, some of these variables define the location of certain commands necessary for code execution. The highlighted variables below show where the hex characters can be found and also where to write the assembled strings.

Step 2: Convert hex to ASCII and write to new location

Things get kind of hairy here and I don’t know if there is a good way to explain this. Find “Start here” and follow the numbers in the dashed boxes.

Step 3: Jump to new location and execute

This part is pretty self explanatory. Once all of those strings have been written, jump to R17978C243 and continue XLM command execution. You may remember that these are the commands that are used to evade sandboxes.

Understanding the Pattern

Now that we understand how this flows, we can see the pattern. After round one is completed, rounds two and three each provide new locations from which to grab hex characters, convert them, write to a new location, and then execute them.

Rounds two and three produce this output:

Making Analysis More Efficent: =PAUSE()

The XLM function =PAUSE() allows for the possibility of debugging. We can make the macro do most of the work for us if we use this strategically.

Looking at the original XLM commands to set up the variables, placing =PAUSE() right after the call to =ebnSmgBKoRc() will allow all of the hex to be decoded and written to R17977C243. Once it hits =PAUSE(), execution will… pause allowing us to inspect it at our leisure. Make sure that you start the code execution at the entry point (R443C185). You need all of the variables populated so that the decoder function can run properly. Right-click on that cell and select run. =PAUSE() can also be placed right after the other two decoding functions to grab that data.

Thanks for reading!

zloader: VBA, R1C1 References, and Other Tomfoolery

The other day, @reecDeep tweeted about new behavior from zloader documents. Another document from the same campaign crossed my path and I decided to take a crack at it.

SHA256: B29C145D4B78DAED34DEA28A0A11BAB857D5583DC6A00578A877511D0D01D3D2


Row and Column References

One of the first things you may notice is that this document doesn’t have the typical letters designating the columns. Instead, this document is using the R1C1 reference style. This was not done by accident. The Excel 4.0 macros used throughout this document depend upon this format.

Getting around “Enable content”

The first sticking point was getting the ability to control execution of the macro. This proved to be a bit difficult. If the ‘Enable Content’ button is showing up, this means that some macros must exist, right? However, the VBAProject contents showed both Sheet1 and ThisWorkbook as blank.

So there were no macros… yet we were still being prompted to enable them. If you did enable them, the macro would execute with no opportunity to interrupt anything before the document would close. I decided to add a simple macro to the project to see if that would help me control execution.

I noticed that when I saved my changes, the size of the document changed.

Opening the copy while holding down the shift key brought up this security notification. Choosing “Enable Macros” allowed me to control execution and continue the analysis.

Finding the Entry Point – R27455C174

As this is an XLM 4.0 macro document, the macro commands in the cells will execute sequentially until the commands send execution path elsewhere. Possible commands for which to search would be =FORMULA or =GOTO. I started by searching for =FORMULA. Once I found one, I started to step through the macro code to see what would happen. It took a few tries, but the entry point for this document is R27455C174. From here, you can right-click that cell and select Run.

We can also see how this document makes use of the R1C1 notation. From what I understand so far, a positive number means you add that number of rows/cells to the current row/cell, and a negative number means you subtract that number of rows/cells to the current row/cell. In this case, it seems that the row being referenced is 51762 rows down and 81 columns to the left. However, I tried going to that cell but found it to be empty. I might be missing something obvious, but in the grand scheme of things, knowing exactly how this particular cell works is more of an academic exercise.

Either way, you could just right-click on the cell, choose Run, Step Into, and then Evaluate a few times get the code execution rolling. You’ll see that =GOTO(”) ends up moving you to cell R46304C95.

BLOCK 1 – R46304C95

This cell is where characters from other cells are assembled into a string. We can see that the first one is “=CLOSE(FALSE)”. We can continue evaluating all of these until we get to the =GOTO() at the bottom.

BLOCK 2 – R48037C63

That =GOTO() takes us to R48037C63. This cell fills in the cells below with the same string. The commands in the following cells take the strings from Block 1 and write them to a new location. For example, let’s look at R48038C63. It says to take the information in a cell that is 1734 rows up and 32 columns to the right and move it 14892 rows up and 20 columns to the left. This continues on until the =GOTO() at the bottom of this block.

BLOCK 3 – R33147C43 – Evasion checks

The next block starts at R33147C43. It contains everything that was written above. Let’s analyze it in pieces. The first portion contains the familiar sandbox checks. Notice how if any of those checks fail, you GOTO(R33146C43). That cell contains =CLOSE(FALSE) which immediately stops execution.

A .vbs file is then created in the C:\Users\Public folder. The lines in cells 33160-65 are written to that file which is then closed.

The next section executes the .vbs file. This file reads information from the system registry containing VBAWarnings. The output is returned to the .txt file. The .vbs file is then deleted, the .txt file is opened, read, and deleted. If the .txt file contains a 1, go back to =CLOSE(FALSE). If not, check environment. If it has a 32 in the results (which it does), GOTO(R13419C196).

BLOCK 4 – R13419C196

This brings us to yet another series of cells getting assembled into strings. We can step through them as before to the =GOTO(”).

BLOCK 5 – R28840C118

Once again, this block takes the strings from above and copies them elsewhere.

BLOCK 6 – R38562C99

And finally, we can see the final execution commands in this document. There are four URLs from which to download a file to C:\Users\Public\lxlGZ4A.html and execute it using rundll32.exe. Notice that if it fails the size check, it doesn’t go through the rest of the URLs. Instead, it immediately jumps down to the ALERT message. I’m guessing this helps to hide the rest of the URLs from showing up in Wireshark or something.


Many of the sandbox evasion techniques used are the same as before. The added difficulty was the use of (nonexistent) VBA macros, more ways to disguise the commands being run, and ways to hide the other URLs.

As always, thanks for reading.

COVID-19, Excel 4.0 Macros, and Sandbox Detection – #zloader

This email came across my desk this morning. It has a COVID-19 theme along with this .xls attachment.


First things first, we can see in the first picture below that it does say we’ve got an Excel 4.0 macro sheet, very hidden. This is good information for how to tackle this document. While olevba.py does extract the OLE stream, the output isn’t all that helpful. I don’t blame that on the tool. I believe it is due to the way this macro has been obfuscated. Either way, we can tell that something is going on here.


This blog post from SANS shows how to examine the OLE object using oledump.py. Again, the output isn’t very helpful.

Getting to the macro

How then can we examine the actual macro inside the document? Notice how the output of both olevba.py and oledump.py showed that the Excel 4.0 macrosheet was very hidden.

Google tells you that there are multiple ways to get these sheets visible. I tried a bunch of them and the method described here worked for me. You will need the following code:

Sub ShowAllSheets()
    Dim sh As Worksheet
    For Each sh In ActiveWorkbook.Sheets
    sh.Visible = True
End Sub

Copy that into the exiting macro sheet like so and click play/Run/(press F5).


After running the macro, you will see the unhidden sheet:


This is where things get interesting. We can see columns A – O full of =CHAR(xx) characters. They all get sewn together in column P and the output is tossed in column Q. I’ve hidden a few of the columns to show what’s going on a bit easier. Please pardon my anemic drawing skills.

In the section below, the formula in P1 takes all of the characters from column A, sews them together, and places them in column Q4. The rest of the commands are put together in a similar manner by continuing down column P and adding more commands to column Q. The last line in column P then says =GOTO(Q4).

Column Q: Macro Analysis and Sandbox Evasion Techniques

Let’s look at the commands in column Q section by section. I found the .pdf here to be very helpful in understanding what is going on.

This first section of the macro shows the first attempt to see if it is being executed in a sandbox. GET.WORKSPACE 19 and 42 check to see if a mouse is present and if the computer is capable of playing sounds, respectively. GET.WORKSPACE(1) checks the environment in which Microsoft Excel is running followed by the environment’s version number.


If those three are true, then copy the Excel security registry keys to C:\Users\public\1.reg.

=CALL("Shell32","ShellExecuteA","JJCCCJJ",0,"open","C:\Windows\system32\reg.exe","EXPORT HKCU\Software\Microsoft\Office\"&GET.WORKSPACE(2)&"\Excel\Security c:\users\public\1.reg /y",0,5)

Next, it waits for three seconds. It then opens 1.reg, starts at byte position 215, and reads the next 255 bytes.

=FPOS(Q10, 215)
=FREAD(Q10, 255)

What is in that section of 1.reg? Below is the highlighted section in a hex editor and Notepad. It contains the registry values for AccessVBOM and especially VBAWarnings.

1.reg gets closed and deleted. It then searches what was read (stored in cell Q12) for the string “0001”. This is the second test to see if it is in a sandbox. If it finds that string, it will close the spreadsheet. If it does not find the string “0001”, it will then attempt to download a file and save it as an .html file in C:\Users\Public\.


What does the string “0001” have to do with anything? Changing the settings for what Excel does with macros changes the registry settings for VBAWarnings. If our macro sees that VBAWarnings are set to 1, this means that “Enable all macros” have been set. And only sandboxes would have macros set to always run… right?

So as a reward for having your company’s group policy set to disable all macros, our malicious document then shows an alert, followed by a call to run the saved .html file (which by the way is actually an .exe).

=ALERT("The workbook cannot be opened or repaired by Microsoft Excel because it's corrupt.",2)

I’m not exactly sure what initial loader gets downloaded, but I will update this when I get more info.

UPDATE: Word on the street is zloader gets downloaded.

Until then, thanks for reading!