Update syscall/funcall implementation (#213)
* Wrap ABI semantics in its own class hierarchy * Define a model invocation for syscalls and function calls * Add unit tests for ABI * Add a common base class for Platform models
This commit is contained in:
parent
3873c3eb5d
commit
3c9653d1d7
@ -6,10 +6,13 @@ from ..smtlib import Expression, Bool, BitVec, Array, Operators, Constant
|
||||
from ..memory import MemoryException, FileMap, AnonMap
|
||||
from ...utils.helpers import issymbolic
|
||||
from ...utils.emulate import UnicornEmulator
|
||||
import sys
|
||||
from functools import wraps
|
||||
from itertools import islice, imap
|
||||
import inspect
|
||||
import sys
|
||||
import types
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger("CPU")
|
||||
register_logger = logging.getLogger("REGISTERS")
|
||||
|
||||
@ -162,7 +165,133 @@ class RegisterFile(object):
|
||||
|
||||
:param register: a register name
|
||||
'''
|
||||
return self._alias(register) in self.all_registers
|
||||
return self._alias(register) in self.all_registers
|
||||
|
||||
class Abi(object):
|
||||
'''
|
||||
Represents the ability to extract arguments from the environment and write
|
||||
back a result.
|
||||
|
||||
Used for function call and system call models.
|
||||
'''
|
||||
def __init__(self, cpu):
|
||||
'''
|
||||
:param manticore.core.cpu.Cpu cpu: CPU to initialize with
|
||||
'''
|
||||
self._cpu = cpu
|
||||
|
||||
def get_arguments(self):
|
||||
'''
|
||||
Extract model arguments conforming to `convention`. Produces an iterable
|
||||
of argument descriptors following the calling convention. A descriptor
|
||||
is either a string describing a register, or an address (concrete or
|
||||
symbolic).
|
||||
|
||||
:return: iterable returning syscall arguments.
|
||||
:rtype: iterable
|
||||
'''
|
||||
raise NotImplementedError
|
||||
|
||||
def write_result(self, result):
|
||||
'''
|
||||
Write the result of a model back to the environment.
|
||||
|
||||
:param result: result of the model implementation
|
||||
'''
|
||||
raise NotImplementedError
|
||||
|
||||
def ret(self):
|
||||
'''
|
||||
Handle the "ret" semantics of the ABI, i.e. reclaiming stack space,
|
||||
popping PC, etc.
|
||||
|
||||
A null operation by default.
|
||||
'''
|
||||
return
|
||||
|
||||
def values_from(self, base):
|
||||
'''
|
||||
A reusable generator for increasing pointer-sized values from an address
|
||||
(usually the stack).
|
||||
'''
|
||||
word_bytes = self._cpu.address_bit_size / 8
|
||||
while True:
|
||||
yield base
|
||||
base += word_bytes
|
||||
|
||||
def invoke(self, model, prefix_args=None, varargs=False):
|
||||
'''
|
||||
Invoke a callable `model` as if it was a native function. If `varargs`
|
||||
is true, model receives a single argument that is a generator for
|
||||
function arguments. Pass a tuple of arguments for `prefix_args` you'd
|
||||
like to precede the actual arguments.
|
||||
|
||||
:param callable model: Python model of the function
|
||||
:param tuple prefix_args: Parameters to pass to model before actual ones
|
||||
:param bool varargs: Whether the function expects a variable number of arguments
|
||||
:return: The result of calling `model`
|
||||
'''
|
||||
prefix_args = prefix_args or ()
|
||||
|
||||
spec = inspect.getargspec(model)
|
||||
|
||||
if spec.varargs:
|
||||
logger.warning("ABI: A vararg model must be a unary function.")
|
||||
|
||||
nargs = len(spec.args) - len(prefix_args)
|
||||
|
||||
# If the model is a method, we need to account for `self`
|
||||
if inspect.ismethod(model):
|
||||
nargs -= 1
|
||||
|
||||
def resolve_argument(arg):
|
||||
if isinstance(arg, str):
|
||||
return self._cpu.read_register(arg)
|
||||
else:
|
||||
return self._cpu.read_int(arg)
|
||||
|
||||
# Create a stream of resolved arguments from argument descriptors
|
||||
descriptors = self.get_arguments()
|
||||
argument_iter = imap(resolve_argument, descriptors)
|
||||
|
||||
try:
|
||||
if varargs:
|
||||
result = model(*(prefix_args + (argument_iter,)))
|
||||
else:
|
||||
argument_tuple = prefix_args + tuple(islice(argument_iter, nargs))
|
||||
result = model(*argument_tuple)
|
||||
except ConcretizeArgument as e:
|
||||
assert e.argnum >= len(prefix_args), "Can't concretize a constant arg"
|
||||
idx = e.argnum - len(prefix_args)
|
||||
|
||||
# Arguments were lazily computed in case of varargs, so recompute here
|
||||
descriptors = self.get_arguments()
|
||||
src = next(islice(descriptors, idx, idx+1))
|
||||
|
||||
msg = 'Concretizing due to model invocation'
|
||||
if isinstance(src, str):
|
||||
raise ConcretizeRegister(src, msg)
|
||||
else:
|
||||
raise ConcretizeMemory(src, self._cpu.address_bit_size, msg)
|
||||
else:
|
||||
if result is not None:
|
||||
self.write_result(result)
|
||||
|
||||
self.ret()
|
||||
|
||||
return result
|
||||
|
||||
class SyscallAbi(Abi):
|
||||
'''
|
||||
A system-call specific ABI.
|
||||
'''
|
||||
def syscall_number(self):
|
||||
'''
|
||||
Extract the index of the invoked syscall.
|
||||
|
||||
:return: int
|
||||
'''
|
||||
raise NotImplementedError
|
||||
|
||||
############################################################################
|
||||
# Abstract cpu encapsulating common cpu methods used by models and executor.
|
||||
@ -205,7 +334,7 @@ class Cpu(object):
|
||||
def __setstate__(self, state):
|
||||
Cpu.__init__(self, state['regfile'], state['memory'])
|
||||
self._icount = state['icount']
|
||||
return
|
||||
return
|
||||
|
||||
@property
|
||||
def icount(self):
|
||||
@ -453,10 +582,6 @@ class Cpu(object):
|
||||
implementation(*instruction.operands)
|
||||
self._icount+=1
|
||||
|
||||
@abstractmethod
|
||||
def get_syscall_description(self):
|
||||
pass
|
||||
|
||||
def emulate(self, instruction):
|
||||
'''
|
||||
If we could not handle emulating an instruction, use Unicorn to emulate
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import struct
|
||||
import sys
|
||||
from .abstractcpu import Cpu, RegisterFile, Operand
|
||||
from .abstractcpu import Abi, SyscallAbi, Cpu, RegisterFile, Operand
|
||||
from .abstractcpu import SymbolicPCException, InvalidPCException, Interruption
|
||||
from .abstractcpu import instruction as abstract_instruction
|
||||
from .register import Register
|
||||
@ -253,6 +253,42 @@ class Armv7RegisterFile(RegisterFile):
|
||||
return ('R0','R1','R2','R3','R4','R5','R6','R7','R8','R9','R10','R11','R12','R13','R14','R15','APSR')
|
||||
|
||||
|
||||
class Armv7LinuxSyscallAbi(SyscallAbi):
|
||||
'''
|
||||
ARMv7 Linux system call ABI
|
||||
'''
|
||||
# EABI standards:
|
||||
# syscall # is in R7
|
||||
# arguments are passed in R0-R6
|
||||
# retval is passed in R0
|
||||
def syscall_number(self):
|
||||
return self._cpu.R7
|
||||
|
||||
def get_arguments(self):
|
||||
for i in range(6):
|
||||
yield 'R{}'.format(i)
|
||||
|
||||
def write_result(self, result):
|
||||
self._cpu.R0 = result
|
||||
|
||||
class Armv7CdeclAbi(Abi):
|
||||
'''
|
||||
ARMv7 Cdecl function call ABI
|
||||
'''
|
||||
def get_arguments(self):
|
||||
# First four passed via R0-R3, then on stack
|
||||
for reg in ('R0', 'R1', 'R2', 'R3'):
|
||||
yield reg
|
||||
|
||||
for address in self.values_from(self._cpu.STACK):
|
||||
yield address
|
||||
|
||||
def write_result(self, result):
|
||||
self._cpu.R0 = result
|
||||
|
||||
def ret(self):
|
||||
self._cpu.PC = self._cpu.LR
|
||||
|
||||
class Armv7Cpu(Cpu):
|
||||
'''
|
||||
Cpu specialization handling the ARMv7 architecture.
|
||||
@ -280,6 +316,7 @@ class Armv7Cpu(Cpu):
|
||||
state['_force_next'] = self._force_next
|
||||
return state
|
||||
|
||||
|
||||
def __setstate__(self, state):
|
||||
super(Armv7Cpu, self).__setstate__(state)
|
||||
self._last_flags = state['_last_flags']
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
from .x86 import AMD64Cpu, I386Cpu
|
||||
from .arm import Armv7Cpu
|
||||
from .x86 import AMD64Cpu, I386Cpu, AMD64LinuxSyscallAbi, I386LinuxSyscallAbi, I386CdeclAbi, SystemVAbi
|
||||
from .arm import Armv7Cpu, Armv7CdeclAbi, Armv7LinuxSyscallAbi
|
||||
|
||||
class CpuFactory(object):
|
||||
_cpus = {
|
||||
@ -12,3 +12,26 @@ class CpuFactory(object):
|
||||
def get_cpu(mem, machine):
|
||||
return CpuFactory._cpus[machine](mem)
|
||||
|
||||
@staticmethod
|
||||
def get_function_abi(cpu, os, machine):
|
||||
if os == 'linux' and machine == 'i386':
|
||||
return I386CdeclAbi(cpu)
|
||||
elif os == 'linux' and machine == 'amd64':
|
||||
return SystemVAbi(cpu)
|
||||
elif os == 'linux' and machine == 'armv7':
|
||||
return Armv7CdeclAbi(cpu)
|
||||
else:
|
||||
return NotImplementedError("OS and machine combination not supported: {}/{}".format(os, machine))
|
||||
|
||||
@staticmethod
|
||||
def get_syscall_abi(cpu, os, machine):
|
||||
if os == 'linux' and machine == 'i386':
|
||||
return I386LinuxSyscallAbi(cpu)
|
||||
elif os == 'linux' and machine == 'amd64':
|
||||
return AMD64LinuxSyscallAbi(cpu)
|
||||
elif os == 'linux' and machine == 'armv7':
|
||||
return Armv7LinuxSyscallAbi(cpu)
|
||||
else:
|
||||
return NotImplementedError("OS and machine combination not supported: {}/{}".format(os, machine))
|
||||
|
||||
|
||||
|
||||
@ -1,15 +1,10 @@
|
||||
from capstone import *
|
||||
from capstone.x86 import *
|
||||
from .abstractcpu import Cpu, RegisterFile, Operand, SANE_SIZES, instruction
|
||||
from .abstractcpu import SymbolicPCException, InvalidPCException, Interruption, Sysenter, Syscall, ConcretizeRegister, ConcretizeArgument
|
||||
import sys
|
||||
import struct
|
||||
import types
|
||||
import weakref
|
||||
from functools import wraps, partial
|
||||
from .abstractcpu import Abi, SyscallAbi, Cpu, RegisterFile, Operand, instruction
|
||||
from .abstractcpu import Interruption, Sysenter, Syscall, ConcretizeRegister, ConcretizeArgument
|
||||
from functools import wraps
|
||||
import collections
|
||||
from ..smtlib import *
|
||||
from ..memory import MemoryException
|
||||
from ...utils.helpers import issymbolic
|
||||
import logging
|
||||
logger = logging.getLogger("CPU")
|
||||
@ -754,17 +749,6 @@ class X86Cpu(Cpu):
|
||||
if address+offset in cache:
|
||||
del cache[address+offset]
|
||||
|
||||
def get_syscall_description(self):
|
||||
# Syscall number is in RAX
|
||||
# Arguments are in RDI, RSI, RDX, R10, R8 and R9
|
||||
# Return is in RAX
|
||||
index = self.RAX
|
||||
arguments = [ self.RDI, self.RSI, self.RDX, self.R10, self.R8, self.R9 ]
|
||||
def writeResult(result, self=self):
|
||||
self.RAX = result
|
||||
return (index, arguments, writeResult)
|
||||
|
||||
|
||||
def canonicalize_instruction_name(self, instruction):
|
||||
#MOVSD
|
||||
if instruction.opcode[0] in (0xa4, 0xa5):
|
||||
@ -5636,99 +5620,104 @@ class X86Cpu(Cpu):
|
||||
################################################################################
|
||||
#Calling conventions
|
||||
|
||||
class ABI:
|
||||
'''IA32 Calling conventions
|
||||
https://en.wikipedia.org/wiki/X86_calling_conventions
|
||||
class I386LinuxSyscallAbi(SyscallAbi):
|
||||
'''
|
||||
@staticmethod
|
||||
def cdecl(function):
|
||||
'''C declaration
|
||||
Subroutine arguments are passed on the stack.
|
||||
Integer values and memory addresses are returned in the EAX register
|
||||
'''
|
||||
argcount = function.func_code.co_argcount - 1
|
||||
assert argcount >= 0
|
||||
def cdecl_function(model):
|
||||
cpu = model.current
|
||||
base = cpu.STACK+4 #skip ret address
|
||||
arguments = [ cpu.read_int(base + (i*4), 32) for i in xrange(argcount) ]
|
||||
try:
|
||||
cpu.EAX = function(model, *arguments)
|
||||
except ConcretizeArgument as cae:
|
||||
assert 0 <= cae.argnum < argcount
|
||||
# concretize here
|
||||
mem_addr = base+cae.argnum*4
|
||||
raise ConcretizeMemory(mem_addr, 32, "Concretizing Function Argument", 'MINMAX')
|
||||
i386 Linux system call ABI
|
||||
'''
|
||||
def syscall_number(self):
|
||||
return self._cpu.EAX
|
||||
|
||||
cpu.EIP = cpu.pop(32)
|
||||
return cdecl_function
|
||||
def get_arguments(self):
|
||||
for reg in ('EBX', 'ECX', 'EDX', 'ESI', 'EDI', 'EBP'):
|
||||
yield reg
|
||||
|
||||
@staticmethod
|
||||
def stdcall(function):
|
||||
'''Standard calling convention
|
||||
Subroutine arguments are passed on the stack.
|
||||
Callee is responsible for cleaning up the stack.
|
||||
Return values are stored in the EAX register.
|
||||
'''
|
||||
argcount = function.func_code.co_argcount - 1
|
||||
assert argcount >= 0
|
||||
def stdcall_function(model):
|
||||
cpu = model.current
|
||||
# skip saved EIP on stack
|
||||
base = cpu.STACK+4
|
||||
arguments = [ cpu.read_int(base+(pos*4), 32) for pos in xrange(argcount) ]
|
||||
try:
|
||||
cpu.EAX = function(model, *arguments)
|
||||
except ConcretizeArgument as cae:
|
||||
assert 0 <= cae.argnum < argcount
|
||||
# concretize here
|
||||
mem_addr = base+cae.argnum*4
|
||||
raise ConcretizeMemory(mem_addr, 32, "Concretizing Function Argument", 'MINMAX')
|
||||
def write_result(self, result):
|
||||
self._cpu.EAX = result
|
||||
|
||||
cpu.EIP = cpu.pop(32)
|
||||
cpu.STACK += argcount*4
|
||||
return stdcall_function
|
||||
class AMD64LinuxSyscallAbi(SyscallAbi):
|
||||
'''
|
||||
AMD64 Linux system call ABI
|
||||
'''
|
||||
|
||||
@staticmethod
|
||||
def thiscall(function):
|
||||
pass
|
||||
#TODO(yan): Floating point or wide arguments that deviate from the norm are
|
||||
# not yet supported.
|
||||
|
||||
@staticmethod
|
||||
def vectorcall(function):
|
||||
pass
|
||||
def syscall_number(self):
|
||||
return self._cpu.RAX
|
||||
|
||||
'''AMD64 Calling conventions '''
|
||||
@staticmethod
|
||||
def systemV(function):
|
||||
'''System V AMD64 calling convention
|
||||
The first six integer or pointer arguments are passed in registers:
|
||||
RDI, RSI, RDX, RCX, R8, and R9,
|
||||
Additional arguments are passed on the stack.
|
||||
Return value is stored in RAX.[16]:22
|
||||
'''
|
||||
argcount = function.func_code.co_argcount - 1
|
||||
assert argcount >= 0
|
||||
def argument(cpu):
|
||||
yield cpu.RDI
|
||||
yield cpu.RSI
|
||||
yield cpu.RDX
|
||||
yield cpu.RCX
|
||||
yield cpu.R8
|
||||
yield cpu.R9
|
||||
stack = cpu.STACK+8
|
||||
while True:
|
||||
yield cpu.read_int(stack,64)
|
||||
stack += 8
|
||||
def systemV_function(model):
|
||||
cpu = model.current
|
||||
arguments = [ next(argument(cpu)) for _ in xrange(argcount) ]
|
||||
cpu.RAX = function(cpu, *arguments)
|
||||
cpu.RIP = cpu.pop(64)
|
||||
return systemV_function
|
||||
def get_arguments(self):
|
||||
for reg in ('RDI', 'RSI', 'RDX', 'R10', 'R8', 'R9'):
|
||||
yield reg
|
||||
|
||||
def write_result(self, result):
|
||||
self._cpu.RAX = result
|
||||
|
||||
|
||||
class I386CdeclAbi(Abi):
|
||||
'''
|
||||
i386 cdecl function call semantics
|
||||
'''
|
||||
def get_arguments(self):
|
||||
base = self._cpu.STACK + self._cpu.address_bit_size / 8
|
||||
for address in self.values_from(base):
|
||||
yield address
|
||||
|
||||
def write_result(self, result):
|
||||
self._cpu.EAX = result
|
||||
|
||||
def ret(self):
|
||||
self._cpu.EIP = self._cpu.pop(self._cpu.address_bit_size)
|
||||
|
||||
class I386StdcallAbi(Abi):
|
||||
'''
|
||||
x86 Stdcall function call convention. Callee cleans up the stack.
|
||||
'''
|
||||
def __init__(self, cpu):
|
||||
super(I386StdcallAbi, self).__init__(cpu)
|
||||
self._arguments = 0
|
||||
|
||||
def get_arguments(self):
|
||||
base = self._cpu.STACK + self._cpu.address_bit_size / 8
|
||||
for address in self.values_from(base):
|
||||
self._arguments += 1
|
||||
yield address
|
||||
|
||||
def write_result(self, result):
|
||||
self._cpu.EAX = result
|
||||
|
||||
def ret(self):
|
||||
self._cpu.EIP = self._cpu.pop(self._cpu.address_bit_size)
|
||||
|
||||
word_bytes = self._cpu.address_bit_size / 8
|
||||
self._cpu.ESP += self._arguments * word_bytes
|
||||
self._arguments = 0
|
||||
|
||||
class SystemVAbi(Abi):
|
||||
'''
|
||||
x64 SystemV function call convention
|
||||
'''
|
||||
|
||||
#TODO(yan): Floating point or wide arguments that deviate from the norm are
|
||||
# not yet supported.
|
||||
|
||||
def get_arguments(self):
|
||||
# First 6 arguments go in registers, rest are popped from stack
|
||||
reg_args = ('RDI', 'RSI', 'RDX', 'RCX', 'R8', 'R9')
|
||||
|
||||
for reg in reg_args:
|
||||
yield reg
|
||||
|
||||
word_bytes = self._cpu.address_bit_size / 8
|
||||
for address in self.values_from(self._cpu.RSP + word_bytes):
|
||||
yield address
|
||||
|
||||
def write_result(self, result):
|
||||
# XXX(yan): Can also return in rdx for wide values.
|
||||
self._cpu.RAX = result
|
||||
|
||||
def ret(self):
|
||||
self._cpu.RIP = self._cpu.pop(self._cpu.address_bit_size)
|
||||
|
||||
@staticmethod
|
||||
def msx64(function):
|
||||
pass
|
||||
|
||||
class AMD64Cpu(X86Cpu):
|
||||
#Config
|
||||
@ -5837,7 +5826,6 @@ class AMD64Cpu(X86Cpu):
|
||||
cpu.AL = cpu.read_int(cpu.RBX + Operators.ZEXTEND(cpu.AL, 64), 8)
|
||||
|
||||
|
||||
|
||||
class I386Cpu(X86Cpu):
|
||||
#Config
|
||||
max_instr_width = 15
|
||||
@ -5845,17 +5833,6 @@ class I386Cpu(X86Cpu):
|
||||
arch = CS_ARCH_X86
|
||||
mode = CS_MODE_32
|
||||
|
||||
def get_syscall_description(self):
|
||||
# Syscall number is in RAX
|
||||
# Arguments are in RDI, RSI, RDX, R10, R8 and R9
|
||||
# Return is in RAX
|
||||
index = self.EAX
|
||||
arguments = [self.EBX, self.ECX, self.EDX, self.ESI, self.EDI, self.EBP]
|
||||
def writeResult(result, self=self):
|
||||
self.RAX = result
|
||||
return (index, arguments, writeResult)
|
||||
|
||||
|
||||
def __init__(self, memory, *args, **kwargs):
|
||||
'''
|
||||
Builds a CPU model.
|
||||
|
||||
@ -59,7 +59,7 @@ def makeLinux(program, argv, env, concrete_start = ''):
|
||||
# If any of the arguments or environment refer to symbolic values, re-
|
||||
# initialize the stack
|
||||
if any(issymbolic(x) for val in argv + env for x in val):
|
||||
model.setup_stack(initial_state.cpu, [program] + argv, env)
|
||||
model.setup_stack([program] + argv, env)
|
||||
|
||||
model.input.transmit(concrete_start)
|
||||
|
||||
@ -486,19 +486,15 @@ class Manticore(object):
|
||||
with open(path, 'r') as fnames:
|
||||
for line in fnames.readlines():
|
||||
address, cc_name, name = line.strip().split(' ')
|
||||
cc = getattr(core.cpu.x86.ABI, cc_name)
|
||||
fmodel = models
|
||||
name_parts = name.split('.')
|
||||
importlib.import_module(".models.{}".format(name_parts[0]), 'manticore')
|
||||
for n in name_parts:
|
||||
fmodel = getattr(fmodel,n)
|
||||
assert fmodel != models
|
||||
logger.debug("[+] Hooking 0x%x %s %s", int(address,0), cc_name, name )
|
||||
def cb_function(cc, fmodel, state):
|
||||
cc(fmodel)(state.model)
|
||||
cb = functools.partial(cb_function, cc, fmodel)
|
||||
# TODO(yan) this should be a dict
|
||||
self._model_hooks.setdefault(int(address,0), set()).add(cb)
|
||||
def cb_function(state):
|
||||
state.model.invoke_model(fmodel, prefix_args=(state.model,))
|
||||
self._model_hooks.setdefault(int(address,0), set()).add(cb_function)
|
||||
|
||||
def _model_hook_callback(self, state):
|
||||
pc = state.cpu.PC
|
||||
|
||||
@ -9,6 +9,7 @@ from ..core.cpu.abstractcpu import Interruption, Syscall, ConcretizeRegister
|
||||
from ..core.cpu.cpufactory import CpuFactory
|
||||
from ..core.memory import SMemory32, SMemory64, Memory32, Memory64
|
||||
from ..core.smtlib import Operators, ConstraintSet
|
||||
from ..models.platform import Platform
|
||||
from elftools.elf.elffile import ELFFile
|
||||
import logging
|
||||
import random
|
||||
@ -256,7 +257,7 @@ class Socket(object):
|
||||
return len(buf)
|
||||
|
||||
|
||||
class Linux(object):
|
||||
class Linux(Platform):
|
||||
'''
|
||||
A simple Linux Operating System Model.
|
||||
This class emulates the most common Linux system calls
|
||||
@ -269,7 +270,7 @@ class Linux(object):
|
||||
:param list argv: The argv array; not including binary.
|
||||
:param list envp: The ENV variables.
|
||||
'''
|
||||
|
||||
super(Linux, self).__init__(program)
|
||||
argv = [] if argv is None else argv
|
||||
envp = [] if envp is None else envp
|
||||
|
||||
@ -303,7 +304,10 @@ class Linux(object):
|
||||
|
||||
#Load process and setup socketpairs
|
||||
arch = {'x86': 'i386', 'x64': 'amd64', 'ARM': 'armv7'}[ELFFile(file(program)).get_machine_arch()]
|
||||
self.procs = [self._mk_proc(arch)]
|
||||
cpu = self._mk_proc(arch)
|
||||
self.procs = [cpu]
|
||||
self._function_abi = CpuFactory.get_function_abi(cpu, 'linux', arch)
|
||||
self._syscall_abi = CpuFactory.get_syscall_abi(cpu, 'linux', arch)
|
||||
|
||||
self._current = 0
|
||||
self.load(program)
|
||||
@ -362,6 +366,8 @@ class Linux(object):
|
||||
state['auxv'] = self.auxv
|
||||
state['program'] = self.program
|
||||
state['syscall_arg_regs'] = self.syscall_arg_regs
|
||||
state['functionabi'] = self._function_abi
|
||||
state['syscallabi'] = self._syscall_abi
|
||||
if hasattr(self, '_arm_tls_memory'):
|
||||
state['_arm_tls_memory'] = self._arm_tls_memory
|
||||
return state
|
||||
@ -410,6 +416,8 @@ class Linux(object):
|
||||
self.auxv = state['auxv']
|
||||
self.program = state['program']
|
||||
self.syscall_arg_regs = state['syscall_arg_regs']
|
||||
self._function_abi = state['functionabi']
|
||||
self._syscall_abi = state['syscallabi']
|
||||
if '_arm_tls_memory' in state:
|
||||
self._arm_tls_memory = state['_arm_tls_memory']
|
||||
|
||||
@ -943,7 +951,6 @@ class Linux(object):
|
||||
def _is_open(self, fd):
|
||||
return fd >= 0 and fd < len(self.files) and self.files[fd] is not None
|
||||
|
||||
|
||||
def sys_lseek(self, fd, offset, whence):
|
||||
'''
|
||||
lseek - reposition read/write file offset
|
||||
@ -1485,20 +1492,13 @@ class Linux(object):
|
||||
|
||||
}
|
||||
|
||||
index, arguments, writeResult = self.current.get_syscall_description()
|
||||
index = self._syscall_abi.syscall_number()
|
||||
|
||||
if index not in syscalls:
|
||||
raise SyscallNotImplemented(64, index)
|
||||
|
||||
func = syscalls[index]
|
||||
return self._syscall_abi.invoke(syscalls[index])
|
||||
|
||||
logger.debug("SYSCALL64: %s %r ", func.func_name
|
||||
, arguments[:func.func_code.co_argcount])
|
||||
nargs = func.func_code.co_argcount
|
||||
|
||||
result = func(*arguments[:nargs-1])
|
||||
writeResult(result)
|
||||
return result
|
||||
|
||||
def int80(self):
|
||||
'''
|
||||
@ -1540,19 +1540,13 @@ class Linux(object):
|
||||
0x00000014: self.sys_getpid,
|
||||
0x000f0005: self.sys_ARM_NR_set_tls,
|
||||
}
|
||||
index, arguments, writeResult = self.current.get_syscall_description()
|
||||
|
||||
index = self._syscall_abi.syscall_number()
|
||||
|
||||
if index not in syscalls:
|
||||
raise SyscallNotImplemented(64, index)
|
||||
func = syscalls[index]
|
||||
|
||||
logger.debug("int80: %s %r ", func.func_name
|
||||
, arguments[:func.func_code.co_argcount])
|
||||
nargs = func.func_code.co_argcount
|
||||
|
||||
result = func(*arguments[:nargs-1])
|
||||
writeResult(result)
|
||||
return result
|
||||
return self._syscall_abi.invoke(syscalls[index])
|
||||
|
||||
def sys_clock_gettime(self, clock_id, timespec):
|
||||
logger.info("sys_clock_time not really implemented")
|
||||
@ -1852,6 +1846,7 @@ class SLinux(Linux):
|
||||
mem = SMemory32(self.constraints)
|
||||
else:
|
||||
mem = SMemory64(self.constraints)
|
||||
|
||||
return CpuFactory.get_cpu(mem, arch)
|
||||
|
||||
@property
|
||||
|
||||
13
manticore/models/platform.py
Normal file
13
manticore/models/platform.py
Normal file
@ -0,0 +1,13 @@
|
||||
|
||||
from itertools import islice, imap
|
||||
import inspect
|
||||
|
||||
class Platform(object):
|
||||
'''
|
||||
Base class for all operating system models.
|
||||
'''
|
||||
def __init__(self, path):
|
||||
self._path = path
|
||||
|
||||
def invoke_model(self, model, prefix_args=None, varargs=False):
|
||||
self._function_abi.invoke(model, prefix_args, varargs)
|
||||
@ -4,11 +4,12 @@ import sys, os, struct
|
||||
from ..core.memory import Memory, MemoryException, SMemory32, Memory32
|
||||
from ..core.smtlib import Expression, Operators, solver
|
||||
# TODO use cpu factory
|
||||
from ..core.cpu.x86 import I386Cpu, Sysenter
|
||||
from ..core.cpu.x86 import I386Cpu, Sysenter, I386StdcallAbi
|
||||
from ..core.cpu.abstractcpu import Interruption, Syscall, \
|
||||
ConcretizeRegister, ConcretizeArgument, IgnoreAPI
|
||||
from ..core.executor import ForkState, SyscallNotImplemented
|
||||
from ..utils.helpers import issymbolic
|
||||
from ..models.platform import Platform
|
||||
|
||||
from ..binary.pe import minidump
|
||||
|
||||
@ -47,7 +48,7 @@ def toStr(state, value):
|
||||
value = minmax[0]
|
||||
return '{:08x}'.format(value)
|
||||
|
||||
class Windows(object):
|
||||
class Windows(Platform):
|
||||
'''
|
||||
A simple Windows Operating System Model.
|
||||
This class emulates some Windows system calls
|
||||
@ -70,6 +71,8 @@ class Windows(object):
|
||||
'''
|
||||
Builds a Windows OS model
|
||||
'''
|
||||
super(Windows, self).__init__(path)
|
||||
|
||||
self.clocks = 0
|
||||
self.files = []
|
||||
self.syscall_trace = []
|
||||
@ -171,6 +174,7 @@ class Windows(object):
|
||||
self.running.append(self.procs.index(cpu))
|
||||
|
||||
|
||||
self._function_abi = I386StdcallAbi(self.procs[0])
|
||||
# open standard files stdin, stdout, stderr
|
||||
logger.info("Not Opening any file")
|
||||
|
||||
@ -193,6 +197,7 @@ class Windows(object):
|
||||
state['syscall_trace'] = self.syscall_trace
|
||||
state['files'] = self.files
|
||||
state['flavor'] = self.flavor
|
||||
state['function_abi'] = self._function_abi
|
||||
|
||||
return state
|
||||
|
||||
@ -208,6 +213,7 @@ class Windows(object):
|
||||
self.syscall_trace = state['syscall_trace']
|
||||
self.files = state['files']
|
||||
self.flavor = state['flavor']
|
||||
self._function_abi = state['function_abi']
|
||||
|
||||
def _read_string(self, cpu, buf):
|
||||
"""
|
||||
|
||||
426
tests/test_abi.py
Normal file
426
tests/test_abi.py
Normal file
@ -0,0 +1,426 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import unittest
|
||||
import sys
|
||||
import shutil
|
||||
import tempfile
|
||||
import os
|
||||
import hashlib
|
||||
import subprocess
|
||||
import collections
|
||||
import time
|
||||
|
||||
from manticore import Manticore, issymbolic
|
||||
from manticore.core.smtlib import BitVecVariable
|
||||
from manticore.core.cpu.abstractcpu import ConcretizeArgument, ConcretizeRegister, ConcretizeMemory
|
||||
from manticore.core.cpu.arm import Armv7Cpu, Armv7LinuxSyscallAbi, Armv7CdeclAbi
|
||||
from manticore.core.cpu.x86 import I386Cpu, AMD64Cpu, I386LinuxSyscallAbi, I386StdcallAbi, I386CdeclAbi, AMD64LinuxSyscallAbi, SystemVAbi
|
||||
from manticore.core.memory import SMemory32, Memory32, SMemory64
|
||||
from manticore.core.smtlib import ConstraintSet, Operators
|
||||
|
||||
class ABITests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
mem32 = SMemory32(ConstraintSet())
|
||||
mem32.mmap(0x1000, 0x1000, 'rw ')
|
||||
mem64 = SMemory64(ConstraintSet())
|
||||
mem64.mmap(0x1000, 0x1000, 'rw ')
|
||||
|
||||
self._cpu_arm = Armv7Cpu(mem32)
|
||||
self._cpu_arm.SP = 0x1080
|
||||
self._cpu_arm.func_abi = Armv7CdeclAbi(self._cpu_arm)
|
||||
self._cpu_arm.syscall_abi = Armv7LinuxSyscallAbi(self._cpu_arm)
|
||||
|
||||
self._cpu_x86 = I386Cpu(mem32)
|
||||
self._cpu_x86.ESP = 0x1080
|
||||
self._cpu_x86.func_abi = I386CdeclAbi(self._cpu_x86)
|
||||
self._cpu_x86.syscall_abi = I386LinuxSyscallAbi(self._cpu_x86)
|
||||
|
||||
self._cpu_x64 = AMD64Cpu(mem64)
|
||||
self._cpu_x64.RSP = 0x1080
|
||||
self._cpu_x64.func_abi = SystemVAbi(self._cpu_x64)
|
||||
self._cpu_x64.syscall_abi = AMD64LinuxSyscallAbi(self._cpu_x64)
|
||||
|
||||
def write(mem, where, val, size):
|
||||
mem[where:where+size/8] = [Operators.CHR(Operators.EXTRACT(val, offset, 8)) for offset in xrange(0, size, 8)]
|
||||
for val in range(0, 0x100, 4):
|
||||
write(mem32, 0x1000+val, val, 32)
|
||||
for val in range(0, 0x100, 8):
|
||||
write(mem64, 0x1000+val, val, 64)
|
||||
|
||||
def test_executor(self):
|
||||
pass
|
||||
|
||||
def test_arm_abi_simple(self):
|
||||
cpu = self._cpu_arm
|
||||
|
||||
for i in range(4):
|
||||
cpu.write_register('R{}'.format(i), i)
|
||||
|
||||
cpu.LR = 0x1234
|
||||
|
||||
def test(one, two, three, four):
|
||||
self.assertEqual(one, 0)
|
||||
self.assertEqual(two, 1)
|
||||
self.assertEqual(three, 2)
|
||||
self.assertEqual(four, 3)
|
||||
return 34
|
||||
|
||||
cpu.func_abi.invoke(test)
|
||||
|
||||
# result is correctly captured
|
||||
self.assertEquals(cpu.R0, 34)
|
||||
# sp is unchanged
|
||||
self.assertEquals(cpu.SP, 0x1080)
|
||||
# returned correctly
|
||||
self.assertEquals(cpu.PC, cpu.LR)
|
||||
|
||||
def test_arm_abi(self):
|
||||
cpu = self._cpu_arm
|
||||
|
||||
for i in range(4):
|
||||
cpu.write_register('R{}'.format(i), i)
|
||||
|
||||
cpu.LR = 0x1234
|
||||
|
||||
self.assertEqual(cpu.read_int(cpu.SP), 0x80)
|
||||
|
||||
def test(one, two, three, four, five, six, seven):
|
||||
self.assertEqual(one, 0)
|
||||
self.assertEqual(two, 1)
|
||||
self.assertEqual(three, 2)
|
||||
self.assertEqual(four, 3)
|
||||
self.assertEqual(five, 0x80)
|
||||
self.assertEqual(six, 0x84)
|
||||
self.assertEqual(seven, 0x88)
|
||||
|
||||
self.assertEqual(cpu.SP, 0x1080)
|
||||
return 34
|
||||
|
||||
cpu.func_abi.invoke(test)
|
||||
|
||||
# result is correctly captured
|
||||
self.assertEquals(cpu.R0, 34)
|
||||
# sp is unchanged
|
||||
self.assertEquals(cpu.SP, 0x1080)
|
||||
# returned correctly
|
||||
self.assertEquals(cpu.PC, cpu.LR)
|
||||
|
||||
def test_arm_abi_concretize_register(self):
|
||||
cpu = self._cpu_arm
|
||||
|
||||
for i in range(4):
|
||||
cpu.write_register('R{}'.format(i), i)
|
||||
|
||||
previous_r0 = cpu.R0
|
||||
self.assertEqual(cpu.read_int(cpu.SP), 0x80)
|
||||
|
||||
def test(one, two, three, four, five, six):
|
||||
raise ConcretizeArgument(0)
|
||||
|
||||
with self.assertRaises(ConcretizeRegister) as cr:
|
||||
cpu.func_abi.invoke(test)
|
||||
|
||||
self.assertEquals(cpu.R0, previous_r0)
|
||||
self.assertEquals(cr.exception.reg_name, 'R0')
|
||||
self.assertEquals(cpu.SP, 0x1080)
|
||||
|
||||
def test_arm_abi_concretize_memory(self):
|
||||
cpu = self._cpu_arm
|
||||
|
||||
for i in range(4):
|
||||
cpu.write_register('R{}'.format(i), i)
|
||||
|
||||
previous_r0 = cpu.R0
|
||||
self.assertEqual(cpu.read_int(cpu.SP), 0x80)
|
||||
|
||||
def test(one, two, three, four, five):
|
||||
raise ConcretizeArgument(4)
|
||||
|
||||
with self.assertRaises(ConcretizeMemory) as cr:
|
||||
cpu.func_abi.invoke(test)
|
||||
|
||||
self.assertEquals(cpu.R0, previous_r0)
|
||||
self.assertEquals(cr.exception.address, cpu.SP)
|
||||
self.assertEquals(cpu.SP, 0x1080)
|
||||
|
||||
def test_i386_cdecl(self):
|
||||
cpu = self._cpu_x86
|
||||
|
||||
base = cpu.ESP
|
||||
|
||||
self.assertEqual(cpu.read_int(cpu.ESP), 0x80)
|
||||
cpu.push(0x1234, cpu.address_bit_size)
|
||||
|
||||
def test(one, two, three, four, five):
|
||||
self.assertEqual(one, 0x80)
|
||||
self.assertEqual(two, 0x84)
|
||||
self.assertEqual(three, 0x88)
|
||||
self.assertEqual(four, 0x8c)
|
||||
self.assertEqual(five, 0x90)
|
||||
return 3
|
||||
|
||||
cpu.func_abi.invoke(test)
|
||||
|
||||
self.assertEquals(cpu.EAX, 3)
|
||||
self.assertEquals(base, cpu.ESP)
|
||||
self.assertEquals(cpu.EIP, 0x1234)
|
||||
|
||||
def test_i386_stdcall(self):
|
||||
cpu = self._cpu_x86
|
||||
|
||||
base = cpu.ESP
|
||||
|
||||
bwidth = cpu.address_bit_size / 8
|
||||
self.assertEqual(cpu.read_int(cpu.ESP), 0x80)
|
||||
|
||||
cpu.push(0x1234, cpu.address_bit_size)
|
||||
|
||||
def test(one, two, three, four, five):
|
||||
self.assertEqual(one, 0x80)
|
||||
self.assertEqual(two, 0x84)
|
||||
self.assertEqual(three, 0x88)
|
||||
self.assertEqual(four, 0x8c)
|
||||
self.assertEqual(five, 0x90)
|
||||
return 3
|
||||
|
||||
abi = I386StdcallAbi(cpu)
|
||||
abi.invoke(test)
|
||||
|
||||
self.assertEquals(cpu.EAX, 3)
|
||||
self.assertEquals(base + bwidth * 5, cpu.ESP)
|
||||
self.assertEquals(cpu.EIP, 0x1234)
|
||||
|
||||
def test_i386_stdcall_concretize(self):
|
||||
cpu = self._cpu_x86
|
||||
|
||||
bwidth = cpu.address_bit_size / 8
|
||||
self.assertEqual(cpu.read_int(cpu.ESP), 0x80)
|
||||
|
||||
cpu.push(0x1234, cpu.address_bit_size)
|
||||
|
||||
eip = 0xDEADBEEF
|
||||
base = cpu.ESP
|
||||
cpu.EIP = eip
|
||||
def test(one, two, three, four, five):
|
||||
raise ConcretizeArgument(2)
|
||||
|
||||
abi = I386StdcallAbi(cpu)
|
||||
with self.assertRaises(ConcretizeMemory) as cr:
|
||||
abi.invoke(test)
|
||||
|
||||
# Make sure ESP hasn't changed if exception was raised
|
||||
self.assertEquals(base, cpu.ESP)
|
||||
# Make sure EIP hasn't changed (i.e. return value wasn't popped)
|
||||
self.assertEquals(cpu.EIP, eip)
|
||||
|
||||
def test_i386_cdecl_concretize(self):
|
||||
cpu = self._cpu_x86
|
||||
|
||||
base = cpu.ESP
|
||||
prev_eax = 0xcc
|
||||
cpu.EAX = prev_eax
|
||||
|
||||
self.assertEqual(cpu.read_int(cpu.ESP), 0x80)
|
||||
cpu.push(0x1234, cpu.address_bit_size)
|
||||
|
||||
def test(one, two, three, four, five):
|
||||
raise ConcretizeArgument(0) # 0x1068
|
||||
return 3
|
||||
|
||||
with self.assertRaises(ConcretizeMemory) as cr:
|
||||
cpu.func_abi.invoke(test)
|
||||
|
||||
# Make sure we're concretizing
|
||||
self.assertEquals(cr.exception.address, 0x1080)
|
||||
# Make sure eax is unchanged
|
||||
self.assertEquals(cpu.EAX, prev_eax)
|
||||
# Make sure EIP wasn't popped
|
||||
self.assertEquals(base, cpu.ESP+4)
|
||||
self.assertNotEquals(cpu.EIP, 0x1234)
|
||||
|
||||
|
||||
def test_i386_vararg(self):
|
||||
cpu = self._cpu_x86
|
||||
|
||||
cpu.push(3, cpu.address_bit_size)
|
||||
cpu.push(2, cpu.address_bit_size)
|
||||
cpu.push(1, cpu.address_bit_size)
|
||||
|
||||
# save return
|
||||
cpu.push(0x1234, cpu.address_bit_size)
|
||||
|
||||
def test(params):
|
||||
for val, idx in zip(params, range(1, 4)):
|
||||
self.assertEqual(val, idx)
|
||||
|
||||
cpu.func_abi.invoke(test, varargs=True)
|
||||
self.assertEquals(cpu.EIP, 0x1234)
|
||||
|
||||
|
||||
def test_amd64_basic_funcall(self):
|
||||
cpu = self._cpu_x64
|
||||
|
||||
cpu.RDI = 1
|
||||
cpu.RSI = 2
|
||||
cpu.RDX = 3
|
||||
cpu.RCX = 4
|
||||
cpu.R8 = 5
|
||||
cpu.R9 = 6
|
||||
|
||||
cpu.push(0x1234, cpu.address_bit_size)
|
||||
|
||||
def test(one, two, three, four, five, six):
|
||||
self.assertEqual(one, 1)
|
||||
self.assertEqual(two, 2)
|
||||
self.assertEqual(three, 3)
|
||||
self.assertEqual(four, 4)
|
||||
self.assertEqual(five, 5)
|
||||
self.assertEqual(six, 6)
|
||||
|
||||
cpu.func_abi.invoke(test)
|
||||
|
||||
self.assertEqual(cpu.RIP, 0x1234)
|
||||
|
||||
def test_amd64_reg_mem_funcall(self):
|
||||
cpu = self._cpu_x64
|
||||
|
||||
cpu.RDI = 1
|
||||
cpu.RSI = 2
|
||||
cpu.RDX = 3
|
||||
cpu.RCX = 4
|
||||
cpu.R8 = 5
|
||||
cpu.R9 = 6
|
||||
|
||||
cpu.push(0x1234, cpu.address_bit_size)
|
||||
|
||||
def test(one, two, three, four, five, six, seven, eight):
|
||||
self.assertEqual(one, 1)
|
||||
self.assertEqual(two, 2)
|
||||
self.assertEqual(three, 3)
|
||||
self.assertEqual(four, 4)
|
||||
self.assertEqual(five, 5)
|
||||
self.assertEqual(six, 6)
|
||||
self.assertEqual(seven, 0x80)
|
||||
self.assertEqual(eight, 0x88)
|
||||
|
||||
cpu.func_abi.invoke(test)
|
||||
|
||||
self.assertEqual(cpu.RIP, 0x1234)
|
||||
|
||||
def test_amd64_basic_funcall_concretize(self):
|
||||
cpu = self._cpu_x64
|
||||
|
||||
cpu.push(0x1234, cpu.address_bit_size)
|
||||
|
||||
def test(one, two, three, four, five, six):
|
||||
raise ConcretizeArgument(0)
|
||||
|
||||
with self.assertRaises(ConcretizeRegister) as cr:
|
||||
cpu.func_abi.invoke(test)
|
||||
|
||||
# Should not update RIP
|
||||
self.assertNotEqual(cpu.RIP, 0x1234)
|
||||
self.assertEquals(cr.exception.reg_name, 'RDI')
|
||||
|
||||
def test_amd64_vararg(self):
|
||||
cpu = self._cpu_x64
|
||||
|
||||
cpu.RDI = 0
|
||||
cpu.RSI = 1
|
||||
cpu.RDX = 2
|
||||
|
||||
# save return
|
||||
cpu.push(0x1234, cpu.address_bit_size)
|
||||
|
||||
def test(params):
|
||||
for val, idx in zip(params, range(3)):
|
||||
self.assertEqual(val, idx)
|
||||
|
||||
cpu.func_abi.invoke(test, varargs=True)
|
||||
|
||||
self.assertEquals(cpu.RIP, 0x1234)
|
||||
|
||||
def test_i386_syscall(self):
|
||||
cpu = self._cpu_x86
|
||||
|
||||
cpu.EAX = 5
|
||||
for idx, reg in enumerate(['EBX', 'ECX', 'EDX', 'ESI', 'EDI', 'EBP']):
|
||||
cpu.write_register(reg, idx)
|
||||
|
||||
def test(one, two, three, four, five, six):
|
||||
self.assertEqual(one, 0)
|
||||
self.assertEqual(two, 1)
|
||||
self.assertEqual(three, 2)
|
||||
self.assertEqual(four, 3)
|
||||
self.assertEqual(five, 4)
|
||||
self.assertEqual(six, 5)
|
||||
return 34
|
||||
|
||||
self.assertEqual(cpu.syscall_abi.syscall_number(), 5)
|
||||
|
||||
cpu.syscall_abi.invoke(test)
|
||||
|
||||
self.assertEqual(cpu.EAX, 34)
|
||||
|
||||
def test_amd64_syscall(self):
|
||||
cpu = self._cpu_x64
|
||||
|
||||
cpu.RAX = 5
|
||||
for idx, reg in enumerate(['RDI', 'RSI', 'RDX', 'R10', 'R8', 'R9']):
|
||||
cpu.write_register(reg, idx)
|
||||
|
||||
def test(one, two, three, four, five, six):
|
||||
self.assertEqual(one, 0)
|
||||
self.assertEqual(two, 1)
|
||||
self.assertEqual(three, 2)
|
||||
self.assertEqual(four, 3)
|
||||
self.assertEqual(five, 4)
|
||||
self.assertEqual(six, 5)
|
||||
return 34
|
||||
|
||||
self.assertEqual(cpu.syscall_abi.syscall_number(), 5)
|
||||
|
||||
cpu.syscall_abi.invoke(test)
|
||||
|
||||
self.assertEqual(cpu.RAX, 34)
|
||||
|
||||
def test_test_prefix(self):
|
||||
cpu = self._cpu_x86
|
||||
|
||||
cpu.push(2, cpu.address_bit_size)
|
||||
cpu.push(0x1234, cpu.address_bit_size)
|
||||
|
||||
def test(prefix, extracted):
|
||||
self.assertEquals(prefix, 1)
|
||||
self.assertEquals(extracted, 2)
|
||||
|
||||
cpu.func_abi.invoke(test, prefix_args=(1,))
|
||||
|
||||
self.assertEquals(cpu.EIP, 0x1234)
|
||||
|
||||
def test_fail_concretize_prefix_arg(self):
|
||||
cpu = self._cpu_x86
|
||||
|
||||
def test(prefix, extracted):
|
||||
raise ConcretizeArgument(0)
|
||||
|
||||
with self.assertRaises(AssertionError) as cr:
|
||||
cpu.func_abi.invoke(test, prefix_args=(1,))
|
||||
|
||||
def test_funcall_method(self):
|
||||
cpu = self._cpu_x86
|
||||
|
||||
cpu.push(2, cpu.address_bit_size)
|
||||
cpu.push(1, cpu.address_bit_size)
|
||||
cpu.push(0x1234, cpu.address_bit_size)
|
||||
|
||||
class Kls(object):
|
||||
def method(self, a, b):
|
||||
return a+b
|
||||
|
||||
obj = Kls()
|
||||
result = cpu.func_abi.invoke(obj.method)
|
||||
|
||||
self.assertEquals(result, 3)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user