Alright here is the summation of this series of articles, it shows you a primitive algorithm for finding such functions, addressing these functions, and then finally calling on these functions. The only things that I did not implement were, an algorithm to reconstruct higher level functions (I figured this was a different project entirely), an algorithm to determine exactly the number of variables that a function expects to be passed to it (the solution I came to works fine in most circumstances), and finally I didn’t write a method that would take the non-exported functions and put them into a dll that would export them (this is primarily meant to be a POC, so this is something that someone could easily come along later and add).
Now I’ll more-or-less explain to you how the code operates. First we give the python script the path of an executable whose functions we want to gain access too. The script then takes the PE and maps it into memory thus allowing us to manipulate said functions. We now go through and scan for functions using our simple algorithm, all the while recording the ends and beginnings of the functions. The next step is to attempt to math the beginnings of the procedures with the ends, thus allowing us to see the function in it’s entirety. This is done in calculate_function_ranges, and let me say, it was an annoying function to write. Following this we are able to go through and make an estimate as to the number of bytes that are allowed for automatic variables in the function. You can also see that I left room open for getting data about nested procedures, as doing so wouldn’t be too terrible. Basically you would just need to convert the physical offset associated with the procedure to it’s virtual address, then scan through all other procedures and see if at any point a call is made to this address. I however, am not going to implement this, at least at this time.
At this point we have the ability to actually call such functions, but doing so is a bit awkward so I’ll attempt to explain here. So, you are allowed to call a function and give it parameters, and in the current code a maximum of 6 parameters are allowed. Now the reason for this is pretty simple, basically I couldn’t figure out a way to take a tuple for instance, and convert it a C pointer thus allowing us to more sensibly pass different amounts of arguments. I also tried a couple other potential solutions and they failed as well, if someone can come up with a way to do this more cleanly, I’d love to hear it. Everything else concerned with calling functions is pretty straightforward, just give the function number, arguments, number of arguments, and you’re good to go. The python function, call_function just makes a call to an asm function, switch_call that I wrote and stuck in a dll, which then makes a call to the actual remote function. The dll + asm, combination was what I considered the most effective way to make the call, most other solutions I considered would have required functions pointers.
This is basically how the software works, feel free to play around with it, improve it, etc.
Here is the source code to the main python file.
# Creative Commons - Aaron Burrow
import mmap
import math
import sys
import ctypes
# When dealing with function locations, we will deal with physical offsets exclusively unless otherwise
# noted at some point. (Some operations will require that we acquire and use the virtual address)
class export_functions:
map = None
def __init__(self, file_name=''):
if file_name != '':
self.map_file(file_name)
return None
def __del__(self):
if self.map != None:
self.map.close()
def map_file(self, file_name):
try:
file = open(file_name, "r+b")
self.map = mmap.mmap(file.fileno(), 0)
except IOError:
print "Failed to create map!"
return None
def get_pattern_physical_offsets(self, pattern):
occurences = []
temp = 0
while True:
temp = self.map.find(pattern, temp+1)
if temp == -1:
break
else:
occurences.append(temp)
return occurences
def find_all_potential_functions(self):
# Setup Stack Frames
func_beg = chr(0x55) + chr(0x89) + chr(0xE5)
# LEAVE, RET endings
func_end = chr(0xC9) + chr(0xC3)
# POP EBP.. endings
alt_func_end = chr(0x89) + chr(0xEC) + chr(0x5D)
starts = self.get_pattern_physical_offsets(func_beg)
ends = self.get_pattern_physical_offsets(func_end)
alt_ends = self.get_pattern_physical_offsets(alt_func_end)
# Now we displace the ends so that the range actually goes to the last byte of the procedure
ends = [x + 1 for x in ends]
alt_ends = [x + 3 for x in alt_ends]
return self.calculate_function_ranges(starts, ends + alt_ends)
def calculate_function_ranges(self, starts, ends):
s = len(starts) - 1
ret = [[-1]*2 for i in range(len(starts) if len(starts) > len(ends) else len(ends))]
starts.sort()
ends.sort()
while s >= 0:
e = 0
while e < len(ends):
if starts[s] < ends[e]:
break
e += 1
ret[s][0] = starts[s]
if e != len(ends):
ret[s][1] = ends[e]
ends[e] = 0
s -= 1
if len(ends) > len(starts):
s = len(starts)
while s < len(ends):
ret[s][0] = -1
while e < len(ends):
if ends[e] != 0:
ret[s][1] = ends[e]
ends[e] = 0
break;
e += 1
s += 1
return ret
def get_function_info(self, ranges):
# This function will return informatio
locals = self.get_amount_local_variables(ranges)
nesteds = self.get_nested_procs(ranges)
return [(ranges[i][0], ranges[i][1], locals[i], nesteds[i]) for i in range(len(ranges))]
def get_amount_local_variables(self, ranges):
ret = [0 for i in ranges]
i = 0
while i < len(ranges):
if self.map[(ranges[i][0]+3):(ranges[i][0]+5)] == chr(0x83) + chr(0xEC):
ret[i] = ord(self.map[ranges[i][0]+5])
else:
ret[i] = -1
i += 1
return ret
def get_nested_procs(self, ranges):
ret = ["Not Implemented" for i in ranges]
# This will require virtual address translation
return ret
def display_function_info(self, info):
print "Function Information\n(-1 Signafies that the Information is Unavailable)\n(All Numbers are Base 10)\nFunc. #) Physical Start - Physical Stop - Local Variable Bytes - Nested Functions"
for i in range(len(info)):
sys.stdout.write(str(i) + ") " + str(info[i][0]) + " - " + str(info[i][1]) + " - " + str(info[i][2]) + " - " + str(info[i][3]) + "\n")
def call_function(self, fn, n_args = 0, *args):
# We just allow for a standard 6 arguments
temp = [x for x in args]
while len(temp) < 6:
temp.append(0)
code = ctypes.c_char_p(self.map[self.find_all_potential_functions()[fn][0]:self.find_all_potential_functions()[fn][1]+1])
return ctypes.windll.export_helper.switch_call(code, temp[0], temp[1], temp[2], temp[3], temp[4], temp[5], n_args)
Now here is the dll code that we need, the assembly dialect here is FASM.
; Creative Commons - Aaron Burrow
mat PE GUI 4.0 DLL
entry DllEntryPoint
include 'win32a.inc'
section '.code' code readable executable
proc DllEntryPoint hinstDLL,fdwReason,lpvReserved
mov eax, 0x1
ret
endp
; long switch_call(char * func, void * args, int n_args);
proc switch_call func, arg0, arg1, arg2, arg3, arg4, arg5, n_args
mov ecx, [n_args]
test ecx, ecx
jz func_call
here:
push [arg0 + (ecx - 1) * 4]
loop here
func_call:
call [func]
ret
endp
section '.idata' import data readable writeable
library kernel,'KERNEL32.DLL',\
user,'USER32.DLL'
section '.edata' export data readable
export 'Ret.DLL',\
switch_call,'switch_call'
section '.reloc' fixups data discardable
Now I’ll show you a simple example.
We will take the following PE and use the only function in it.
; Simple Example
include 'win32ax.inc'
section '.text' code readable executable
trial:
push ebp
mov ebp, esp
mov eax, [ebp + 0x8]
add eax, [ebp + 0xC]
mov ecx, 0x0A
mul ecx
mov esp, ebp
pop ebp
ret
Now we will want to make these calls within our python script.
x = export_functions(“C:\Users\Burrows\Code\FunctionExport\exportme.exe”)
x.display_function_info(x.get_function_info(x.find_all_potential_functions()))
print x.call_function(0, 2, 5, 4)
del(x)
Which will result in this output.
Function Information
(-1 Signafies that the Information is Unavailable)
(All Numbers are Base 10)
Func. #) Physical Start – Physical Stop – Local Variable Bytes – Nested Functions
0) 512 – 531 – -1 – Not Implemented
90
Which in fact did exactly what we wanted it to do.
I haven’t done a ton of testing with this software, although it has performed correctly in all the scenarios I’ve presented. If you find a bug, definitely let me know about it, and if you find anything cool to do with the software, then let’s hear it.
Until later.