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 RawBinaryLoader
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, RawBinaryLoader, LoadableSegment
BASE_ADDRESS= 0x8000000
ENTRY_POINT = 0x81dc46e
STACK_ADDR = 0x1000000
STACK_SIZE = 1024*6
raw_f = Path("./bugged_json_parser.bin").read_bytes()
ldr = RawBinaryLoader(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.