Loaders

Loaders are the objects enabling loading a given binary intro TritonDSE memory so that it can get symbolically executed. In essence, they map the program in memory and to initialize registers.

Linux ELF loader

TritonDSE primarly supports userland Linux programs and provides a basic ELF file loader based on LIEF. It will only loads the main binary but not other shared libraries.

[4]:
from tritondse import ProcessState, Program

p = Program("crackme_xor")

ps = ProcessState.from_loader(p)

list(ps.memory.get_maps())
[4]:
[Map(start=4194304, size=2036, perm=<Perm.R|X: 5>, name='seg2'),
 Map(start=6295056, size=576, perm=<Perm.R|W: 6>, name='seg3'),
 Map(start=251662336, size=4096, perm=<Perm.R|W: 6>, name='[extern]'),
 Map(start=1879048192, size=2147483648, perm=<Perm.R|W: 6>, name='[stack]')]

The loader maps the LOAD segment of the ELF file and creates two additional segments for imported functions and for the stack.

CLE Loader

To enable supporting more file format and shared libraries TritonDSE uses the infamous cle project from the angr symbolic executor.

[5]:
from tritondse import ProcessState, CleLoader

p = CleLoader("crackme_xor")

ps = ProcessState.from_loader(p)

list(ps.memory.get_maps())
[5]:
[Map(start=0, size=8192, perm=<Perm.R|W: 6>, name='[fs]'),
 Map(start=4194304, size=2036, perm=<Perm.R|W|X: 7>, name='seg-crackme_xor'),
 Map(start=6295056, size=496, perm=<Perm.R|W|X: 7>, name='seg-crackme_xor'),
 Map(start=6295552, size=80, perm=<Perm.R|W|X: 7>, name='seg-crackme_xor'),
 Map(start=7340032, size=152376, perm=<Perm.R|W|X: 7>, name='seg-libc.so.6'),
 Map(start=7495680, size=1395900, perm=<Perm.R|W|X: 7>, name='seg-libc.so.6'),
 Map(start=8892416, size=338734, perm=<Perm.R|W|X: 7>, name='seg-libc.so.6'),
 Map(start=9234640, size=14128, perm=<Perm.R|W|X: 7>, name='seg-libc.so.6'),
 Map(start=9248768, size=61264, perm=<Perm.R|W|X: 7>, name='seg-libc.so.6'),
 Map(start=9437184, size=3464, perm=<Perm.R|W|X: 7>, name='seg-ld-linux-x86-64.so.2'),
 Map(start=9441280, size=151249, perm=<Perm.R|W|X: 7>, name='seg-ld-linux-x86-64.so.2'),
 Map(start=9592832, size=39932, perm=<Perm.R|W|X: 7>, name='seg-ld-linux-x86-64.so.2'),
 Map(start=9636320, size=5664, perm=<Perm.R|W|X: 7>, name='seg-ld-linux-x86-64.so.2'),
 Map(start=9641984, size=4824, perm=<Perm.R|W|X: 7>, name='seg-ld-linux-x86-64.so.2'),
 Map(start=251662336, size=4096, perm=<Perm.R|W: 6>, name='[extern]'),
 Map(start=1879048192, size=2147483649, perm=<Perm.R|W: 6>, name='[stack]')]

As expected it resolved all the required shared libraries and loaded them in the ProcessState memory.

Warning

Using CLE requires emulating binaries using the same architecture than the host.

Warning

While CLE can theoretically load MachO or PE binary they have not been loaded.

Firmware Loading

Performing symbolic execution on low-level firmware requires a specific loader. TritonDSE provides the MonolithicLoader that enables loading monolithic firmware by defining the memory segments manually.

The following example shows how to load a small firmware:

[ ]:
from tritondse import Architecture, MonolithicLoader, LoadableSegment

BASE_ADDRESS= 0x8000000
ENTRY_POINT = 0x81dc46e
STACK_ADDR  = 0x1000000
STACK_SIZE  = 1024*6

raw_f = Path("./bugged_json_parser.bin").read_bytes()

ldr = MonolithicLoader(Architecture.ARM32,
                       cpustate = {"pc": ENTRY_POINT,
                                   "sp": STACK_ADDR+STACK_SIZE},
                       set_thumb=True,
                       maps = [LoadableSegment(BASE_ADDRESS, len(raw_f), Perm.R|Perm.X, raw_f, name="bugged_json_parser"),
                               LoadableSegment(STACK_ADDR, STACK_SIZE, Perm.R|Perm.W, name="[stack]")])

In this example we define two memory segments, one for the firmware itself, and one for an arbitrary stack. We also adjust pc and sp to point respectively to the entry point and the base of the stack.

Writing a Loader

If none of the available loaders are available for the program to emulate, one can define its own loader. It has to inherit Loader and have to implement all methods of this class. The class to inherit have the following interface:

[ ]:
class Loader(object):
    def __init__(self, path: str):
        self.bin_path = Path(path)

    @property
    def name(self) -> str:
        raise NotImplementedError()

    @property
    def entry_point(self) -> Addr:
        raise NotImplementedError()

    @property
    def architecture(self) -> Architecture:
        raise NotImplementedError()

    @property
    def arch_mode(self) -> Optional[ArchMode]:
        return None

    @property
    def platform(self) -> Optional[Platform]:
        return None

    def memory_segments(self) -> Generator[LoadableSegment, None, None]:
        raise NotImplementedError()

    @property
    def cpustate(self) -> Dict[str, int]:
        return {}


    def imported_functions_relocations(self) -> Generator[Tuple[str, Addr], None, None]:
        yield from ()

    def imported_variable_symbols_relocations(self) -> Generator[Tuple[str, Addr], None, None]:
        yield from ()

    def find_function_addr(self, name: str) -> Optional[Addr]:
        return None

Function find_function_addr is used to attach a callback using the name of the function. As such, the loader has to provide a function to resolve a function name to its address.