diff options
| author | Theofilos Augoustis <theofilos.augoustis@gmail.com> | 2023-11-10 12:56:38 +0100 |
|---|---|---|
| committer | Theofilos Augoustis <theofilos.augoustis@gmail.com> | 2023-11-10 12:56:38 +0100 |
| commit | 1d649ee44c2f49c11077d5b851d3ed110c2d6f65 (patch) | |
| tree | 50b0a476d183efeaa7dff0b86d84debb8b786d6c /interpreter.py | |
| parent | ca7044cdc7fe99d8065594d455b7f41505f796ab (diff) | |
| download | focaccia-1d649ee44c2f49c11077d5b851d3ed110c2d6f65.tar.gz focaccia-1d649ee44c2f49c11077d5b851d3ed110c2d6f65.zip | |
Implement interpreter for symbolic expressions
Diffstat (limited to 'interpreter.py')
| -rw-r--r-- | interpreter.py | 125 |
1 files changed, 125 insertions, 0 deletions
diff --git a/interpreter.py b/interpreter.py new file mode 100644 index 0000000..8e876f5 --- /dev/null +++ b/interpreter.py @@ -0,0 +1,125 @@ +"""Interpreter for claripy ASTs""" + +from inspect import signature +from logging import debug + +import claripy as cp + +class SymbolResolver: + def __init__(self): + pass + + def resolve(self, symbol_name: str) -> cp.ast.Base: + raise NotImplementedError() + +class SymbolResolveError(Exception): + def __init__(self, symbol, reason: str = ""): + super().__init__(f'Unable to resolve symbol name \"{symbol}\" to a' + ' concrete value' + + f': {reason}' if len(reason) > 0 else '.') + +def eval(resolver: SymbolResolver, expr) -> int: + """Evaluate a claripy expression to a concrete value. + + :param resolver: A `SymbolResolver` implementation that can resolve symbol + names to concrete values. + :param expr: The claripy AST to evaluate. Should be a subclass of + `claripy.ast.Base`. + + :return: A concrete value if the expression was resolved successfully. + If `expr` is not a claripy AST, `expr` is returned immediately. + :raise NotImplementedError: + :raise SymbolResolveError: If `resolver` is not able to resolve a symbol. + """ + if not issubclass(type(expr), cp.ast.Base): + return expr + + if expr.depth == 1: + if expr.symbolic: + name = expr._encoded_name.decode() + val = resolver.resolve(name) + if val is None: + raise SymbolResolveError(name) + return eval(resolver, val) + else: # if expr.concrete + assert(expr.concrete) + return expr.v + + # Expression is a non-trivial AST, i.e. a function + return _eval_op(resolver, expr.op, *expr.args) + +def _eval_op(resolver: SymbolResolver, op, *args) -> int: + """Evaluate a claripy operator expression. + + :param *args: Arguments to the function `op`. These are NOT evaluated yet! + """ + assert(type(op) is str) + + def concat(*vals): + res = 0 + for val in vals: + assert(type(val) is cp.ast.BV) + res = res << val.length + res = res | eval(resolver, val) + return res + + # Handle claripy's operators + if op == 'Concat': + res = concat(*args) + debug(f'Concatenating {args} to {hex(res)}') + return res + if op == 'Extract': + assert(len(args) == 3) + start, end, val = (eval(resolver, arg) for arg in args) + size = start - end + 1 + res = (val >> end) & ((1 << size) - 1) + debug(f'Extracing range [{start}, {end}] from {hex(val)}: {hex(res)}') + return res + if op == 'If': + assert(len(args) == 3) + cond, iftrue, iffalse = (eval(resolver, arg) for arg in args) + debug(f'Evaluated branch condition {args[0]} to {cond}') + return iftrue if bool(cond) else iffalse + if op == 'Reverse': + assert(len(args) == 1) + return concat(*reversed(args[0].chop(8))) + + # `op` is not one of claripy's special operators, so treat it as the name + # of a python operator function (because that is how claripy names its OR, + # EQ, etc.) + + # Convert some of the non-python names to magic names + # NOTE: We use python's signed comparison operators for unsigned + # comparisons. I'm not sure that this is legal. + if op in ['SGE', 'SGT', 'SLE', 'SLT', 'UGE', 'UGT', 'ULE', 'ULT']: + op = '__' + op[1:].lower() + '__' + + if op in ['And', 'Or']: + op = '__' + op.lower() + '__' + + resolved_args = [eval(resolver, arg) for arg in args] + try: + func = getattr(int, op) + except AttributeError: + raise NotImplementedError(op) + + # Sometimes claripy doesn't build its AST in an arity-respecting way if + # adjacent operations are associative. For example, it might pass five + # arguments to an XOR function instead of nesting the AST deeper. + # + # That's why we have to check with the python function's signature for its + # number of arguments and manually apply parentheses. + sig = signature(func) + assert(len(args) >= len(sig.parameters)) + + debug(f'Trying to evaluate function {func} with arguments {resolved_args}') + if len(sig.parameters) == len(args): + return func(*resolved_args) + else: + # Fold parameters from left by successively applying `op` to a + # subset of them + return _eval_op(resolver, + op, + func(*resolved_args[0:len(sig.parameters)]), + *resolved_args[len(sig.parameters):] + ) |