Analyzing Functions

angr to analyze and trace functions

Calling functions to trace or find bugs

angr provides a callable interface to either concretely or concolically run functions. The current documentation on angr does not describe the process of translating a function prototype to a callable interfaces very well.

This interface enables you perform program slice execution to see the results of an individual function, or test functions without the need of running previous code.

The binary used for this page can be found here

Function Prototypes

Building function prototypes isn't as hard as the documentation would have you believe. Using radare2 to pull function argument types is pretty fast.

$ r2 binaries/crackme/crackme0x04
[0x080483d0]> aaa
[0x080483d0]> s sym.check
[0x08048484]> afi~arg
args: 1
arg char * s @ ebp+0x8

With these arguments you can convert the string "char *" into an angr point with either explicitly stating it or having angr parse it for you and provide the right sim type:

#Long way
charstar = angr.sim_type.SimTypePointer(angr.sim_type.SimTypeChar())
#Easy way
charstar = angr.sim_type_.parse_type("char *")

The final angr prototype requires the arguments to the function and the return values and should look similar to the code below.

#SimTypeFunction(argument_tuple, return_value)
prototype = angr.sim_type.SimTypeFunction((charstar,), angr.sim_type.SimTypeInt(False))

Creating a callable

A calling convention tells angr how to handle the arguments being passed to the given callable function and how to handle the return value out of it. Using the prototype from above, the calling convention should be pretty simple.

#Calling convention
cc = p.factory.cc(func_ty=prototype)

The final callable function will need the function address, and the calling convention. To ensure symbolic arguments work, concrete_only needs to explicitly be disabled. When running with exclusively concrete arguments the concrete_only mode will enable the "tracing" suite of options for angr which expect no symbolic values.

check_func = p.factory.callable(find_func.addr, concrete_only=False, cc=cc)

Using a callable

Callables work just like regular python functions and will transparently run a simulation manager in the background and return the results upon the call.

my_args = ["abcd", "96", "87", "55", "qqqq"]
print("[+] Running angr callable with concrete arguments")
for arg in my_args:
ret_val = check_func(arg)
stdout = check_func.result_state.posix.dumps(1)
print("Input : {}".format(arg))
print("Stdout : {}".format(stdout))

callables work great for concrete analysis and tracing, however when using symbolic values the callables will not return until ALL paths finish which can mean never returning callables. The below code will not finish and will eventually exhaust all memory.

#Does not return
my_sym_arg = claripy.BVS('my_arg', 10*8) #10 byte long str
ret_val = check_func(my_sym_arg)
stdout = check_func.result_state.posix.dumps(1)
print("Stdout : {}".format(stdout))

To fix this issue, a callstate should be used instead.

Using a call state

A call state is an angr state which initialize an program state to get ready to call a single function and return. A callable will create a call state and continue to run until all paths are exhausted. A call state can use the explore and step functions provided by the simulation manager.

my_sym_arg = claripy.BVS('my_arg', 10*8) #10 byte long str
#Same calling convention from earlier
state = p.factory.call_state(find_func.addr, my_sym_arg, cc=cc)
simgr = p.factory.simgr(state)
simgr.explore(find=crack_me_good_addr)

Once the simulation manager returns you can use the "my_sym_arg" to discover the constraints imposed on it to yield the final program state!

found_state = simgr.found[0]
my_input = found_state.se.eval(my_sym_arg, cast_to=bytes).decode("utf-8", "ignore")
print("One solution : {}".format(my_input))

When using the simulation manager explore option you can add in the step_func option to enable vulnerability detectors to scan these functions.

simgr.explore(find=crack_me_good_addr, step_func=check_mem_corruption)

Full code

import angr
import claripy
import argparse
#angr logging is way too verbose
import logging
log_things = ["angr", "pyvex", "claripy", "cle"]
for log in log_things:
logger = logging.getLogger(log)
logger.disabled = True
logger.propagate = False
def main():
#file_name = "/home/chris/----/binaries/crackme/crackme0x04"
#Download crackme file from https://github.com/angr/angr-doc/raw/master/examples/CSCI-4968-MBE/challenges/crackme0x04/crackme0x04
parser = argparse.ArgumentParser()
parser.add_argument("File")
args = parser.parse_args()
file_name = args.File
crack_me_good_addr = 0x80484dc
function_name = "check"
p = angr.Project(file_name)
#Populates project knowledge base...
#CFG no longer needed
CFG = p.analyses.CFGEmulated()
#Look at all my functions!
find_func = None
for func in p.kb.functions.values():
#print(func.name)
if function_name in func.name:
find_func = func
print("[+] Function {}, found at {}".format(find_func.name, hex(find_func.addr)))
#Build function prototype
charstar = angr.sim_type.SimTypePointer(angr.sim_type.SimTypeChar())
prototype = angr.sim_type.SimTypeFunction((charstar,), angr.sim_type.SimTypeInt(False))
#Calling convention
cc = p.factory.cc(func_ty=prototype)
check_func = p.factory.callable(find_func.addr, concrete_only=False, cc=cc)
my_sym_arg = claripy.BVS('my_arg', 10*8) #10 byte long str
my_args = ["abcd", "96", "87", "55", "qqqq"]
print("[+] Running angr callable with concrete arguments")
#Solution is "96"... or "87"
for arg in my_args:
ret_val = check_func(arg)
stdout = check_func.result_state.posix.dumps(1)
print("Input : {}".format(arg))
print("Stdout : {}".format(stdout))
#The callable waits till ALL paths finish...
#The below code will take FOREVER, since it keeps
#Forking off new paths
'''
ret_val = check_func(my_sym_arg)
stdout = check_func.result_state.posix.dumps(1)
print("Stdout : {}".format(stdout))
'''
print("[+] Running modified angr callable with symbolic arguments")
#Instead try this
#Build a callable state using that calling convention we defined earlier
state = p.factory.call_state(find_func.addr, my_sym_arg, cc=cc)
simgr = p.factory.simgr(state)
simgr.explore(find=crack_me_good_addr)
if len(simgr.found):
found_state = simgr.found[0]
my_input = found_state.se.eval(my_sym_arg, cast_to=bytes).decode("utf-8", "ignore")
print("One solution : {}".format(my_input))
solutions = found_state.se.eval_upto(my_sym_arg, 20, cast_to=bytes)
for soln in solutions:
print("Many solutions : {}".format(soln.decode('utf-8', 'ignore')))
if __name__ == "__main__":
main()