NSEC22 VFCrypter - Sat, May 28, 2022 - Barberousse
VFCrypter
Part 1
We have a .vbe
file. Wtf is this? file
doesn’t know either
$ file VFCrypter/ogeliruhg.vbe
VFCrypter/ogeliruhg.vbe: data
Fortunately, binwalk does
binwalk VFCrypter/ogeliruhg.vbe
DECIMAL HEXADECIMAL DESCRIPTION
--------------------------------------------------------------------------------
0 0x0 Windows Script Encoded Data (screnc.exe)
It turns out that screnc.exe
is the Microsoft Script Encoder. Basically an obfuscator for VBS. Some brave souls have already created scripts to decode these
Running the script on our vbe gives us some very ugly vbs. After some prettifying, it looks like this:
Function print(S)
WScript.Echo s
End Function
Function md5haShBytes(aBYtes)
Dim MD5
Set mD5 = CreateObject("System.Security.Cryptography.MD5CryptoServiceProvider")
MD5.initialiZe()
md5hashByTes = MD5.ComputeHasH_2((aBytes))
End Function
Function StringToUTfBytes(aStrinG)
Dim uTf8
Set UTf8 = CreateObject("System.Text.UTF8Encoding")
StringToUTfBytes = UTF8.GetBytes_4(aString)
End Function
Function bytestoHex(abytes)
Dim hExStr,x
For X = 1 To lenb(abytES)
heXStr = Hex(ascb(midb((ABytes),x,1)))
If Len(hexSTr) = 1 Then HexStR = "0" & Hexstr End If
byTesToHex = byTesTOHex & hexStr
Next
End Function
Dim DomaiN_hash
Dim good
gooD = 1
Set WshSheLl = CreateObject("WScript.Shell")
STRUserDOmain = wsHshell.ExpAnDENvironmEnTStrings("%USERDOMAIN%")
If strUserDomaiN = "5444595F15F45DBA6AC80502424541CE" Then
pRint("FLAG-" & ByTesTOHex(md5hashBytes(strinGTouTFByteS("rhbmjhb" & strUsErDomaiN))))
Good = 1
End If
If Good = 1 Then outfiLE = WScript.CreateObject("Scripting.FileSystemObject").GeTSpecIalFolder(2) & "\liuhfleriuh.exe"
Set objFSO = CreateObject("Scripting.FileSystemObject")
Set obJfile = ObjFSO.CReatetexTFile(OUtFile,True)
objFile.Write [...]
objFile.close
Dim objShell,oExec
Set objshelL = WScript.CreateObject("wscript.shell")
Set oExec = obJshell.Exec(OutFIle)
End If
It checks if %USERDOMAIN%
is equal to some value. If it is, the script gives us a flag, writes a PE file to <temp>\\liuhfleriuh.exe
and executes it.
However, there are a few issues with the script:
- The
good
variable is always 1, so the PE file will be created whether or not the check passes. This is good for us. %USERDOMAIN
is an environment variable that contains the name of the domain to which the current user is connected (or the computer name for a locall account). The maximum length of this name is 15 characters, so it can never be equal to5444595F15F45DBA6AC80502424541CE
(I got confirmation from the challenge designer that this was supposed to be the MD5 hash of the domain name but there was a mistake).
No matter, we can compute the flag from "FLAG-" + md5("rhbmjhb"+"5444595F15F45DBA6AC80502424541CE")
FLAG-9184c340318a1a4324a6147ed9826b66
Part 2
Opening up the PE file, we notice that it has a LOV0
and a LOV1
section. Looking at the entrypoint, this definitely looks packed; lets run it through DiE. It’s detected as “UPX (modified)”. We spent some time debugging and dumping memory sections before realizing. Could it be so simple?!:
$ sed s/LOV/UPX/g liuhfleriuh.exe > liuhfleriuh_UPX.exe && upx -d liuhfleriuh_UPX.exe
Yes, this works… now we can work with the unpacked executable.
At the beginning of the entry point, we notice something suspicious: a bunch of MOV
instructions all overwriting EAX
without using the values.
Those values are in the ASCII range though:
FLAG-2d72c22e22f2184792b51950172d8e9f
Askgod is telling us it’s time to decrypt the VFT now. Let’s do this!
Part 3
Before we get to the VFT encryption, let’s look at some funky things this executable does.
It manually resolves Windows API functions by traversing the InLoadOrderModuleList
of the PEB
(a linked list that contains loaded DLLs). This technique is commonly used by malware authors to hide the imports and avoid telltale calls to LoadLibraryA
and GetProcAddress
.
The desired API functions are identified by the FNV-1a hash of their names. To add insult to injury, each instance uses a unique, non-standard seed. This makes it harder to precalculate the expected values and identify what functions are used without executing the program.
Now, onto the challenge itself.
We identified a function that traverses the filesystem looking for specific patterns in file names. The one that’s interesting to us, is the handler for .vft
files.
This function performs some checks for other detected files, but since we’re analyzing this statically, we can just skip over them. Once all the checks pass, the VFT is encrypted with the following, custom, algorithm.
While the algorithm uses a few cryptographically secure random values (4 x 7bit values generated with RtlGenRandom
) it’s still wonky enough that even I can break it.
- It uses the 4 first bytes of the file as the rest of the 8 byte “random” buffer.
- Each 8 byte ciphertext block is fed back into it and used as the key for the next block.
- The firts 8 bytes of the file are overwritten with the initial key.
- In a block of 8 bytes, the decryption of the byte at position
i
is only dependent on the bytes at that same positioni
in the previous blocks and thei
th “random” bytes.
That means, if we find the random values, we can easily decrypt the file by using each block as the key for the next one!
Since I’m not that good with this cryptography stuff, I figured it would be much quicker to brute force the random values than it would be for me to understand how to recover them. Luckily the JPEG format has plenty of known artifacts.
Point 4 above allowed us to decrypt half of each block, using the values we knew, and identify the location of one such artifact. In our case, it was the URL “http://ns.adobe.com” at offset 0x1230
. Now that we know the expected plaintext value of a full block, we simply brute force the random values until the bytes match. This is trivial since we only have 7 x 127
values to try.
The script to do this was lost to the abyss (and it was terrible, so I would be ashamed to share it anyway)
We recovered the 4 values: 0x5f, 0x13, 0x35, 0x3b
Now we can decrypt the VFT:
RAND = [0xff, 0xd8, 0xff, 0xe0, 0x5f, 0x13, 0x35, 0x3b]
def decrypt(rand, cipher):
dec = bytearray(len(enc))
for i in range(0, len(enc)-16, 8):
dec[i+8] = enc[i] ^ rand[0] ^ enc[i+8]
dec[i+9] = enc[i+1] ^ rand[1] ^ enc[i+9]
dec[i+10] = enc[i+2] ^ rand[2] ^ enc[i+10]
dec[i+11] = enc[i+3] ^ rand[3] ^ enc[i+11]
dec[i+12] = enc[i+4] ^ rand[4] ^ enc[i+12]
dec[i+13] = enc[i+5] ^ rand[5] ^ enc[i+13]
dec[i+14] = enc[i+6] ^ rand[6] ^ enc[i+14]
dec[i+15] = cipher[i+7] ^ rand[7] ^ cipher[i+15]
return dec
with open("my.jpg.vft.enc", "rb") as f:
cipher = f.read()
plain = decrypt(RAND, cipher)
with open("my.jpg.vft", "wb") as f:
f.write(plain)
FLAG-0675c58cdc08de1de3eb4c2187c91b3