Sanitizers & Probes

Probe mechanism

TritonDSE introduces a probe mechanism. A probe is a class exposing a set of callbacks to register on a given exploration or execution. That enables writing more complex checkers that can be easily registered on a callback manager.

A probe as the following interface:

class ProbeInterface(object):
    """ The Probe interface """
    def __init__(self):
        self._cbs: List[Tuple[CbType, Callable, Optional[str]]] = []

    @property
    def callbacks(self) -> List[Tuple[CbType, Callable, Optional[str]]]:
        return self._cbs

    def _add_callback(self, typ: CbType, callback: Callable, arg: str = None):
        """ Add a callback """
        self._cbs.append((typ, callback, arg))

So to write its own probe one, just have to call _add_callback with its own hooks (usually some other methods). They will automatically be picked by the callback manager and registered.

Using built-in sanitizers

TritonDSE provides few simple sanitizers developped as probes. These sanitizers are the following:

  • UAFSanitizer: checks for UaF using the simple built-in allocator (in ProcessState)

  • NullDerefSanitizer: checks that no read or write in memory is performed at address 0

  • FormatStringSanitizer: hooks various libc functions and checks that format string are not controlled

The registration of a probe can be done as follows:

[2]:
from tritondse import SymbolicExecutor, Config, Seed, Program, ProcessState, SymbolicExplorator, CoverageStrategy
from tritondse.sanitizers import NullDerefSanitizer

dse = SymbolicExplorator(Config(), Program("crackme_xor"))

dse.callback_manager.register_probe(NullDerefSanitizer())

The probe will now be enabled for all executions.

Writing a sanitizer

For the purpose of this tutorial, let’s write a sanitizer that will checks fopen libc call and its variant freopen cannot be hijacked to open an unwanted file (here /etc/passwd). We are first going to check that the string given in input is controllable and if it is, checking by SMT that it can be the intended string.

[3]:
from tritondse import ProbeInterface, SymbolicExecutor, ProcessState, CbType, SeedStatus
from tritondse.types import Addr, SolverStatus

class OpenSanitizer(ProbeInterface):

    PASSWD_FILE = "/etc/passwd"

    def __init__(self):
        super(OpenSanitizer, self).__init__()
        self._add_callback(CbType.PRE_RTN, self.fopen_check, 'fopen')
        self._add_callback(CbType.PRE_RTN, self.fopen_check, 'freopen')

    def fopen_check(self, se: SymbolicExecutor, pstate: ProcessState, rtn_name: str, addr: Addr):
        # the filepath is located in arg0 (ptr to a string)
        string_ptr = se.pstate.get_argument_value(0)

        symbolic = True
        cur_ptr = string_ptr
        while pstate.read_memory_int(cur_ptr, 1):        # while different from 0
            if not se.pstate.is_memory_symbolic(cur_ptr, 1): # check that the byte is symbolic
                symbolic = False
            cur_ptr += 1

        # if all memory bytes are symbolic and we have enough place to fit our string
        if symbolic and (cur_ptr - string_ptr) > len(self.PASSWD_FILE):

            # Try to solve by SMT that the filepath string is /etc/passwd
            constraints = [pstate.read_symbolic_memory_byte(string_ptr+i).getAst() == ord(pwd_byte) for i, pwd_byte in enumerate(self.PASSWD_FILE)]
            st, model = pstate.solve(constraints)

            if st == SolverStatus.SAT:                          # if formula satisfiable
                new_seed = se.mk_new_seed_from_model(model)  # create a new input from the model
                new_seed.status = SeedStatus.CRASH           # mark it as crash
                se.enqueue_seed(new_seed)                    # enqueue it *(so that it will put in the workspace..)*

Then we just have to register our new sanitizer!

[4]:
dse.callback_manager.register_probe(OpenSanitizer())

There are no open in the crackme (but one can try with another example)