From d85fca9b7231cdd06fdb56d3b257503065984d73 Mon Sep 17 00:00:00 2001 From: Florian Magin Date: Mon, 25 Nov 2024 17:57:25 +0100 Subject: [PATCH 1/4] Add Clemory indirection to raise exception on accesses to encrypted memory areas --- .../macho/encrypted_sentinel_backer.py | 72 +++++++++++++++++++ cle/backends/macho/macho.py | 18 ++--- 2 files changed, 82 insertions(+), 8 deletions(-) create mode 100644 cle/backends/macho/encrypted_sentinel_backer.py diff --git a/cle/backends/macho/encrypted_sentinel_backer.py b/cle/backends/macho/encrypted_sentinel_backer.py new file mode 100644 index 00000000..b8f0fd1f --- /dev/null +++ b/cle/backends/macho/encrypted_sentinel_backer.py @@ -0,0 +1,72 @@ +from cle.errors import CLEMemoryError +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): + def __init__(self, message, addr): + super().__init__(message) + self.addr = addr + pass \ No newline at end of file diff --git a/cle/backends/macho/macho.py b/cle/backends/macho/macho.py index 6ee2da38..5859141e 100644 --- a/cle/backends/macho/macho.py +++ b/cle/backends/macho/macho.py @@ -36,6 +36,8 @@ dyld_chained_starts_in_segment, ) from .symbol import AbstractMachOSymbol, DyldBoundSymbol, SymbolTableSymbol +from .encrypted_sentinel_backer import CryptSentinel + log = logging.getLogger(name=__name__) @@ -244,6 +246,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 @@ -296,7 +302,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) @@ -586,13 +595,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") From c75852fdce670a5fcf36c7a5f8d5484257f82865 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 26 Nov 2024 09:06:40 +0000 Subject: [PATCH 2/4] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- cle/backends/macho/encrypted_sentinel_backer.py | 6 ++++-- cle/backends/macho/macho.py | 3 +-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/cle/backends/macho/encrypted_sentinel_backer.py b/cle/backends/macho/encrypted_sentinel_backer.py index b8f0fd1f..36d21f58 100644 --- a/cle/backends/macho/encrypted_sentinel_backer.py +++ b/cle/backends/macho/encrypted_sentinel_backer.py @@ -1,4 +1,5 @@ -from cle.errors import CLEMemoryError +from __future__ import annotations + from cle.memory import Clemory @@ -69,4 +70,5 @@ class EncryptedDataAccessException(Exception): def __init__(self, message, addr): super().__init__(message) self.addr = addr - pass \ No newline at end of file + + pass diff --git a/cle/backends/macho/macho.py b/cle/backends/macho/macho.py index 5859141e..09a2af78 100644 --- a/cle/backends/macho/macho.py +++ b/cle/backends/macho/macho.py @@ -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 @@ -36,8 +37,6 @@ dyld_chained_starts_in_segment, ) from .symbol import AbstractMachOSymbol, DyldBoundSymbol, SymbolTableSymbol -from .encrypted_sentinel_backer import CryptSentinel - log = logging.getLogger(name=__name__) From 1f6ab202126e2b0be665dba620cc04be9a90c379 Mon Sep 17 00:00:00 2001 From: Florian Magin Date: Tue, 26 Nov 2024 16:05:04 +0100 Subject: [PATCH 3/4] fixup! Add Clemory indirection to raise exception on accesses to encrypted memory areas --- cle/backends/macho/encrypted_sentinel_backer.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cle/backends/macho/encrypted_sentinel_backer.py b/cle/backends/macho/encrypted_sentinel_backer.py index 36d21f58..500d6c41 100644 --- a/cle/backends/macho/encrypted_sentinel_backer.py +++ b/cle/backends/macho/encrypted_sentinel_backer.py @@ -67,8 +67,9 @@ def _assert_unencrypted_access(self, addr, size): 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 - - pass From 8be5adc2db24ac452c398bc5b4ffc3130bd86313 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 26 Nov 2024 15:05:44 +0000 Subject: [PATCH 4/4] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- cle/backends/macho/encrypted_sentinel_backer.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cle/backends/macho/encrypted_sentinel_backer.py b/cle/backends/macho/encrypted_sentinel_backer.py index 500d6c41..2e0f72d8 100644 --- a/cle/backends/macho/encrypted_sentinel_backer.py +++ b/cle/backends/macho/encrypted_sentinel_backer.py @@ -70,6 +70,7 @@ 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