Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Clemory indirection to raise exception on accesses to encrypted memory areas #529

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions cle/backends/macho/encrypted_sentinel_backer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
from __future__ import annotations

from cle.memory import Clemory


class CryptSentinel(Clemory):
"""
Mach-O binaries are often encrypted, and some area of memory is only decrypted at runtime later in the loading
process. This decryption process can't easily be implemented in CLE and is typically done with separate tools
But not all data is encrypted, and various metadata is still accessible.

This Clemory serves as a shim that allows us to notice accesses to encrypted areas of memory and raise an exception
This means that all code that was written will loudly fail on access to encrypted memory, instead of silently
reading garbage data.
"""

def __init__(self, arch, root=False):
super().__init__(arch, root)
self._crypt_start = None
self._crypt_end = None
self._is_encrypted: bool = False

def load(self, addr, n):
self._assert_unencrypted_access(addr, n)
return super().load(addr, n)

def store(self, addr, data):
self._assert_unencrypted_access(addr, len(data))
return super().store(addr, data)

def find(self, data, search_min=None, search_max=None):
if self._is_encrypted:
raise EncryptedDataAccessException("Cannot search encrypted memory region", self._crypt_start)
return super().find(data, search_min, search_max)

def set_crypt_info(self, cryptid, start, size):
self._is_encrypted = cryptid != 0 and size > 0
self._crypt_start = start
self._crypt_end = start + size

def backers(self, addr=0):
if self._is_encrypted:
if self._crypt_start <= addr < self._crypt_end:
raise EncryptedDataAccessException("Accessing encrypted memory region", addr)
return super().backers(addr)

def _assert_unencrypted_access(self, addr, size):
"""
Make sure that the access does not cover encrypted memory regions
If it does, raise an error

Cases:
- Access starts before encrypted region and ends after it
- Access starts within encrypted region
- Access ends within encrypted region

:param addr:
:param size:
:return:
"""
if not self._is_encrypted:
return

encrypted_range = range(self._crypt_start, self._crypt_end)
if addr in encrypted_range or (addr + size) in encrypted_range or (addr < self._crypt_start < addr + size):
raise EncryptedDataAccessException("Accessing encrypted memory region", addr)


class EncryptedDataAccessException(Exception):
"""
Special exception to be raised when access to encrypted memory is attempted
"""

def __init__(self, message, addr):
super().__init__(message)
self.addr = addr
17 changes: 9 additions & 8 deletions cle/backends/macho/macho.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from cle.backends.regions import Regions
from cle.errors import CLECompatibilityError, CLEInvalidBinaryError, CLEOperationError

from .encrypted_sentinel_backer import CryptSentinel
from .macho_enums import LoadCommands as LC
from .macho_enums import MachoFiletype, MH_flags
from .section import MachOSection
Expand Down Expand Up @@ -244,6 +245,10 @@ def __init__(self, *args, **kwargs):
log.info("Parsing binding bytecode stream")
self.do_binding()

def set_arch(self, arch):
super().set_arch(arch)
self.memory = CryptSentinel(arch=arch)

@property
def min_addr(self):
return self.mapped_base
Expand Down Expand Up @@ -296,7 +301,10 @@ def _parse_load_commands(self, lc_offset):
self._load_lc_data_in_code(binary_file, offset)
elif cmd in [LC.LC_ENCRYPTION_INFO, LC.LC_ENCRYPTION_INFO_64]: # LC_ENCRYPTION_INFO(_64)
log.debug("Found LC_ENCRYPTION_INFO @ %#x", offset)
# self._assert_unencrypted(binary_file, offset)
# Store the offset and size of the encrypted section for later
(_, _, cryptoff, cryptsize, cryptid) = self._unpack("5I", binary_file, offset, 20)
self.memory.set_crypt_info(cryptid, cryptoff, cryptsize)

elif cmd in [LC.LC_DYLD_CHAINED_FIXUPS]:
log.info("Found LC_DYLD_CHAINED_FIXUPS @ %#x", offset)
(_, _, dataoff, datasize) = self._unpack("4I", binary_file, offset, 16)
Expand Down Expand Up @@ -586,13 +594,6 @@ def _load_lc_data_in_code(self, f, off):

log.debug("Done parsing data in code")

def _assert_unencrypted(self, f, off):
log.debug("Asserting unencrypted file")
(_, _, _, _, cryptid) = self._unpack("5I", f, off, 20)
if cryptid > 0:
log.error("Cannot load encrypted files")
raise CLEInvalidBinaryError()

def _load_lc_function_starts(self, f, off):
# note that the logic below is based on Apple's dyldinfo.cpp, no official docs seem to exist
log.debug("Parsing function starts")
Expand Down