Skip to content

Commit

Permalink
Version 1.3 - Improved constructor function detection, Support 32 bit…
Browse files Browse the repository at this point in the history
… executables.
  • Loading branch information
CySHell committed Oct 31, 2022
1 parent bbd5cce commit 50f1640
Show file tree
Hide file tree
Showing 6 changed files with 145 additions and 70 deletions.
138 changes: 88 additions & 50 deletions ClassDataStructureDetection/Constructors/DetectConstructor.py
Original file line number Diff line number Diff line change
@@ -1,61 +1,99 @@
import binaryninja as bn
from typing import List
from ... import Config
from ...RttiInfomation.VirtualTableInference import VirtualFunctionTable


def detect(bv: bn.binaryview, choice: int):
# choice = 0 : Only add comment to suspected constructor
# choice = 1 : Change the name of the constructor function
for func in bv.functions:
try:
for instr in func.hlil.instructions:
# <HighLevelILOperation.HLIL_ASSIGN: 17>
if instr.operation == 17:
# Check if Arg1 is being assigned to.
func_params = func.hlil.source_function.parameter_vars.vars
if func_params and instr.vars:
if func_params[0] == instr.vars[0]:
# <HighLevelILOperation.HLIL_CONST_PTR: 27>
if instr.operands[1].operation == 27:
# <HighLevelILOperation.HLIL_DEREF: 23> De-referencing the pointer, meaning if this
# pointer is to a struct, this is de-referencing offset 0x0.
if instr.operands[0].operation == 23:
if type(instr.operands[0].operands[0]) == bn.highlevelil.HighLevelILVar:
pointer: int = instr.operands[1].operands[0]
data_refs = list(bv.get_data_refs_from(pointer))
if data_refs:
if len(data_refs) != 1:
# print(f'Error, too many data refs for {pointer}')
pass
else:
# Check if this is a function pointer
if bv.get_function_at(data_refs[0]):
constructor_addr: List[
bn.function.Function] = bv.get_functions_containing(
instr.address)
if len(constructor_addr) == 1:
class_name: str = bv.get_data_var_at(pointer).name
print(
f'Suspected constructor at - {hex(constructor_addr[0].start)},\n '
f'Class name: {class_name} \n'
f' vfTable address is - {hex(pointer)} \n\n')
if choice == 0:
AddComment(bv, constructor_addr[0].start, pointer,
class_name)
elif choice == 1:
ChangeFuncName(bv, constructor_addr[0].start, pointer,
class_name)
else:
# print(f'Error in instruction {instr}')
pass
except Exception as e:
print(f"Constructor Detection encountered an error in {func.start}! Exception: {e}")
pass
def GetVirtualTableAssignmentInstruction(func: bn.function.Function):
current_candidate_instr = None
for instr in func.hlil.instructions:
# <HighLevelILOperation.HLIL_ASSIGN: 17>
if instr.operation == 17:
# Check if Arg1 is being assigned to.
func_params = func.hlil.source_function.parameter_vars.vars
if func_params and instr.vars:
if func_params[0] == instr.vars[0]:
# <HighLevelILOperation.HLIL_CONST_PTR: 27>, <HighLevelILOperation.HLIL_CONST: 26>
if instr.operands[1].operation == 27 or instr.operands[1].operation == 26:
# <HighLevelILOperation.HLIL_DEREF: 23> De-referencing the pointer, meaning if this
# pointer is to a struct, this is de-referencing offset 0x0.
if instr.operands[0].operation == 23:
if type(instr.operands[0].operands[0]) == bn.highlevelil.HighLevelILVar:
current_candidate_instr = instr

# We are only interested in the last assignment of a vfTable in a function, since the ones before it are
# base classes.
# [
return current_candidate_instr


def GetPotentialConstructors(bv: bn.binaryview, vfTable_addr: int) -> \
List[bn.function.Function]:
# Each function that references the vfTable address has the potential to be the constructor function.
# MSVC does not place the constructor (at least for 99% of use cases) address inside the vfTable, we can
# infer that if a function references the vfTable but is contained within the table then it is not a constructor
# and probably is a destructor.
# TODO: Try to define destructors as well.
potential_constructors: List[bn.function.Function] = []
for code_ref in bv.get_code_refs(vfTable_addr):
func_containing_code_ref = code_ref.function
if func_containing_code_ref.start not in VirtualFunctionTable.global_functions_contained_in_all_vfTables:
print(f"table: {hex(vfTable_addr)}, function containing code ref: {hex(func_containing_code_ref.start)}")
potential_constructors.append(func_containing_code_ref)
return potential_constructors


def DetectConstructorForVTable(bv: bn.binaryview, vfTable_addr: int, vfTable_contained_functions: List[int]) -> bool:
found_constructors = 0
potential_constructors: List[bn.function.Function] = GetPotentialConstructors(bv, vfTable_addr)
for potential_constructor in potential_constructors:
if VerifyConstructor(bv, potential_constructor, found_constructors):
found_constructors += 1
return found_constructors != 0


def VerifyConstructor(bv: bn.binaryview, potential_constructor: bn.function.Function, found_constructurs: int) -> bool:
try:
if instr := GetVirtualTableAssignmentInstruction(potential_constructor):
pointer: int = instr.operands[1].operands[0]
data_refs = list(bv.get_data_refs_from(pointer))
if data_refs:
if len(data_refs) != 1:
# print(f'Error, too many data refs for {pointer}')
pass
else:
# Check if this is a function pointer
if bv.get_function_at(data_refs[0]):
class_name: str = bv.get_data_var_at(pointer).name
if class_name.endswith("_vfTable"):
# Remove the _vfTable tag from the name
class_name = class_name[:-8]
print(
f'Suspected constructor at - {potential_constructor.start},\n '
f'Class name: {class_name} \n'
f' vfTable address is - {hex(pointer)} \n\n')
if Config.CONSTRUCTOR_FUNCTION_HANDLING == 0:
AddComment(bv, potential_constructor.start, pointer,
class_name)
elif Config.CONSTRUCTOR_FUNCTION_HANDLING == 1:
ChangeFuncName(bv, potential_constructor.start, found_constructurs,
class_name)
else:
# invalid choice
return False
return True
else:
# print(f'Error in instruction {instr}')
return False
except Exception as e:
print(f"Constructor Detection encountered an error in {potential_constructor.start}! Exception: {e}")
return False


def AddComment(bv: bn.binaryview, constructor_addr: int, vtable_addr: int, class_name: str):
bv.set_comment_at(constructor_addr, f"Suspected constructor function for class {class_name}, virtual table"
f"at {hex(vtable_addr)}")


def ChangeFuncName(bv: bn.binaryview, constructor_addr: int, vtable_addr: int, class_name: str):
bv.get_function_at(constructor_addr).name = f"{class_name}::Constructor"
def ChangeFuncName(bv: bn.binaryview, constructor_addr: int, found_constructurs: int, class_name: str):
bv.get_function_at(constructor_addr).name = f"{class_name}::Constructor_{found_constructurs}"
17 changes: 9 additions & 8 deletions Common/Utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,16 @@ def DemangleName(mangled_name: str) -> str:
return demangled_name.split(" `RTTI")[0]


try:
log_file = open(Config.LOGFILE_FULL_PATH, 'w')
except FileNotFoundError:
log_file = open(Config.LOGFILE_FULL_PATH, 'w+')
if log_file:
log_file.close()
log_file = open(Config.LOGFILE_FULL_PATH, 'w')
if Config.ENABLE_LOGGING or Config.ENABLE_DEBUG_LOGGING:
try:
log_file = open(Config.LOGFILE_FULL_PATH, 'w')
except FileNotFoundError:
log_file = open(Config.LOGFILE_FULL_PATH, 'w+')
if log_file:
log_file.close()
log_file = open(Config.LOGFILE_FULL_PATH, 'w')


def LogToFile(log_str: str):
if Config.ENABLE_LOGGING:
if Config.ENABLE_LOGGING or Config.ENABLE_DEBUG_LOGGING:
log_file.write(f'\n {log_str} \n')
10 changes: 9 additions & 1 deletion Config.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,20 @@
PTR_SIZE_X32 = INT32_T


######################################################
# User choices
######################################################
# 0 - Add comment for constructor function
# 1 - Change name of constructor function
# 2 - Do not detect constructors
CONSTRUCTOR_FUNCTION_HANDLING = 2


######################################################
# Logging
######################################################
ENABLE_LOGGING = False
ENABLE_DEBUG_LOGGING = False

COMPLETE_OBJECT_LOCATOR_RECORD_FILE = os.path.expandvars(
f'%USERPROFILE%\\{BINARYNINJA_PLUGIN_FOLDER}\\ClassyPP\\Logs\\ColRecord.txt')
CLASS_HIERARCHY_DESCRIPTORS_RECORD_FILE = os.path.expandvars(
Expand Down
2 changes: 1 addition & 1 deletion RttiInfomation/CompleteObjectLocator.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ def VerifyClassHierarchyDescriptor(self) -> bool:
f'VerifyClassHierarchyDescriptor: FAILED to defined CHD at - {hex(class_hierarchy_descriptor.base_addr)}.')
return False

def DefineVirtualFuncTable(self):
def DefineVirtualFuncTable(self) -> bool:
# Define the vfTable of this class
vfTable_address = self.GetVtableAddr()
Utils.LogToFile(f'CompleteObjectLocator: Processing vfTable at: {vfTable_address}')
Expand Down
21 changes: 19 additions & 2 deletions RttiInfomation/VirtualTableInference/VirtualFunctionTable.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import binaryninja as bn

from ...Common import Utils
from typing import *

# {vfTable_addr: [ContainedFunctions]}
global_vfTables: Dict[int, List[int]] = dict()
global_functions_contained_in_all_vfTables: List[int] = list()


class VFTABLE:

Expand All @@ -11,16 +16,28 @@ def __init__(self, bv: bn.binaryview, base_addr: int, demangled_name: str):
self.vfTable_length: int = 0
# Strip the "class " from the start of the demangled name.
self.demangled_name: str = demangled_name[6:] if demangled_name.startswith("class ") else demangled_name
self.verified: bool = False
self.contained_functions: List[int] = list()
self.verified = self.VerifyVFT()

def VerifyVFT(self) -> bool:
data_refs_from_base_addr = list(self.bv.get_data_refs_from(self.base_addr))
if len(data_refs_from_base_addr) == 0:
# This is a situation that occurs due to a bug in binary ninja - if the data var at base_addr
# is defined as a symbol from a previously loaded PDB file then binja will not recognize any data refs.
# Since we are positive at this point that this location is a vTable (since we verified the COL for this
# class) then it is safe to change its type to 'void *' in order to "fix" the binja bug and allow it to
# recognize data refs.
vTable_data_var = self.bv.get_data_var_at(self.base_addr)
vTable_data_var.type = self.bv.parse_type_string("void*")[0]
# Now we should see data refs from this address
data_refs_from_base_addr = list(self.bv.get_data_refs_from(self.base_addr))

if len(data_refs_from_base_addr) > 0:
if self.bv.get_function_at(data_refs_from_base_addr[0]):
if self.DefineVFT():
Utils.LogToFile(f'VFTABLE: verified table at address {self.base_addr}')
# Update this vfTable in the global table, this will be used later for locating constructor funcs
global_vfTables.update({self.base_addr: self.contained_functions})
Utils.LogToFile(f'VFTABLE: verified table at address {hex(self.base_addr)}')
return True
return False

Expand Down
27 changes: 19 additions & 8 deletions StartInspection.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from . import Config
from .RttiInfomation import TypeCreation
from .ClassDataStructureDetection.Constructors import DetectConstructor
from .RttiInfomation.VirtualTableInference import VirtualFunctionTable


def is_bv_valid_for_plugin(bv: bn.binaryview) -> bool:
Expand All @@ -21,21 +22,31 @@ def is_bv_valid_for_plugin(bv: bn.binaryview) -> bool:
return False


def GetUserInputs() -> bool:
choice = bn.interaction.ChoiceField("",
["Add comment for function", "Change name of function",
"Do not detect constructors"])
bn.interaction.get_form_input([choice], "Constructor functions handling mode")
Config.CONSTRUCTOR_FUNCTION_HANDLING = choice.result
return True


class InspectInBackground(bn.BackgroundTaskThread):

def __init__(self, bv: bn.binaryview):
bn.BackgroundTaskThread.__init__(self, "ClassyPP - Performing inspection and extraction...", True)
self.bv = bv

def run(self):
choice = bn.interaction.ChoiceField("",
["Add comment for function", "Change name of function",
"Do not detect constructors"])
bn.interaction.get_form_input([choice], "Constructor functions handling mode")
if self.RTTI_inspection():
if choice.result != 2:
# choice = 2 : Do not detect constructors
DetectConstructor.detect(self.bv, choice.result)
if GetUserInputs():
self.RTTI_inspection()
self.DetectAndVerifyConstructor()

def DetectAndVerifyConstructor(self):
if Config.CONSTRUCTOR_FUNCTION_HANDLING != 2:
# Iterate over all found vfTables and detect their constructors
for base_addr, contained_functions in VirtualFunctionTable.global_vfTables.items():
DetectConstructor.DetectConstructorForVTable(self.bv, base_addr, contained_functions)

def RTTI_inspection(self) -> bool:
Utils.LogToFile(f'Logging filename: {Config.LOGFILE_FULL_PATH}')
Expand Down

0 comments on commit 50f1640

Please sign in to comment.