Skip to content

Instantly share code, notes, and snippets.

@mgeeky
Created May 20, 2022 16:38
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mgeeky/b9f0e6849b8bb53dabb27c05cfe53f22 to your computer and use it in GitHub Desktop.
Save mgeeky/b9f0e6849b8bb53dabb27c05cfe53f22 to your computer and use it in GitHub Desktop.
Call WinAPI dynamically from VBA using oleaut32.DispCallFunc to minimize number of Declare PtrSafe import statements

Synopsis

This PoC is currently not working properly.

The PoC demonstrates how to dynamically call WinAPI imported functions from VBA using oleaut32!DispCallFunc(...).

The idea is to get rid of most of the Private Declare PtrSafe Function SomeFunction Lib "kernel32.dll" Alias "Sleep" ( ... ) statements, revealing intent of a dodgy VBA code.

From the offensive perspective we'd prefer to have the least amount of WinAPI import statements in our VBA to lower detection rate on the security aware scanners.

Failed approach

One approach I've tried, yet unsuccessfully was to use DispCallFunc. The reas I dub the approach unsuccessful is twofold:

  1. I was unable to avoid using LoadLibrary and GetProcAddress
  2. There are things I couldn't get to work - such as the string address resolution.

Demo

Below one can find example input file containing following VBA:

Declare PtrSafe Function MessageBox Lib "User32.dll" Alias "MessageBoxA" ( _
    ByVal hWnd As Long, _
    ByVal lpText As String, _
    ByVal lpCaption As String, _
    ByVal uType As Long _
) As Integer


Sub Auto_Open()
    MessageBox 0, "Oh! We knows! We knows safe paths for hobbitses!", "My Preeeciousssss", vbOKOnly
End Sub

We can feed it into generateDispCallFunc.py like so:

PS D:\> py .\generateDispCallFunc.py .\test.vba

Private Declare PtrSafe Function obf_DispCallFunc Lib "oleaut32.dll" Alias "DispCallFunc" ( _
    ByVal obf_pvInstance As LongPtr, _
    ByVal obf_oVft As LongPtr, _
    ByVal obf_cc As Long, _
    ByVal obf_vtReturn As Integer, _
    ByVal obf_cActuals As Long, _
    ByRef obf_prgvt As Integer, _
    ByRef obf_prgpvarg As LongPtr, _
    ByRef obf_pvargResult As Variant _
) As Long

Private Declare PtrSafe Function obf_LoadLibrary Lib "kernel32" Alias "LoadLibraryA" ( _
    ByVal obf_lpLibFileName As String _
) As LongPtr

Private Declare PtrSafe Function obf_GetProcAddress Lib "kernel32" Alias "GetProcAddress" ( _
    ByVal obf_hModule As LongPtr, _
    ByVal obf_lpProcName As String _
) As LongPtr

Private obf_ptr_MessageBoxA As LongPtr

Sub obf_ResolveFunctions()
    Dim obf_user32
    obf_user32 = obf_LoadLibrary("user32.dll")

    obf_ptr_MessageBoxA = obf_GetProcAddress(obf_user32, "MessageBoxA")
End Sub

'
' dynamically invokes user32.dll!MessageBoxA
'
Private Function obf_wrap_MessageBox ( _
    ByVal obf_hWnd As Long, _
    ByVal obf_lpText As String, _
    ByVal obf_lpCaption As String, _
    ByVal obf_uType As Long) As Integer

    Dim obf_lDispCallFuncResult As Long
    Dim obf_vFuncResult As Variant
    Dim obf_iVarTypes(0 To 3) As Integer
    Dim obf_lVarPntrs(0 To 3) As LongPtr
    obf_vFuncResult = Empty

    Dim obf_hWnd_vl As Variant
    Dim obf_lpText_vl As Variant
    Dim obf_lpCaption_vl As Variant
    Dim obf_uType_vl As Variant

    obf_hWnd_vl = obf_hWnd
    obf_lpText_vl = StrConv(obf_lpText_vl, vbFromUnicode)
    obf_lpCaption_vl = StrConv(obf_lpCaption_vl, vbFromUnicode)
    obf_uType_vl = obf_uType

    obf_iVarTypes(0) = VarType(obf_hWnd_vl)
    obf_lVarPntrs(0) = VarPtr(obf_hWnd_vl)

    obf_iVarTypes(1) = VarType(obf_lpText_vl)
    obf_lVarPntrs(1) = VarPtr(obf_lpText_vl)

    obf_iVarTypes(2) = VarType(obf_lpCaption_vl)
    obf_lVarPntrs(2) = VarPtr(obf_lpCaption_vl)

    obf_iVarTypes(3) = VarType(obf_uType_vl)
    obf_lVarPntrs(3) = VarPtr(obf_uType_vl)

    obf_lDispCallFuncResult = obf_DispCallFunc( 0, _
        obf_ptr_MessageBoxA, _
        4, _
        VbVarType.vbLong, _
        4, _
        obf_iVarTypes(0), _
        obf_lVarPntrs(0), _
        obf_vFuncResult )

    obf_wrap_MessageBoxA = obf_vFuncResult
End Function

Sub Auto_Open()

    obf_ResolveFunctions
    obf_wrap_MessageBox 0, "Oh! We knows! We knows safe paths for hobbitses!", "My Preeeciousssss", vbOKOnly
End Sub

The code generated was supposed to invoke our User32!MessageBoxA but without import statements ( ノ ゚ー゚)ノ

Sharing it anyway, maybe someone will pick it up and finish what I've started :)

import re, sys, os
import string
import argparse
import json
class DispCallFuncGenerator:
# & -> Long
# $ -> String
# % -> Integer
# # -> Double
# ! -> Single
# @ -> Decimal
SymbolNameRegexSingle = r'\w_\$\&'
VariableNameRegexSingle = SymbolNameRegexSingle
SymbolNameRegex = r'[' + SymbolNameRegexSingle + r']+'
VariableNameRegex = r'[' + VariableNameRegexSingle + r']+'
regexes = {
'functionParams' : r'^(?:(?:Public|Protected|Private)\s+)?(?:(?<!End)(?:Function|Sub)\s+)' + SymbolNameRegex + r'\s*\((?![\(])(.*)(?<![\)])\)',
'functionParamNameAndType' : r'(ByVal|ByRef)?\s*((?!ByVal|ByRef)' + VariableNameRegex + r')(?:\s+As\s+(\w+))?',
'functionParamNameAndType2' : r'(?<=ByVal|ByRef)\s*(' + VariableNameRegex + r')\s+As\s+([\w_\.]+)',
'declareFunction' : r'(?:Private|Protected|Public)?\s*Declare\s+(?:PtrSafe\s+)?(?:Sub|Function)\s+(' + SymbolNameRegex + r')\s+Lib\s*#?"([^"]+)"#?(?:\s*Alias\s*#?"([^"]+)"#?)?',
'declareFunctionEnd' : r'.*\)\s*(?:As\s*([^\s]+)\s*)?\s*$',
}
Auto_Runs = (
'AutoOpen', 'Auto_Open', 'Document_', 'Workbook_', 'Document', 'Workbook'
)
callconv = {
'fastcall': 0,
'cdecl': 1,
'mscpascal': 2,
'pascal': 2,
'macpascal': 3,
'stdcall': 4,
'fpfastcall': 5,
'syscall': 6,
'mpwcdecl': 7,
'mpwpascal': 8,
'max': 8,
}
def __init__(self):
self.functions = {}
self.addedResolveFuncsCall = False
def extractDeclares(self, declares):
lines = declares.split('\n')
restOfCode = ''
outlines = []
i = 0
lastDeclareLine = 0
openDeclare = False
while i < len(lines):
line = lines[i]
if len(line) < 5:
i += 1
continue
if line.strip().startswith('end '):
continue
match = re.search(DispCallFuncGenerator.regexes['declareFunction'], line.strip(), re.I)
match2 = re.search(DispCallFuncGenerator.regexes['declareFunctionEnd'], line.strip(), re.I)
if match:
outlines.append(line)
openDeclare = True
lastDeclareLine = i
if match2:
i += 1
lastDeclareLine = i
openDeclare = False
continue
j = 1
found = False
if openDeclare:
while j + i < len(lines):
line2 = lines[i+j]
if len(line2) < 3:
j += 1
continue
outlines.append(line2)
match2 = re.search(DispCallFuncGenerator.regexes['declareFunctionEnd'], line2.strip(), re.I)
if match2:
i += j
lastDeclareLine = i
found = True
openDeclare = False
break
j += 1
if not found:
i += 1
if lastDeclareLine == 0: lastDeclareLine = 1
restOfCode = '\r\n'.join(lines[lastDeclareLine-1:]) + '\r\n\r\n'
out = ('\r\n'.join(outlines), restOfCode)
return out
def reformatDeclareLines(self, text):
combined = text.replace(' _\r\n', ' ').replace(' _\n', ' ')
combined = re.sub(r'[ ]{1,}', ' ', combined)
out = ''
for line in combined.split('\n'):
if len(line.strip()) == 0 or line.strip().startswith('#'):
continue
line = line.strip()
pos = line.find('(')
pos2 = line.rfind(')')
out += line[:pos + 1] + ' _\r\n'
params = line[pos + 1:pos2].split(',')
for i in range(len(params)):
param = params[i]
param = param.strip()
if param.lower().startswith('byval') or param.lower().startswith('byref'):
pass
else:
param = 'ByVal ' + param
if i == len(params) - 1:
out += f' {param} _\r\n'
else:
out += f' {param}, _\r\n'
out += line[pos2:] + '\r\n\r\n'
#out = re.sub(r'\s*\(\s*_\s*\r\n\s*', ' ( ', out)
#out = out.replace('_\r\n)', ')')
return out
def parseDeclares(self, declares):
declares = self.reformatDeclareLines(declares)
lines = declares.split('\n')
func = ''
function = {}
i = 0
while i < len(lines):
line = lines[i]
m = re.search(DispCallFuncGenerator.regexes['declareFunction'], line, re.I)
if m:
if len(function) > 0:
self.functions[func] = function
function = {}
func = m.group(1)
lib = m.group(2).lower()
if not lib.endswith('.dll'):
lib += '.dll'
alias = ''
if len(m.groups()) >= 3 and m.group(3) != None:
alias = m.group(3)
importName = func
if len(alias) > 0:
importName = alias
function = {
'name' : func,
'symbol' : importName,
'lib' : lib,
'alias' : alias,
'returns' : '',
'params': []
}
i += 1
continue
params = []
for n in re.finditer(DispCallFuncGenerator.regexes['functionParamNameAndType'], line, re.I):
paramRef = 'ByVal'
if n.group(1) == None:
break
if n.group(1).lower() == 'byref':
paramRef = 'ByRef'
paramName = n.group(2)
paramType = n.group(3)
if not paramName.startswith('obf_'):
paramName = 'obf_' + paramName
param = {
'ref' : paramRef,
'name' : paramName,
'type' : paramType,
}
params.append(param)
function['params'].append(param)
if len(params) > 0:
i += len(params)
continue
o = re.search(DispCallFuncGenerator.regexes['declareFunctionEnd'], line, re.I)
if o:
returns = o.group(1)
function['returns'] = returns
i += 1
if func not in self.functions.keys():
self.functions[func] = function
return self.functions
def normalizeCode(self, code):
out = code.replace('\r\n', '\n').replace('\n', '\r\n')
out = re.sub(r'(?:\r\n){2,}', '\r\n\r\n', out).strip()
return out
def generate(self):
code = ''
code += self.reformatDeclareLines(f'''
Private Declare PtrSafe Function obf_DispCallFunc Lib "oleaut32.dll" Alias "DispCallFunc" ( _
ByVal obf_pvInstance As LongPtr, _
ByVal obf_oVft As LongPtr, _
ByVal obf_cc As Long, _
ByVal obf_vtReturn As Integer, _
ByVal obf_cActuals As Long, _
ByRef obf_prgvt As Integer, _
ByRef obf_prgpvarg As LongPtr, _
ByRef obf_pvargResult As Variant _
) As Long
''')
code += self.reformatDeclareLines(f'''
Private Declare PtrSafe Function obf_LoadLibrary Lib "kernel32" Alias "LoadLibraryA" ( _
ByVal obf_lpLibFileName As String _
) As LongPtr
Private Declare PtrSafe Function obf_GetProcAddress Lib "kernel32" Alias "GetProcAddress" ( _
ByVal obf_hModule As LongPtr, _
ByVal obf_lpProcName As String _
) As LongPtr
''')
code += f'\r\n\r\n'
for k, v in self.functions.items():
name = v['symbol']
code += f'Private obf_ptr_{name} As LongPtr\r\n'
code += self.genResolveFunctionsSub() + '\r\n\r\n'
for k, v in self.functions.items():
code += self.genWrapper(v) + '\r\n'
return self.normalizeCode(code)
def genResolveFunctionsSub(self):
code = ''
code += f'\r\n\r\nSub obf_ResolveFunctions()\r\n'
libs = set()
for k, v in self.functions.items():
libs.add(v['lib'])
for lib in libs:
libvar = lib.replace('.dll', '')
code += f' Dim obf_{libvar}\r\n'
for lib in libs:
libvar = lib.replace('.dll', '')
code += f' obf_{libvar} = obf_LoadLibrary("{lib}")\r\n'
code += f' \r\n'
for k, v in self.functions.items():
name = v['symbol']
libvar = v['lib'].replace('.dll', '')
code += f' obf_ptr_{name} = obf_GetProcAddress(obf_{libvar}, "{name}")\r\n'
code += f'End Sub\r\n\r\n'
return code
def genWrapper(self, function):
funcName = function['symbol']
funcName2 = function['name']
code = f'''
'
' dynamically invokes {function['lib']}!{funcName}
'
'''
code += f'Private Function obf_wrap_{funcName2} ( _'
if len(function["params"]) == 0:
code += ') '
else:
code += '\r\n'
for i in range(len(function['params'])):
p = function['params'][i]
if i == len(function['params']) - 1:
code += f' {p["ref"]} {p["name"]} As {p["type"]}) '
else:
code += f' {p["ref"]} {p["name"]} As {p["type"]}, _\r\n'
code += f'As {function["returns"]}\r\n'
paramNum = len(function['params'])
if paramNum == 0:
paramNum = 1
code += f"""
Dim obf_lDispCallFuncResult As Long
Dim obf_vFuncResult As Variant
Dim obf_iVarTypes(0 To {paramNum-1}) As Integer
Dim obf_lVarPntrs(0 To {paramNum-1}) As LongPtr
obf_vFuncResult = Empty
"""
for p in function['params']:
code += f' Dim {p["name"]}_vl As Variant\r\n'
code += f' \r\n'
if funcName.endswith('A'):
for p in function['params']:
if p['type'].lower() == 'string':
code += f' {p["name"]}_vl = StrConv({p["name"]}_vl, vbFromUnicode)\r\n'
else:
if p['ref'] == 'ByVal':
code += f' {p["name"]}_vl = {p["name"]}\r\n'
else:
code += f' {p["name"]}_vl = VarPtr({p["name"]})\r\n'
else:
for p in function['params']:
if p['ref'] == 'ByVal':
code += f' {p["name"]}_vl = {p["name"]}\r\n'
else:
code += f' {p["name"]}_vl = VarPtr({p["name"]})\r\n'
num = 0
for p in function['params']:
code += f'''
obf_iVarTypes({num}) = VarType({p["name"]}_vl)
obf_lVarPntrs({num}) = VarPtr({p["name"]}_vl)
'''
num += 1
if not self.addedResolveFuncsCall:
code += f'''
If obf_ptr_{funcName} = 0 Then
obf_ResolveFunctions
End If
'''
code += f'''
obf_lDispCallFuncResult = obf_DispCallFunc( 0, _
obf_ptr_{funcName}, _
{DispCallFuncGenerator.callconv["stdcall"]}, _
VbVarType.vbLong, _
{len(function["params"])}, _
obf_iVarTypes(0), _
obf_lVarPntrs(0), _
obf_vFuncResult )
obf_wrap_{funcName} = obf_vFuncResult
'''.replace('\r', '').replace('\n', '\r\n')
code += 'End Function\r\n'
return code
def convert(self, code):
(declares, restOfCode) = self.extractDeclares(code)
callResolver = f'''
obf_ResolveFunctions
'''
for autoRun in DispCallFuncGenerator.Auto_Runs:
if autoRun.lower() in restOfCode.lower():
rex = r'(' + re.escape(autoRun) + r'\s*\(\s*\))\s*\r?\n'
restOfCode = re.sub(rex, r'\1\r\n' + callResolver, restOfCode, re.I)
self.addedResolveFuncsCall = True
break
self.parseDeclares(declares)
code = self.generate()
for funcName, func in self.functions.items():
name = func['name']
newName = f'obf_wrap_{name}'
restOfCode = re.sub(r'\b' + re.escape(name) + r'\b', newName, restOfCode)
return code + '\r\n\r\n' + restOfCode
def getopts(argv):
parse = argparse.ArgumentParser(
usage = 'generateDispCallFunc.py [options] <code.vba>'
)
req = parse.add_argument_group('Required arguments')
req.add_argument('infile', help = 'Input file containing VBA code with Declare Function used.')
args = parse.parse_args()
return args
def main(argv):
args = getopts(argv)
code = ''
with open(args.infile) as f:
code = f.read()
gen = DispCallFuncGenerator()
converted = gen.convert(code)
print(converted)
if __name__ == '__main__':
main(sys.argv)
Declare PtrSafe Function MessageBox Lib "User32.dll" Alias "MessageBoxA" ( _
ByVal hWnd As Long, _
ByVal lpText As String, _
ByVal lpCaption As String, _
ByVal uType As Long _
) As Integer
Sub Auto_Open()
MessageBox 0, "Oh! We knows! We knows safe paths for hobbitses!", "My Preeeciousssss", vbOKOnly
End Sub
Private Declare PtrSafe Function obf_DispCallFunc Lib "oleaut32.dll" Alias "DispCallFunc" ( _
ByVal obf_pvInstance As LongPtr, _
ByVal obf_oVft As LongPtr, _
ByVal obf_cc As Long, _
ByVal obf_vtReturn As Integer, _
ByVal obf_cActuals As Long, _
ByRef obf_prgvt As Integer, _
ByRef obf_prgpvarg As LongPtr, _
ByRef obf_pvargResult As Variant _
) As Long
Private Declare PtrSafe Function obf_LoadLibrary Lib "kernel32" Alias "LoadLibraryA" ( _
ByVal obf_lpLibFileName As String _
) As LongPtr
Private Declare PtrSafe Function obf_GetProcAddress Lib "kernel32" Alias "GetProcAddress" ( _
ByVal obf_hModule As LongPtr, _
ByVal obf_lpProcName As String _
) As LongPtr
Private obf_ptr_MessageBoxA As LongPtr
Sub obf_ResolveFunctions()
Dim obf_user32
obf_user32 = obf_LoadLibrary("user32.dll")
obf_ptr_MessageBoxA = obf_GetProcAddress(obf_user32, "MessageBoxA")
End Sub
'
' dynamically invokes user32.dll!MessageBoxA
'
Private Function obf_wrap_MessageBox ( _
ByVal obf_hWnd As Long, _
ByVal obf_lpText As String, _
ByVal obf_lpCaption As String, _
ByVal obf_uType As Long) As Integer
Dim obf_lDispCallFuncResult As Long
Dim obf_vFuncResult As Variant
Dim obf_iVarTypes(0 To 3) As Integer
Dim obf_lVarPntrs(0 To 3) As LongPtr
obf_vFuncResult = Empty
Dim obf_hWnd_vl As Variant
Dim obf_lpText_vl As Variant
Dim obf_lpCaption_vl As Variant
Dim obf_uType_vl As Variant
obf_hWnd_vl = obf_hWnd
obf_lpText_vl = StrConv(obf_lpText_vl, vbFromUnicode)
obf_lpCaption_vl = StrConv(obf_lpCaption_vl, vbFromUnicode)
obf_uType_vl = obf_uType
obf_iVarTypes(0) = VarType(obf_hWnd_vl)
obf_lVarPntrs(0) = VarPtr(obf_hWnd_vl)
obf_iVarTypes(1) = VarType(obf_lpText_vl)
obf_lVarPntrs(1) = VarPtr(obf_lpText_vl)
obf_iVarTypes(2) = VarType(obf_lpCaption_vl)
obf_lVarPntrs(2) = VarPtr(obf_lpCaption_vl)
obf_iVarTypes(3) = VarType(obf_uType_vl)
obf_lVarPntrs(3) = VarPtr(obf_uType_vl)
obf_lDispCallFuncResult = obf_DispCallFunc( 0, _
obf_ptr_MessageBoxA, _
4, _
VbVarType.vbLong, _
4, _
obf_iVarTypes(0), _
obf_lVarPntrs(0), _
obf_vFuncResult )
obf_wrap_MessageBoxA = obf_vFuncResult
End Function
Sub Auto_Open()
obf_ResolveFunctions
obf_wrap_MessageBox 0, "Oh! We knows! We knows safe paths for hobbitses!", "My Preeeciousssss", vbOKOnly
End Sub
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment