report.xls was my kind of challenge. It’s an Excel book with an macro with some relatively standard obfuscation and sandbox evasion. In analyzing the VBA, I see more and more hints that something odd is going on. Eventually I’ll extract an mp3 file with several more hints that the VBA has been stomped, replacing the p-code with something different from the VBA. When I dump the p-code and analyze it, I’ll find an image with the flag.

## Challenge

Nobody likes analysing infected documents, but it pays the bills. Reverse this macro thrill-ride to discover how to get it to show you the key.

The file contains a single Excel workbook:

root@kali# file report.xls
report.xls: Composite Document File V2 Document, Little Endian, Os: Windows, Version 10.0, Code page: 1252, 0x17: 1048576CDFV2 Microsoft Excel


Also, the prompt is flat out wrong - I love analyzing infected documents.

## Running It

When I open the document, it’s got a classic image suggesting that I’ve got the wrong version (of Word?) and I need to enable macros. But also, a message box pops up with an error: “Invalid procedure call or argument”:

Click for full size image

## RE - VBA

### General Layout

There are macros in both “ThisWorkbook” and “Sheet1”, and there’s a form:

ThisWorkbook has two functions to try to run when the workbook is opened, Workbook_Open() and Auto_Open(), and both run Sheet1.folderol:

Sub Workbook_Open()
Sheet1.folderol
End Sub

Sub Auto_Open()
Sheet1.folderol
End Sub


Sheet1 contains four custom functions (which I’ll get much more in-depth into) - GetInternetConnectedState(), rigmarole(), folderol(), and canoodle(). It also imports three functions from Windows Libraries, InternetGetConnectedState, mciSendString, and GetShortPathName:

Private Declare Function InternetGetConnectedState Lib "wininet.dll" _
(ByRef dwflags As Long, ByVal dwReserved As Long) As Long

Private Declare PtrSafe Function mciSendString Lib "winmm.dll" Alias _
"mciSendStringA" (ByVal lpstrCommand As String, ByVal _
lpstrReturnString As Any, ByVal uReturnLength As Long, ByVal _
hwndCallback As Long) As Long

Private Declare Function GetShortPathName Lib "kernel32" Alias "GetShortPathNameA" _
(ByVal lpszLongPath As String, ByVal lpszShortPath As String, ByVal lBuffer As Long) As Long


### Debugging

One approach to this (and other macro challenges / phishing docs) is to use olevba to dump the VBA and statically analyze. For more challenging documents, it’s nice to have the ability to debug, either in the document itself, in the immediate window, or in another empty document.

No macro in this workbook will run. Any time I try to start, that message box pops up. To get around this, I’ll do a combination of writing my own similar code in Python, and create a new .xls file and then drag the form into it as a copy in the macro editor. This allows me access to the form and the data it holds, and I can start to drop parts of the macros that I want to test into this new workbook as well.

### rigamarole

I’ll start in folderol, as it’s the function called by the auto_open(), but it quickly becomes clear that I’ll need to understand rigamarole to understand folderol, so I’ll go there first.

One thing I’ll notice pretty quickly is that the static strings are encoded and stored in the form. The first instruction is

onzo = Split(F.L, ".")


From my new workbook, I can run print F.L in the Immediate window to see the data:

print F.L


Given the split on ., this looks like it creates an array of hex strings. Each time one of these is referenced, it’s done so as rigamarole(onzo(#)). So to understand folderol, I need to decode these strings first.

rigmarole is fairly simple:

Function rigmarole(es As String) As String
Dim furphy As String
Dim c As Integer
Dim s As String
Dim cc As Integer
furphy = ""
For i = 1 To Len(es) Step 4
c = CDec("&H" & Mid(es, i, 2))
s = CDec("&H" & Mid(es, i + 2, 2))
cc = c - s
furphy = furphy + Chr(cc)
Next i
rigmarole = furphy
End Function


It takes a hex string four characters at a time, breaking them into two pairs, and converting each to an int. Then it subtracts the second from the first to generate a character. It does this down the input to generate a string.

I wrote a short Python script to do that based on the code in rigmarole:

#!/usr/bin/env python3

def rigmarole(hexstr):

res = ""
for i in range(0, len(hexstr), 4):
res += chr(int(hexstr[i:i+2],16) - int(hexstr[i+2:i+4],16))
return res

onzo = fl.split('.')

for i,x in enumerate(onzo):
print(f'[{i:02d}] {rigmarole(x)}')


It prints a list of the decoded strings:

root@kali# ./decode.py
[00] AppData
[01] \Microsoft\stomp.mp3
[02] play
[03] FLARE-ON
[04] Sorry, this machine is not supported.
[05] FLARE-ON
[06] Error
[07] winmgmts:\\.\root\CIMV2
[08] SELECT Name FROM Win32_Process
[09] vbox
[10] WScript.Network
[11] \Microsoft\v.png


### folderol

With those strings, I can more easily understand folderol. There’s two sections that remain.

#### Anti-VM Measures

The first is some attempts to look for certain conditions on the machine that might look like a sandbox. This is really common in phishing documents, that don’t want to take malicious actions in sandboxes. This code has two checks:

    If GetInternetConnectedState = False Then
MsgBox "Cannot establish Internet connection.", vbCritical, "Error"
End
End If

Set fudgel = GetObject(rigmarole(onzo(7)))
Set twattling = fudgel.ExecQuery(rigmarole(onzo(8)), , 48)
For Each p In twattling
Dim pos As Integer
pos = InStr(LCase(p.Name), "vmw") + InStr(LCase(p.Name), "vmt") + InStr(LCase(p.Name), rigmarole(onzo(9)))
If pos > 0 Then
MsgBox rigmarole(onzo(4)), vbCritical, rigmarole(onzo(6))
End
End If
Next


The first calls GetInternetConnectedState (which is just a wrapper around a call to the wininet.dll function InternetGetConnectedState(0&, 0&)), and throws an error message box if not connected to the internet, and then calls End to exit.

The second block is checking processes. Here’s how it looks with the strings substituted in:

Set fudgel = GetObject("winmgmts:\\.\root\CIMV2")
Set twattling = fudgel.ExecQuery("SELECT Name FROM Win32_Process", , 48)
For Each p In twattling
Dim pos As Integer
pos = InStr(LCase(p.Name), "vmw") + InStr(LCase(p.Name), "vmt") + InStr(LCase(p.Name), "vbox")
If pos > 0 Then
MsgBox "Sorry, this machine is not supported.", vbCritical, "Error"
End
End If
Next


It uses a WMI object to query for the list of processes. Then it loops over them, for each checking (case insensitively) if the strings “vmw”, “vmt”, or “vbox” are present, and if it finds anything, pops a message box and exits.

#### Create and Run File

The rest of the code creates and runs a file. Here it is (with deobfuscated strings substituted in):

xertz = Array(&H11, &H22, &H33, &H44, &H55, &H66, &H77, &H88, &H99, &HAA, &HBB, &HCC, &HDD, &HEE)

wabbit = canoodle(F.T.Text, 0, 168667, xertz)
mf = Environ("AppData") & "\Microsoft\stomp.mp3"
Open mf For Binary Lock Read Write As #fn
Put #fn, , wabbit
Close #fn

mucolerd = mciSendString("play " & mf, 0&, 0, 0)


xertz is a short array of numbers. Without looking at canoodle, I can guess that it takes the obfuscated string at F.T.Text and decodes it. I’m guessing that xertz is some kind of key. The result is written to the current users AppData\Microsoft directory as stomp.mp3. Finally, it plays the mp3 audio file using the mciSendString function.

### Decode File (canoodle)

canoodle does decode a hex string using a passed in array of xor values:

Function canoodle(panjandrum As String, ardylo As Integer, s As Long, bibble As Variant) As Byte()
Dim quean As Long
Dim cattywampus As Long
Dim kerfuffle() As Byte
ReDim kerfuffle(s)
quean = 0
For cattywampus = 1 To Len(panjandrum) Step 4
kerfuffle(quean) = CByte("&H" & Mid(panjandrum, cattywampus + ardylo, 2)) Xor bibble(quean Mod (UBound(bibble) + 1))
quean = quean + 1
If quean = UBound(kerfuffle) Then
Exit For
End If
Next cattywampus
canoodle = kerfuffle
End Function


It loops over the string four characters at a time. It takes two of those characters, creates a hex value, and then xors against a value from the input key, bibble.

The second arg, ardylo is weird, as it’s 0 and unnecessary. This is something to think about moving forward. Why did they write this such that it grabs only the first two of four characters and decodes them. There’s another half of the hex string that’s being ignored, and there’s a way to call it (with adrylo as 2) to access those instead.

Additionally, the third arg, s is basically a length check. Why is this needed if canoodle is only called once and it starts by looping over the length of the hex encoded input?

Leaving the questions aside for now, I’ll show two ways to decode this. First, in my workbook that isn’t broken, I’ll just copy a modified folderol and an unmodified canoodle into a module and run folderol:

Function canoodle(panjandrum As String, ardylo As Integer, s As Long, bibble As Variant) As Byte()
Dim quean As Long
Dim cattywampus As Long
Dim kerfuffle() As Byte
ReDim kerfuffle(s)
quean = 0
For cattywampus = 1 To Len(panjandrum) Step 4
kerfuffle(quean) = CByte("&H" & Mid(panjandrum, cattywampus + ardylo, 2)) Xor bibble(quean Mod (UBound(bibble) + 1))
quean = quean + 1
If quean = UBound(kerfuffle) Then
Exit For
End If
Next cattywampus
canoodle = kerfuffle
End Function

Function folderol()
Dim wabbit() As Byte
Dim fn As Integer: fn = FreeFile
Dim onzo() As String
Dim mf As String
Dim xertz As Variant

xertz = Array(&H11, &H22, &H33, &H44, &H55, &H66, &H77, &H88, &H99, &HAA, &HBB, &HCC, &HDD, &HEE)

wabbit = canoodle(F.T.Text, 0, 168667, xertz)
mf = "F:\04-report\stomp.mp3"
Open mf For Binary Lock Read Write As #fn
Put #fn, , wabbit
Close #fn

End Function


Alternatively, I could throw together some Python that does the same thing:

#!/usr/bin/env python3

from itertools import cycle

with open('ft-olevba.txt', 'r') as f:

key = [i*0x11 for i in range(1, 15)]
out = b''
for i in range(0, len(encoded), 4):
out += bytes([int(encoded[i:i+2], 16) ^ key[(i//4) % len(key)]])
if i > 168667:
break

with open('stomp.mp3', 'wb') as f:
f.write(out)


To get the hex string, I ran olevba against the notebook and directed all the output to a file. Then I went in and deleted all the lines around the long hex string to get just that line.

Either way, I get an mp3 file:

root@kali# file stomp.mp3
stomp.mp3: Audio file with ID3 version 2.3.0, contains:MPEG ADTS, layer III, v1, 160 kbps, 44.1 kHz, JntStereo


The song just plays a beat. But there’s a bunch of hints here. The file name is “stomp”. Looking at the information about the track, the “Title” and “Band” are also hints:

root@kali# exiftool stomp.mp3
...[snip]...
ID3 Size                        : 4096
Publisher                       : FLARE
Year                            : 2020
Title                           : This is not what you should be looking at...
Band                            : P. Code
Date/Time Original              : 2020
Duration                        : 8.23 s (approx)


“This is not what you should be looking at…” is a good hint, as is the Band, “P. Code”.

When you edit a VBA macro, the source is kept in the document, but it’s also compiled into what is called p-code, which is what is actually run. There’s a technique in VBA documents called VBA stomping, which replaces the source code for the VBA (what shows in the editor) with something different than what the compiled p-code does. Because the p-code is os / architecture specific, this will break the document so that it will only run on machines that match the one it was written on. That actually could explain why I’m getting errors no matter what in this file.

Evil Clippy is one tool that can be used to stomp VBA code.

## RE - p-code

### pcodedmp

To understand how p-code works, pcodedmp is a good tool to view the raw code. In addition to other things, it will show the raw pcode in each sheet / module. For example, here’s the p-code related to the rigmarole function in report.xls (with comments added by me):

Line #10:
FuncDefn (Function rigmarole(es As String, id_FFFE As String) As String)
Line #11:
Dim
VarDefn furphy (As String)
Line #12:
Dim
VarDefn c (As Integer)
Line #13:
Dim
VarDefn s (As String)
Line #14:
Dim
VarDefn cc (As Integer)
Line #15:
LitStr 0x0000 ""
St furphy
Line #16:              // for loop in lines 16-21
StartForVariable    // identify variable as i
Ld i
EndForVariable
LitDI2 0x0001
Ld es
FnLen
LitDI2 0x0004
ForStep             // sets for loop with three args, (1, Len(es), 4)
Line #17:
LitStr 0x0002 "&H"
Ld es
Ld i
LitDI2 0x0002
ArgsLd Mid 0x0003   // Mid(es, i, 2)
Concat
ArgsLd CDec 0x0001  // CDes("&H" & result of Mid)
St c                // store as c
Line #18:
LitStr 0x0002 "&H"
Ld es
Ld i
LitDI2 0x0002
LitDI2 0x0002
ArgsLd Mid 0x0003   // Mid(es, i + 2, 2)
Concat
ArgsLd CDec 0x0001  // CDes("&H" & result of Mid)
St s                // store as s
Line #19:
Ld c
Ld s
Sub
St cc              // cc = c - s
Line #20:
Ld furphy
Ld cc
ArgsLd Chr 0x0001
St furphy          // furphy = furphy + Chr(cc)
Line #21:
StartForVariable
Ld i
EndForVariable
NextVar
Line #22:
Ld furphy
St rigmarole
Line #23:
EndFunc


I can compare this to the VBA to get a feel for how it works. Arguments are pushed into a stack, and then function pull from the start and return values to that stack.

Looking through the code, I noticed some differences at line 31:

Line #31:
Dim
LitDI2 0x0000
LitDI2 0x0007
VarDefn buff (As Byte)


That’s a new array of bytes being defined, buff, which isn’t in the VBA. Later at line 53, there’s more:

Line #53:
SetStmt
LitDI2 0x000A
ArgsLd onzo 0x0001
ArgsLd rigmarole 0x0001
ArgsLd CreateObject 0x0001
Set groke         // groke = CreateObject(rigmarole(onzo(0xA)))

Line #54:
Ld groke
MemLd UserDomain
St firkin        // firkin = groke.UserDomain


This code has definitely been stompped.

### pcode2code

In Googling around for p-code references, I found pcode2code. It will try to convert p-code back into VBA and display it. When I ran it against report.xls, there was a new section that replaced what where the MP3 was decoded, written to disk, and played (with rigmarole encoded strings replaced):

Set groke = CreateObject("WScript.Network")
firkin = groke.UserDomain
If firkin <> "FLARE-ON" Then
MsgBox "Sorry, this machine is not supported.", vbCritical, "Error"
End
End If

n = Len(firkin)
For i = 1 To n
buff(n - i) = Asc(Mid$(firkin, i, 1)) Next wabbit = canoodle(F.T.Text, 2, 285729, buff) mf = Environ("AppData") & "\Microsoft\v.png" Open mf For Binary Lock Read Write As #fn ' a generic exception occurred at line 68: can only concatenate str (not "NoneType") to str ' # Ld fn ' # Sharp ' # LitDefault ' # Ld wabbit ' # PutRec Close #fn Set panuding = Sheet1.Shapes.AddPicture(mf, False, True, 12, 22, 600, 310)  It’s slightly broken at the file write, but the general idea is clear. It first creates a WScript.Network object and gets the UserDomain attribute. If that is not “FLARE-ON”, it pops a message box and exits. Then it creates the key which is “NO-ERALF” (FLARE-ON backwards). The offset is two, so it is getting the bytes that were ignored in the previous decode. This time it saves as AppData\Microsoft\v.png. ### Decode Image Just like before, I can do this in the macro editor, making a function to write the file: Function dumpimage() Dim buff(0 To 7) As Byte Dim fn As Integer: fn = FreeFile Dim mf As String firkin = "FLARE-ON" n = Len(firkin) For i = 1 To n buff(n - i) = Asc(Mid$(firkin, i, 1))
Next

wabbit = canoodle(F.T.Text, 2, 285729, buff)
mf = "F:\04-report\av.png"
Open mf For Binary Lock Read Write As #fn
Put #fn, , wabbit
Close #fn

End Function


Or I can create a new Python script with slight mods to the previous:

#!/usr/bin/env python3

from itertools import cycle

with open('ft-olevba.txt', 'r') as f:

key = b'FLARE-ON'[::-1]
out = b''
for i in range(0, len(encoded), 4):
out += bytes([int(encoded[i+2:i+4], 16) ^ key[(i//4) % len(key)]])
if i//4 > 285729:
break

with open('v.png', 'wb') as f:
f.write(out)


Either way, I get an image with the flag:

Flag: thi5_cou1d_h4v3_b33n_b4d@flare-on.com@flare-on.com