You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
289 lines
9.4 KiB
289 lines
9.4 KiB
import os
|
|
import re
|
|
|
|
# If doing it on device with gdbserver
|
|
DEVICE = os.environ.get('GDBSCRIPT_ON_DEVICE', False)
|
|
# Path of the file on device
|
|
DEVICE_FILEPATH = os.environ.get('GDBSCRIPT_FILENAME', None)
|
|
# GDBServer's port
|
|
DEVICE_PORT = os.environ.get('GDBSCRIPT_DEVICE_PORT', 4444)
|
|
# Serial number of device for adb
|
|
DEVICE_SERIAL = os.environ.get('GDBSCRIPT_DEVICE_SERIAL', None)
|
|
|
|
def check_device_args():
|
|
"""
|
|
Checks if FILEPATH is provided if the execution is on device
|
|
"""
|
|
if not DEVICE:
|
|
return
|
|
|
|
if not DEVICE_FILEPATH:
|
|
raise ValueError("Filename (GDBSCRIPT_FILEPATH) not provided")
|
|
|
|
class RecordPoint(gdb.Breakpoint):
|
|
"""
|
|
A custom breakpoint that records the arguments when the breakpoint is hit and continues
|
|
Also enables the next breakpoint and disables all the ones after it
|
|
"""
|
|
def stop(self):
|
|
"""
|
|
The function that's called when a breakpoint is hit. If we return true,
|
|
it halts otherwise it continues
|
|
We always return false because we just need to record the value and we
|
|
can do it without halting the program
|
|
"""
|
|
self.args[self.times_hit % self.count] = get_function_args()
|
|
self.times_hit += 1
|
|
|
|
if self.next_bp != None:
|
|
self.next_bp.previous_hit()
|
|
|
|
return False
|
|
|
|
def previous_hit(self):
|
|
"""
|
|
This function is called if the previous breakpoint is hit so it can enable
|
|
itself and disable the next ones
|
|
"""
|
|
self.enabled = True
|
|
|
|
if self.next_bp != None:
|
|
self.next_bp.propagate_disable()
|
|
|
|
def propagate_disable(self):
|
|
"""
|
|
Disabled all the breakpoints after itself
|
|
"""
|
|
self.enabled = False
|
|
if self.next_bp != None:
|
|
self.next_bp.propagate_disable()
|
|
|
|
def process_arguments(self):
|
|
"""
|
|
Orders the recorded arguments into the right order (oldest to newest)
|
|
"""
|
|
current_hit_point = self.times_hit % self.count
|
|
# Split at the point of current_hit_point because all the entries after it
|
|
# are older than the ones before it
|
|
self.processed_args = self.args[current_hit_point:] + self.args[:current_hit_point]
|
|
self.current_arg_idx = 0
|
|
|
|
def get_arguments(self):
|
|
"""
|
|
Gets the current argument value.
|
|
Should be called the same amount of times as the function was called
|
|
in the stacktrace
|
|
First call returns the arguments recorded for the first call in the stacktrace
|
|
and so on.
|
|
"""
|
|
if self.current_arg_idx >= len(self.processed_args):
|
|
raise ValueError("Cannot get arguments more times than the function \
|
|
was present in stacktrace")
|
|
|
|
cur = self.processed_args[self.current_arg_idx]
|
|
self.current_arg_idx += 1
|
|
return cur
|
|
|
|
def init_gdb():
|
|
"""
|
|
Initialized the GDB specific stuff
|
|
"""
|
|
gdb.execute('set pagination off')
|
|
gdb.execute('set print frame-arguments all')
|
|
if DEVICE:
|
|
gdb.execute('target extended-remote :{}'.format(DEVICE_PORT))
|
|
gdb.execute('set remote exec-file /data/local/tmp/{}'.format(DEVICE_FILEPATH))
|
|
|
|
def initial_run():
|
|
"""
|
|
The initial run of the program which captures the stacktrace in init.log file
|
|
"""
|
|
gdb.execute('r > init.log 2>&1',from_tty=True, to_string=True)
|
|
if DEVICE:
|
|
if DEVICE_SERIAL:
|
|
os.system('adb -s "{}" pull /data/local/tmp/init.log'.format(DEVICE_SERIAL))
|
|
else:
|
|
os.system("adb pull /data/local/tmp/init.log")
|
|
with open("init.log", "rb") as f:
|
|
out = f.read().decode()
|
|
return out
|
|
|
|
def gdb_exit():
|
|
"""
|
|
Exits the GDB instance
|
|
"""
|
|
gdb.execute('q')
|
|
|
|
def get_stacktrace_functions(stacktrace):
|
|
"""
|
|
Gets the functions from ASAN/HWASAN's stacktrace
|
|
Args:
|
|
stacktrace: (string) ASAN/HWASAN's stacktrace output
|
|
Returns:
|
|
functions: (list) functions in the stacktrace
|
|
"""
|
|
stacktrace_start = stacktrace[stacktrace.index('==ERROR: '):].split("\n")
|
|
functions = []
|
|
|
|
# skip the first two lines of stacktrace
|
|
for line in stacktrace_start[2:]:
|
|
if line == "":
|
|
break
|
|
|
|
# Extracts the function name from a line like this
|
|
# "#0 0xaddress in function_name() file/path.cc:xx:yy"
|
|
func_name = line.strip().split(" ")[3]
|
|
if '(' in func_name:
|
|
func_name = func_name[:func_name.index('(')]
|
|
|
|
functions.append(func_name)
|
|
|
|
#remove last function from stacktrace because it would be _start
|
|
return functions
|
|
|
|
def parse_function_arguments(func_info):
|
|
"""
|
|
Parses the output of 'whatis' command into a list of arguments
|
|
"void (teststruct)" --> ["teststruct"]
|
|
"int (int (*)(int, char **, char **), int, char **, int (*)(int, char **, char **),
|
|
void (*)(void), void (*)(void), void *)" --> ['int (*)(int, char **, char **)',
|
|
'int', 'char **', 'int (*)(int, char **, char **)', 'void (*)(void)',
|
|
'void (*)(void)', ' void *']
|
|
|
|
Args:
|
|
func_info: (string) output of gdb's 'whatis' command for a function
|
|
Returns:
|
|
parsed_params: (list) parsed parameters of the function
|
|
"""
|
|
if '(' not in func_info:
|
|
return []
|
|
func_params = func_info[func_info.index('(')+1:-1]
|
|
parentheses_count = 0
|
|
current_param = ""
|
|
parsed_params = []
|
|
|
|
for token in func_params:
|
|
# Essentially trying to get the data types from a function declaration
|
|
if token == '(':
|
|
parentheses_count += 1
|
|
elif token == ')':
|
|
parentheses_count -= 1
|
|
|
|
# If we are not inside any paren and see a ',' it signals the start of
|
|
#the next parameter
|
|
if token == ',' and parentheses_count == 0:
|
|
parsed_params.append(current_param.strip())
|
|
current_param = ""
|
|
else:
|
|
current_param += token
|
|
|
|
parsed_params.append(current_param)
|
|
return parsed_params
|
|
|
|
def parse_stacktrace(stacktrace):
|
|
"""
|
|
Parses the ASAN/HWASAN's stacktrace to a list of functions, their addresses
|
|
and argument types
|
|
Args:
|
|
stacktrace: (string) ASAN/HWASAN's stacktrace output
|
|
Returns:
|
|
functions_info: (list) parsed function information as a dictionary
|
|
"""
|
|
stacktrace_functions = get_stacktrace_functions(stacktrace)[:-1]
|
|
functions_info = []
|
|
for function in stacktrace_functions:
|
|
# Gets the value right hand side of gdb's whatis command.
|
|
# "type = {function info}" -> "{function info}"
|
|
func_info = gdb.execute('whatis {}'.format(function),
|
|
to_string=True).split(' = ')[1].strip()
|
|
# Uses gdb's x/i to print its address and parse it from hex to int
|
|
address = int(gdb.execute("x/i {}".format(function),
|
|
to_string=True).strip().split(" ")[0], 16)
|
|
functions_info.append({'name': function, 'address':address,
|
|
'arguments' : parse_function_arguments(func_info)})
|
|
#In the order they are called in the execution
|
|
return functions_info[::-1]
|
|
|
|
def get_function_args():
|
|
"""
|
|
Gets the current function arguments
|
|
"""
|
|
args = gdb.execute('info args -q', to_string=True).strip()
|
|
return args
|
|
|
|
def functions_to_breakpoint(parsed_functions):
|
|
"""
|
|
Sets the breakpoint at every function and returns a dictionary mapping the
|
|
function to it's breakpoint
|
|
Args:
|
|
parsed_functions: (list) functions in the stacktrace (in the same order) as
|
|
dictionary with "name" referring to the function name
|
|
({"name" : function_name})
|
|
Returns:
|
|
function_breakpoints: (dictionary) maps the function name to its
|
|
breakpoint object
|
|
"""
|
|
function_breakpoints = {}
|
|
last_bp = None
|
|
|
|
for function in reversed(parsed_functions):
|
|
function_name = function['name']
|
|
if function_name in function_breakpoints:
|
|
function_breakpoints[function_name].count += 1
|
|
function_breakpoints[function_name].args.append(None)
|
|
continue
|
|
|
|
cur_bp = RecordPoint("{}".format(function_name))
|
|
cur_bp.count = 1
|
|
cur_bp.times_hit = 0
|
|
cur_bp.args = []
|
|
cur_bp.args.append(None)
|
|
cur_bp.next_bp = last_bp
|
|
|
|
function_breakpoints[function['name']] = cur_bp
|
|
last_bp = cur_bp
|
|
|
|
return function_breakpoints
|
|
|
|
def run(parsed_functions):
|
|
"""
|
|
Runs the whole thing by setting up breakpoints and printing them after
|
|
excecution is done
|
|
Args:
|
|
parsed_functions: A list of functions in the stacktrace (in the same order)
|
|
as dictionary with "name" referring to the function name
|
|
({"name" : function_name})
|
|
"""
|
|
names = [function['name'] for function in parsed_functions]
|
|
breakpoints = functions_to_breakpoint(parsed_functions)
|
|
|
|
#Disable all breakpoints at start
|
|
for bp in breakpoints:
|
|
breakpoints[bp].enabled = False
|
|
|
|
breakpoints[names[0]].enabled = True
|
|
|
|
gdb.execute('r')
|
|
for breakpoint in breakpoints:
|
|
breakpoints[breakpoint].process_arguments()
|
|
|
|
function_args = []
|
|
for name in names:
|
|
print("-----------")
|
|
print("Function -> {}".format(name))
|
|
|
|
function_args.append({'function':name,
|
|
'arguments' : breakpoints[name].get_arguments()})
|
|
print(function_args[-1]['arguments'])
|
|
|
|
return function_args
|
|
|
|
|
|
if __name__ == '__main__':
|
|
check_device_args()
|
|
init_gdb()
|
|
initial_out = initial_run()
|
|
function_data = parse_stacktrace(initial_out)
|
|
run(function_data)
|
|
gdb_exit()
|