diff options
| author | Fabrice Desclaux <fabrice.desclaux@cea.fr> | 2017-10-03 17:20:38 +0200 |
|---|---|---|
| committer | Fabrice Desclaux <fabrice.desclaux@cea.fr> | 2018-03-15 14:46:36 +0100 |
| commit | 1b534d9ad543473f12ddcb631e0cddb0cbd54ff4 (patch) | |
| tree | 5d5248ceb9358a1f497f4830e821e50afb340dba /miasm2/ir/symbexec.py | |
| parent | edabfcda0fa8c0dd8ab3017853b375b1ee24b754 (diff) | |
| download | miasm-1b534d9ad543473f12ddcb631e0cddb0cbd54ff4.tar.gz miasm-1b534d9ad543473f12ddcb631e0cddb0cbd54ff4.zip | |
Symbexec: use hashtable for mem symbols
Diffstat (limited to 'miasm2/ir/symbexec.py')
| -rw-r--r-- | miasm2/ir/symbexec.py | 1414 |
1 files changed, 1032 insertions, 382 deletions
diff --git a/miasm2/ir/symbexec.py b/miasm2/ir/symbexec.py index 8ecde21c..3cde2af7 100644 --- a/miasm2/ir/symbexec.py +++ b/miasm2/ir/symbexec.py @@ -1,13 +1,12 @@ import warnings import logging +from collections import MutableMapping -import miasm2.expression.expression as m2_expr -from miasm2.expression.modint import int32 +from miasm2.expression.expression import ExprOp, ExprId, ExprInt, ExprMem, \ + ExprCompose, ExprSlice, ExprCond, ExprAff from miasm2.expression.simplifications import expr_simp from miasm2.core import asmblock from miasm2.ir.ir import AssignBlock -from miasm2.core.interval import interval - log = logging.getLogger("symbexec") console_handler = logging.StreamHandler() @@ -28,87 +27,6 @@ def get_block(ir_arch, mdis, addr): return irblock -class SymbolMngr(object): - """ - Store registers and memory symbolic values - """ - - def __init__(self, init=None): - if init is None: - init = {} - self.symbols_id = {} - self.symbols_mem = {} - for expr, value in init.items(): - self[expr] = value - - def __contains__(self, expr): - if not isinstance(expr, m2_expr.ExprMem): - return self.symbols_id.__contains__(expr) - if not self.symbols_mem.__contains__(expr.arg): - return False - return self.symbols_mem[expr.arg][0].size == expr.size - - def __getitem__(self, expr): - if not isinstance(expr, m2_expr.ExprMem): - return self.symbols_id.__getitem__(expr) - if not expr.arg in self.symbols_mem: - raise KeyError(expr) - mem, value = self.symbols_mem.__getitem__(expr.arg) - if mem.size != expr.size: - raise KeyError(expr) - return value - - def get(self, expr, default=None): - if not isinstance(expr, m2_expr.ExprMem): - return self.symbols_id.get(expr, default) - if not expr.arg in self.symbols_mem: - return default - mem, value = self.symbols_mem.__getitem__(expr.arg) - if mem.size != expr.size: - return default - return value - - def __setitem__(self, expr, value): - if not isinstance(expr, m2_expr.ExprMem): - self.symbols_id.__setitem__(expr, value) - return - assert expr.size == value.size - self.symbols_mem.__setitem__(expr.arg, (expr, value)) - - def __iter__(self): - for expr in self.symbols_id: - yield expr - for expr in self.symbols_mem: - yield self.symbols_mem[expr][0] - - def __delitem__(self, expr): - if not isinstance(expr, m2_expr.ExprMem): - self.symbols_id.__delitem__(expr) - else: - self.symbols_mem.__delitem__(expr.arg) - - def items(self): - return self.symbols_id.items() + [x for x in self.symbols_mem.values()] - - def keys(self): - return (self.symbols_id.keys() + - [x[0] for x in self.symbols_mem.values()]) - - def copy(self): - new_symbols = SymbolMngr() - new_symbols.symbols_id = dict(self.symbols_id) - new_symbols.symbols_mem = dict(self.symbols_mem) - return new_symbols - - def inject_info(self, info): - new_symbols = SymbolMngr() - for expr, value in self.items(): - expr = expr_simp(expr.replace_expr(info)) - value = expr_simp(value.replace_expr(info)) - new_symbols[expr] = value - return new_symbols - - class StateEngine(object): """Stores an Engine state""" @@ -140,6 +58,7 @@ class SymbolicState(StateEngine): yield dst, src def iteritems(self): + """Iterate on stored memory/values""" return self.__iter__() def merge(self, other): @@ -163,10 +82,721 @@ class SymbolicState(StateEngine): return dict(self._symbols) +INTERNAL_INTBASE_NAME = "__INTERNAL_INTBASE__" + + +def get_expr_base_offset(expr): + """Return a couple representing the symbolic/concrete part of an addition + expression. + + If there is no symbolic part, ExprId(INTERNAL_INTBASE_NAME) is used + If there is not concrete part, 0 is used + @expr: Expression instance + + """ + if expr.is_int(): + internal_intbase = ExprId(INTERNAL_INTBASE_NAME, expr.size) + return internal_intbase, int(expr) + + if not expr.is_op('+'): + return expr, 0 + if expr.args[-1].is_int(): + args, offset = expr.args[:-1], int(expr.args[-1]) + if len(args) == 1: + return args[0], offset + return ExprOp('+', *args), offset + return expr, 0 + + +class MemArray(MutableMapping): + """Link between base and its (offset, Expr) + + Given an expression (say *base*), this structure will store every memory + content relatively to an integer offset from *base*. + + The value associated to a given offset is a description of the slice of a + stored expression. The slice size depends on the configutation of the + MemArray. For example, for a slice size of 8 bits, the affectation: + - @32[EAX+0x10] = EBX + + will store for the base EAX: + - 0x10: (EBX, 0) + - 0x11: (EBX, 1) + - 0x12: (EBX, 2) + - 0x13: (EBX, 3) + + If the *base* is EAX+EBX, this structure can store the following contents: + - @32[EAX+EBX] + - @8[EAX+EBX+0x100] + But not: + - @32[EAX+0x10] (which is stored in another MemArray based on EAX) + - @32[EAX+EBX+ECX] (which is stored in another MemArray based on + EAX+EBX+ECX) + + """ + + def __init__(self, base, expr_simp=expr_simp): + self._base = base + self.expr_simp = expr_simp + self._mask = int(base.mask) + self._offset_to_expr = {} + + @property + def base(self): + """Expression representing the symbolic base address""" + return self._base + + @property + def mask(self): + """Mask offset""" + return self._mask + + def __getitem__(self, offset): + assert 0 <= offset <= self._mask + return self._offset_to_expr.__getitem__(offset) + + def __setitem__(self, offset, value): + raise RuntimeError("Use write api to update keys") + + def __delitem__(self, offset): + assert 0 <= offset <= self._mask + return self._offset_to_expr.__delitem__(offset) + + def __iter__(self): + for offset, _ in self._offset_to_expr.iteritems(): + yield offset + + def __len__(self): + return len(self._offset_to_expr) + + def __repr__(self): + out = [] + out.append("Base: %s" % self.base) + for offset, (index, value) in sorted(self._offset_to_expr.iteritems()): + out.append("%16X %d %s" % (offset, index, value)) + return '\n'.join(out) + + def copy(self): + """Copy object instance""" + obj = MemArray(self.base, self.expr_simp) + obj._offset_to_expr = self._offset_to_expr.copy() + return obj + + @staticmethod + def offset_to_ptr(base, offset): + """ + Return an expression representing the @base + @offset + @base: symbolic base address + @offset: relative offset integer to the @base address + """ + if base.is_id(INTERNAL_INTBASE_NAME): + ptr = ExprInt(offset, base.size) + elif offset == 0: + ptr = base + else: + ptr = base + ExprInt(offset, base.size) + return ptr + + def read(self, offset, size): + """ + Return memory at @offset with @size as an Expr list + @offset: integer (in bytes) + @size: integer (in bits), byte aligned + + Consider the following state: + - 0x10: (EBX, 0) + - 0x11: (EBX, 1) + - 0x12: (EBX, 2) + - 0x13: (EBX, 3) + + A read at 0x10 of 32 bits should return: EBX + """ + + assert size % 8 == 0 + # Parts is (Expr's offset, size, Expr) + parts = [] + for index in xrange(size / 8): + # Wrap read: + # @32[EAX+0xFFFFFFFF] is ok and will read at 0xFFFFFFFF, 0, 1, 2 + request_offset = (offset + index) & self._mask + if request_offset in self._offset_to_expr: + # Known memory portion + off, data = self._offset_to_expr[request_offset] + parts.append((off, 1, data)) + continue + + # Unknown memory portion + ptr = self.offset_to_ptr(self.base, request_offset) + data = ExprMem(ptr, 8) + parts.append((0, 1, data)) + + # Group similar datas + # XXX TODO: only little endian here + index = 0 + while index + 1 < len(parts): + off_a, size_a, data_a = parts[index] + off_b, size_b, data_b = parts[index+1] + if data_a == data_b and off_a + size_a == off_b: + # Read consecutive bytes of a variable + # [(0, 8, x), (1, 8, x)] => (0, 16, x) + parts[index:index+2] = [(off_a, size_a + size_b, data_a)] + continue + if data_a.is_int() and data_b.is_int(): + # Read integer parts + # [(0, 8, 0x11223344), (1, 8, 0x55667788)] => (0, 16, 0x7744) + int1 = self.expr_simp(data_a[off_a*8:(off_a+size_a)*8]) + int2 = self.expr_simp(data_b[off_b*8:(off_b+size_b)*8]) + assert int1.is_int() and int2.is_int() + int1, int2 = int(int1), int(int2) + result = ExprInt((int2 << (size_a * 8)) | int1, (size_a + size_b) * 8) + parts[index:index+2] = [(0, size_a + size_b, result)] + continue + if data_a.is_mem() and data_b.is_mem(): + # Read consecutive bytes of a memory variable + ptr_base_a, ptr_offset_a = get_expr_base_offset(data_a.arg) + ptr_base_b, ptr_offset_b = get_expr_base_offset(data_b.arg) + if ptr_base_a != ptr_base_b: + index += 1 + continue + if (ptr_offset_a + off_a + size_a) & self._mask == (ptr_offset_b + off_b) & self._mask: + assert size_a <= data_a.size / 8 - off_a + assert size_b <= data_b.size / 8 - off_b + # Successive comparable symbolic pointers + # [(0, 8, @8[ptr]), (0, 8, @8[ptr+1])] => (0, 16, @16[ptr]) + ptr = self.offset_to_ptr(ptr_base_a, (ptr_offset_a + off_a) & self._mask) + + data = ExprMem(ptr, (size_a + size_b) * 8) + parts[index:index+2] = [(0, size_a + size_b, data)] + + continue + + index += 1 + + # Slice datas + read_mem = [] + for off, bytesize, data in parts: + if data.size / 8 != bytesize: + data = data[off * 8: (off + bytesize) * 8] + read_mem.append(data) + + return read_mem + + def write(self, offset, expr): + """ + Write @expr at @offset + @offset: integer (in bytes) + @expr: Expr instance value + """ + assert expr.size % 8 == 0 + assert offset <= self._mask + for index in xrange(expr.size / 8): + # Wrap write: + # @32[EAX+0xFFFFFFFF] is ok and will write at 0xFFFFFFFF, 0, 1, 2 + request_offset = (offset + index) & self._mask + # XXX TODO: only little endian here + self._offset_to_expr[request_offset] = (index, expr) + + tmp = self.expr_simp(expr[index * 8: (index + 1) * 8]) + # Special case: Simplify slice of pointer (simplification is ok + # here, as we won't store the simplified expression) + if tmp.is_slice() and tmp.arg.is_mem() and tmp.start % 8 == 0: + new_ptr = self.expr_simp(tmp.arg.arg + ExprInt(tmp.start / 8, tmp.arg.arg.size)) + tmp = ExprMem(new_ptr, tmp.stop - tmp.start) + # Test if write to original value + if tmp.is_mem(): + src_ptr, src_off = get_expr_base_offset(tmp.arg) + if src_ptr == self.base and src_off == request_offset: + del self._offset_to_expr[request_offset] + + + def _get_variable_parts(self, index, known_offsets, forward=True): + """ + Find consecutive memory parts representing the same variable. The part + starts at offset known_offsets[@index] and search is in offset direction + determined by @forward + Return the number of consecutive parts of the same variable. + + @index: index of the memory offset in known_offsets + @known_offsets: sorted offsets + @forward: Search in offset growing direction if True, else in reverse + order + """ + + offset = known_offsets[index] + value_byte_index, value = self._offset_to_expr[offset] + assert value.size % 8 == 0 + + if forward: + start, end, step = value_byte_index + 1, value.size / 8, 1 + else: + start, end, step = value_byte_index - 1, -1, -1 + + partnum = 1 + for value_offset in xrange(start, end, step): + offset += step + # Check if next part is in known_offsets + next_index = index + step * partnum + if not 0 <= next_index < len(known_offsets): + break + + offset_next = known_offsets[next_index] + if offset_next != offset: + break + + # Check if next part is a part of the searched value + byte_index, value_next = self._offset_to_expr[offset_next] + if byte_index != value_offset: + break + if value != value_next: + break + partnum += 1 + + return partnum + + + def _build_value_at_offset(self, value, offset, start, length): + """ + Return a couple. The first element is the memory Expression representing + the value at @offset, the second is its value. The value is truncated + at byte @start with @length + + @value: Expression to truncate + @offset: offset in bytes of the variable (integer) + @start: value's byte offset (integer) + @length: length in bytes (integer) + """ + + ptr = self.offset_to_ptr(self.base, offset) + size = length * 8 + if start == 0 and size == value.size: + result = value + else: + result = self.expr_simp(value[start * 8: start * 8 + size]) + + return ExprMem(ptr, size), result + + + def memory(self): + """ + Iterate on stored memory/values + + The goal here is to group entities. + + Consider the following state: + EAX + 0x10 = (0, EDX) + EAX + 0x11 = (1, EDX) + EAX + 0x12 = (2, EDX) + EAX + 0x13 = (3, EDX) + + The function should return: + @32[EAX + 0x10] = EDX + """ + + if not self._offset_to_expr: + raise StopIteration + known_offsets = self._offset_to_expr.keys() + known_offsets.sort() + index = 0 + # Test if the first element is the continuation of the last byte. If + # yes, merge and output it first. + min_int = 0 + max_int = (1 << self.base.size) - 1 + limit_index = len(known_offsets) + + first_element = None + # Special case where a variable spreads on max_int/min_int + if known_offsets[0] == min_int and known_offsets[-1] == max_int: + min_offset, max_offset = known_offsets[0], known_offsets[-1] + min_byte_index, min_value = self._offset_to_expr[min_offset] + max_byte_index, max_value = self._offset_to_expr[max_offset] + if min_value == max_value and max_byte_index + 1 == min_byte_index: + # Look for current variable start + partnum_before = self._get_variable_parts(len(known_offsets) - 1, known_offsets, False) + # Look for current variable end + partnum_after = self._get_variable_parts(0, known_offsets) + + partnum = partnum_before + partnum_after + offset = known_offsets[-partnum_before] + index_value, value = self._offset_to_expr[offset] + + mem, result = self._build_value_at_offset(value, offset, index_value, partnum) + first_element = mem, result + index = partnum_after + limit_index = len(known_offsets) - partnum_before + + # Special cases are done, walk and merge variables + while index < limit_index: + offset = known_offsets[index] + index_value, value = self._offset_to_expr[offset] + partnum = self._get_variable_parts(index, known_offsets) + mem, result = self._build_value_at_offset(value, offset, index_value, partnum) + yield mem, result + index += partnum + + if first_element is not None: + yield first_element + + def dump(self): + """Display MemArray content""" + for mem, value in self.memory(): + print "%s = %s" % (mem, value) + + +class MemSparse(object): + """Link a symbolic memory pointer to its MemArray. + + For each symbolic memory object, this object will extract the memory pointer + *ptr*. It then splits *ptr* into a symbolic and an integer part. For + example, the memory @[ESP+4] will give ESP+4 for *ptr*. *ptr* is then split + into its base ESP and its offset 4. Each symbolic base address uses a + different MemArray. + + Example: + - @32[EAX+EBX] + - @8[EAX+EBX+0x100] + Will be stored in the same MemArray with a EAX+EBX base + + """ + + def __init__(self, addrsize, expr_simp=expr_simp): + """ + @addrsize: size (in bits) of the addresses manipulated by the MemSparse + @expr_simp: an ExpressionSimplifier instance + """ + self.addrsize = addrsize + self.expr_simp = expr_simp + self.base_to_memarray = {} + + def __contains__(self, expr): + """ + Return True if the whole @expr is present + For partial check, use 'contains_partial' + """ + if not expr.is_mem(): + return False + ptr = expr.arg + base, offset = get_expr_base_offset(ptr) + memarray = self.base_to_memarray.get(base, None) + if memarray is None: + return False + for i in xrange(expr.size / 8): + if offset + i not in memarray: + return False + return True + + def contains_partial(self, expr): + """ + Return True if a part of @expr is present in memory + """ + if not expr.is_mem(): + return False + ptr = expr.arg + base, offset = get_expr_base_offset(ptr) + memarray = self.base_to_memarray.get(base, None) + if memarray is None: + return False + for i in xrange(expr.size / 8): + if offset + i in memarray: + return True + return False + + def clear(self): + """Reset the current object content""" + self.base_to_memarray.clear() + + def copy(self): + """Copy the current object instance""" + base_to_memarray = {} + for base, memarray in self.base_to_memarray.iteritems(): + base_to_memarray[base] = memarray.copy() + obj = MemSparse(self.addrsize, self.expr_simp) + obj.base_to_memarray = base_to_memarray + return obj + + def __delitem__(self, expr): + """ + Delete a value @expr *fully* present in memory + For partial delete, use delete_partial + """ + ptr = expr.arg + base, offset = get_expr_base_offset(ptr) + memarray = self.base_to_memarray.get(base, None) + if memarray is None: + raise KeyError + # Check if whole entity is in the MemArray before deleting it + for i in xrange(expr.size / 8): + if (offset + i) & memarray.mask not in memarray: + raise KeyError + for i in xrange(expr.size / 8): + del memarray[(offset + i) & memarray.mask] + + def delete_partial(self, expr): + """ + Delete @expr from memory. Skip parts of @expr which are not present in + memory. + """ + ptr = expr.arg + base, offset = get_expr_base_offset(ptr) + memarray = self.base_to_memarray.get(base, None) + if memarray is None: + raise KeyError + # Check if whole entity is in the MemArray before deleting it + for i in xrange(expr.size / 8): + real_offset = (offset + i) & memarray.mask + if real_offset in memarray: + del memarray[real_offset] + + def read(self, ptr, size): + """ + Return the value associated with the Expr at address @ptr + @ptr: Expr representing the memory address + @size: memory size (in bits), byte aligned + """ + assert size % 8 == 0 + base, offset = get_expr_base_offset(ptr) + memarray = self.base_to_memarray.get(base, None) + if memarray is not None: + mems = memarray.read(offset, size) + ret = ExprCompose(*mems) + else: + ret = ExprMem(ptr, size) + return ret + + def write(self, ptr, expr): + """ + Update the corresponding Expr @expr at address @ptr + @ptr: Expr representing the memory address + @expr: Expr instance + """ + assert ptr.size == self.addrsize + base, offset = get_expr_base_offset(ptr) + memarray = self.base_to_memarray.get(base, None) + if memarray is None: + memarray = MemArray(base, self.expr_simp) + self.base_to_memarray[base] = memarray + memarray.write(offset, expr) + + def iteritems(self): + """Iterate on stored memory variables and their values.""" + for _, memarray in sorted(self.base_to_memarray.iteritems()): + for mem, value in memarray.memory(): + yield mem, value + + def items(self): + """Return stored memory variables and their values.""" + return list(self.iteritems()) + + def dump(self): + """Display MemSparse content""" + for mem, value in self.iteritems(): + print "%s = %s" % (mem, value) + + def __repr__(self): + out = [] + for _, memarray in sorted(self.base_to_memarray.iteritems()): + out.append(repr(memarray)) + return '\n'.join(out) + + +class SymbolMngr(object): + """Symbolic store manager (IDs and MEMs)""" + + def __init__(self, init=None, addrsize=None, expr_simp=expr_simp): + assert addrsize is not None + if init is None: + init = {} + self.addrsize = addrsize + self.expr_simp = expr_simp + self.symbols_id = {} + self.symbols_mem = MemSparse(addrsize, expr_simp) + self.mask = (1 << addrsize) - 1 + for expr, value in init.iteritems(): + self.write(expr, value) + + def __contains__(self, expr): + if expr.is_id(): + return self.symbols_id.__contains__(expr) + if expr.is_mem(): + return self.symbols_mem.__contains__(expr) + return False + + def __getitem__(self, expr): + return self.read(expr) + + def __setitem__(self, expr, value): + self.write(expr, value) + + def __delitem__(self, expr): + if expr.is_id(): + del self.symbols_id[expr] + elif expr.is_mem(): + del self.symbols_mem[expr] + else: + raise TypeError("Bad source expr") + + def copy(self): + """Copy object instance""" + obj = SymbolMngr(self, addrsize=self.addrsize, expr_simp=self.expr_simp) + return obj + + def clear(self): + """Forget every variables values""" + self.symbols_id.clear() + self.symbols_mem.clear() + + def read(self, src): + """ + Return the value corresponding to Expr @src + @src: ExprId or ExprMem instance + """ + if src.is_id(): + return self.symbols_id.get(src, src) + elif src.is_mem(): + # Only byte aligned accesses are supported for now + assert src.size % 8 == 0 + return self.symbols_mem.read(src.arg, src.size) + else: + raise TypeError("Bad source expr") + + def write(self, dst, src): + """ + Update @dst with @src expression + @dst: ExprId or ExprMem instance + @src: Expression instance + """ + assert dst.size == src.size + if dst.is_id(): + if dst == src: + if dst in self.symbols_id: + del self.symbols_id[dst] + else: + self.symbols_id[dst] = src + elif dst.is_mem(): + # Only byte aligned accesses are supported for now + assert dst.size % 8 == 0 + self.symbols_mem.write(dst.arg, src) + else: + raise TypeError("Bad destination expr") + + def dump(self, ids=True, mems=True): + """Display memory content""" + if ids: + for variable, value in self.ids(): + print '%s = %s' % (variable, value) + if mems: + for mem, value in self.memory(): + print '%s = %s' % (mem, value) + + def __repr__(self): + out = [] + for variable, value in self.iteritems(): + out.append('%s = %s' % (variable, value)) + return "\n".join(out) + + def iteritems(self): + """ExprId/ExprMem iteritems of the current state""" + for variable, value in self.ids(): + yield variable, value + for variable, value in self.memory(): + yield variable, value + + def items(self): + """Return variables/values of the current state""" + return list(self.iteritems()) + + def __iter__(self): + for expr, _ in self.iteritems(): + yield expr + + def ids(self): + """Iterate on variables and their values.""" + for expr, value in self.symbols_id.iteritems(): + yield expr, value + + def memory(self): + """Iterate on memory variables and their values.""" + for mem, value in self.symbols_mem.iteritems(): + yield mem, value + + def keys(self): + """Variables of the current state""" + return list(self) + + def get(self, expr, default=None): + """Deprecated version of read""" + warnings.warn('DEPRECATION WARNING: use "read(self, expr)" instead of get') + ret = self.read(expr) + if default is not None and ret == expr: + return default + return ret + + +def merge_ptr_read(known, ptrs): + """ + Merge common memory parts in a multiple byte memory. + @ptrs: memory bytes list + @known: ptrs' associated boolean for present/unpresent memory part in the + store + """ + assert known + out = [] + known.append(None) + ptrs.append(None) + last, value, size = known[0], ptrs[0], 8 + for index, part in enumerate(known[1:], 1): + if part == last: + size += 8 + else: + out.append((last, value, size)) + last, value, size = part, ptrs[index], 8 + return out + + class SymbolicExecutionEngine(object): """ Symbolic execution engine Allow IR code emulation in symbolic domain + + + Examples: + from miasm2.ir.symbexec import SymbolicExecutionEngine + from miasm2.ir.ir import AssignBlock + + ir_arch = ir_x86_32() + + init_state = { + ir_arch.arch.regs.EAX: ir_arch.arch.regs.EBX, + ExprMem(id_x+ExprInt(0x10, 32), 32): id_a, + } + + sb_exec = SymbolicExecutionEngine(ir_arch, init_state) + + >>> sb_exec.dump() + EAX = a + @32[x + 0x10] = a + >>> sb_exec.dump(mems=False) + EAX = a + + >>> print sb_exec.eval_expr(ir_arch.arch.regs.EAX + ir_arch.arch.regs.ECX) + EBX + ECX + + Inspecting state: + - dump + - modified + State manipulation: + - '.state' (rw) + + Evaluation (read only): + - eval_expr + - eval_assignblk + Evaluation with state update: + - eval_updt_expr + - eval_updt_assignblk + - eval_updt_irblock + + Start a symbolic execution based on provisioned '.ir_arch' blocks: + - run_block_at + - run_at """ StateEngine = SymbolicState @@ -176,90 +806,31 @@ class SymbolicExecutionEngine(object): func_write=None, sb_expr_simp=expr_simp): - self.symbols = SymbolMngr() + self.expr_to_visitor = { + ExprInt: self.eval_exprint, + ExprId: self.eval_exprid, + ExprMem: self.eval_exprmem, + ExprSlice: self.eval_exprslice, + ExprCond: self.eval_exprcond, + ExprOp: self.eval_exprop, + ExprCompose: self.eval_exprcompose, + } + + self.symbols = SymbolMngr(addrsize=ir_arch.addrsize, expr_simp=expr_simp) + for dst, src in state.iteritems(): - self.symbols[dst] = src + self.symbols.write(dst, src) + + if func_read: + warnings.warn('DEPRECATION WARNING: override function "mem_read(self, expr)" instead of func_read') + if func_write: + warnings.warn('DEPRECATION WARNING: override function "mem_write(self, dsr, src)" instead of func_write') self.func_read = func_read self.func_write = func_write self.ir_arch = ir_arch self.expr_simp = sb_expr_simp - def find_mem_by_addr(self, expr): - """ - Return memory keys with pointer equal to @expr - @expr: address of the searched memory variable - """ - if expr in self.symbols.symbols_mem: - return self.symbols.symbols_mem[expr][0] - return None - - def get_mem_state(self, expr): - """ - Evaluate the @expr memory in the current state using @cache - @expr: the memory key - """ - ptr, size = expr.arg, expr.size - ret = self.find_mem_by_addr(ptr) - if not ret: - overlaps = self.get_mem_overlapping(expr) - if not overlaps: - if self.func_read and ptr.is_int(): - expr = self.func_read(expr) - return expr - - out = [] - off_base = 0 - for off, mem in overlaps: - if off >= 0: - new_size = min(size - off * 8, mem.size) - tmp = self.expr_simp(self.symbols[mem][0:new_size]) - out.append((tmp, off_base, off_base + new_size)) - off_base += new_size - else: - new_size = min(size - off * 8, mem.size) - tmp = self.expr_simp(self.symbols[mem][-off * 8:new_size]) - new_off_base = off_base + new_size + off * 8 - out.append((tmp, off_base, new_off_base)) - off_base = new_off_base - - missing_slice = self.rest_slice(out, 0, size) - for slice_start, slice_stop in missing_slice: - ptr = self.expr_simp(ptr + m2_expr.ExprInt(slice_start / 8, ptr.size)) - mem = m2_expr.ExprMem(ptr, slice_stop - slice_start) - if self.func_read and ptr.is_int(): - mem = self.func_read(mem) - out.append((mem, slice_start, slice_stop)) - out.sort(key=lambda x: x[1]) - args = [expr for (expr, _, _) in out] - ret = self.expr_simp(m2_expr.ExprCompose(*args)[:size]) - return ret - - # bigger lookup - if size > ret.size: - rest = size - out = [] - while rest: - mem = self.find_mem_by_addr(ptr) - if mem is None: - mem = m2_expr.ExprMem(ptr, 8) - if self.func_read and ptr.is_int(): - value = self.func_read(mem) - else: - value = mem - elif rest >= mem.size: - value = self.symbols[mem] - else: - value = self.symbols[mem][:rest] - out.append(value) - rest -= value.size - ptr = self.expr_simp(ptr + m2_expr.ExprInt(mem.size / 8, ptr.size)) - ret = self.expr_simp(m2_expr.ExprCompose(*out)) - return ret - # part lookup - ret = self.expr_simp(self.symbols[ret][:size]) - return ret - def get_state(self): """Return the current state of the SymbolicEngine""" state = self.StateEngine(dict(self.symbols)) @@ -269,214 +840,161 @@ class SymbolicExecutionEngine(object): """Restaure the @state of the engine @state: StateEngine instance """ - self.symbols = SymbolMngr() + self.symbols = SymbolMngr(addrsize=self.ir_arch.addrsize, expr_simp=self.expr_simp) for dst, src in dict(state).iteritems(): self.symbols[dst] = src - def apply_expr_on_state_visit_cache(self, expr, state, cache, level=0): + state = property(get_state, set_state) + + def eval_expr_visitor(self, expr, cache=None): """ - Deep First evaluate nodes: - 1. evaluate node's sons - 2. simplify + [DEV]: Override to change the behavior of an Expr evaluation. + This function recursively applies 'eval_expr*' to @expr. + This function uses @cache to speedup re-evaluation of expression. """ + if cache is None: + cache = {} - expr = self.expr_simp(expr) + ret = cache.get(expr, None) + if ret is not None: + return ret - #print '\t'*level, "Eval:", expr - if expr in cache: - ret = cache[expr] - #print "In cache!", ret - elif expr.is_int(): - return expr - elif expr.is_id(): - if isinstance(expr.name, asmblock.AsmLabel) and expr.name.offset is not None: - ret = m2_expr.ExprInt(expr.name.offset, expr.size) - else: - ret = state.get(expr, expr) - elif expr.is_mem(): - ptr = self.apply_expr_on_state_visit_cache(expr.arg, state, cache, level+1) - ret = m2_expr.ExprMem(ptr, expr.size) - ret = self.get_mem_state(ret) - assert expr.size == ret.size - elif expr.is_cond(): - cond = self.apply_expr_on_state_visit_cache(expr.cond, state, cache, level+1) - src1 = self.apply_expr_on_state_visit_cache(expr.src1, state, cache, level+1) - src2 = self.apply_expr_on_state_visit_cache(expr.src2, state, cache, level+1) - ret = m2_expr.ExprCond(cond, src1, src2) - elif expr.is_slice(): - arg = self.apply_expr_on_state_visit_cache(expr.arg, state, cache, level+1) - ret = m2_expr.ExprSlice(arg, expr.start, expr.stop) - elif expr.is_op(): - args = [] - for oarg in expr.args: - arg = self.apply_expr_on_state_visit_cache(oarg, state, cache, level+1) - assert oarg.size == arg.size - args.append(arg) - ret = m2_expr.ExprOp(expr.op, *args) - elif expr.is_compose(): - args = [] - for arg in expr.args: - args.append(self.apply_expr_on_state_visit_cache(arg, state, cache, level+1)) - ret = m2_expr.ExprCompose(*args) - else: + new_expr = self.expr_simp(expr) + ret = cache.get(new_expr, None) + if ret is not None: + return ret + + func = self.expr_to_visitor.get(new_expr.__class__, None) + if func is None: raise TypeError("Unknown expr type") - #print '\t'*level, "Result", ret + + ret = func(new_expr, cache=cache) ret = self.expr_simp(ret) - #print '\t'*level, "Result simpl", ret + assert ret is not None - assert expr.size == ret.size cache[expr] = ret + cache[new_expr] = ret return ret - def apply_expr_on_state(self, expr, cache): - if cache is None: - cache = {} - ret = self.apply_expr_on_state_visit_cache(expr, self.symbols, cache) + def eval_exprint(self, expr, **kwargs): + """[DEV]: Evaluate an ExprInt using the current state""" + return expr + + def eval_exprid(self, expr, **kwargs): + """[DEV]: Evaluate an ExprId using the current state""" + if isinstance(expr.name, asmblock.AsmLabel) and expr.name.offset is not None: + ret = ExprInt(expr.name.offset, expr.size) + else: + ret = self.symbols.read(expr) return ret - def eval_expr(self, expr, eval_cache=None): - return self.apply_expr_on_state(expr, eval_cache) + def eval_exprmem(self, expr, **kwargs): + """[DEV]: Evaluate an ExprMem using the current state + This function first evaluate the memory pointer value. + Override 'mem_read' to modify the effective memory accesses + """ + ptr = self.eval_expr_visitor(expr.arg, **kwargs) + mem = ExprMem(ptr, expr.size) + ret = self.mem_read(mem) + return ret - def modified_regs(self, init_state=None): - if init_state is None: - init_state = self.ir_arch.arch.regs.regs_init - ids = self.symbols.symbols_id.keys() - ids.sort() - for i in ids: - if i in init_state and \ - i in self.symbols.symbols_id and \ - self.symbols.symbols_id[i] == init_state[i]: - continue - yield i + def eval_exprcond(self, expr, **kwargs): + """[DEV]: Evaluate an ExprCond using the current state""" + cond = self.eval_expr_visitor(expr.cond, **kwargs) + src1 = self.eval_expr_visitor(expr.src1, **kwargs) + src2 = self.eval_expr_visitor(expr.src2, **kwargs) + ret = ExprCond(cond, src1, src2) + return ret - def modified_mems(self, init_state=None): - if init_state is None: - init_state = self.ir_arch.arch.regs.regs_init - mems = self.symbols.symbols_mem.values() - mems.sort() - for mem, _ in mems: - if mem in init_state and \ - mem in self.symbols.symbols_mem and \ - self.symbols.symbols_mem[mem] == init_state[mem]: - continue - yield mem + def eval_exprslice(self, expr, **kwargs): + """[DEV]: Evaluate an ExprSlice using the current state""" + arg = self.eval_expr_visitor(expr.arg, **kwargs) + ret = ExprSlice(arg, expr.start, expr.stop) + return ret - def modified(self, init_state=None): - for reg in self.modified_regs(init_state): - yield reg - for mem in self.modified_mems(init_state): - yield mem + def eval_exprop(self, expr, **kwargs): + """[DEV]: Evaluate an ExprOp using the current state""" + args = [] + for oarg in expr.args: + arg = self.eval_expr_visitor(oarg, **kwargs) + args.append(arg) + ret = ExprOp(expr.op, *args) + return ret - def dump_id(self): - """ - Dump modififed registers symbols only - """ - ids = self.symbols.symbols_id.keys() - ids.sort() - for expr in ids: - if (expr in self.ir_arch.arch.regs.regs_init and - expr in self.symbols.symbols_id and - self.symbols.symbols_id[expr] == self.ir_arch.arch.regs.regs_init[expr]): - continue - print expr, "=", self.symbols.symbols_id[expr] + def eval_exprcompose(self, expr, **kwargs): + """[DEV]: Evaluate an ExprCompose using the current state""" + args = [] + for arg in expr.args: + args.append(self.eval_expr_visitor(arg, **kwargs)) + ret = ExprCompose(*args) + return ret - def dump_mem(self): + def eval_expr(self, expr, eval_cache=None): """ - Dump modififed memory symbols + Evaluate @expr + @expr: Expresion instance to evaluate + @cache: None or dictionary linking variables to their values """ - mems = self.symbols.symbols_mem.values() - mems.sort() - for mem, value in mems: - print mem, value + if eval_cache is None: + eval_cache = {} + ret = self.eval_expr_visitor(expr, cache=eval_cache) + assert ret is not None + return ret - def rest_slice(self, slices, start, stop): + def modified(self, init_state=None, ids=True, mems=True): """ - Return the complementary slices of @slices in the range @start, @stop - @slices: base slices - @start, @stop: interval range + Return the modified variables. + @init_state: a base dictionary linking variables to their initial values + to diff. Can be None. + @ids: track ids only + @mems: track mems only """ - out = [] - last = start - for _, slice_start, slice_stop in slices: - if slice_start == last: - last = slice_stop - continue - out.append((last, slice_start)) - last = slice_stop - if last != stop: - out.append((slice_stop, stop)) - return out - - def substract_mems(self, arg1, arg2): + if init_state is None: + init_state = self.ir_arch.arch.regs.regs_init + if ids: + for variable, value in self.symbols.symbols_id.iteritems(): + if variable in init_state and init_state[variable] == value: + continue + yield variable, value + if mems: + for mem, value in self.symbols.memory(): + if mem in init_state and init_state[mem] == value: + continue + yield mem, value + + def dump(self, ids=True, mems=True): """ - Return the remaining memory areas of @arg1 - @arg2 - @arg1, @arg2: ExprMem + Display modififed variables + @ids: display modified ids + @mems: display modified memory """ - ptr_diff = self.expr_simp(arg2.arg - arg1.arg) - ptr_diff = int(int32(ptr_diff.arg)) - - zone1 = interval([(0, arg1.size/8-1)]) - zone2 = interval([(ptr_diff, ptr_diff + arg2.size/8-1)]) - zones = zone1 - zone2 - - out = [] - for start, stop in zones: - ptr = arg1.arg + m2_expr.ExprInt(start, arg1.arg.size) - ptr = self.expr_simp(ptr) - value = self.expr_simp(self.symbols[arg1][start*8:(stop+1)*8]) - mem = m2_expr.ExprMem(ptr, (stop - start + 1)*8) - assert mem.size == value.size - out.append((mem, value)) - - return out + for variable, value in self.modified(None, ids, mems): + print "%-18s" % variable, "=", "%s" % value - def get_mem_overlapping(self, expr): - """ - Gives mem stored overlapping memory in @expr - Hypothesis: Max mem size is 64 bytes, compute all reachable addresses - @expr: target memory + def eval_assignblk(self, assignblk): """ + Evaluate AssignBlock using the current state - overlaps = [] - base_ptr = self.expr_simp(expr.arg) - for i in xrange(-7, expr.size / 8): - new_ptr = base_ptr + m2_expr.ExprInt(i, expr.arg.size) - new_ptr = self.expr_simp(new_ptr) - - mem, origin = self.symbols.symbols_mem.get(new_ptr, (None, None)) - if mem is None: - continue - - ptr_diff = -i - if ptr_diff >= origin.size / 8: - # access is too small to overlap the memory target - continue - overlaps.append((i, mem)) + Returns a dictionary containing modified keys associated to their values - return overlaps - - def eval_ir_expr(self, assignblk): - """ - Evaluate AssignBlock on the current state @assignblk: AssignBlock instance """ pool_out = {} eval_cache = {} for dst, src in assignblk.iteritems(): src = self.eval_expr(src, eval_cache) - if isinstance(dst, m2_expr.ExprMem): + if dst.is_mem(): ptr = self.eval_expr(dst.arg, eval_cache) - # test if mem lookup is known - tmp = m2_expr.ExprMem(ptr, dst.size) + # Test if mem lookup is known + tmp = ExprMem(ptr, dst.size) pool_out[tmp] = src - - elif isinstance(dst, m2_expr.ExprId): + elif dst.is_id(): pool_out[dst] = src else: raise ValueError("Unknown destination type", str(dst)) - return pool_out.iteritems() + return pool_out def apply_change(self, dst, src): """ @@ -484,41 +1002,25 @@ class SymbolicExecutionEngine(object): @dst: Expr, destination @src: Expr, source """ - if isinstance(dst, m2_expr.ExprMem): - mem_overlap = self.get_mem_overlapping(dst) - for _, base in mem_overlap: - diff_mem = self.substract_mems(base, dst) - del self.symbols[base] - for new_mem, new_val in diff_mem: - self.symbols[new_mem] = new_val - src_o = self.expr_simp(src) - - # Force update. Ex: - # EBX += 1 (state: EBX = EBX+1) - # EBX -= 1 (state: EBX = EBX, must be updated) - self.symbols[dst] = src_o - if dst == src_o: - # Avoid useless X = X information - del self.symbols[dst] - if isinstance(dst, m2_expr.ExprMem): - if self.func_write and isinstance(dst.arg, m2_expr.ExprInt): - self.func_write(self, dst, src_o) - del self.symbols[dst] + if dst.is_mem(): + self.mem_write(dst, src) + else: + self.symbols.write(dst, src) - def eval_ir(self, assignblk): + def eval_updt_assignblk(self, assignblk): """ Apply an AssignBlock on the current state @assignblk: AssignBlock instance """ mem_dst = [] - src_dst = self.eval_ir_expr(assignblk) - for dst, src in src_dst: + dst_src = self.eval_assignblk(assignblk) + for dst, src in dst_src.iteritems(): self.apply_change(dst, src) - if isinstance(dst, m2_expr.ExprMem): + if dst.is_mem(): mem_dst.append(dst) return mem_dst - def emulbloc(self, irb, step=False): + def eval_updt_irblock(self, irb, step=False): """ Symbolic execution of the @irb on the current state @irb: irbloc instance @@ -530,35 +1032,38 @@ class SymbolicExecutionEngine(object): print 'Assignblk:' print assignblk print '_' * 80 - self.eval_ir(assignblk) + self.eval_updt_assignblk(assignblk) if step: - self.dump_id() - self.dump_mem() + self.dump(mems=False) + self.dump(ids=False) print '_' * 80 return self.eval_expr(self.ir_arch.IRDst) - def emul_ir_bloc(self, _, addr, step=False): - warnings.warn('DEPRECATION WARNING: use "emul_ir_block(self, addr, step=False)" instead of emul_ir_bloc') - return self.emul_ir_block(addr, step) - - def emul_ir_block(self, addr, step=False): + def run_block_at(self, addr, step=False): + """ + Symbolic execution of the block at @addr + @addr: address to execute (int or ExprInt or label) + @step: display intermediate steps + """ irblock = self.ir_arch.get_block(addr) if irblock is not None: - addr = self.emulbloc(irblock, step=step) + addr = self.eval_updt_irblock(irblock, step=step) return addr - def emul_ir_blocs(self, _, addr, lbl_stop=None, step=False): - warnings.warn('DEPRECATION WARNING: use "emul_ir_blocks(self, addr, lbl_stop=None, step=False):" instead of emul_ir_blocs') - return self.emul_ir_blocks(addr, lbl_stop, step) - - def emul_ir_blocks(self, addr, lbl_stop=None, step=False): + def run_at(self, addr, lbl_stop=None, step=False): + """ + Symbolic execution starting at @addr + @addr: address to execute (int or ExprInt or label) + @lbl_stop: AsmLabel to stop execution on + @step: display intermediate steps + """ while True: irblock = self.ir_arch.get_block(addr) if irblock is None: break if irblock.label == lbl_stop: break - addr = self.emulbloc(irblock, step=step) + addr = self.eval_updt_irblock(irblock, step=step) return addr def del_mem_above_stack(self, stack_ptr): @@ -568,32 +1073,177 @@ class SymbolicExecutionEngine(object): * pointer below current stack pointer """ stack_ptr = self.eval_expr(stack_ptr) - for mem_addr, (mem, _) in self.symbols.symbols_mem.items(): - diff = self.expr_simp(mem_addr - stack_ptr) - if not isinstance(diff, m2_expr.ExprInt): - continue - sign_bit = self.expr_simp(diff.msb()) - if sign_bit.arg == 1: - del self.symbols[mem] - - def apply_expr(self, expr): - """Evaluate @expr and apply side effect if needed (ie. if expr is an - assignment). Return the evaluated value""" + base, stk_offset = get_expr_base_offset(stack_ptr) + memarray = self.symbols.symbols_mem.base_to_memarray.get(base, None) + if memarray: + to_del = set() + for offset in memarray: + if ((offset - stk_offset) & int(stack_ptr.mask)) >> (stack_ptr.size - 1) != 0: + to_del.add(offset) + + for offset in to_del: + del memarray[offset] + + def eval_updt_expr(self, expr): + """ + Evaluate @expr and apply side effect if needed (ie. if expr is an + assignment). Return the evaluated value + """ # Update value if needed - if isinstance(expr, m2_expr.ExprAff): + if expr.is_aff(): ret = self.eval_expr(expr.src) - self.eval_ir(AssignBlock([expr])) + self.eval_updt_assignblk(AssignBlock([expr])) else: ret = self.eval_expr(expr) return ret + def _resolve_mem_parts(self, expr): + """For a given ExprMem @expr, get known/unknown parts from the store. + @expr: ExprMem instance + + Return a list of (known, value) where known is a bool representing if + the value has been resolved from the store or not. + """ + + # Extract known parts in symbols + assert expr.size % 8 == 0 + ptr = expr.arg + known = [] + ptrs = [] + for index in xrange(expr.size / 8): + offset = self.expr_simp(ptr + ExprInt(index, ptr.size)) + ptrs.append(offset) + mem = ExprMem(offset, 8) + known.append(mem in self.symbols) + + reads = merge_ptr_read(known, ptrs) + out = [] + for is_known, ptr_value, size in reads: + mem = ExprMem(ptr_value, size) + if is_known: + mem = self.symbols.read(mem) + out.append((is_known, mem)) + return out + + def mem_read(self, expr): + """ + [DEV]: Override to modify the effective memory reads + + Read symbolic value at ExprMem @expr + @expr: ExprMem + """ + + parts = self._resolve_mem_parts(expr) + + out = [] + for known, part in parts: + if not known and part.is_mem() and self.func_read is not None: + ret = self.func_read(part) + else: + ret = part + + out.append(ret) + ret = self.expr_simp(ExprCompose(*out)) + + assert ret.size == expr.size + return ret + + def mem_write(self, dst, src): + """ + [DEV]: Override to modify the effective memory writes + + Write symbolic value @src at ExprMem @dst + @dst: destination ExprMem + @src: source Expression + """ + if self.func_write is not None: + self.func_write(self, dst, src) + else: + self.symbols.write(dst, src) + + + # Deprecated methods + + def apply_expr_on_state(self, expr, cache): + """Deprecated version of eval_expr""" + warnings.warn('DEPRECATION WARNING: use "eval_expr" instead of apply_expr_on_state') + + if cache is None: + cache = {} + ret = self.eval_expr(expr, eval_cache=cache) + return ret + + def modified_mems(self, init_state=None): + """Deprecated version of modified(ids=False)""" + warnings.warn('DEPRECATION WARNING: use "modified(self, ids=False)" instead of modified_mems') + for mem in self.modified(init_state=init_state, ids=False): + yield mem + + def modified_regs(self, init_state=None): + """Deprecated version of modified(mems=False)""" + warnings.warn('DEPRECATION WARNING: use "modified(self, mems=False)" instead of modified_regs') + for reg in self.modified(init_state=init_state, mems=False): + yield reg + + def dump_id(self): + """Deprecated version of dump(mems=False)""" + warnings.warn('DEPRECATION WARNING: use "dump(self, mems=False)" instead of dump_id') + self.dump(mems=False) + + def dump_mem(self): + """Deprecated version of dump(ids=False)""" + warnings.warn('DEPRECATION WARNING: use "dump(self, ids=False)" instead of dump_mem') + self.dump(ids=False) + + def eval_ir_expr(self, assignblk): + """Deprecated version of eval_ir_expr(self, assignblk)""" + warnings.warn('DEPRECATION WARNING: use "eval_assignblk(self, assignblk)" instead of eval_ir_expr') + return self.eval_assignblk(assignblk).iteritems() + + def eval_ir(self, assignblk): + """Deprecated version of eval_updt_assignblk(self, assignblk)""" + warnings.warn('DEPRECATION WARNING: use "eval_assignblk(self, assignblk)" instead of eval_ir') + return self.eval_updt_assignblk(assignblk) + + def emulbloc(self, irb, step=False): + """Deprecated version of eval_updt_irblock(self, irb, step=False)""" + warnings.warn('DEPRECATION WARNING: use "eval_updt_irblock(self, irb, step=False)" instead of emulbloc') + return self.eval_updt_irblock(irb, step) + + def emul_ir_bloc(self, _, addr, step=False): + """Deprecated version of run_block_at""" + warnings.warn('DEPRECATION WARNING: use "run_block_at(self, addr, step=False)" instead of emul_ir_bloc') + return self.run_block_at(addr, step) + + def emul_ir_block(self, addr, step=False): + """Deprecated version of run_block_at""" + warnings.warn('DEPRECATION WARNING: use "run_block_at(self, addr, step=False)" instead of emul_ir_block') + return self.run_block_at(addr, step) + + def emul_ir_blocks(self, addr, lbl_stop=None, step=False): + """Deprecated version of run_at""" + warnings.warn('DEPRECATION WARNING: use "run_at(self, addr, lbl_stop=None, step=False):" instead of emul_ir_blocks') + return self.run_at(addr, lbl_stop, step) + + def emul_ir_blocs(self, _, addr, lbl_stop=None, step=False): + """Deprecated version of run_at""" + warnings.warn('DEPRECATION WARNING: use "run_at(self, addr, lbl_stop=None, step=False):" instead of emul_ir_blocs') + return self.run_at(addr, lbl_stop, step) + + def apply_expr(self, expr): + """Deprecated version of eval_updt_expr""" + warnings.warn('DEPRECATION WARNING: use "eval_updt_expr" instead of apply_expr') + return self.eval_updt_expr(expr) + def as_assignblock(self): """Return the current state as an AssignBlock""" - return AssignBlock({ - dst: self.symbols[dst] for dst in self.modified() - }) + warnings.warn('DEPRECATION WARNING: use "modified(ids=True, mems=True)" instead of as_assignblock') + out = [] + for dst, src in self.modified(ids=True, mems=True): + out.append((dst, src)) + return AssignBlock(dict(out)) class symbexec(SymbolicExecutionEngine): |