Breaking Bits
  • What this gitbook is
  • Vulnerability Discovery
    • Reverse Engineering
      • Modern Vulnerability Research Techniques on Embedded Systems
      • Remote Dynamic Blackbox Java App Analysis
    • Emulation
      • QEMU Usermode Tracing
      • Building QEMU on Ubuntu
    • Fuzzing with AFL
    • Automated Vulnerability Discovery
      • Buffer Overflows
      • Analyzing Functions
    • Automatic Exploit Generation
      • Automatic Rop Chain Generation
  • CTF
  • Battelle Shmoocon 2024
    • Time Jump Planner
  • Spaceheros CTF 2022
    • RE: Shai-Hulud
  • UMDCTF 2020
    • UMDCTF 2020: Evil Santa's Mysterious Box of Treats
  • UMDCTF 2022
    • Tracestory
  • Spaceheroes CTF 2023
    • Everything-is-wrong
  • US CyberGames RE-Cruise 4
  • Firmware Emulator
  • Interactive Firmware Emulator Usage
  • Recreating CVE-2015-1187 in the DIR-820L
  • Exploit Development
    • Linux kernel exploit development
      • Setup
      • Interacting with Kernel Modules
      • Kernel stack cookies
      • Kernel Address Space Layout Randomization (KALSR)
      • Supervisor mode execution protection (SMEP)
      • Kernel page table isolation (KPTI)
      • Supervisor Mode Access Prevention (SMAP)
Powered by GitBook
On this page
  • Calling functions to trace or find bugs
  • Function Prototypes
  • Creating a callable
  • Using a callable
  • Using a call state
  • Full code

Was this helpful?

  1. Vulnerability Discovery
  2. Automated Vulnerability Discovery

Analyzing Functions

angr to analyze and trace functions

PreviousBuffer OverflowsNextAutomatic Exploit Generation

Last updated 6 years ago

Was this helpful?

Calling functions to trace or find bugs

angr provides a callable interface to either concretely or concolically run functions. The current 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

Function Prototypes

Building function prototypes isn't as hard as the documentation would have you believe. Using 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()
documentation on angr
here
radare2