def mmapx(sm, p, syscall_name, args, offset): MAP_SHARED = 0x1 memory_region_name = syscall_name handle = sm.z.handles.get(args.fd) if handle is not None: memory_region_name = f"{syscall_name} -> {handle.Name}" addr = args.addr if addr == 0: addr = p.memory._find_free_space(args.length) data = b"" if handle is not None: f = sm.z.files.open_library(handle.Name) if f is not None: f.seek(offset) data = f.read(args.length) f.close() data += b"\0" * (args.length - len(data)) # If this is shared, map it with the pointer if args.flags & MAP_SHARED != 0: print(len(data)) ptr = ctypes.POINTER(ctypes.c_void_p)(ctypes.c_void_p.from_buffer( bytearray(data))) p.memory.map( addr, align(args.length), name=memory_region_name, kind=syscall_name, ptr=ptr, ) return addr try: p.memory.map( addr, align(args.length), name=memory_region_name, kind=syscall_name, ) except Exception: if args.flags & 0x10 > 0: # This must be mapped to this region, we should be able to # just write over the existing data. # This should crash if we are unable to write to the desired # region pass else: sm.logger.notice(f"Address {addr:x} already mapped") addr = p.memory.map_anywhere(args.length, name=memory_region_name, kind=syscall_name) p.memory.write(addr, data) return addr
def alloc(self, size: int, name: str = None, align: int = 0x4) -> int: """ Allocates memory to the heap. These are rounded up to the size of the alignment. Args: size: Number of bytes to allocate. name: Used to keep track of what information was allocated. Used for debugging align: Ensures that the memory allocated is a multiple of this value. Defaults to 4. Returns: Address of the new heap boundary """ self.logger.debug(f"Allocating {size:x} bytes named {name}") ret = self.current_offset requested_size = util.align(size, alignment=align) if (self.current_offset + requested_size >= self.heap_start + self.heap_max_size): self.logger.error( "Ran out of heap memory . Try increasing max heap size.") return ret self.current_offset += requested_size # TODO(kvalakuzhy): It would be nice if this could be moved into # a heap tracking class. self.heap_objects.add(_HeapObjInfo(ret, size, name=name)) return ret
def map_file_anywhere( self, filename: str, offset: int = 0, size: int = 0, preferred_address: int = None, min_addr: int = 0x1000, max_addr: int = 0xFFFFFFFFFFFFFFFF, alignment: int = 0x1000, top_down: bool = False, prot: int = ProtType.RWX, shared: bool = False, ) -> int: """ Maps a region of memory with requested size, within the addresses specified. The size and start address will respect the alignment. Args: filename: Name of the file to memory map offset: Page-aligned offset of file to start mapping size: # of bytes to map. This will be rounded up to the nearest page. preferred_address: If the specified address is available, it will be used for the mapping. min_addr: The lowest address that could be mapped. max_addr: The highest address that could be mapped. alignment: Ensures the size and start address are multiples of this. Must be a multiple of 0x1000. Default 0x1000. top_down: If True, the region will be mapped to the highest available address instead of the lowest. prot: RWX permissions of the mapped region. Defaults to granting all permissions. shared: if True, region is shared with subprocesses. Returns: Start address of mapped region. """ if size == 0: with open(filename, "rb") as f: size = os.fstat(f.fileno()).st_size size = util.align(size) address = self.find_free_space( size, preferred_address=preferred_address, min_addr=min_addr, max_addr=max_addr, alignment=alignment, top_down=top_down, ) if address is None: raise OutOfMemoryException() self.map_file( address, filename, offset=offset, size=size, prot=prot, shared=shared, ) return address
def protect(self, address: int, size: int, prot: int) -> None: """ Sets memory permissions on the specified memory region. Respects alignment of 0x1000. Args: address: Address of memory region to modify permissions. Rounds down to nearest 0x1000. size: Size of region to protect. Rounds up to nearest 0x1000. prot: Desired RWX permissions. TODO: This does not correspond to Sections at the moment. """ if self.disableNX: prot = prot | ProtType.EXEC aligned_address = address & 0xFFFFF000 # Address needs to align with aligned_size = util.align((address & 0xFFF) + size) try: self.emu.mem_protect(aligned_address, aligned_size, prot) self.logger.debug( "Protected region 0x%x + 0x%x, Prot: %x", aligned_address, aligned_size, prot, ) except Exception as e: self.logger.error(f"Error trying to protect region " f"0x{aligned_address:x} + 0x{aligned_size:x}, " f"Prot: {prot:x}: {e}")
def _load_module(self, elf, module_name): module_path, normalized_module_name = self._get_module_name( module_name) data = bytearray(elf.Data) base = self.memory._alloc_at( "", "main", basename(normalized_module_name), elf.ImageBase, elf.VirtualSize, ) self.memory.write(base, bytes(data)) # Set proper permissions for each section of the module for s in elf.Sections: try: self.memory.protect( s.Address, align(s.VirtualSize, s.Alignment), s.Permissions, # s.Name, # "main", # module_name=basename(module_path), ) except Exception: raise ZelosLoadException( f"Bad section {hex(s.Address)} {hex(s.VirtualSize)}") return base
def __init__(self, memory, gdt_base=0x80000000, size=0x1000): self.emu = memory.emu memory.map(gdt_base, align(size), prot=0x3) self.emu.set_reg("gdtr", (0, gdt_base, size, 0x0)) self.gdt_base = gdt_base self._init_gdt()
def sys_getdents64(sm, p): global run_once if run_once is not None: return 0 args = sm.get_args([ ("unsigned int", "fd"), ("struct linux_dirent64 *", "dirp"), ("unsigned int", "count"), ]) # struct linux_dirent64 { # ino64_t d_ino; /* 64-bit inode number */ # off64_t d_off; /* 64-bit offset to next struct */ # unsigned short d_reclen; /* Size of this dirent */ # unsigned char d_type; /* File type */ # char d_name[]; /* Filename (null-terminated) */ # }; p.memory.write_int(args.dirp + 0x0, 56, sz=8) p.memory.write_int(args.dirp + 0x8, 0x0, sz=8) p.memory.write_int(args.dirp + 0x12, 6, sz=1) s_len = p.memory.write_string(args.dirp + 0x13, "FolderContents") struct_size = align(0x13 + s_len, 4) # val = bytes([0xb + s_len, 0xb + s_len]) # from zelos.util import p16 # val2 = p16(0x13+s_len +0x100 ) # p.memory.write(args.dirp + 0x10, bytes([0x30])) p.memory.write_int(args.dirp + 0x10, struct_size, sz=2) run_once = 1 return struct_size
def _find_free_space(self, size, min_addr=0, max_addr=MAX_UINT64, alignment=0x10000): """ Finds a region of memory that is free, larger than 'size' arg, and aligned. """ sections = list(self.memory_info.values()) for i in range(0, len(sections)): addr = util.align(sections[i].address + sections[i].size, alignment=alignment) # Enable allocating memory in the middle of a gap when the # min requested address falls in the middle of a gap if addr < min_addr: addr = min_addr # Cap the gap's max address by accounting for the next # section's start address, requested max address, and the # max possible address max_gap_addr = (self.max_addr if i == len(sections) - 1 else sections[i + 1].address) max_gap_addr = min(max_gap_addr, max_addr) # Ensure the end address is less than the max and the start # address is free if addr + size < max_gap_addr and self._is_free(addr): return addr raise OutOfMemoryException()
def hook_first_read(self, region_addr, hook): region = self.get_region(region_addr) size = self.get_size(region_addr) perms = self.get_perms(region_addr) if region is None or size is None or perms is None: return False addr = region.address try: self.emu.mem_protect(addr, util.align(size), ProtType.NONE) except Exception: self.logger.exception( "Error trying to protect portion 0x%x + 0x%x, Prot: %x", addr, util.align(size), ProtType.NONE, ) self.mem_hooks[addr] = _MemHook(addr, size, perms, hook) return True
def _hook_read_prot(self, uc, access, address, size, value, user_data): region = self.get_region(address) addr = region.address if region is not None else None if addr not in self.mem_hooks: return False mem_hook = self.mem_hooks[addr] self.emu.mem_protect(mem_hook.addr, util.align(mem_hook.size), mem_hook.orig_perms) del self.mem_hooks[addr] return mem_hook.hook(uc, access, address, size, value, user_data)
def copy_section(self, section: Section, other_memory: "Memory") -> None: """ Copies a section from this instance of memory into another instance of memory. Args: section: The section to copy. Must correspond to a section within this memory object. other_memory: An instance of memory to copy the specified section to. """ start = section.address size = section.size end = start + size # We have the beginning mapped for special addresses if start == 0: return # Some sections are added to differentiate different sections in # the binary. These are typically not aligned. If they are, # should only be an extra copy. if start != util.align(start) or end != util.align(end): return self.logger.spam(f"Copying {start:x}-{end:x}") if section.ptr is None: data = other_memory.read(start, size) self.map(start, size) self.write(start, bytes(data)) else: self.map(start, size, ptr=section.ptr) self._new_section( section.address, section.size, name=section.name, kind=section.kind, module_name=section.module_name, ptr=section.ptr, )
def test_align(self): self.assertEqual(0x1000, util.align(0x1000)) self.assertEqual(0x2000, util.align(0x1001)) self.assertEqual(0x1000, util.align(1)) self.assertEqual(0x12000, util.align(0x11002)) self.assertEqual(0x14, util.align(0x11, alignment=0x4)) self.assertEqual(0x10, util.align(0xF, alignment=0x4))
def find_free_space( self, size, preferred_address=None, min_addr=0x1000, max_addr=MAX_UINT32, alignment=0x10000, top_down=False, ): """ Returns the start address of the next region between start and end that allows for memory of the given size to be mapped. """ if preferred_address is not None and not self._has_overlap( preferred_address, size): return preferred_address regions = self.get_regions() # Check if space before first region is free min_addr = util.align(min_addr, alignment=alignment) if len(regions) == 0 or min_addr + size <= regions[0].address: return min_addr # Check if space between regions is free for i in range(len(regions) - 1): gap_begin = util.align(max(regions[i].end, min_addr), alignment=alignment) gap_end = min(min(max_addr, gap_begin + size), regions[i + 1].start) gap_size = gap_end - gap_begin if gap_size >= size: return gap_begin # Check if space after last region is free gap_begin = util.align(max(regions[-1].end, min_addr), alignment=alignment) gap_end = max_addr gap_size = gap_end - gap_begin if gap_size >= size: return gap_begin return None
def _alloc_at( self, name, kind, module_name, requested_addr, size, min_addr=0x60000000, max_addr=0x90000000, prot=ProtType.RWX, ptr=None, ): # if requested_addr < min_addr: # requested_addr = min_addr if requested_addr > max_addr: requested_addr = min_addr size = util.align(size) relocated_addr = 0 if self._has_overlap(requested_addr, size): relocated_addr = self._get_next_gap(size, min_addr, max_addr) self.logger.debug("[Loader] Relocating Overlapping Region from " "0x{0:08x} to 0x{1:08x}".format( requested_addr, relocated_addr)) try: self.map( relocated_addr, size, name, kind, module_name=module_name, prot=prot, ptr=ptr, ) return relocated_addr except Exception: self.logger.exception("Couldn't relocate properly") exit() else: self.map( requested_addr, size, name, kind, module_name=module_name, prot=prot, ptr=ptr, ) return requested_addr
def _load_state(self, data): for meminfo, mem in data: self.logger.debug("Loading: ", hex(meminfo.address), hex(meminfo.size), meminfo) mem = bytes(mem) try: self.emu.mem_write(meminfo.address, mem) except Exception: self.map( meminfo.address, util.align(meminfo.size), meminfo.name, meminfo.kind, ) self.emu.mem_write(meminfo.address, mem)
def sys_getdents(sm, p): args = sm.get_args([ ("unsigned int", "fd"), ("struct linux_dirent *", "dirp"), ("unsigned int", "count"), ]) handle = sm.z.handles.get(args.fd) if handle is None: return -1 folder_contents = handle.data.get("dents", None) if folder_contents is None: # Get the dents and run this function folder_contents = sm.z.files.list_dir(handle.Name) if len(folder_contents) == 0: handle.data["dents"] = folder_contents return 0 prev_struct_start = None struct_start = args.dirp total_bytes_written = 0 while len(folder_contents) > 0: full_name = os.path.join(handle.Name, folder_contents[-1]) bytes_written = _write_dirent_x86_64( sm, p, full_name, folder_contents[-1], struct_start, prev_struct_start, args.dirp + args.count, ) if bytes_written == 0: break else: folder_contents.pop() total_bytes_written += bytes_written prev_struct_start = struct_start struct_start = align(struct_start + bytes_written, alignment=0x4) handle.data["dents"] = folder_contents return total_bytes_written
def mem_map_file( self, address: int, filename: str, offset: int = 0, size: int = 0, prot: int = ProtType.RW, shared: bool = False, ): if address % 0x1000 != 0: raise ValueError("invalid argument: address not aligned") if offset % 0x1000 != 0: raise ValueError("invalid argument: offset not aligned") with open(filename, "rb") as f: basename = os.path.basename(filename) file_map = mmap.mmap(f.fileno(), length=0, offset=offset, access=mmap.ACCESS_COPY) ptr = ctypes.POINTER(ctypes.c_void_p)( ctypes.c_void_p.from_buffer(file_map)) if size == 0: size = os.fstat(f.fileno()).st_size size = align(size) if self._mem_area_overlaps(address, size): raise ValueError("invalid argument: {address, size} overlaps") mr = MemoryRegion( self, address, size, prot, basename, "mapped", basename, shared=shared, host_address=ctypes.addressof(ptr.contents), managed_object=file_map, ) self._mem_map_region(mr) return raise ValueError("invalid argument: filename")
def _load_module(self, elf, module_name): module_path, normalized_module_name = self._get_module_name( module_name) data = bytearray(elf.Data) base = self.memory.map_anywhere( elf.VirtualSize, preferred_address=elf.ImageBase, name="", kind="main", module_name=basename(normalized_module_name), ) self.memory.write(base, bytes(data)) # Set proper permissions for each section of the module for s in elf.Sections: try: self.memory.protect(s.Address, align(s.VirtualSize, s.Alignment), s.Permissions) except Exception: raise ZelosLoadException( f"Bad section {hex(s.Address)} {hex(s.VirtualSize)}") return base
def _get_next_gap(self, size, start, end): """ Returns the start address of the next region between start and end that allows for memory of the given size to be mapped. """ min_addr_so_far = start for region in sorted(list(self.emu.mem_regions()), key=lambda x: x[0]): region_begin = region[0] region_end = region[1] if region_begin >= end or region_end < start: continue gap = region_begin - min_addr_so_far if gap < size: min_addr_so_far = util.align(region_end) continue return min_addr_so_far # Check to see there is a gap after the last region gap = end - min_addr_so_far if gap < size: self.logger.error("No gap of size %x between %x and %x" % (size, start, end)) return min_addr_so_far
def __init__( self, emu, address: int, size: int, prot: int, name: str, kind: str, module_name: str, shared: bool = False, reserve: bool = False, host_address: int = None, managed_object: any = None, ): if address % 0x1000 != 0: raise ValueError("invalid argument: address not aligned") if size <= 0: raise ValueError("invalid argument: size invalid") size = align(size) self.emu = emu self.address = address self.size = size self.prot = prot self.name = name self.kind = kind self.module_name = module_name self.reserved = reserve self.shared = shared if host_address is None: self._managed_object = ctypes.create_string_buffer(size) host_pointer = ctypes.cast(self._managed_object, ctypes.POINTER(ctypes.c_char)) self.host_address = ctypes.addressof(host_pointer.contents) else: self.host_address = host_address self._managed_object = managed_object self.host_data = (ctypes.c_char * size).from_address(self.host_address)
def map_anywhere( self, size: int, name: str = "", kind: str = "", min_addr: int = 0, max_addr: int = 0xFFFFFFFFFFFFFFFF, alignment: int = 0x1000, prot: int = ProtType.RWX, ) -> int: """ Maps a region of memory with requested size, within the addresses specified. The size and start address will respect the alignment. Args: size: # of bytes to map. This will be rounded up to match the alignment. name: String used to identify mapped region. Used for debugging. kind: String used to identify the purpose of the mapped region. Used for debugging. min_addr: The lowest address that could be mapped. max_addr: The highest address that could be mapped. alignment: Ensures the size and start address are multiples of this. Must be a multiple of 0x1000. Default 0x1000. prot: RWX permissions of the mapped region. Defaults to granting all permissions. Returns: Start address of mapped region. """ address = self._find_free_space(size, min_addr=min_addr, max_addr=max_addr, alignment=alignment) self.map(address, util.align(size), name, kind) return address
def _write_dirent_x86_64(sm, p, full_name, basename, struct_start, prev_struct_start, max_addr): struct_len = align(len(basename) + 2 + 0x12, 4) if struct_start + struct_len > max_addr: return 0 library_path = sm.z.files.find_library(full_name) if library_path is None or not path.exists(library_path): return -1 statinfo = os.stat(library_path) p.memory.write_uint64(struct_start, statinfo.st_ino) # This will be overridden in the next call to this func p.memory.write_uint64(struct_start + 0x8, 0) # next struct_start p.memory.write_uint16(struct_start + 0x10, struct_len) p.memory.write_string(struct_start + 0x12, basename, terminal_null_byte=True) p.memory.write_uint8(struct_start + struct_len - 1, 8) # regular if prev_struct_start is not None: p.memory.write_uint64(prev_struct_start + 0x8, struct_start) return struct_len
def parse(self, path, binary): # Get symbols for the target dynamic binary before we replace # it with the loader in the loading process. Keep in mind, # these still need to be relocated. # TODO: Need OS agnostic place to put this information self._elf_dynamic_import_addrs = { x.symbol.name: x.address for x in binary.pltgot_relocations } self._target_entrypoint = binary.entrypoint self._target_imagebase = binary.imagebase interpreter = self._get_interpreter(binary) if interpreter is not None: # TODO: automatically do setup to run dynamic linux binaries (path, binary) = self._setup_dynamic_binary(interpreter, binary) self.Filepath = path self.binary = binary # Refer parsed binary and symbols for better logging # @@NOTE binary.get_function_address on binary.symbols invokes # a _lot_ of brk() functions = {} for symbol in binary.static_symbols: if symbol.is_function: text_sections = binary.get_section(".text") text_va = text_sections.virtual_address text_offset = text_sections.offset text_base = text_va - text_offset symbol_offset = symbol.value - text_base if symbol_offset > 0: functions[symbol.value] = symbol.name self.exported_functions = functions # Parse Architecture machine = binary.header.machine_type if machine == lief.ELF.ARCH.i386: self.Architecture = "x86" self.Mode = "32" self.Bits = 32 elif machine == lief.ELF.ARCH.x86_64: self.Architecture = "x86_64" self.Mode = "64" self.Bits = 64 elif machine == lief.ELF.ARCH.ARM: self.Architecture = "arm" self.Mode = "32" self.Bits = 32 # When looking at other archs, this gives information about # stack for arm: # https://stackoverflow.com/questions/1802783/initial-state-of-program-registers-and-stack-on-linux-arm/6002815#6002815 elif machine == lief.ELF.ARCH.MIPS: self.Architecture = "mips" self.mode = "32" self.bits = 32 else: raise UnsupportedBinaryError(f"Unsupported arch {machine} for ELF") if binary.is_pie: raise UnsupportedBinaryError("Can't handle PIE binaries") self.Data = [0] * binary.virtual_size # TODO: More time should be invested here to figure out whether # this is legit. # lets arbitrarily load things at 0x0b000000 self.logger.debug(f"Binary's imagebase is {binary.imagebase:x}") relocated_base = 0 if binary.imagebase != 0 else 0xB000000 base = relocated_base + binary.imagebase self.base = base # Only load segments that are the LOAD type. segments_to_load = [] for s in binary.segments: if s.type == lief.ELF.SEGMENT_TYPES.LOAD: segments_to_load.append(s) if len(segments_to_load) == 0: raise UnsupportedBinaryError("No loadable segment") for segment in segments_to_load: virtual_offset = segment.virtual_address - binary.imagebase self.Data[virtual_offset:virtual_offset + len(segment.content)] = segment.content self.logger.debug( f"Load segment from {binary.imagebase + virtual_offset:x} to" f" {binary.imagebase+virtual_offset+len(segment.content):x}") for s in segment.sections: section = Section() section.Name = s.name alignment = s.alignment section.Size = util.align(s.size, alignment) section.VirtualSize = util.align(s.size, alignment) section.Address = relocated_base + s.virtual_address section.Permissions = 7 section.Alignment = 0 if s.alignment < 2 else s.alignment # print(s) # print(dir(s)) self.Sections.append(section) offset = section.Address - self.base self.logger.verbose( "Adding data for section %s at offset %x of size %x", s.name, offset, len(s.content), ) # Load the ELF header and the program/section headers. ph_offset = binary.header.program_header_offset ph_data_size = (binary.header.program_header_size * binary.header.numberof_segments) self.Data[:ph_offset + ph_data_size] = binary.get_content_from_virtual_address( binary.imagebase, ph_offset + ph_data_size) self.set_tls_data(binary) # Set Misc. Binary Attributes self.Filename = basename(self.Filepath) self.Shortname = splitext(self.Filename)[0] self.ImageBase = base self.EntryPoint = relocated_base + binary.entrypoint self.VirtualSize = binary.virtual_size self.HeaderAddress = base + binary.header.program_header_offset self.HeaderSize = binary.header.program_header_size self.NumberOfProgramHeaders = binary.header.numberof_segments return
def map_anywhere( self, size: int, preferred_address: int = None, name: str = "", kind: str = "", module_name: str = "", min_addr: int = 0x1000, max_addr: int = 0xFFFFFFFFFFFFFFFF, alignment: int = 0x1000, top_down: bool = False, prot: int = ProtType.RWX, shared: bool = False, ) -> int: """ Maps a region of memory with requested size, within the addresses specified. The size and start address will respect the alignment. Args: size: # of bytes to map. This will be rounded up to match the alignment. preferred_address: If the specified address is available, it will be used for the mapping. name: String used to identify mapped region. Used for debugging. kind: String used to identify the purpose of the mapped region. Used for debugging. module_name: String used to identify the module that mapped this region. min_addr: The lowest address that could be mapped. max_addr: The highest address that could be mapped. alignment: Ensures the size and start address are multiples of this. Must be a multiple of 0x1000. Default 0x1000. top_down: If True, the region will be mapped to the highest available address instead of the lowest. prot: RWX permissions of the mapped region. Defaults to granting all permissions. shared: if True, region is shared with subprocesses. Returns: Start address of mapped region. """ address = self.find_free_space( size, preferred_address=preferred_address, min_addr=min_addr, max_addr=max_addr, alignment=alignment, top_down=top_down, ) if address is None: raise OutOfMemoryException() self.map( address, util.align(size), name=name, kind=kind, module_name=module_name, prot=prot, shared=shared, ) return address